👁️‍🗨️

Go言語+Dockerのホットリロード(Air)

2025/02/14に公開

ホットリロードをしたかったので、Airを使ってやってみました。
3ステップで進みます。

  1. Airを入れる
  2. Dockerfileを記述する
  3. 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を使ったホットリロードを実施しました。

https://zenn.dev/shuji0425/articles/eddae9d3b0c47c

Discussion