Files

6.9 KiB
Raw Permalink Blame History

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:

  1. Log in with your existing password (hashes migrate intact).
  2. Open a set and confirm the cover image renders — that proves the volume path and the uploads route are both working.
  3. Open a PDF and an extra file.
  4. 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)