# 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) 1. Dockerfile, .dockerignore and docker-compose.yml contain no comment lines (no line starting with `#`, no trailing ` #`). 2. .dockerignore lists tmp/, bin/, node_modules (also web/node_modules), .git, internal/infrastructure/assets/dist, .env and docker.env. 3. Dockerfile: three stages (bun, go, final). The Go build sets CGO_ENABLED=0, has no `-tags nofrontend`, and uses `-ldflags` containing `-X main.version=` from a build arg. The final stage has USER non-root, exec-form ENTRYPOINT (no `sh -c`), and no ENV, ARG or COPY holding JWT_SECRET or any SMTP_* value. 4. docker-compose.yml: one service, a named volume mounted at the data directory, a `ports` entry, `env_file`, a healthcheck on /healthz, `stop_grace_period` >= 15s. 5. 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. 6. docs/docker.md exists and is linked from README.md and docs/deployment.md. 7. `task --list` shows docker:build, docker:run, docker:up, docker:down and docker:logs. ## Build 8. `task docker:build` succeeds from the clean checkout, with no pre-built SPA. 9. `docker run --rm --version` prints the version, which is not `dev`. 10. `docker image ls` size recorded. Flag it if it is above roughly 40 MB. 11. `docker image inspect` shows a non-root User, and `docker run --rm --entrypoint id ` shows uid not 0. 12. `docker history --no-trunc ` contains none of the values in docker.env. ## Run 13. `task docker:up` starts the service. `docker compose ps` shows it healthy within the healthcheck start period. 14. `curl -i http://localhost:8080/healthz` returns 200 and body `ok`. 15. `curl -i http://localhost:8080/` returns 200 text/html, and the SPA loads in a browser. 16. `curl -i http://localhost:8080/v1/auth/registration` returns 200 with JSON. 17. Logs show the listening line, and no permission error creating DB_FILE or UPLOAD_DIR on the volume. 18. 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 19. `docker compose down` (no -v), then `task docker:up`. The account from step 18 can log in and the image is still served. 20. `docker compose down -v` removes the volume, and the next start gives an empty instance. Documented in docs/docker.md. 21. The backup procedure in docs/docker.md is followed once and restores into a fresh volume. ## Failure and shutdown 22. 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 contains `Environment variable is not set`. 23. `docker compose stop` (timed) completes in under 10 s, the log contains `shutting down...`, and the exit code is 0. This proves PID 1 receives SIGTERM. 24. 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) 25. With an external SMTP host, trigger a registration or password-reset email and confirm delivery. 26. With a host MTA via host.docker.internal, repeat. On Linux this needs the extra_hosts mapping documented. 27. Confirm the failure mode is documented when SMTP is unreachable. ## Reverse proxy and TLS (manual) 28. Put Caddy or nginx in front, per docs/docker.md, with ENVIRONMENT=production over HTTPS. Log in and confirm the Secure cookie is accepted. 29. Set TRUSTED_PROXIES to the proxy address as seen by the container and confirm the login rate limiter uses X-Real-IP. ## Upgrade 30. Follow docs/docker.md: back up, rebuild with `task docker:build`, `task docker:up`. Data is intact and `--version` shows the new version. 31. 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 |