# Docker

> The shipped Dockerfile, and the build-arg split that puts a secret in the image if you get it wrong.

Page: https://sdk.chatfuel.com/docs/deploy/docker
Markdown: https://sdk.chatfuel.com/docs/deploy/docker.md

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-arg`s, 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 [#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-app
```

## Which variable goes where [#which-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.

<Callout type="warn">
  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.
</Callout>

## What the image is [#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 [#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 [#sub-paths-work-here]

Unlike Vercel — see [Serving from a sub-path](/docs/deploy/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.
