💡

Docker+Airでホットリロード可能なGo(Echo)の開発環境を構築した話

2024/04/20に公開

はじめに

今回は技育キャンプというハッカソン参加を機にGoとDockerによるバックエンドの環境構築に取り組みましたが、この段階で想像以上に時間が掛かってしまったので、共有もかねてこの記事を書くに至りました。

この記事について

AirとDockerでホットリロード可能なGo(Echo)の開発環境の構築に取り組んだ際に出くわしたエラーと解決方法、正しく動く環境を構築するまでの一連の流れを解説します。

必要なもの

Docker for Desktopのインストール

最終的なディレクトリ構成

.
├backend/
  ├src/
   └main.go
  ├tmp/
   └main
  └.air.toml
  └Dockerfile
 └go.mod
 └go.sum
└docker-compose.yml

これから手順を解説していきます。

Dockerfile,docker-compose.ymlファイルの作成

まず、以下のようなディレクトリ構成になるようにDockerfileとdocker-compose.ymlファイルを作成していきます。

.
├backend/
  └Dockerfile
└docker-compose.yml
Dockerfile
FROM golang:1.22-alpine

WORKDIR /app/

# 今はコメントアウトしておく

# COPY backend/go.sum . 
# COPY backend/go.mod .

# RUN apk upgrade --update && apk --no-cache add git gcc musl-dev

# RUN go install github.com/cosmtrek/air@latest

# COPY ./backend .

# CMD ["air", "-c", ".air.toml"]
docker-compose.yml
version: "3"

services:
  backend:
    build:
      context: .
      dockerfile: ./backend/Dockerfile
    ports:
      - 8080:8080
    volumes:
      - ./backend:/app

main.goファイルの作成

次に、以下のようなディレクトリ構成になるようにsrc/main.goファイルを作成していきます。

.
├backend/
 ├src/
   └main.go
  └Dockerfile
└docker-compose.yml

そして、main.goには以下のように記述します。

main.go
package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
)

func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":8080"))
}

モジュールエラーなどが出ているかもしれませんが、今は無視して次に進んで下さい。

dockerイメージの作成

初めに、Docker for Desktopを起動しておいてください。インストールをしていない場合はインストールを行ってから以下の手順を行ってください。

Docker for Desktopを起動したら、以下のコマンドを実行しイメージを作成します。

docker compose build

ここでエラーが出る場合は、Docker関係のファイルに空白などが含まれていないか確認してみてください。
上記のコマンドを実行できたら、以下のコマンドを実行しコンテナの中に入ります。

docker compose run --rm backend sh

コンテナに入ったら、以下のコマンドを実行します。
backendディレクトリ直下に「go.mod」が作成されます。

go mod init backend

次に、今回はEchoを使用するので以下のコマンドを実行し、Echoモジュールを追加します。そうすることでbackendディレクトリ直下に「go.sum」が作成されます。

go get github.com/labstack/echo/v4

そして、以上の手順を終えたらexitコマンドで一旦コンテナから抜けます。

exit

そうしたら、Dockerfileのコメントアウトを外します。

Dockerfile
FROM golang:1.22-alpine

WORKDIR /app/

COPY backend/go.sum . 
COPY backend/go.mod .

RUN apk upgrade --update && apk --no-cache add git gcc musl-dev

RUN go install github.com/cosmtrek/air@latest

COPY ./backend .

CMD ["air", "-c", ".air.toml"]

そして、再度以下のコマンドを実行します。

docker compose build

docker compose run --rm backend sh

コンテナに入ったら、以下のコマンドを実行します。そうすると、backend直下に.air.tomlファイルが生成されます。

air init

そうしたら、exitでコンテナから抜けます。

コンテナ起動

ここまでのディレクトリ構成は以下のようになっています。

.
├backend/
  ├src/
   └main.go
  └.air.toml
  └Dockerfile
 └go.mod
 └go.sum
└docker-compose.yml

エラーが出るやり方(この部分は行う必要はありません)

以下のコマンドを実行します。

docker compose up

すると、おそらくターミナルに以下のようなコマンドが表示されるはずです。

[+] Building 0.0s (0/0)                                                                 docker:default
[+] Running 1/1
 ✔ Container hackathon-backend-1  Recreated 3.2s 
Attaching to hackathon-backend-1
hackathon-backend-1  | 
hackathon-backend-1  |   __    _   ___
hackathon-backend-1  |  / /\  | | | |_)
hackathon-backend-1  | /_/--\ |_| |_| \_ v1.51.0, built with Go go1.22.2
hackathon-backend-1  |
hackathon-backend-1  | mkdir /app/tmp
hackathon-backend-1  | watching .
hackathon-backend-1  | watching src
hackathon-backend-1  | !exclude tmp
hackathon-backend-1  | building...
hackathon-backend-1  | no Go files in /app
hackathon-backend-1  | failed to build, error: exit status 1
hackathon-backend-1  | running...
hackathon-backend-1  | /bin/sh: /app/tmp/main: not found

