Add Docker deployment runbook
This commit is contained in:
1 parent
8a9966f7b0
commit
842ef065e1
1 file changed
+226
@@ -0,0 +1,226 @@
|
||||
# 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`](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:
|
||||
|
||||
```bash
|
||||
pct set <CTID> -features nesting=1,keyctl=1
|
||||
pct reboot <CTID>
|
||||
```
|
||||
|
||||
Then inside the container or VM (Debian/Ubuntu):
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
python3 -c "import secrets; print(secrets.token_hex(32))" # SECRET_KEY
|
||||
python3 -c "import secrets; print(secrets.token_urlsafe(24))" # POSTGRES_PASSWORD
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```powershell
|
||||
cd E:\LIM
|
||||
flask backup create --label pre-docker
|
||||
```
|
||||
|
||||
Copy the archive into the volume and restore:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```cron
|
||||
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)
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```nginx
|
||||
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:
|
||||
|
||||
```python
|
||||
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)
|
||||
Reference in new issue
Block a user