> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gocopper.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Docker

Since a Copper application compiles to a single binary, it fits Docker naturally: build the frontend in a JavaScript image, compile the app in a Go image, and run it in a minimal image that contains little more than your binaries and config. Keeping the stages separate also gives you precise layer caching, so a frontend-only change does not rebuild your Go code, and vice versa.

### The Dockerfile

The following multi-stage build covers the default GRIP application:

```docker Dockerfile theme={null}
# Stage 1: build the frontend
FROM oven/bun:1 AS web
WORKDIR /src/web
COPY web/package.json web/bun.lock ./
RUN bun install --frozen-lockfile
COPY web ./
RUN bun run build

# Stage 2: compile the app
FROM golang:1.24-alpine AS build
RUN go install github.com/gocopper/cli/cmd/copper@v1.4.0
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
COPY --from=web /src/web/build ./web/build
RUN copper build --only=app,migrate

# Stage 3: the runtime image
FROM alpine:3.21
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=build /src/build ./build
COPY --from=build /src/config ./config
EXPOSE 5901
CMD ["./build/app.out", "-config", "config/prod.toml"]
```

The frontend stage runs the Vite production build, and its output is copied into the Go stage before `copper build` embeds it. This ordering matters: `copper build` embeds whatever is in `web/build` but does not run the frontend build itself, so the Go image never needs a JavaScript toolchain. If you use npm instead of bun, swap the first stage for a `node` image and copy `package-lock.json` in place of `bun.lock`.

### Caching Dependencies

Each stage copies its dependency manifests and installs before copying the rest of the source. Docker caches those layers, so day-to-day builds skip `bun install` and `go mod download` entirely unless a lockfile changes. You are free to tighten this further with a `.dockerignore`:

```text .dockerignore theme={null}
build/
web/build/
web/node_modules/
config/local.toml
*.db
.git/
```

<Warning>
  Always exclude `config/local.toml` and any other secret-bearing files from the image. Production secrets should reach the app through the environment or a secrets manager, as described in [Deployment](/digging-deeper/deployment).
</Warning>

### Running Migrations

The image includes `migrate.out`, so you may run migrations as a release step before starting the new version:

```
docker run --rm my-app ./build/migrate.out -config config/prod.toml
```

Orchestrators offer a natural home for this: an init container in Kubernetes, a release command in your deploy tool, or simply a step in your pipeline. To learn more, see [Migrations](/database/migrations).

### API-Only Applications

If your application was created with `-frontend none`, drop the first stage and the `COPY --from=web` line; the rest of the Dockerfile works as-is.

<Note>
  If you use the `sqlite3` storage, the binary depends on cgo. Build with `CGO_ENABLED=1` and a C toolchain (`apk add gcc musl-dev` on alpine), and keep the builder and runtime libc in sync. The Postgres and MySQL drivers are pure Go, so the default stack needs none of this.
</Note>

### Graceful Shutdown

Copper shuts down cleanly on `SIGTERM`, which is what `docker stop` and most orchestrators send. Since in-flight requests, background goroutines, and cleanup hooks are given up to 30 seconds, you should allow at least that long before the platform escalates to `SIGKILL`: for example, `docker stop --timeout 35` or `terminationGracePeriodSeconds: 35` in Kubernetes.

### Next

Now that your application is containerized, you may want to review [Deployment](/digging-deeper/deployment) for production configuration and secrets, or [Metrics](/digging-deeper/metrics) to monitor it once it is running.
