docker-composeでgin+reactの開発環境を作る

ずっとバックエンドAPIにGoを使った開発がしたくて、趣味で作ってたRailアプリを作り直そうと思いまずは開発環境を作ってみた。
フロントに関しては別にこだわりはなかったんだけど、仕事はインフラ+バックエンドがメインでフロントはそこまでスキルがないので担当しているプロダクトにあわせて勉強がてらといういう意味でReactにしてみた。

構成

docker-compose.yml
|-- api(ginのソースコード格納)
|-- react(フロントのソースコード格納)
`-- docker
    |-- api
    |     |-- Dockerfile
    |     |-- Gopkg.toml(※)
    |     `-- main.go(※)
    |-- nginx
    |     |-- Dockerfile
    |     `-- nginx.conf
    `-- node
          `-- Dockerfile

docker/api 配下にある Gopkg.tomlmain.go だが、後述する docker-compose.yml内でapiコンテナにマウントさせている./api配下のものに上書きされる。
ここにわざわざ配置しているのは、apiコンテナ作成時にdep ensureによるパッケージインストールを実施したいため、両ファイル必要なのだが、マウントのタイミングがこのdep ensureより後に実施されてしまうため事前にDockerfile内でCOPYしてやる必要があったため。
本当はdocker/api 配下ではなくapi配下にあるファイルを置きたかったが、DockerfileのCOPYで指定するファイルがDockerfileより上位のディレクトリは指定できないようなので仕方なくこのような形にした。
ちなみにdocker/api/main.goは空ファイルでもよい。

docker-compose.yml

作ったdocker-compose.ymlはこんな感じ

version: "3"
services:
  node:
    build: ./docker/node
    volumes:
      - ./react:/var/www/html/
    working_dir: /var/www/html/horsebase
    command: yarn start
    ports:
      - "3000:3000"
  db:
    image: mysql:5.7.22
    environment:
      MYSQL_ROOT_PASSWORD: [password]
      MYSQL_PASSWORD: [password]
      MYSQL_DATABASE: [database]
    volumes:
      - ./mysql:/var/lib/mysql
    ports:
      - "3306:3306"
  api:
    build: ./docker/api
    volumes:
      - ./api:/go/src/project
  nginx:
    build: ./docker/nginx
    ports:
      - "1234:1234"

ginはサーバサイドレンダリングはせずにAPI提供のみとして構成している。

各Dockerfile

node

FROM node:8.9.4-alpine

WORKDIR /var/www/html/
RUN npm install -g create-react-app

api

FROM golang:1.10.2-alpine3.7

COPY Gopkg.toml /go/src/horsebase-prophet/
COPY main.go /go/src/horsebase-prophet/

WORKDIR /go/src/horsebase-prophet/

RUN apk update \
  && apk add --no-cache git \
  && go get -u github.com/codegangsta/gin \
  && go get -u github.com/gin-gonic/gin \ 
  && go get -u github.com/golang/dep/cmd/dep \
  && dep ensure

CMD gin -i run

nginx

FROM nginx:latest

RUN rm -rf /etc/nginx/conf.d/default.conf
COPY ./nginx.conf /etc/nginx/conf.d/default.conf

CORS(Cross-Origin Resource Sharing)問題

Reactのaxiosを使って直接ginのAPIを叩いてもCORSのため、通信できない。
gin側のレスポンスヘッダにAccess-Control-Allow-Originを設定することで通信可能になるという情報もあったが、Chromeだとうまくいかなかった。
Chromeの場合は起動オプションを指定する必要があるらしい。
(参考)
開発時にCORSを無視するGoogleChromeの起動オプション - Qiita

今回はnginxをproxyとして利用し、react→nginx→ginのようにアクセスするようにした。

nginx.conf(抜粋)(これだけでも動く)

server {
    listen       1234;
    server_name  localhost;

    location / {
        proxy_pass  http://api:9000;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

react/project/src/App.js

import React, { Component } from 'react';
import axios from 'axios';

class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      message: ""
    };
    this.getData = this.getData.bind(this);
  }

  getData() {
    axios
      .get('http://localhost:1234/api/greeting')
      .then(results => {
        const data = results.data;
        this.setState({
          message: data['message']
        });
      });
  }
 
  render() {
    return (
      <div>
        <button onClick={this.getData}>getData</button>
        <p>{this.state.message}</p>
    </div>
    );
  }
}
export default App;

APIへのアクセスはnginxで待ち受けている1234ポートに投げる。
nginxはnginx.confproxy_passで定義したapiコンテナ内でginが待ち受けているポートへ。

api/main.go

package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    r.GET("/api/greeting", func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")

        c.JSON(200, gin.H{
            "message": "hello, world",
        })
    })
    r.Run(":9000")
}

レスポンスヘッダのAccess-Control-Allow-Originにアクセス元のURLをセットする。
これをセットしないとCORSではじかれる。

まとめ

まとめというか、以下なんとかならんかな

  • 同じcompose配下のコンテナならCORS意識しなくていい仕組みほしい
  • volumesで指定したマウントのタイミングDockerfile内で定義したCMDやdocker-compose.yml内で定義したcommandのタイミングをコントロールしたい