6.9 KiB
Docker Deployment
Runs LIM as two containers — the Flask app and Postgres 16 — with uploads and backups on a named Docker volume. Intended for a Proxmox VM or LXC.
Companion to BACKUP_AND_MIGRATION.md, which covers
getting your existing data out of E:\LIM.
What the stack looks like
| Container | Image | Purpose |
|---|---|---|
lim-app |
built from Dockerfile |
Flask + gunicorn on port 8000 |
lim-db |
postgres:16-alpine |
database, not published to the host |
| Volume | Mounted at | Holds |
|---|---|---|
lim-data |
/data |
uploads/ and backups/ |
lim-db |
/var/lib/postgresql/data |
the database |
Only lim-app is reachable from outside; Postgres is on the internal compose
network only.
Two deliberate choices
One gunicorn worker, eight threads. The backup job registry lives in process memory. With several workers, the browser's progress-poll can hit a worker that has never heard of the job and the bar sticks at 0%. Threads keep it coherent and are ample for household use. If you ever need real concurrency, the job state has to move into Postgres or Redis first.
Uploads get their own route. Stored paths render as
/static/uploads/<path>, which only worked because the folder happened to sit
inside app/static. app/__init__.py now registers an explicit
/static/uploads/<path:filename> route reading from UPLOAD_FOLDER, so the
same URLs keep working with the folder on a volume. It takes precedence over
Flask's static handler; ordinary static files are unaffected.
Proxmox host prep
An LXC is fine, but Docker inside an unprivileged container needs nesting on. A small VM avoids the whole question — pick whichever you normally run.
For an LXC, on the Proxmox host:
pct set <CTID> -features nesting=1,keyctl=1
pct reboot <CTID>
Then inside the container or VM (Debian/Ubuntu):
curl -fsSL https://get.docker.com | sh
docker compose version
Give it enough disk for the collection plus room for backups. PDFs dominate and backups store them uncompressed, so budget roughly 2.5× the size of your current uploads folder.
Deploy
git clone https://gitea.hideawaygaming.com.au/jessikitty/lego-instructions-manager.git lim
cd lim
cp .env.docker.example .env
Fill in the two required values:
python3 -c "import secrets; print(secrets.token_hex(32))" # SECRET_KEY
python3 -c "import secrets; print(secrets.token_urlsafe(24))" # POSTGRES_PASSWORD
Then:
docker compose up -d --build
docker compose logs -f app
You want to see Database is up., Schema ready. and gunicorn binding to
0.0.0.0:8000. The UI is on http://<host>:8000.
The entrypoint runs db.create_all() on every start. That is additive — it
creates missing tables and leaves existing ones alone. It does not alter
existing columns, so a future schema change still needs a migration. Set
SKIP_DB_INIT=1 to turn it off.
Move your data in
On the Windows box:
cd E:\LIM
flask backup create --label pre-docker
Copy the archive into the volume and restore:
# from wherever you dropped the .zip
docker cp lim-backup-....zip lim-app:/data/backups/
docker compose exec app flask backup verify lim-backup-....zip
docker compose exec app flask backup restore lim-backup-....zip --mode fresh --yes
Expected: row counts matching the source, and a file count matching the manifest. Restore preserves primary keys, rewrites Windows backslashes to forward slashes, and realigns Postgres sequences afterwards so new rows don't collide with restored IDs.
Then check, in this order:
- Log in with your existing password (hashes migrate intact).
- Open a set and confirm the cover image renders — that proves the volume path and the uploads route are both working.
- Open a PDF and an extra file.
- Take a backup from inside the container to prove the trip is reversible:
docker compose exec app flask backup create --label first-container-backup
Day-to-day
docker compose ps
docker compose logs -f app
docker compose restart app
docker compose down # stop, keep data
docker compose up -d --build # after a git pull
Nightly backup on the host:
0 2 * * * cd /srv/lim && docker compose exec -T app flask backup create --label nightly --keep 7
Copy the archives off the host as well — docker cp lim-app:/data/backups .,
or point rsync at the volume under /var/lib/docker/volumes/lim_lim-data/_data/backups.
A backup living only on the machine it backs up is half a backup.
Database dump (belt and braces)
docker compose exec -T db pg_dump -U lim lim | gzip > lim-$(date +%F).sql.gz
The LIM backup already carries a portable row dump, so this is optional —
useful if you want something psql can restore directly.
Reverse proxy
The app speaks plain HTTP on 8000. Front it with whatever you already run and pass the standard headers:
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 100M;
}
client_max_body_size must be at least MAX_CONTENT_LENGTH or large PDF
uploads fail at the proxy with a 413 before Flask ever sees them.
If you terminate TLS at the proxy, add ProxyFix in run.py so Flask builds
correct external URLs:
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
Troubleshooting
Permission denied writing uploads — the entrypoint chowns /data on
every start, including after create_all() has run as root. If you mounted a
host path instead of a named volume, chown it to UID 1000 yourself.
Images 404 but the page loads — UPLOAD_FOLDER and the restored files
disagree. Check docker compose exec app ls /data/uploads against
manifest.uploads in the archive.
App restarts in a loop — read docker compose logs app. Usually a missing
SECRET_KEY or POSTGRES_PASSWORD; compose fails loudly on both.
Database did not become available in time — Postgres didn't pass its
healthcheck. docker compose logs db; after a bad shutdown the data volume
sometimes needs a plain docker compose up -d db on its own first.
Backup progress bar stuck at 0% — more than one gunicorn worker. Check the
CMD in the Dockerfile hasn't been changed to --workers 2 or higher.
Rolling back
Nothing here touches E:\LIM. If the container instance misbehaves, keep using
Windows and try again later. Going the other way, take a backup from the
container and restore it onto Windows with the same commands — the format is
identical in both directions.
Created by: Jess Rogerson (yelling commands at Claude.AI)