Air is a live-reloading runner for Go: it watches your files, runs go build, and restarts the binary when something changes. Install it, run air, and the compile-restart cycle disappears. The configuration that matters is the exclusion list — the default watches a lot, so an unconfigured Air rebuilds when you save a README, a test file, or something your editor wrote to tmp/.
Go compiles fast enough that people ask whether this is worth it. On a small service the answer is marginal; once the build takes three seconds and you are iterating on a handler, it is the difference between staying in flow and not.
Table of contents
- Install and run
- A config worth using
- Excluding what your editor and tooling generate
- Air in Docker Compose
- The alternatives, briefly
- Development reload is not deployment
- How this fits the rest of the stack
- FAQ
Install and run
# Install the binary
go install github.com/air-verse/air@latest
# Make sure it is on PATH
export PATH="$PATH:$(go env GOPATH)/bin"
# Generate a config in the project root
air init
# Run
air
Note the module path: the project moved from cosmtrek/air to air-verse/air. Older tutorials and Dockerfiles still reference the old path, which now redirects but is worth updating.
air init writes .air.toml with defaults. Running bare air without a config uses built-in defaults, which work but watch more than you want.
A config worth using
root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -o ./tmp/main ./cmd/server"
bin = "./tmp/main"
full_bin = "APP_ENV=dev ./tmp/main"
include_ext = ["go", "tpl", "tmpl", "html", "sql"]
exclude_dir = ["assets", "tmp", "vendor", "node_modules", ".git", "testdata"]
exclude_regex = ["_test\\.go", "_gen\\.go"]
exclude_unchanged = true
follow_symlink = false
delay = 300
stop_on_error = true
send_interrupt = true
kill_delay = 2000
[log]
time = true
[misc]
clean_on_exit = true
The settings that earn their place:
include_ext— only these extensions trigger a rebuild. Without it, saving a markdown file restarts your server.exclude_regexwith_test\.go— test files do not affect the running binary, so rebuilding on them is pure waste.delay— debounce in milliseconds. Editors that save several files at once otherwise trigger several builds.send_interruptandkill_delay— send SIGINT and wait, so your graceful shutdown runs. Without this Air kills the process outright and you never exercise the shutdown path you wrote.stop_on_error— keep the last working binary running when a build fails, rather than leaving nothing serving.
send_interrupt is the one worth calling out. If your server has a Shutdown handler on SIGTERM, this is what lets you see it working during development instead of discovering in production that it never ran.
Excluding what your editor and tooling generate
The most common complaint about Air is a rebuild loop, and it is almost always something writing into a watched directory.
tmp/must be excluded — Air writes the binary there, and watching it means the build triggers a rebuild.- Code generators writing
.gofiles into the tree cause a loop unless the output matchesexclude_regex. vendor/is large and static; watching it wastes file descriptors.- Editor swap and backup files —
.swp,.tmp,#file#— should be excluded by extension. - SQLite databases and log files written by the app itself, if they live in the project directory.
If Air rebuilds continuously with no input from you, run it and watch which path it reports as changed. The message names the file, and the file is always something being written by a process you forgot about.
Air in Docker Compose
Useful when the service needs a database beside it, so the whole development environment comes up with one command.
# Dockerfile.dev
FROM golang:1.24-alpine
RUN go install github.com/air-verse/air@latest
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
CMD ["air", "-c", ".air.toml"]
services:
api:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- .:/app
- go-mod-cache:/go/pkg/mod
ports:
- "8080:8080"
environment:
DATABASE_URL: postgres://dev:dev@db:5432/dev?sslmode=disable
depends_on:
- db
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
POSTGRES_DB: dev
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
go-mod-cache:
pgdata:
The go-mod-cache volume is what stops every container restart re-downloading your dependencies. Without it the developer experience is worse than not using Docker at all.
One platform note: file change events do not always propagate reliably through bind mounts on macOS and Windows. If Air sees no changes inside the container, add poll = true to the [build] section — it is less efficient but it works everywhere.
The alternatives, briefly
wgo— smaller and simpler, no config file, good when you wantwgo run ./cmd/serverand nothing else.reflex— general-purpose file watcher, not Go-specific, more flexible and more configuration.entr— Unix tool, composes well:ls **/*.go | entr -r go run ./cmd/server.- Nothing — for a small service,
go run ./cmd/serverand a keystroke is honestly fine.
Air is the most featured and the most widely used. entr is the most Unix-shaped and needs no Go-specific tooling at all. Pick based on whether you want configuration or composition.
Development reload is not deployment
Worth stating plainly because the phrase “hot reload” invites the confusion: Air is a development tool. It rebuilds on your machine, from your working tree, with whatever uncommitted changes you have.
Production does not rebuild in place. It builds a specific commit in a clean environment, produces an artefact, and starts that. The reason is repeatability — a binary built from your working tree is not reproducible by anyone else, and the second it fails you cannot tell whether the code or the environment is responsible.
Go makes the production side easy, since go build yields one static binary and the runtime image can be a few megabytes. Building from a connected GitHub repository — with a build log for the compile, a live route, environment variables for the database URL, and the previous deploy still there to roll back to — is the shape RunxBuild handles for a Go service, and it is deliberately not the same mechanism as the one watching your files.
How this fits the rest of the stack
Install from github.com/air-verse/air, run air init, then spend five minutes on the config. Set include_ext so only real source changes rebuild, exclude tmp/ and vendor/ and test files, add a delay to debounce multi-file saves, and turn on send_interrupt so your graceful shutdown actually runs in development.
If it rebuilds in a loop, something is writing into a watched directory — Air names the file. And keep it clearly separate from deployment, which should build a known commit rather than your working tree. If you are pricing that out, the RunxBuild hosting calculator itemises service, database, storage, and bandwidth.
Useful related references:
- Golang Hosting in 2026: Where a Go App Actually Wants to Live
- Golang Environment Variables: The Boring Truth About os.Getenv, the Library Most Teams Reach For, and One They Should Not
- Builds on RunxBuild
FAQ
How do I install Air for Go?
go install github.com/air-verse/air@latest, and make sure $(go env GOPATH)/bin is on your PATH. Note the module moved from cosmtrek/air to air-verse/air; older tutorials reference the previous path, which still redirects but is worth updating.
Why does Air rebuild continuously?
Something is writing into a watched directory. The usual culprits are the tmp/ directory where Air puts the binary, a code generator producing .go files, or editor swap files. Air prints the path that changed, which identifies it immediately. Add the directory to exclude_dir or the pattern to exclude_regex.
How do I stop Air rebuilding on test files?
Add _test\.go to exclude_regex in the [build] section of .air.toml. Test files do not affect the running binary, so rebuilding on them is wasted time. Setting include_ext to just the extensions that matter also prevents markdown and config saves triggering builds.
Does Air work inside Docker?
Yes — install it in a development Dockerfile and bind-mount your source. Add a named volume for /go/pkg/mod so dependencies are not re-downloaded on every restart. If file change events do not propagate through the bind mount on macOS or Windows, set poll = true in the build section.
Should I use Air in production?
No. Air rebuilds from your working tree, which is not reproducible by anyone else. Production should build a specific commit in a clean environment and run that artefact, so a failure is attributable and the previous version is still available to roll back to.