Go言語+Dockerのホットリロード(Air)
ホットリロードをしたかったので、Airを使ってやってみました。
3ステップで進みます。
- Airを入れる
- Dockerfileを記述する
- docker-compose.ymlを書く
ディレクトリ構成
ホットリロードのときはディレクトリ構成が重要なので最初に記載します。
参考にしてください。
フロントエンドをReact、バックエンドをGoにしたくて下記構成にしています。
.
├── backend/
│ ├── Dockerfile
│ ├── tmp/
│ │ └── main ## air コマンドでtmpとmainは勝手に作られる
│ ├── .air.toml ## air init で作成
│ ├── go.mod
│ ├── go.sum
│ └── main.go
├── frontend/
│ └── ## Reactを追加予定
└── docker-compose.yml
ソースツリー作成ツール
ホットリロードを実装しよう!
「Go言語 ホットリロード」で調べると、下記コマンドが出てきます。
go install github.com/cosmtrek/air@latest
これは現在では使えないみたいです。(2025/04/14)
そのため、次のコマンドで実装してください。
Airを入れよう
ローカルはこちら
go get github.com/air-verse/air@latest
Dockerfileは下記で記述してください。
RUN go install github.com/air-verse/air@latest
ローカルに入れたら、下記コマンドで確認してください。
air -v
airが入っていたら、下記がでてきます。
__ _ ___
/ /\ | | | |_)
/_/--\ |_| |_| \_ v1.61.7, built with Go go1.23.6
.air.tomlを作ろう
作り方は簡単
air init
これで完成。
初期は下記です。
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
# 変更後のビルド時間
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
# リロード後の待機時間
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true
わかっている範囲で重要そうな場所を解説します。
root = "."
これはDockerfileで設定する「WORKDIR」部分です。
これを起点に
watching .
watching bin
!exclude tmp
building...
running...
となるのでズレるとホットリロードしてくれません。
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
これらは、"."からどの位置にtmpがあるかで記述が変わります。
Dockerが起動できたら、
docker ps
「CONTAINER ID」を取得し(私の場合はbackendのコンテナID)
docker exec -it <コンテナID> bash
ls
を行ってディレクトリ構成を確認してください。
lsの結果
Dockerfile bin go.mod go.sum main.go tmp
私の場合はこの状態だったのでホットリロードできました。
Dockerfileを書こう
backend配下のDockerfileを記述していきます。
# ベースイメージ
FROM golang:1.23
# 作業ディレクトリ
WORKDIR /app/backend
# モジュールのキャッシュ
COPY go.mod go.sum ./
RUN go mod download
# ソースのコピー
COPY . .
# airをインストール
RUN go install github.com/air-verse/air@latest
# 初回のairが始まるまで少し待つので先にbuild
# ビルド
RUN go build -o main .
CMD ["air"]
これで動くのでパクってください。
補足1
WORKDIRを「/app」にして、.air.tomlのrootを「"."」にすると、
tmpファイルは、/appに記述されます。
私の場合は、「/app/backend」としたので
tmpファイルは「/app/backend/tmp」と作られました。
# Dockerfileの作業ディレクトリ
WORKDIR /app
# この場合に/app/backend配下にtmpを入れたい場合
# .air.toml
root = "./backend"
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
これでも大丈夫だと思います。
補足2
初回に少し待たされるので、
docker立ち上げ時にビルドすることにしました。
# ビルド
RUN go build -o main .
仕上げのdocker-compose.yml
version: "3.8"
services:
backend:
build: ./backend
ports:
- "8080:8080"
# /appだけだとfrontendと競合
volumes:
- ./backend:/app/backend
# imageに名前つけとくと<none>ができづらくなる
image: backend-go
# command: air は重要
command: air
environment:
- GIN_MODE=release # 推奨記述に変更
# DBが接続可能になったらbackendを作る
depends_on:
db:
condition: service_healthy
frontend:
build: ./frontend
ports:
- "5173:5173"
# /appだけだとbackendと競合
# /app/frontend/node_modules の記述でbuild爆速!
volumes:
- ./frontend:/app/frontend
- /app/frontend/node_modules
image: frontend-react
command: ["npm", "run", "dev"]
depends_on:
- backend
db:
image: postgres:16
restart: always
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
ports:
- "5432:5432"
volumes:
- db-data:/var/lib/postgresql/data
# 接続可能かのチェック
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 5s
timeout: 3s
retries: 5
volumes:
db-data:
docker上でホットリロードするときは、airコマンドが重要でした。
command: air
これがないために何時間も格闘していました。
補足
初回のビルド時に、DBに接続できないエラーが出たため、ymlファイルに追記します。(2025/02/17)
追記部分
backend
# DBが接続可能になったらbackendを作る
depends_on:
db:
condition: service_healthy
db
# 接続可能かのチェック
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 5s
timeout: 3s
retries: 5
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
testの箇所は.envとかで管理でもいいかもしれません。
ホットリロードを試してみよう!
// main.go
package main
import (
"net/http"
"github.com/gin-gonic/contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Use(cors.Default())
// シンプルなAPI
r.GET("/api/hello", func(c *gin.Context) {
// "Hello from Go!"を修正してリロード
c.JSON(http.StatusOK, gin.H{"message": "Hello from Go!"})
})
// 8080ポートで起動
r.Run(":8080")
}
せっかくなのでmain.goを操作してホットリロードを体験してみてください。
コードを書いて確認するたびに
docker-compose up --build
していたのが嘘のようです。
次は、Reactのホットリロードに挑戦します。
フロントエンド側のホットリロード
React+Dockerでやってます。
Viteを使ったホットリロードを実施しました。
Discussion