How to Rebuild a Go Project Efficiently with Docker Compose

DevOpsDevOps Tools

YOUR COSMETIC CARE STARTS HERE

Find the Best Cosmetic Hospitals

Trusted • Curated • Easy

Looking for the right place for a cosmetic procedure? Explore top cosmetic hospitals in one place and choose with confidence.

“Small steps lead to big changes — today is a perfect day to begin.”

Explore Cosmetic Hospitals Compare hospitals, services & options quickly.

✓ Shortlist providers • ✓ Review options • ✓ Take the next step with confidence

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 restart restarts the service using the command defined in docker-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 run starts 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 triggers go build on 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

ApproachBuild speedPort stabilityComplexityBest for
Fix layer cache orderFast after first buildStable (full rebuild)LowCI / production images
exec + stop/startFast (no image rebuild)StableMediumOccasional manual rebuilds
Live-reload tool (Air)Fastest (automatic)StableLow–MediumActive development loops

Key Takeaways

  • Never run docker-compose build on 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 (not run) when you need to run a command inside an already-running service container. run creates 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.
0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x