6.7 KiB
6.7 KiB
QA: Docker deployment
Packaging and docs only. There is no Go test in test/ and no Vitest spec for this slice. All steps are manual command steps run from a clean checkout on a machine with Docker and Compose v2. Record results in the Notes table. Anything not run is reported NOT RUN, never PASS.
Preconditions
- Clean checkout, no bin/, tmp/, node_modules or internal/infrastructure/assets/dist present.
- docker.env created from docker.env.example. JWT_SECRET is generated (openssl rand -hex 32) and SMTP values are real or placeholders. Nothing is committed.
- For local HTTP trial, ENVIRONMENT=development (otherwise the Secure cookie is not stored over HTTP).
Static checks (no Docker daemon needed)
- Dockerfile, .dockerignore and docker-compose.yml contain no comment lines (no line starting with
#, no trailing#). - .dockerignore lists tmp/, bin/, node_modules (also web/node_modules), .git, internal/infrastructure/assets/dist, .env and docker.env.
- Dockerfile: three stages (bun, go, final). The Go build sets CGO_ENABLED=0, has no
-tags nofrontend, and uses-ldflagscontaining-X main.version=from a build arg. The final stage has USER non-root, exec-form ENTRYPOINT (nosh -c), and no ENV, ARG or COPY holding JWT_SECRET or any SMTP_* value. - docker-compose.yml: one service, a named volume mounted at the data directory, a
portsentry,env_file, a healthcheck on /healthz,stop_grace_period>= 15s. - git diff --stat shows no change under api/, internal/, web/src/, cmd/ and test/. Only Dockerfile, .dockerignore, docker-compose.yml, docker.env.example, Taskfile.yml, .gitignore, README.md and docs/ changed. No new env var appears in cmd/server/main.go.
- docs/docker.md exists and is linked from README.md and docs/deployment.md.
task --listshows docker:build, docker:run, docker:up, docker:down and docker:logs.
Build
task docker:buildsucceeds from the clean checkout, with no pre-built SPA.docker run --rm <image> --versionprints the version, which is notdev.docker image lssize recorded. Flag it if it is above roughly 40 MB.docker image inspectshows a non-root User, anddocker run --rm --entrypoint id <image>shows uid not 0.docker history --no-trunc <image>contains none of the values in docker.env.
Run
task docker:upstarts the service.docker compose psshows it healthy within the healthcheck start period.curl -i http://localhost:8080/healthzreturns 200 and bodyok.curl -i http://localhost:8080/returns 200 text/html, and the SPA loads in a browser.curl -i http://localhost:8080/v1/auth/registrationreturns 200 with JSON.- Logs show the listening line, and no permission error creating DB_FILE or UPLOAD_DIR on the volume.
- Register an account in the SPA, then upload an image, to confirm both the DB and UPLOAD_DIR are writable by the container user.
Persistence
docker compose down(no -v), thentask docker:up. The account from step 18 can log in and the image is still served.docker compose down -vremoves the volume, and the next start gives an empty instance. Documented in docs/docker.md.- The backup procedure in docs/docker.md is followed once and restores into a fresh volume.
Failure and shutdown
- For each required variable in turn, remove it from the env file and run
docker compose up. The container exits non-zero and the log containsEnvironment variable <NAME> is not set. docker compose stop(timed) completes in under 10 s, the log containsshutting down..., and the exit code is 0. This proves PID 1 receives SIGTERM.- Break the healthcheck (stop the app inside the container, or point it at the wrong port): the service becomes unhealthy.
SMTP (manual, needs real SMTP)
- With an external SMTP host, trigger a registration or password-reset email and confirm delivery.
- With a host MTA via host.docker.internal, repeat. On Linux this needs the extra_hosts mapping documented.
- Confirm the failure mode is documented when SMTP is unreachable.
Reverse proxy and TLS (manual)
- Put Caddy or nginx in front, per docs/docker.md, with ENVIRONMENT=production over HTTPS. Log in and confirm the Secure cookie is accepted.
- Set TRUSTED_PROXIES to the proxy address as seen by the container and confirm the login rate limiter uses X-Real-IP.
Upgrade
- Follow docs/docker.md: back up, rebuild with
task docker:build,task docker:up. Data is intact and--versionshows the new version. - Confirm docs state that browser-update Restart is unsupported in Docker (Settings shows "Not prepared").
Notes to record
| Item | Value |
|---|---|
| Image size | |
| Base images and tags used | |
| Container uid | |
| Build time (cold) | |
| Docker and Compose versions | |
| Steps 25-29 run? |
Not run in this slice: CI, registry publishing, Kubernetes or Helm, multi-arch. SPA tests and visual checks are out of scope.
Results
Run on Windows 11, Docker 29.8.1, Compose 5.5.1, with a throwaway docker.env (fake secrets, ENVIRONMENT=development) that was deleted afterwards.
| Steps | Result |
|---|---|
| 1-7, 31 | PASS |
| 8 | PASS after a fix: bun run build failed in the image because vue-tsc cannot resolve .vue modules under the Bun-only node fallback; the Dockerfile now runs bun run build-only. vue-tsc passes on the host and under real node 22 |
| 9, 11, 12 | PASS (version v2.8.0-beta-dirty, uid 10001, entrypoint exec form, no secret names in history) |
| 10 | 54.5 MB, above the 40 MB flag (binary 29.9 MB on alpine 3.22) |
| 13-17 | PASS (healthy, /healthz 200 ok, / 200 text/html, /v1/auth/registration 200 JSON, no permission errors) |
| 18-19 | PASS (register, verify, login, image upload to /data/uploads as uid 10001; all survive down and recreate; uploaded image still served) |
| 20 | PASS (down -v removed the volume) |
| 21 | PASS (tar backup, down -v, restore, login and media list intact) |
| 22 | PASS for all 7 vars (exit 2, Environment variable X is not set) |
| 23 | PASS (stop 0.4 s, exit 0, shutting down... logged) |
| 24 | PASS (wrong-port healthcheck reports unhealthy) |
| 25 | PARTIAL: mail delivered over STARTTLS to a Mailpit container on the compose network with the CA trusted via SSL_CERT_FILE; no real external host |
| 26-29 | NOT RUN (host MTA, Caddy/nginx TLS, TRUSTED_PROXIES) |
| 30 | NOT RUN as a before/after upgrade |
task docker:run |
PASS (healthz 200, graceful stop, separate volume renewcms-data) |
Notes
| Item | Value |
|---|---|
| Image size | 54.5 MB |
| Base images and tags used | oven/bun:1-alpine, golang:1.27.1-alpine, alpine:3.22 |
| Container uid | 10001 |
| Build time (cold) | not measured, layers were partly cached; about 18 s warm |
| Docker and Compose versions | 29.8.1, 5.5.1 |
| Steps 25-29 run? | 25 partial, 26-29 no |