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:
- Updating the files in the Docker volume.
- 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
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:
# 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"]
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