ReactとGoを使ってWebアプリを書くことが少しだけ増えたんですが、自分向けのメモ程度に作ったテンプレートを公開しておきます。
sandboxというリポジトリに気になったことを実験して放り込んでいます。
今回も特に頻繁に使うものでもないし、メンテする予定もないのでSandboxに置いておきました。
- テンプレートのゴール
- ディレクトリ構成
- frontendディレクトリを作成する
- backendディレクトリを作成する
- 開発用のdocker-compose.yamlを作成する
- 開発環境にアクセスする
- 本番向けのDockerfileを作成する
- おわりに
テンプレートのゴール
ゴールとしては、以下のように設定しました。
- 最終的な成果物(本番向け)は、単一コンテナイメージになること
- 開発でもコンテナを使うこと
- 開発で利用するコンテナは、両方ともホットリロードされること
また、以下のような項目はやらない、もしくは考慮しないようにしています。
ディレクトリ構成
ディレクトリ構成はいたってシンプルな構成になっています。
. ├── Dockerfile # 本番向けのコンテナイメージ ├── backend # バックエンドアプリ(Golang) ├── frontend # フロントエンドアプリ(Vite+React+TypeScript) └── docker-compose.yaml # 開発用のdocker-compose.yaml
dockerディレクトリで各種Dockerfileを管理するのも良いのですが、今回はシンプルさのためだけにこういった構成にしています。
frontendディレクトリを作成する
frontendディレクトリは、簡単に書くと
- Vite用のプロジェクトを作成する
- 開発用コンテナの定義を書く(Dockerfileなど)
- Viteの設定ファイルを書き換えてバックエンドにリクエストをプロキシさせる
のみで完結します。
1. Vite用のプロジェクトを作成する
ViteのScaffolding Your First Vite Projectを参考にしながら、frontendディレクトリを作成します。 以下は例ですが、package.jsonのnameがfrontendになってしまうので注意してください。
docker run \ -it \ --rm \ -v $PWD:/app \ # カレントディレクトリをマウント -w /app \ # マウントしたディレクトリをWorkingDirectoryとして指定してこくことでcdなどは不要 node:16-bullseye \ yarn create vite frontend --template react-ts
2. 開発用コンテナの定義を作成する
開発で利用するフロントエンド用のコンテナのDockerfileとentrypoint.shを定義しておきます。
yarn install(npm iなど)をentrypoint.shで実行していますが、個人のお好みの場所でやっていいと思います。
FROM node:16-bullseye # ENTRYPOINT.shを設定する COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] ENV APP_PATH /app WORKDIR $APP_PATH CMD ["yarn", "dev"]
Dockerfileに関しては特筆した何かはないので、必要に応じてカスタマイズすることで、 キャッシュの最適化のしやすさなどの恩恵が得られると思います。
entrypoint.shでは以下のようにyarn install
を実行して、CMDに指定されているコマンドを実行するようにしています。
#!/usr/bin/env bash yarn install --flat --frozen-lockfile exec "$@" # CMDに指定されているコマンドを実行
3. Viteの設定ファイルを書き換えてバックエンドにリクエストをプロキシさせる
frontend/vite.config.ts
を書き換えて、バックエンドアプリにリクエストをプロキシさせます。
これを書いておかないと、CORSでハマるのは間違いないので適切に設定しておくことをオススメします。
これが終わればフロントエンド側の準備は終了です。
export default defineConfig({ server: { host: '0.0.0.0', port: parseInt(process.env.PORT || '3000'), proxy:{ '/api': { target: process.env.API_URL, // docker-compose.yamlで指定したURLに対してリクエストを投げさせる changeOrigin: true } } }, })
backendディレクトリを作成する
バックエンド側は特に依存するライブラリを追加するわけではないので、2ステップほどで完了です。
1. main.goを作成する
適当にmain.goを作成しておきます。
ここでポイントとしては、最終成果物を単一イメージにするため、Viteでビルドされたファイルを返せるようにhttp.FileServer
を使うようにしておきます。
package main import ( "os" "log" "net/http" "encoding/json" ) type Pod struct { Namespace string `json:"namespace"` Name string `json:"name"` Status string `json:"status"` } func handleApiPods(w http.ResponseWriter, r *http.Request) { pods := []Pod{ Pod { Namespace: "default", Name: "test-xxx", Status: "Running", }, Pod { Namespace: "kube-system", Name: "coredns-yyy", Status: "Running", }, } w.Header().Set("Content-Type", "application/json") result, err := json.Marshal(pods) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Write(result) } func main() { http.Handle("/", http.FileServer(http.Dir("./public"))) # 本番向けイメージでViteでビルドされたファイルを返せるようにする http.HandleFunc("/api/pods", handleApiPods) # テスト用のエンドポイント port := os.Getenv("PORT") if (len(port) == 0) { port = "8080" } log.Println("Listening on :" + port + "...") err := http.ListenAndServe(":" + port, nil) if err != nil { log.Fatal(err) } }
2. 開発用コンテナの定義を作成する
フロントエンドと同様にDockerfileとentrypoint.shを作成します。
Golang側でもできるだけ、ホットリロードしてほしいのでAirをインストールしておきます。
FROM golang:1.17-bullseye COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] ENV APP_PATH /go/src/app WORKDIR $APP_PATH # ホットリロードさせたいのでAirを入れる RUN go install github.com/cosmtrek/air@latest CMD ["air"]
また、こちらでも同様にentrypoint.shで依存関係を解決させます。
依存ライブラリが多い場合は、docker-compose.yamlでvolumesの定義をしっかり書いておいた方が良さそうです。
#!/usr/bin/env bash go mod tidy exec "$@"
これらのファイルができれば一旦は完了です。
go.modやgo.sumは開発用コンテナ起動時に作成されるはずですが、できない場合は自分でgo mod init
などを実行してください。
開発用のdocker-compose.yamlを作成する
frontendディレクトリとbackendディレクトリを作り終えたので、後はdocker-compose.yamlを作れば開発に必要なファイルの作成は終わりです。
ここもかなり手抜きをしているので、ボリュームのキャッシュなどをうまく利用したい人は随時追記するのが良いです。
version: '3.9' services: frontend: build: context: frontend environment: PORT: 3000 API_URL: http://backend:8080 # backendコンテナにリクエストされるように設定 ports: - 3000:3000 volumes: - ./frontend:/app backend: build: context: backend environment: PORT: 8080 volumes: - ./backend:/go/src/app
これで開発環境は整いました。 ViteやAirを使っているため、どちらのコンテナでもうまくホットリロードができ、コンテナを一々再起動する手間がかなり省けているように感じています。
開発環境にアクセスする
docker-compose up -d
を実行して、開発環境を起動します。
起動後に http://localhost:3000` にアクセスするとフロントエンドアプリが表示され、
http://localhost:3000/api/pods にアクセスするとバックエンドアプリにリクエストがいくのが確認できると思います。
本番向けのDockerfileを作成する
さて、本番向けのDcokerfileを作成します。
こちらも特筆するようなことはないですが、フロントエンド・バックエンドをそれぞれビルドして、最終成果物のイメージには必要最小限のファイルのみを渡すようにしています。
scratchにしていることで、最終成果物のファイルは数MB程度のイメージになるため、非常に軽量です。
# Backend FROM golang:1.17-bullseye as backend WORKDIR /go/src/app # 今回は外部ライブラリを使用していないため、go.sumが存在しないが、存在する場合は以下のようにしておく # COPY backend/go.mod backend/go.sum ./ COPY backend/go.mod ./ RUN go mod tidy COPY backend . ARG CGO_ENABLED=0 ARG GOOS=linux ARG GOARCH=amd64 RUN go build \ -o /go/bin/main \ -ldflags '-s -w' # Frontend FROM node:16-bullseye as frontend WORKDIR /app COPY frontend/package.json frontend/yarn.lock ./ RUN yarn install --frozen-lockfile --ignore-optional COPY frontend . RUN yarn build # App FROM scratch WORKDIR /app COPY --from=backend /go/bin/main ./main COPY --from=frontend /app/dist ./public CMD ["./main"]
以下のようなコマンドでイメージをビルドして、動作確認すれば分かるかと思いますが、単一イメージで動作できるようになっています。
docker build -t react-golang . docker run --rm -p 8080:8080 react-golang
おわりに
自分向けのReactとGoで作るWebアプリテンプレートを簡単に紹介しました。 説明はかなり省いていることもあるので、sandboxリポジトリの内容を見ながら、動かしながら試してもらえると良さそうです。