Docker + React + Go API 通信できる環境を構築する

Docker

概要

React と Go でフロントエンドとバックエンド間で通信する環境を構築します。

React の ビルドツールには Vite を使い、エディターには VSCode を使ってやっていきます。

動作環境は WSL2 の Ubuntu。以下の記事でセットアップした前提で進んでいきますが、Ubuntu ならだいたい大丈夫なはずです。

WSL2 + Docker + Docker Compose V2 セットアップ

今回は以下のふたつの Dockerfile を用意します。

  • web コンテナ
    • ベースイメージ: node:20.9-alpine
  • app コンテナ
    • ベースイメージ: golang:1.21-alpine

2023年11月時点の最新の安定リリース版です。

僕は alpine 信者なので alpine を使います。

対象読者

  • Docker のことがなんとなくわかってる
  • Docker の環境がある
  • Golang の知識が少しある
  • mkdir, ls, cd などの基本的な Linux コマンドを理解できる

到達目標

  • Golang のサーバーから RTK Query で取得したデータをレンダリング

Go で API サーバー立てる

まずは今回の作業のためのディレクトリを作成します。

mkdir react-go-api
cd react-go-api

Docker コンテナの準備

docker/go/Dockerfile というファイルを作り、以下の記述をします。

FROM golang:1.21-alpine # ディレクトリ指定 ENV ROOT=/go/src/app WORKDIR ${ROOT}

次に docker-compose.yaml を作成します。

version: "3.9" services: app: build: context: . dockerfile: ./docker/go/Dockerfile volumes: - ./backend/:/go/src/app tty: true

この状態で、一旦コンテナを起動します。

docker compose up -d

すると以下のようなメッセージが最後に表示されます。

[+] Running 2/2 ✔ Network react-go-api_default Created ✔ Container react-go-api-app-1 Started

この状態で、以下のコマンドを叩いてコンテナに入ります。

ちなみに alpine は ash、普通のやつは bash という違いがあります。

docker compose exec app ash

コンテナの中に入れたら、以下のコマンドで GOPATH モードから module モードにします。

go mod init react-go-api

すると backend/go.mod というファイルが作成されました。

一応動作確認ができるように、backend/main.go を作成し……と言いたいところですが、コンテナを root で起動して go mod init を叩いたため、backend ディレクトリの所有権が root になってしまっていまい、VSCode などのエディターからは作成できなくなってしまいました。

一旦 Ctrl + D でコンテナから出ます。

以下のコマンドを叩くと、ディレクトリやファイルの所有者などが把握できます。

ls -l

sudo 権限で backend の所有者を再帰的に自分に戻します。

sudo chown $USER:$USER -R backend

もう一度確認してみると、所有者が自分になっていることがわかります。

ls -l

改めて、動作確認ができるように、backend/main.go を作成し、以下の記述をしておきます。

package main import "fmt" func main() { fmt.Println("Hello Golang!") }

もう一度コンテナに入り、動作を確認してみます。

docker compose exec app ash
go run main.go

Hello Golang! とターミナルに表示されたら成功です。

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

. ├── backend │ ├── go.mod │ └── main.go ├── docker │ └── go │ └── Dockerfile └── docker-compose.yaml

API サーバーから JSON レスポンスをできるようにする

次は Golang で API サーバーを立てて、 JSON をレスポンスできるようにします。

ルーターには gorilla/mux を使うことにします。

app コンテナに入った状態で、以下のコマンドを叩きます。

go get github.com/gorilla/mux

すると go.mod が更新、 go.sum が生成されました。

これで mux が使えるようになります。

backend/controllers/webserver.go を作成し、以下を記載します。

