Air is a live-reloading command line utility for developing Go applications. Run air in your project root directory, leave it alone, and focus on your code. Note that this tool has nothing to do with hot-deploy for production.
- Air - Live reload for Go apps
With go 1.25 or higher:
go install github.com/air-verse/air@latestMake sure your Go bin directory is in your PATH:
export PATH="$PATH:$(go env GOPATH)/bin"With go 1.25 or higher:
go get -tool github.com/air-verse/air@latest
# then use it like so:
go tool air -v# binary will be $(go env GOPATH)/bin/air
curl -sSfL https://raw.githubusercontent.com/air-verse/air/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
# or install it into ./bin/
curl -sSfL https://raw.githubusercontent.com/air-verse/air/master/install.sh | sh -s
air -vSee goblin.run.
# binary will be /usr/local/bin/air
curl -sSfL https://goblin.run/github.com/air-verse/air | sh
# to put to a custom path
curl -sSfL https://goblin.run/github.com/air-verse/air | PREFIX=/tmp shbrew install go-airscoop install airmise use -g airPull the cosmtrek/air image and see Docker for usage.
# enter your project
cd /path/to/your_project
# first tries `.air.toml` in the current directory; if not found, uses defaults
airTo generate a config file you can edit, run air init once and then air from then on:
# writes .air.toml with the default settings
air init
# picks up .air.toml automatically
airTo use a specific config file explicitly, pass -c:
air -c .air.tomlFor every available option refer to the air_example.toml file.
- Colorful log output
- Customize build or any command
- Support excluding subdirectories
- Allow watching new directories after Air started
- Better building process
- Configurable
.envfile loading
You can pass arguments for running the built binary by adding them after the air command.
# Will run ./tmp/main bench
air bench
# Will run ./tmp/main server --port 8080
air server --port 8080You can separate the arguments passed for the air command and the built binary with the -- argument.
# Will run ./tmp/main -h
air -- -h
# Will run air with custom config and pass -h argument to the built binary
air -c .air.toml -- -hair -d prints all logs.
Air config fields are supported as command-line arguments. You can view the available arguments by running:
air -h
# or
air --helpIf you want to configure the build command and run command, you can use the following command without a config file:
air --build.cmd "go build -o bin/api cmd/run.go" --build.entrypoint "./bin/api"Use a comma to separate items for arguments that take a list as input:
air --build.cmd "go build -o bin/api cmd/run.go" --build.entrypoint "./bin/api" --build.exclude_dir "templates,build"List arguments can also be repeated, and the values are appended in the order they appear. This is handy when the command line is generated by a script or Makefile:
# equivalent to --env_files ".env,.env.local,.env.secret"
air --env_files ".env,.env.local" --env_files ".env.secret"Use misc.startup_banner to control what Air prints at startup.
[misc]
# Not set (default): show built-in ASCII banner with version.
# Set to empty string: print nothing.
startup_banner = ""
# Set to custom text: print this text instead of the built-in banner.
# startup_banner = "API watcher"Use build.entrypoint to point at the binary generated by build.cmd and describe how it should be executed. The value can be either a string (just the executable) or an array of strings. When using an array, the first element is the executable (resolved relative to root unless it lacks a path separator, in which case $PATH is consulted) and every subsequent element is treated as a default argument. Values from build.args_bin and the command line are appended after the inline arguments. The legacy build.bin field is deprecated and will be removed in a future release, so prefer the entrypoint form going forward.
[build]
entrypoint = ["./tmp/main"]
args_bin = ["server", ":8080"]
# Inline the default arguments directly after the binary.
entrypoint = ["./tmp/main", "server", ":8080"]
# Use PATH-resolved tools like dlv by omitting path separators.
entrypoint = [
"dlv", "exec", "--accept-multiclient", "--log", "--headless", "--continue",
"--listen=:8999", "--api-version", "2", "./tmp/main",
]Air can automatically load environment variables from .env files before both building and running when env_files is configured.
# Loads .env.development and then .env files.
# Values in the lattermost file overwrite any preceding ones.
# Does not overwrite variables that were present before running air.
env_files = [".env.development", ".env"]You can override build settings per OS with [build.windows], [build.darwin], and [build.linux]. These blocks override the base [build] values when running on the matching platform. Only the following fields are supported in platform blocks: pre_cmd, cmd, post_cmd, bin, entrypoint, full_bin, args_bin.
[build]
cmd = "go build -o ./tmp/main ."
bin = "./tmp/main"
[build.windows]
cmd = "go build -o ./tmp/main.exe ."
bin = "tmp\\main.exe"
entrypoint = ["tmp\\main.exe"]Running air init adds a platform block for the current OS when its defaults differ from the base configuration.
Sometimes a change should run a command rather than rebuild your app — frontend assets served from disk, templ/sqlc/go generate pipelines, and so on. Declare a [[build.rules]] block for each of them:
[build]
cmd = "go build -o ./tmp/main ."
# the main build ignores the frontend...
exclude_dir = ["web"]
# ...but this rule watches it and rebuilds the assets on change
[[build.rules]]
name = "assets"
include_dir = ["web"]
include_ext = ["js", "ts", "css"]
cmd = "npm run build"
[[build.rules]]
name = "templ"
include_ext = ["templ"]
cmd = "templ generate"A file matched by a rule runs the rule's cmd and never triggers a rebuild, even if it would also match the main build's watch settings. Rule directories are watched even when listed in exclude_dir. If a rule's command generates files the main build watches (for example, templ generate writing .go files), the rebuild follows naturally.
Each rule supports include_dir, include_ext, include_file, exclude_regex, and a delay (debounce in milliseconds, default 1000). At least one of the include_* matchers is required. Rules run their commands to completion; changes arriving meanwhile queue a follow-up run.
Air can put a small proxy in front of your web app that refreshes the browser after every successful rebuild, so you no longer have to hit reload yourself.
[proxy]
enabled = true
# the port you open in the browser
proxy_port = 8090
# the port your app listens on
app_port = 8080Start air as usual and open http://localhost:8090 instead of your app's own port. Requests are forwarded to app_port, and Air injects a tiny script before the </body> tag of every HTML response; when a rebuild finishes, the script reloads the page.
Two things are required for this to work:
- your HTML must contain a
</body>tag — there is nowhere to inject the script otherwise, and the page is served unchanged; - the files you edit must be watched, so static assets need to be covered by
include_dir,include_ext, orinclude_file.
If your app is slow to boot (database connections, config loading) and you see "unable to reach app" errors, give it more time:
[proxy]
# how long to keep retrying the app after a rebuild, in milliseconds (default 5000)
app_start_timeout = 10000Pull this Docker image: cosmtrek/air.
docker/podman run -it --rm \
-w "<PROJECT>" \
-e "air_wd=<PROJECT>" \
-v $(pwd):<PROJECT> \
-p <PORT>:<APP SERVER PORT> \
cosmtrek/air \
-c <CONF><PROJECT> is your project path in the container, e.g. /go/example. If you want to enter the container, add --entrypoint=bash.
One of my projects runs in Docker:
docker run -it --rm \
-w "/go/src/github.com/cosmtrek/hub" \
-v $(pwd):/go/src/github.com/cosmtrek/hub \
-p 9090:9090 \
cosmtrek/airIf you want to use air continuously like a normal app, create a function in your ${SHELL}rc (Bash, Zsh, etc…):
air() {
podman/docker run -it --rm \
-w "$PWD" -v "$PWD":"$PWD" \
-p "$AIR_PORT":"$AIR_PORT" \
docker.io/cosmtrek/air "$@"
}$PWD is replaced with the current directory, $AIR_PORT is the port to publish, and $@ accepts arguments of the application itself, for example -c:
cd /go/src/github.com/cosmtrek/hub
AIR_PORT=8080 air -c "config.toml"services:
my-project-with-air:
image: cosmtrek/air
# working_dir value has to be the same of mapped volume
working_dir: /project-package
ports:
- <any>:<any>
environment:
- ENV_A=${ENV_A}
- ENV_B=${ENV_B}
- ENV_C=${ENV_C}
volumes:
- ./project-relative-path/:/project-package/Dockerfile
# Choose whatever you want, version >= 1.25
FROM golang:1.25-alpine
WORKDIR /app
RUN go install github.com/air-verse/air@latest
COPY go.mod go.sum ./
RUN go mod download
CMD ["air", "-c", ".air.toml"]docker-compose.yaml
version: "3.8"
services:
web:
build:
context: .
# Correct the path to your Dockerfile
dockerfile: Dockerfile
ports:
- 8080:3000
# Important to bind/mount your codebase dir to /app dir for live reload
volumes:
- ./:/appAlso reported as "No such file or directory". Make sure the Go bin directory is on your PATH:
export GOPATH=$HOME/xxxxx
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
export PATH=$PATH:$(go env GOPATH)/bin #Confirm this line in your .profile and make sure to source the .profile if you add it!!!Use \ to escape the ' in the bin. Related issue: #305
See #365.
[build]
cmd = "/usr/bin/true"Enable the proxy — see Proxy: reload the browser automatically. Make sure your static files are covered by include_dir, include_ext, or include_file, otherwise editing them never triggers a reload. Refer to issue #512 for additional details.
Please note that it requires Go 1.25+ (see go.mod).
# Fork this project
# Clone it
mkdir -p $GOPATH/src/github.com/cosmtrek
cd $GOPATH/src/github.com/cosmtrek
git clone git@github.com:<YOUR USERNAME>/air.git
# Install dependencies
cd air
make ci
# Explore it and happy hacking!
make installPull requests are welcome.
# Checkout to master
git checkout master
# Add the version that needs to be released
git tag v1.xx.x
# Push to remote
git push origin v1.xx.x
# The CI will process and release a new version. Wait about 5 min, and you can fetch the latest versionWhen I started developing websites in Go and using the gin framework, it was a pity that gin lacked a live-reloading function. So I searched around and tried fresh; it seemed not much flexible, so I intended to rewrite it better. Finally, Air's born.
In addition, great thanks to pilu, no fresh, no air :)
Give huge thanks to lots of supporters. I've always been remembering your kindness.