そして、backendディレクトリにtmpディレクトリとエラーファイルが生成されます。
この手順を行った場合はこのtmpディレクトリを削除してから次の手順に進んでください。


Air実装後のエラーの修正

上記のエラーを修正していきます。

.air.tomlファイルを開き、以下の色部分を変更します。

.air.toml
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
  args_bin = []
  bin = "./tmp/main"
- cmd = "go build -o ./tmp/main ."
+ cmd = "go build -o ./tmp/main ./src/main.go"
  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
  time = false

[misc]
  clean_on_exit = false

[screen]
  clear_on_rebuild = false
  keep_scroll = true

そして、再度

docker compose up

を実行すると、エラーは表示されなくなり、以下がターミナルに表示されます。

[+] Building 0.0s (0/0)                                                                 docker:default
[+] Running 2/2
 ✔ Network hackathon_default      Created                                                         0.2s 
 ✔ Container hackathon-backend-1  Created                                                         0.5s 
Attaching to hackathon-backend-1
hackathon-backend-1  | 
hackathon-backend-1  |   __    _   ___  
hackathon-backend-1  |  / /\  | | | |_) 
hackathon-backend-1  | /_/--\ |_| |_| \_ v1.51.0, built with Go go1.22.2
hackathon-backend-1  | 
hackathon-backend-1  | watching .
hackathon-backend-1  | watching src
hackathon-backend-1  | !exclude tmp
hackathon-backend-1  | building...
hackathon-backend-1  | go: downloading github.com/labstack/echo/v4 v4.12.0
hackathon-backend-1  | go: downloading github.com/labstack/gommon v0.4.2
hackathon-backend-1  | go: downloading golang.org/x/crypto v0.22.0
hackathon-backend-1  | go: downloading golang.org/x/net v0.24.0
hackathon-backend-1  | go: downloading github.com/valyala/fasttemplate v1.2.2
hackathon-backend-1  | go: downloading golang.org/x/sys v0.19.0
hackathon-backend-1  | go: downloading github.com/valyala/bytebufferpool v1.0.0
hackathon-backend-1  | running...
hackathon-backend-1  | 
hackathon-backend-1  |    ____    __
hackathon-backend-1  |   / __/___/ /  ___
hackathon-backend-1  |  / _// __/ _ \/ _ \
hackathon-backend-1  | /___/\__/_//_/\___/ v4.12.0
hackathon-backend-1  | High performance, minimalist Go web framework
hackathon-backend-1  | https://echo.labstack.com
hackathon-backend-1  | ____________________________________O/_______
hackathon-backend-1  |                                     O\
hackathon-backend-1  | ⇨ http server started on [::]:8080

そして、http://localhost:8080/にアクセスすると、Hello World!と表示されるはずです。

しかし、main.goファイルを変更してもホットリロードが行われません。これを次に修正していきます。

ホットリロードが出来ないエラーの修正

再度.air.tomlファイルを開き、以下の色部分を変更します。

.air.toml
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
  args_bin = []
  bin = "./tmp/main"
 cmd = "go build -o ./tmp/main ./src/main.go"
  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 = true 
  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
  time = false

[misc]
  clean_on_exit = false

[screen]
  clear_on_rebuild = false
  keep_scroll = true

そして、再度

docker compose up

を実行し、main.goを変更すると正しくホットリロードが行われるようになります。

ポートのエラーについて

以下のエラーが出た

Port scan timeout reached, no open ports detected. Bind your service to at least one port. If you don't need to receive traffic on any port, create a background worker instead.
==> Docs on specifying a port: https://render.com/docs/web-services#port-binding

これに対し、私はdb.goのNewDBを以下のように修正した。

func NewDB() *gorm.DB {
    //変更前
    err := godotenv.Load(fmt.Sprintf(".env"))
	if err != nil {
		log.Print(err)
	}

    //変更後
	if os.Getenv("GO_ENV") == "dev" {
		err := godotenv.Load()
		if err != nil {
			log.Fatalln(err)
		}
	}
	//以下省略
}

同時に、効果があったかは不明だが、mainやプログラムフォルダをbackend/srcからbackend直下に移動し、Dockerfileに以下のコードを追加した。

FROM golang:1.22-alpine

WORKDIR /app/

# ローカルの場合

COPY backend/go.sum . 
COPY backend/go.mod .

# RUN apk upgrade --update && apk --no-cache add git gcc musl-dev

COPY ./backend .

/* 追加*/
RUN go build -tags netgo -ldflags '-s -w' -o app

EXPOSE 8080
/*追加ここまで*/

CMD ["go", "run", "main.go"]

参考

https://qiita.com/tkms13/items/74a9ea4b41302323c4b1

Discussion