Docker
The shipped Dockerfile, and the build-arg split that puts a secret in the image if you get it wrong.
A build arg ends up in the image. That is the whole reason the split exists: VITE_* values are
compiled into the client bundle and have to be --build-args, and everything the server reads —
CHATFUEL_TOKEN, SUPABASE_SERVICE_ROLE_KEY, CHATFUEL_WORKSPACE_ID — is runtime-only and must
never be one.
Build and run
docker build -t chatfuel-app \
--build-arg VITE_CHATFUEL_WORKSPACE_ID=... \
--build-arg VITE_SUPABASE_URL=... \
--build-arg VITE_SUPABASE_ANON_KEY=... \
--build-arg VITE_APP_NAME=... \
--build-arg VITE_APP_LOGO=... .
docker run -p 3000:3000 --env-file .env chatfuel-appWhich variable goes where
| Variable | Build arg | Runtime env |
|---|---|---|
VITE_CHATFUEL_WORKSPACE_ID, VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY, VITE_APP_NAME, VITE_APP_LOGO | yes | yes — the proxy reads them again at request time |
CHATFUEL_TOKEN, SUPABASE_SERVICE_ROLE_KEY | never | yes |
CHATFUEL_WORKSPACE_ID | never | yes |
CHATFUEL_API_BASE, PORT, PUBLISHING_SECRET, PUBLIC_URL, ADMIN_PASSWORD | never | yes |
The VITE_* row is the one people get half right: passing them as build args alone produces a
client that works and a proxy that does not, because the proxy reads the same values at runtime.
Pass them both ways, with the same values.
A --build-arg is in the image. A secret passed that way stays in it whatever the container's
runtime environment says, and nothing at run time takes it back out.
What the image is
Two stages on node:22-alpine. The builder declares the five VITE_* args, puts them in its
environment, installs from package.json and the lockfile with npm ci, copies the source and
runs npm run build. The runtime stage installs with npm ci --omit=dev, copies dist/ and
server/dist/ across from the builder, drops to the node user, exposes 3000 and runs
node server/dist/entry.js.
ws, undici and https-proxy-agent are runtime dependencies rather than dev ones for exactly
that last step: under npm ci --omit=dev the production server still has to relay WebSockets and
honour HTTPS_PROXY.
The environment at run time
--env-file .env is one way in; variables set by your orchestrator are another. If a .env ends
up inside the image next to the app it is loaded on start, and real environment variables win over
it — so an image built with a file in it still takes its configuration from the platform.
PORT defaults to 3000, which is what EXPOSE, the runtime stage's own ENV and the
-p 3000:3000 above all assume. The runtime stage also sets NODE_ENV=production.
Sub-paths work here
Unlike Vercel — see Serving from a sub-path. BASE_PATH goes in the
runtime environment like any other server variable. VITE_BASE_PATH is not one of the five args
the shipped Dockerfile declares, so add an ARG and an ENV line for it beside the others.