DockerでGo、Vue.js、Mysqlを統合した開発環境を構築する方法

## 概要

この記事では、Dockerを使用してGoやVue.js、Mysqlの開発環境を構築する方法を紹介します。
間違っている部分や改善すべき点などありましたら、コメントでご指摘いただけますと幸いです。

## 対象読者

- Dockerを使ってGoとVue.jsの開発環境を構築したい方
- GoとVue.jsを使用したWebアプリケーションの開発に興味がある方

## 前提

- Dockerがインストールされていること
- node.jsがインストールされていること
- npmがインストールされていること

## バージョン情報

- Go: 1.20.2
- Vue.js: 3.24
- Docker: 4.14

## リポジトリ
https://github.com/tomoki737/tomoki737-go_vue_project

## 始め方

```bash
# Gitリポジトリを取得する
$ git clone https://github.com/tomoki737/tomoki737-go_vue_project.git

# ディレクトリに移動
$ cd go_vue_project

# ネットワークの作成やコンテナの起動
$ make init
```

## 使い方

### SQLを実行

```bash
$ docker-compose exec db bash -c 'mysql -ppassword < docker-entrypoint-initdb.d/*.sql'
```

### Dockerコンテナに接続

```bash
# dbコンテナに接続
$ docker-compose exec -it db bash

# backコンテナに接続
$ docker-compose exec -it back sh

# frontコンテナに接続
$ docker-compose exec -it front bash

#sqlに接続
$ docker-compose exec db bash -c 'mysql -u user -ppassword myapp'
```

### Goを実行

```bash
$ docker-compose exec back sh -c 'go run <ファイル名>'
```
### npmを実行

```bash
$ docker-compose exec front bash -c 'npm run dev'
```

## **ディレクトリ構成**

```

.
├──Makefile
├── cmd
│   └── main.go
├── database
│   ├── database.go
│   └── sql
│       └── articles.sql
├── docker
│   ├── back
│   │   └── Dockerfile
│   ├── front
│   │   └── Dockerfile
│   └── mysql
│       └── Dockerfile
├── docker-compose.yml
├── frontend
├── go.mod
└── go.sum
```

- frontendディレクトリ: Vue.jsのフロントエンドアプリケーションが含まれるディレクトリ
- main.go: Goのバックエンドアプリケーションが含まれるファイル

# ハンズオン

1. Dockerfileを作成する
2. docker-compose.ymlを作成する
3. Vueプロジェクトを作成する
4. Dockerコンテナをビルド&起動
5. サンプルコードの作成
6. サーバーを起動
7. ブラウザでアクセスする

## 1. Dockerfileを作成する

### バックエンド

```docker:back/Dockerfile
# alpineを使ってDockerイメージを軽量化する
FROM golang:1.20.2-alpine

# Alpine Linuxにはgitが含まれていないため、gitをインストールする
RUN apk update && apk add git

# 作業ディレクトリを設定する
WORKDIR /src/back

# go.mod、go.sumをDockerコンテナ内にコピー
COPY ../../go.mod ./
COPY ../../go.sum ./

# コピーした依存関係をダウンロード
RUN go mod download
```

### フロントエンド

```docker:front/Dockerfile
FROM node:18.15.0-bullseye-slim

# 作業ディレクトリを設定する
WORKDIR /src/front

# 依存関係をダウンロード
COPY ../../frontend/package*.json ./

# コピーした依存関係をダウンロード
RUN npm install
```

- package*を指定し、package.json, package.lock.jsonをDockerコンテナ内にコピーしています。

### データベース

```docker:mysql/Dockerfile
FROM mysql:8.0.27
```

## 2. docker-compose.ymlの作成

```yaml:docker-compose.yml
version: "3"
services:
  back:
    build:
      context: .
      dockerfile: ./docker/back/Dockerfile
    container_name: back
    tty: true 
    volumes: # プロジェクトのコードを/src/backにマウント
      - ./:/src/back
    env_file: # envファイルを指定
      - .env
    ports:
      - "8080:8080"
    depends_on: 
      - db
    networks:
      - go_network

  front:
    build:
      context: .
      dockerfile: ./docker/front/Dockerfile
    container_name: front
    tty: true
    environment: 
      - NODE_ENV=development # 環境設定を開発用にする
    ports:
      - "3000:3000"
    volumes: # frontendのコードを/src/frontにマウント
      - ./frontend:/src/front
    depends_on: 
      - db
    networks:
      - go_network

  db:
    build:
      context: .
      dockerfile: ./docker/mysql/Dockerfile
    container_name: db
    env_file:
      - .env
    ports:
      - "3306:3306"
    volumes:
      - db:/var/lib/mysql
      - ./database/sql:/docker-entrypoint-initdb.d 
    networks:
      - go_network

volumes:
  db:

networks:
  go_network:
    external: true
```

- tty: trueにすることでDockerコンテナを起動し続けることができます。
- networksを記述することでサービス名を使って通信ができるようになります。
- depends_onを使用してコンテナが起動する順番を設定しています。
    - 今回はdbコンテナが起動してからfront、backコンテナが起動するようにしています。
- dbボリュームを作成して、/var/lib/mysqlにマウントすることでDBを永続化する(コンテナを停止してもデータが残る)よう設定しています。
- このコードでは./database/sqlに書いたsqlを/docker-entrypoint-initdb.dにマウントしています。
    
    ```yml:docker-compose.yml
        volumes:
          - ./database/sql:/docker-entrypoint-initdb.d 
    ```
    

