Skip to content

Add optional Docker / Docker Compose support - #372

Open
opastorello wants to merge 4 commits into
ArnasDon:mainfrom
opastorello:feat/docker-support
Open

Add optional Docker / Docker Compose support#372
opastorello wants to merge 4 commits into
ArnasDon:mainfrom
opastorello:feat/docker-support

Conversation

@opastorello

Copy link
Copy Markdown
Contributor

Summary

Add an optional, self-contained container setup (Dockerfile + Docker Compose) for forkers who self-host on their own infra. Purely additive — next start and the Hostinger path are unchanged.

What changed

  • Dockerfile — multi-stage (deps → build → runtime) using Next.js output: "standalone"; runs as a non-root user; only .next/standalone, .next/static, and public land in the final image.
  • docker-compose.yml — single app service. Supabase stays external (no DB container). NEXT_PUBLIC_* are forwarded as build args; server secrets come from .env.local via env_file at runtime. Includes a healthcheck and restart: unless-stopped.
  • .dockerignore — excludes .git, node_modules, .next, docs, supabase, all .env*, etc.
  • docs/docker.md — usage guide, including the build-time (NEXT_PUBLIC_*, inlined into the client bundle) vs runtime (secrets) distinction and the --env-file .env.local note.
  • next.config.tsoutput: "standalone". No-op for next start / Hostinger; only the Docker build consumes it.
  • README.md — one line pointing to docs/docker.md after the quick start.

Database migrations remain out of scope for the container (applied via the Supabase CLI, matching the existing external-Supabase model).

Test plan

  • npm run typecheck clean.
  • npm run build succeeds (runs inside the Docker build).
  • docker compose --env-file .env.local up --build -d builds and starts; container reports healthy; / → 307 /dashboard, /login → 200.
  • Verified end-to-end on a real deploy (Coolify + external self-hosted Supabase): app boots, auth and DB reachable.

Note: the 5 local test failures in currency/date-utils are timezone/ICU-locale artifacts of a non-UTC dev machine and are unrelated to this change (the only source edit is output: "standalone"); they pass in CI (Linux/UTC).

Related

Closes #371.

Give forkers who self-host on their own infra a first-class container
path instead of hand-rolling a Dockerfile and guessing the Next.js
standalone wiring.

- Multi-stage Dockerfile (deps -> build -> runtime) using Next.js
  output: "standalone"; runs as a non-root user.
- Single-service docker-compose.yml — Supabase stays external, so no
  database container is included.
- .dockerignore and docs/docker.md documenting the setup, including the
  build-time (NEXT_PUBLIC_*, inlined into the client bundle) vs runtime
  (server secrets via env_file) split.
- Set output: "standalone" in next.config.ts. This is a no-op for
  next start and the Hostinger path; only the Docker build consumes it.

Purely additive: no dependency or stack changes, existing deploy paths
are unaffected. Database migrations remain out of scope for the
container (applied via the Supabase CLI, matching the external-Supabase
model).
@opastorello
opastorello requested a review from ArnasDon as a code owner July 12, 2026 03:30
@ArnasDon

Copy link
Copy Markdown
Owner

Great, well-documented addition — the multi-stage standalone Dockerfile is textbook: running as non-root and keeping server secrets out of the image (build args limited to NEXT_PUBLIC_*) is exactly right. I built it locally and confirmed output: "standalone" emits server.js plus the static/public paths your final stage copies, so it runs.

A couple of things before merge:

  1. PORT override breaks the container. docs/docker.md suggests setting PORT in .env.local, but env_file: .env.local injects that into the container, so Next starts listening on that port inside the container — while the "${PORT:-3000}:3000" mapping and the healthcheck still target 3000. A separate var, e.g. "${HOST_PORT:-3000}:3000" (leaving PORT for the container only), fixes it cleanly.

  2. Node version parity. The Dockerfile uses node:22-alpine while CI and engines are on Node 20 — worth matching to node:20-alpine for build/prod parity.

  3. Dead doc link. docs/docker.md links to docs/automations-and-cron.md, which doesn't exist yet (the /api/automations/cron endpoint does).

None of these affect the default happy path. Thanks for making self-hosting first-class!

@ArnasDon ArnasDon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added in the main thread.

Setting PORT in .env.local moved the port the server listens on
*inside* the container (env_file injects it), while the mapping and
the healthcheck stayed on 3000 — so the container came up
unreachable.

The published port is now HOST_PORT, and PORT is pinned to 3000 via
`environment` (which takes precedence over env_file) so a leftover
PORT in .env.local can no longer desync the app from the mapping.
Documented both, including for the plain-docker path.
The image was on node:22-alpine while CI pins node-version: 20 and
package.json declares engines.node >=20.0.0. Align all three stages
so the container builds and runs on the same major the project is
tested against.
docs/automations-and-cron.md was never written. Replace the pointer
with the details it was standing in for: both cron endpoints, the
x-cron-secret header, and the 503 they return until
AUTOMATION_CRON_SECRET is set.
@opastorello

Copy link
Copy Markdown
Contributor Author

Thanks for the careful read — all three are pushed, one commit each.

1. PORT override. You were right, and it's a bit worse than the mapping being off: env_file injects PORT into the container, so the server itself moves off 3000 while the mapping and the healthcheck stay there. The published port is now HOST_PORT, and I also pinned PORT: 3000 under environment (it takes precedence over env_file) so a leftover PORT in someone's .env.local can't desync it again. If you'd rather keep compose minimal and just swap the variable, say so and I'll drop the pin.

Verified with both PORT=8080 and HOST_PORT=8080 set in .env.local — the case that used to come up unreachable:

$ docker compose port app 3000
0.0.0.0:8080
$ docker logs wacrm-app-1 | tail -1
- Network:  http://0.0.0.0:3000
$ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost:8080/
307 http://localhost:8080/dashboard
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/login
200

Container reports healthy. The plain docker run example had the same trap, so it now passes -e PORT=3000 too.

2. Node parity. All three stages are on node:20-alpine. Rebuilt from scratch; node --version inside the image is v20.20.2.

3. Dead doc link. docs/automations-and-cron.md was never written, so instead of pointing at nothing I inlined what it was standing in for: both cron endpoints (/api/automations/cron and /api/flows/cron), the x-cron-secret header, and the 503 they return until AUTOMATION_CRON_SECRET is set.

Two things worth flagging:

  • .env.local.example:77-81 carries the same dangling reference to that file. I left it alone since it predates this PR and is outside its scope — happy to fix it here or separately, your call.
  • npm run format also rewrites the healthcheck array in docker-compose.yml to single quotes (prettier's singleQuote), so that shows up in the diff. Purely cosmetic.

npm run typecheck and npm run lint are clean. npm test still shows the same 5 failures in currency/date-utils on my machine, and I can now be specific that it's environment and not code: with TZ=UTC the two date-utils ones pass, and the currency ones are Intl.NumberFormat picking up my system locale's grouping (ZZZ 1.234 where the test wants 1,234).

One last thing — CI on this branch is sitting at action_required, so it needs your approval to actually run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add optional Docker / Docker Compose support

2 participants