package controllers import ( "encoding/json" "fmt" "net/http" "github.com/gorilla/mux" ) // 頭文字を大文字にするとパッケージ外部から呼び出しできる func StartWebServer() error { fmt.Println("Start Web Server!") r := mux.NewRouter().StrictSlash(true) // URL に呼び出したい関数を登録する r.HandleFunc("/api/todos", getTodos).Methods("GET") // ポートを指定してサーバーを起動する return http.ListenAndServe(":8080", r) } type Todo struct { Id int Title string Completed bool } func getTodos(w http.ResponseWriter, r *http.Request) { // フロントエンドとバックエンドのポートが違うので許可しておく // (すべてを許可する設定にしているので、本番ではより制限を厳しくしておくように) w.Header().Set("Access-Control-Allow-Origin", "*") // 返却したい値を構造体で定義 todo1 := Todo{ Id: 1, Title: "チャーハン作るよ!", Completed: true, } todo2 := Todo{ Id: 2, Title: "豚肉も入れるよ!", Completed: false, } todos := []Todo{todo1, todo2} // JSON にして返却 responseBody, err := json.Marshal(todos) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write(responseBody) }

main.go に以下を以下に変更します。

package main import ( "log" "react-go-api/controllers" ) func main() { // controllers パッケージから StartWebServer を呼び出す err := controllers.StartWebServer() if err != nil { log.Fatal(err) } }

ディレクトリ構成は以下のようになりました。

. ├── backend │ ├── controllers │ │ └── webserver.go │ ├── go.mod │ ├── go.sum │ └── main.go ├── docker │ └── go │ └── Dockerfile └── docker-compose.yaml

これでサーバーは起動できるようになったのですが、このままだとコンテナの外からアクセスできないので、 docker-compose.yaml も変更して、8080 を外部に公開します。

version: "3.9" services: app: build: context: . dockerfile: ./docker/go/Dockerfile volumes: - ./backend/:/go/src/app tty: true ports: - 8080:8080

Dockerfiledocker-compose.yaml など Docker に関わる変更を行なったら、再度ビルドする必要があるので、 一旦 Ctrl + D でコンテナの外に出たあと、以下のコマンドを叩きます。

docker compose build

そしてコンテナを起動し、app コンテナに入ります。

docker compose up -d docker compose exec app ash

コンテナの中で main.go を実行します。

go run main.go

サーバーが起動された状態で http://localhost:8080/api/todos にアクセスすると、以下のような JSON が返却されていることが確認できます。

[{"Id":1,"Title":"チャーハン作るよ!","Completed":true},{"Id":2,"Title":"豚肉も入れるよ!","Completed":false}]

とりあえず Golang で作った API からレスポンスを受け取ることはできるようになりました。

Ctrl + C でサーバーを停止することができます。 その後、 Ctrl + D でコンテナから出て、以下のコマンドでコンテナを終了しておきます。

docker compose down --remove-orphans

Node.js で API レスポンスを表示する画面を作る

次は React 側を整備していきます。

Docker コンテナの準備

docker/node/Dockerfile というファイルを作成し、以下の記述をします。

FROM node:20.9-alpine WORKDIR /home/node/app

docker-compose.yaml を以下のように書き換えます。

version: "3.9" services: # app コンテナの記述... web: build: context: . dockerfile: ./docker/node/Dockerfile volumes: - ./frontend/:/home/node/app tty: true ports: - 5173:5173

docker をビルドし、コンテナを起動します。

docker compose build docker compose up -d

Vite React TypeScript 環境の構築

今回ビルドツールには Vite を使い、TypeScript で記述していくことにします。

以下のコマンドで web コンテナの中に入ります。

docker compose exec web ash

コンテナの中で以下のコマンドを叩きます。

npm create vite@latest .

都度質問が出てくるので、以下のように選択します。

Select a freamwork: React Select a Variant: TypeScript + SWC

Done. と出たら、ターミナルにも表示されていると思いますが、以下のコマンドを実行します。

npm install npm run dev

するとフロントエンドの開発サーバーが起動し、 http://localhost:5173 という URL が提示されます。

しかし、アクセスしても表示できません。

これは Vite が Docker コンテナ内へブラウザアクセスを許可していないためです。 (厳密に言うと、web コンテナから web コンテナの開発サーバーへアクセスすることはできるが、Windows から web コンテナにある開発サーバーにアクセスすることができない)

これを許可するために frontend/vite.config.ts を少し書き換えなければならないのですが、やはりコンテナの root 権限で npm install したため、frontend 以下のディレクトリとファイルの所有権が root になっており、 VSCode からファイルを操作することができなくなっています。

ということで backend のときと同じように、ディレクトリ全体の所有者を自分にします。

一旦 Ctrl + C でサーバーを止めておき、 Ctrl + D で web コンテナから出た状態で、以下のコマンドを叩きます。

sudo chown $USER:$USER -R frontend

ディレクトリの権限を確認してみると自分になっていることがわかります。

ls -l

ではさっそく、frontend/vite.config.ts に少し変更を加えます。

import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], server: { host: true } })

