feat(ops): deploy.sh — build, tag, replace, prune

One-shot deploy script that wraps the podman build + run dance:

  ./deploy.sh           # build → replace container → prune (default)
  ./deploy.sh build     # build + tag only
  ./deploy.sh run       # restart from existing :latest, no rebuild
  ./deploy.sh prune     # drop old timestamped images
  ./deploy.sh config    # print resolved configuration

Each build tags the image with both :latest AND a UTC timestamp
(YYYYMMDD-HHMMSS), so rollback is a tag retag away. Prune keeps the
N most recent timestamped images (KEEP_IMAGES, default 3); :latest
is never touched. The matching regex is strict — only the exact
YYYYMMDD-HHMMSS pattern — so a stray "dev" or hand-typed tag can't
get caught.

Settings come from an optional deploy.env (gitignored; example in
deploy.env.example). Parser is allowlist-based: only recognised
keys apply, malformed lines and command-substitution forms are
ignored. Available overrides: IMAGE_NAME, CONTAINER_NAME,
VOLUME_NAME, HOST_PORT, BIND_ADDR, KEEP_IMAGES, PUBLIC_BASE_URL,
EXTRA_PODMAN_RUN_ARGS. HOST_PORT and KEEP_IMAGES are integer-
validated before use.

Uses podman run --replace per the global ops guidance (atomic,
idempotent, no stop→rm→run race). BUILDAH_FORMAT=docker so the
HEALTHCHECK directive in the Containerfile survives. shellcheck
clean.

README's Deployment section rewritten to lead with the script;
manual podman snippet kept as fallback.
This commit is contained in:
Ole-Morten Duesund 2026-05-25 21:39:44 +02:00
commit d29e1fd3d5
4 changed files with 273 additions and 11 deletions

View file

@ -103,10 +103,53 @@ still works.
## Deployment
### Container (podman)
### One-shot deploy: `./deploy.sh`
The provided `Containerfile` builds a single image that serves API + frontend
and persists the SQLite database in `/app/data` (one volume).
The repository ships a small bash deploy script that wraps the usual
`podman build` + `podman run --replace` dance, tags images with both
`:latest` and a UTC timestamp, and prunes older timestamped images so
local disk doesn't grow forever:
```bash
./deploy.sh # build → replace container → prune (default)
./deploy.sh build # build + tag only
./deploy.sh run # restart from existing :latest, no rebuild
./deploy.sh prune # drop old timestamped images
./deploy.sh config # print resolved configuration
```
Settings come from `deploy.env` (gitignored). Copy the example and edit:
```bash
cp deploy.env.example deploy.env
```
Available overrides:
| Variable | Default | Notes |
|---------------------------|-----------------------|------------------------------------------------------------------|
| `IMAGE_NAME` | `vinterliste` | Local image-tag prefix. |
| `CONTAINER_NAME` | `vinterliste` | `podman` container name. |
| `VOLUME_NAME` | `vinterliste-data` | Named volume holding the SQLite file. |
| `HOST_PORT` | `3000` | Host port mapped to the container's `:3000`. |
| `BIND_ADDR` | `0.0.0.0` | Use `127.0.0.1` when fronting with a reverse proxy on the host. |
| `KEEP_IMAGES` | `3` | Number of timestamped images to keep on prune. |
| `PUBLIC_BASE_URL` | (auto) | Canonical URL injected into OpenGraph tags. |
| `EXTRA_PODMAN_RUN_ARGS` | (empty) | Appended verbatim to `podman run`. Word-split, quote properly. |
The `.env` parser is allowlist-based — unknown keys log a warning and are
ignored; lines that aren't `KEY=value` are skipped; quoted values are
stripped of one layer of `'` or `"`. Command-substitution forms like
`KEY=$(…)` are NOT evaluated, so a malicious `deploy.env` can't run code.
The container exposes `/api/health` for healthchecks and bakes the build
date / git revision into both OCI labels and `/etc/build-info`. The
script uses `podman run --replace` for atomicity (no `stop``rm`
`run` race).
### Manual `podman build` / `podman run`
If you don't want the script:
```bash
BUILDAH_FORMAT=docker podman build \
@ -114,10 +157,9 @@ BUILDAH_FORMAT=docker podman build \
--build-arg GIT_REVISION="$(git describe --always --dirty 2>/dev/null || echo dev)" \
-t vinterliste:latest .
# Create a named volume for the SQLite file
podman volume create vinterliste-data
podman volume create --ignore vinterliste-data
podman run --replace --name vinterliste \
podman run --replace --name vinterliste -d \
-p 3000:3000 \
-v vinterliste-data:/app/data:Z \
vinterliste:latest
@ -125,11 +167,6 @@ podman run --replace --name vinterliste \
# Visit http://localhost:3000
```
The container exposes `/api/health` for healthchecks and bakes the build date /
git revision into both OCI labels and `/etc/build-info`. Use `podman run
--replace ...` for redeploys — it's atomic and avoids the "container exists"
race.
### Environment variables
| Variable | Default | Notes |