Skip to main content
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:
Dockerfile
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:
.dockerignore
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.

Running Migrations

The image includes migrate.out, so you may run migrations as a release step before starting the new version:
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.

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.
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.

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 for production configuration and secrets, or Metrics to monitor it once it is running.