Building live reloading Go applications with Docker watch

Recently I have been building REST APIs in Go and have been missing tooling of most of the Javascript/Typescript projects that support live-reloading changes. I want to show you how I decided to approach live-reloading Golang applications with Docker and Air.

TL;DR; Skip to the code

I decided to use docker development containers because I wanted to explore optimizing the new developer experience and figured the easiest way to get started should be running a single command.

I broke down my live reloading goals into 2 distinct steps:

  1. Updating the files in the Docker volume.
  2. Recompiling my application when the files are updated.

Luckily there are some good initiatives for both of these stages.

Step 1: Syncing files into Docker with compose watch

Docker compose watch is specifically designed to automatically update watched” files from the host to the container for local development. I preferred it over the normal bind mounts I tried the first time around because it allows for more granular control.

Step 2: Recompiling Go with Air

The Air project is an awesome initiative to bring live reloading to Go apps. This carries the brunt of the load when building Go apps. Air also support live-reloading browser windows if accessed through a proxy port.

Tying it all together

Running: docker compose watch now starts the docker container in watch mode and when I change the contents of any files set to sync” the changes are automatically copied to the container. The Go application is run with Air listening for file changes which then triggers a recompilation and websocket reload on the proxy port. Any files set to rebuild” will recreate the changed layers of the docker image and restart the container which will trigger a full re-compile in Air.

Code

Example Repo

Docker Compose
services:
  golang_hmr:
    container_name: golang_hmr
    build:
      context: .
      dockerfile: Dockerfile
      target: development
    volumes:
      # Cache Go modules and build cache for faster rebuilds
      - go_modules:/go/pkg/mod
      - go_build_cache:/root/.cache/go-build
    ports:
      - "8080:8080"
      - "8090:8090" # For Air live-reloading proxy
    restart: unless-stopped
    develop:
      watch:
        # Sync files
        - action: sync
          path: .
          target: /app
          ignore:
            - .git/
        # Rebuild the container if any of the dependency files update
        - action: rebuild
          path: ./go.mod
          target: /app/go.mod
        - action: rebuild
          path: ./go.sum
          target: /app/go.sum
        - action: rebuild
          path: ./.air.toml
          target: /app/.air.toml

volumes:
go_modules:
go_build_cache:
Dockerfile
# Development stage
FROM golang:1.24.4-alpine AS development

# Install build dependencies and Air
RUN apk add --no-cache git ca-certificates tzdata
RUN go install github.com/air-verse/air@latest

# Set working directory
WORKDIR /app

# Copy go mod files first for better layer caching
COPY go.mod go.sum .air.toml ./
RUN go mod download

# Copy all backend source code
COPY . .

# Default command for development (will be overridden by docker-compose)
CMD ["air", "-c", "/app/.air.toml"]
Air configuration
root = "."
tmp_dir = "tmp"
[build]
# Binary file yields from `cmd`.
bin = "./tmp/golang_hmr"
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/golang_hmr main.go"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000
# Delay after sending Interrupt signal
kill_delay = 500 # nanoseconds
# This log file is placed in your tmp_dir.
log = "build-errors-air.log"
# Poll interval (defaults to the minimum interval of 500ms).
poll_interval = 0
# Delay after each execution
rerun_delay = 500
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
[proxy]
# Enable live-reloading on the browser.
enabled = true
proxy_port = 8090
app_port = 8080
Yash My Musings.