This article is based on a common Docker Compose + Go development question originally discussed on Stack Overflow.
The Problem: Slow Rebuilds in Docker Compose
If you are new to Docker Compose and working on a Go project, you may have noticed that every time you change a source file, the feedback loop feels painfully slow. The naive workflow looks like this:
docker-compose build
docker-compose up
The issue is that docker-compose build rebuilds the entire image from scratch—including re-downloading and re-compiling all Go dependencies—even when only one source file changed. On a project with many dependencies this can take minutes per iteration.
Why the Volume Mount Approach Breaks
A natural instinct is to skip the full image rebuild by running a build command directly against a running container:
docker-compose run web go build .
docker-compose run web ./app
or
docker-compose run web go build .
docker-compose restart
This seems reasonable because a host-to-container volume mount means the container already has access to updated source files—there is no need to copy them in again. However, two problems occur:
- The new binary is not picked up.
docker-compose restartrestarts the service using thecommanddefined indocker-compose.yml, but the running process image is already in memory. The container does not re-exec the binary after restart in the way you might expect. - Port forwarding breaks.
docker-compose runstarts a brand-new, one-off container. It does not reuse the port bindings of the up-managed container, so the published port is no longer mapped correctly.
Reference Configuration
Here is the Dockerfile and Compose file that illustrate the typical starting point for this problem:
Dockerfile
FROM golang:1.8
COPY . /go/src/github.com/codeblooded/test1
WORKDIR /go/src/github.com/codeblooded/test1
RUN echo $PATH
RUN go get -d -v ./...
RUN go install -v ./...
RUN go build -o test1 .
CMD ["test1"]
EXPOSE 3470Code language: PHP (php)
docker-compose.yml
version: '3'
services:
postgres:
image: postgres
volumes:
- ./db/data/psql:/var/lib/postgresql/data
- ./db/schema:/db/schema
redis:
image: redis
volumes:
- ./db/data/redis:/data
server:
build: .
command: test1
volumes:
- .:/go/src/github.com/codeblooded/test1
ports:
- "3470:3470"
depends_on:
- postgres
- redisCode language: PHP (php)
Root Cause: The Image Layer Cache Is Not Being Used Properly
The Dockerfile above copies the entire project directory before running go get. This means Docker cannot cache the dependency-download layer separately from the source code. Every time a source file changes, Docker invalidates the COPY . . layer, which forces every subsequent layer—including go get—to re-run.
Solution 1: Fix the Layer Cache Order
If you are using Go modules (go.mod / go.sum), copy only those files first, run go mod download, and then copy the rest of the source. Docker caches each layer independently, so the dependency download layer is only invalidated when go.mod or go.sum changes—not on every source edit.
FROM golang:1.21
WORKDIR /app
# Cache dependencies separately from source code
COPY go.mod go.sum ./
RUN go mod download
# Now copy the rest of the source
COPY . .
RUN go build -o server .
CMD ["./server"]
EXPOSE 3470Code language: PHP (php)
With this structure, docker-compose build only re-downloads dependencies when the module files change. Rebuilding after a source-only edit becomes much faster.
Solution 2: Use exec + Stop/Start Instead of restart
If you want to rebuild inside the running container without a full image rebuild, use docker-compose exec (not run) to compile inside the already-running service container, then stop and start the service:
# Compile inside the existing container
docker-compose exec server go build -o /app/server .
# Stop and start (not restart) so CMD is re-executed
docker-compose stop server
docker-compose start serverCode language: PHP (php)
docker-compose exec runs the command inside the container that is already listening on the correct port, so port forwarding remains intact. stop + start (rather than restart) ensures the entry-point binary is re-executed from disk.
Note: the container’s CMD must point to the binary at a predictable path that the new build overwrites.
Solution 3: Use a Live-Reload Tool
For the fastest inner-loop experience during development, use a file-watching live-reload tool as the container’s entrypoint. The tool watches your source files via the shared volume and automatically recompiles and restarts the Go binary when files change—no manual rebuild step needed.
Popular options for Go include:
- Air (
github.com/air-verse/air) — a widely used live-reload tool for Go applications. - CompileDaemon (
github.com/githubnemo/CompileDaemon) — watches Go files and triggersgo buildon change. - Reflex (
github.com/cespare/reflex) — a general-purpose file watcher that can run any command.
A Dockerfile using Air for development might look like this:
FROM golang:1.21
WORKDIR /app
# Install Air
RUN go install github.com/air-verse/air@latest
COPY go.mod go.sum ./
RUN go mod download
COPY . .
EXPOSE 3470
CMD ["air"]Code language: PHP (php)
With the host source directory mounted as a volume (.:/app), Air detects file changes on the host, rebuilds the binary inside the container, and restarts it—all automatically. Port bindings remain stable because the container itself never stops.
Choosing the Right Approach
| Approach | Build speed | Port stability | Complexity | Best for |
|---|---|---|---|---|
| Fix layer cache order | Fast after first build | Stable (full rebuild) | Low | CI / production images |
| exec + stop/start | Fast (no image rebuild) | Stable | Medium | Occasional manual rebuilds |
| Live-reload tool (Air) | Fastest (automatic) | Stable | Low–Medium | Active development loops |
Key Takeaways
- Never run
docker-compose buildon every source change if you can avoid it. Fix your Dockerfile layer order so that dependencies are cached separately from application code. - Use
docker-compose exec(notrun) when you need to run a command inside an already-running service container.runcreates a new container with different port bindings. - For active Go development inside Docker Compose, a live-reload tool like Air gives you the fastest and most ergonomic feedback loop with minimal configuration.
- Volume mounts alone do not cause the container to pick up a new binary—you still need to stop and restart the process, or use a watcher that does this automatically.