### .envファイルの作成

```: .env
MYSQL_DATABASE=myapp
MYSQL_USER=user
MYSQL_PASSWORD=password
MYSQL_ROOT_PASSWORD=password
```

docker-composeのenv_fileに指定する.envファイルを作成します。

### ネットワークの作成

```bash
$ docker network create go_network
```

docker-compose.ymlで記述したネットワークを作成します。

## 3. Vueプロジェクトの作成

```bash
$ npm init vue@latest
```
- 最新バージョンをインストールしています。

```bash
# 依存関係をダウンロード
$ docker-compose exec front bash -c 'npm install'
```

## 3. Dockerコンテナをビルド & 起動

```bash
$ docker-compose build

$ docker-compose up -d
```

## 4. サンプルコードの作成

以下のサンプルコードをコピペしてください
### Goのサンプルコード
```go:cmd/main.go
package main

import (
	"bytes"
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"app/database"
)

type Article struct {
	Title string `json:"title"`
	Body  string `json:"body"`
}

var db *sql.DB

func index(w http.ResponseWriter, r *http.Request) {
	var buf bytes.Buffer
	var articles []Article

	enc := json.NewEncoder(&buf)
	rows, err := db.Query("SELECT title, body FROM articles")
	if err != nil {
		log.Fatal(err)
	}

	defer rows.Close()

	for rows.Next() {
		article := &Article{}
		if err := rows.Scan(&article.Title, &article.Body); err != nil {
			log.Fatal(err)
		}
		articles = append(articles, Article{
			Title: article.Title,
			Body:  article.Body,
		})
	}
	enc.Encode(&articles)
	fmt.Fprintf(w, buf.String())
}

func makeHandler(fn func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Headers", "*")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
		fn(w, r)
	}
}

func main() {
	db = database.GetDB()
	http.HandleFunc("/", makeHandler(index))
	log.Fatal(http.ListenAndServe(":8080", nil))
}
```

```go:database/database.go
package database

import (
	"database/sql"
	"fmt"
	"log"
	"os"

	_ "github.com/go-sql-driver/mysql"
	"github.com/joho/godotenv"
)

var db *sql.DB

func init() {
	err := godotenv.Load()

	if err != nil {
		log.Fatal("Error loading .env file")
	}

	user := os.Getenv("MYSQL_USER")
	password := os.Getenv("MYSQL_PASSWORD")
	database := os.Getenv("MYSQL_DATABASE")

	dataSourceName := fmt.Sprintf("%s:%s@tcp(db:3306)/%s?charset=utf8&parseTime=true", user, password, database)

	db, err = sql.Open("mysql", dataSourceName)

	if err != nil {
		log.Fatal(err)
	}
	err = db.Ping()
	if err != nil {
		log.Fatal(err)
	}
}

func GetDB() *sql.DB {
	return db
}
```
### sqlファイルの作成と実行
```sql:sql/articles.sql
USE myapp

CREATE TABLE IF NOT EXISTS articles (
  id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(40) NOT NULL,
  body VARCHAR(255) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT current_timestamp,
  updated_at TIMESTAMP NOT NULL DEFAULT current_timestamp ON UPDATE current_timestamp
);

INSERT INTO articles(title, body) VALUES ('title','body');
INSERT INTO articles(title, body) VALUES ('test_title','test_body');
```

```bash
docker-compose exec db bash -c 'mysql -ppassword < docker-entrypoint-initdb.d/*.sql'
```

### 依存関係のインストール

```:go.mod
module app

go 1.20

require (
	github.com/go-sql-driver/mysql v1.7.0
	github.com/joho/godotenv v1.5.1
)
```

```bash
$ docker-compose exec back sh -c "go mod download"
```

## 5. サーバーを起動

### フロントエンド

```json:frontend/package.json
"scripts": {
		// "dev": "vite" <-- 変更
    "dev": "vite --port 3000 --host 0.0.0.0",
    # 省略
  },
```

viteのオプションでポートとホストIPアドレスを指定しています。

```bash
# frontコンテナに入り、npmを実行
$ docker-compose exec -it front bash -c 'npm run dev'
```
### バックエンド

```bash
# backコンテナに入り、main.goを実行
$ docker-compose exec -it back sh -c 'go run cmd/main.go'
```

## 6. ブラウザでアクセスする

### バックエンド

http://localhost:8080/

### フロントエンド

http://localhost:3000/

# 補足

## Makefile

Dockerコマンドは長いのでコマンドを打つのに時間がかかってしまいます。
そこで短いコマンドでDockerコマンドが打てるようにMakefileを作成しました。
詳細はリポジトリのMakefileをご覧ください。

```bash
#　コンテナを起動する
$ make up

# コンテナを破棄する
$ make down

# backコンテナに接続する
$ make back

# frontコンテナに接続する
$ make front

# dbコンテナに接続する
$ make db

# sqlに接続する
$ make sql
```

## 参考記事

https://qiita.com/ucan-lab/items/5fc1281cd8076c8ac9f4

https://zenn.dev/ajapa/articles/443c396a2c5dd1#golang/dockerfile

https://docs.docker.jp/engine/reference/builder.html

https://vuejs.org/guide/quick-start.html#creating-a-vue-application