Track your model kit stash, paint shelf, and builds — self-hosted, on your own hardware.
1.1K
Your model kit stash, paint shelf, and builds — self-hosted, on your own hardware.
Website · Quick start · Changelog · Discussions · Report a bug
No more guessing whether you already own that Revell kit, digging through drawers for the right shade of Tamiya paint, or losing track of build progress. Model Workshop Manager keeps your catalog, inventory, and builds in one place — installable as an app on your phone or desktop, and running entirely on your own server.
Two ways to run it: pull the published image (no clone needed — this is the one to use for a Portainer stack), or build from source.
Multi-arch (linux/amd64 + linux/arm64 — Raspberry Pi and ARM NAS boxes included):
ghcr.io/lucifix/model-workshop-managerdocker.io/lucifix/model-workshop-managerPaste this straight into Portainer (Stacks → Add stack → Web editor) or save it as
docker-compose.yml anywhere — nothing needs to be filled in:
services:
app:
image: ghcr.io/lucifix/model-workshop-manager:latest
container_name: model-workshop-manager
restart: unless-stopped
ports:
- 8080:3000
environment:
# Set true once you're serving this over HTTPS, or login will silently fail.
SESSION_COOKIE_SECURE: "false"
# Number of reverse proxies in front of the app, if any. Omit if none.
# TRUST_PROXY: 1
volumes:
- workshop-data:/data
- workshop-backups:/backups
volumes:
workshop-data:
workshop-backups:
Everything else (database path, uploads, port inside the container) is baked into the image.
For the full file — including the optional nightly-backup sidecar and bind-mount options — use
docker-compose.yml from this repo:
mkdir model-workshop-manager && cd model-workshop-manager
curl -O https://raw.githubusercontent.com/Lucifix/model-workshop-manager/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/Lucifix/model-workshop-manager/main/.env.example
cp .env.example .env # optional tweaks — nothing required
docker compose pull
docker compose up -d
Updating: docker compose pull && docker compose up -d — see CHANGELOG.md
for what's new.
git clone https://github.com/Lucifix/model-workshop-manager.git && cd model-workshop-manager
cp .env.example .env # optional tweaks — nothing required
docker compose up -d --build # migrations run automatically on boot
Updating: git pull && docker compose up -d --build
Both options open http://localhost:8080 (or whatever HOST_PORT you set) — first visit shows a
one-time setup screen to create your username/password, then you're logged in. To start with a
full paint catalog instead of an empty one, open Import & Export → Import starter catalog.
Stopping: docker compose down (your data lives in the workshop-data volume, untouched)
Before you expose this anywhere: it's a single-user app meant for your LAN or a VPN (Tailscale/WireGuard), not the open internet. See Security below.
Nothing is required — every variable has a working default.
All set in .env (copied from .env.example), read by docker-compose.yml.
| Variable | Required | Default | What it does |
|---|---|---|---|
SESSION_SECRET | No | (generated) | Signs the session cookie. Unset, a random key is generated on first boot into the data volume (/data/session-secret). Set it only to pin your own key. |
SESSION_COOKIE_SECURE | No | false | Set true once served over HTTPS — otherwise the browser won't send the cookie and login silently fails. |
TRUST_PROXY | No | (off) | Set to the number of reverse proxies in front of the app (usually 1) so the login rate limiter sees real client IPs. true is refused — it's spoofable. |
HOST_PORT | No | 8080 | Host port the app is served on. |
APP_PORT | No | 3000 | Port the app listens on inside the container. Rarely needs changing. |
APP_IMAGE_TAG | No | latest | Which published image tag to run (latest, or a pinned version like 1.2.3). Ignored when building from source. |
APP_UID / APP_GID | No | 1000 | uid:gid the container runs as. Set these to match the owner of WORKSHOP_DATA_DIR/BACKUP_DIR if bind-mounting. |
WORKSHOP_DATA_DIR | No | (named volume) | Host path for the database + uploaded photos, instead of the workshop-data Docker volume — e.g. a NAS mount. |
BACKUP_DIR | No | ./backups | Host path where backups (in-app, manual script, and the nightly job) are written. |
BACKUP_RETENTION_DAYS | No | 14 | How many days of nightly backups to keep before pruning. |
A single React Router v8 (framework mode) app → Express → Drizzle ORM → SQLite, one Node process behind Docker Compose. React + TypeScript UI and API resource routes live in the same app; photos live on disk, never as DB blobs.
Requires Node.js 22+ (see .nvmrc — better-sqlite3 needs a matching native build; older Node
versions fail silently instead of erroring).
npm install
npm run db:migrate && npm run db:seed # first time only
npm run dev # :3000 — one process, UI + API
Open http://localhost:3000. Run tests with npm test.
This is a single-user private app and it requires login — but login raises the bar, it isn't a substitute for not exposing an unaudited personal app to the open internet. Keep it behind a reverse proxy, LAN-only or on a VPN.
/api/health, /api/auth/login, /api/auth/me, /api/auth/setup) — new routes are guarded
automatically.AUTH_USERNAME/AUTH_PASSWORD in .env are an optional way to seed that same
row on first boot instead (e.g. bringing an existing deployment forward) and are never read
again afterwards. Change your password any time from Settings. Forgot it? There's no email
flow — run docker compose exec app npm run auth:reset and you'll get the setup screen again
(or, if AUTH_USERNAME/AUTH_PASSWORD are still set in .env, the next restart quietly
recreates that same login instead — remove them from .env first if you want a blank setup
screen).httpOnly cookie (React Router's cookie session storage) — no
server-side session store to run or lose. The 30-day expiry is stored inside the signed
payload and checked on every request, not left to the browser's cookie lifetime.auth:reset) invalidates every other
existing session immediately, the same way — no reason to also rotate the signing key for that.
Deleting /data/session-secret (or changing SESSION_SECRET, if you set one) and restarting
remains the blunter, whole-app option.TRUST_PROXY (see the table above) or that budget is shared
across all clients rather than counted per client.0600
permissions at /data/session-secret, outside the database — so a backup restore doesn't sign
you out. SESSION_SECRET overrides it if set.SESSION_COOKIE_SECURE=true once this is served over HTTPS, otherwise the browser won't
send the cookie and login will silently fail.Three ways to get a backup, easiest first: the Backups tab in the app (create, list,
download, restore, delete), an automated nightly snapshot (a docker-volume-backup sidecar
container, opt-in — see below), or ./scripts/backup.sh for a manual one from the shell. All
three produce the same tarball format, so a backup from any one can be restored via any other.
docker compose --profile backup up -d. The backup
service in docker-compose.yml
(offen/docker-volume-backup) snapshots the
workshop-data volume to BACKUP_DIR every night at 03:00, pruning by BACKUP_RETENTION_DAYS.
Point BACKUP_DIR at a real host path already covered by whatever backs up your other apps. It's
opt-in because it needs access to the Docker socket to pause the app during the snapshot, which
is root-equivalent on the host — the in-app Backups tab above doesn't need it../scripts/backup.sh writes a timestamped .tar.gz of the database and
uploaded photos to $BACKUP_DIR. Restore steps are in the comments at the top of the script:
stop the stack, extract the tarball's database/ and uploads/ into the workshop-data
volume, restart.Bug reports, feature requests, and PRs are welcome — see CONTRIBUTING.md for dev setup and conventions. Please also read the Code of Conduct. Found a security issue? See SECURITY.md instead of opening a public issue.
If this saves you from buying the same kit twice, a ⭐ helps other modellers find it.
Content type
Image
Digest
sha256:f5060686d…
Size
134.7 MB
Last updated
2 days ago
docker pull lucifix/model-workshop-manager