server.host の項目を追加して、true に設定します。 これでもう一度 web コンテナに入り、開発サーバーを起動すれば、問題なく http://localhost:5173 をブラウザで開くことができます。

docker compose exec web ash
npm run dev

RTK Query を導入して Go からのデータを取得する

web コンテナの中で NPM で Redux と Redux Tool Kit を入れます。

npm install react-redux @reduxjs/toolkit

frontend/src/services/TodoService.ts を作成し、以下を記述します。

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' export type Todo = { Id: number Title: string Completed: boolean } type Todos = Todo[] export const todoApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8080/api/', // header に設定したい内容 prepareHeaders: (headers, { getState }) => { headers.set('accept', 'application/json') return headers }, // 今回はポートが違うので cors 対応 mode: 'cors' }), endpoints: (builder) => ({ getTodos: builder.query<Todos, void>({ query: () => `todos`, }), }), }) // 自動的に hooks が生成される export const { useGetTodosQuery } = todoApi

frontend/src/store.ts を作成し、middleware に追加します。

import { configureStore } from '@reduxjs/toolkit' import { todoApi } from './services/TodoService' export const store = configureStore({ reducer: { [todoApi.reducerPath]: todoApi.reducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(todoApi.middleware), });

frontend/src/main.tsx を開いて、Provider でラップして Store を設定します。

import React from 'react' import ReactDOM from 'react-dom/client' import { Provider } from 'react-redux' import App from './App.tsx' import './index.css' import { store } from "./store.ts" ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, )

そして frontend/src/App.tsx で実際に呼び出してみます。

import './App.css' import { useGetTodosQuery } from './services/TodoService' function App() { const {data,error,isFetching } = useGetTodosQuery() return ( <> <div> {error ? <p>Error!</p> : isFetching ? <p>Loading...</p> : data ? <p>{data[0].Title} {data[1].Title}</p> : <p>No Data.</p> } </div> </> ) } export default App

ディレクトリ構成はこんな感じになりました。(node_modules は除外)

. ├── backend │ ├── controllers │ │ └── webserver.go │ ├── go.mod │ ├── go.sum │ └── main.go ├── docker │ ├── go │ │ └── Dockerfile │ └── node │ └── Dockerfile ├── docker-compose.yaml └── frontend ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── index.css │ ├── main.tsx │ ├── services │ │ └── TodoService.ts │ ├── store.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts

ちゃんと取得されているか確認する

で、さっそく確認したいのですが、この状態だと Vite の開発サーバーと Golang の API サーバーを同時に起動するというのが少し難しいので、Dockerfile を書き換えます。

docker/go/Dockerfile

FROM golang:1.21-alpine # ディレクトリ指定 ENV ROOT=/go/src/app WORKDIR ${ROOT} CMD ["go", "run", "main.go"]

docker/node/Dockerfile

FROM node:20.9-alpine WORKDIR /home/node/app CMD ["npm", "run", "dev"]

CMD コマンドを書くと、コンテナ起動時に自動的にこのコマンドを実行してくれます。

どちらもサーバー起動のコマンドなので、コンテナが立ち上がったときに自動で両方のサーバーが立ち上がります。

Docker に関わるファイルを書き換えたので、さっそくビルドして試してみます。

docker compose build docker compose up -d

これで http://localhost:5173 にアクセスすると、backend/controllers/webserver.go で定義した todo の内容が API サーバーから RTK Query を通して取得され、React でレンダリングされていることが確認できます。

確認が済んだら、以下のコマンドでコンテナを終了しておきます。

docker compose down --remove-orphans

広告

関連記事

新着記事

広告

Skeb と Skeb Coin ってどういう関係性のものなのか気になったので調べた
ゼロから始めない Docker + WordPress ハンズオン