Nuxt.js・Go・Mysqlのアプリケーションをdocker-composeで作る

Nuxtとgoのアプリケーションをdocker-composeで作っていこうと思う。以前、Nuxtの方は作ったからそれを元に作っていこう。

www.y-techmemo.work

ディレクトリ構成

ディレクトリ構成はこうしよう。backendとかfrontendディレクトリにそれぞれのソースコードが入るイメージ。

├── backend
│   └── Dockerfile
├── docker-compose.yml
└── frontend
    └── Dockerfile

ベストプラクティス的にはダメだけど、とりあえず動かす事が目的だから簡単にいきたい。

ほとんどの場合、空のディレクトリに個々の Dockerfile を置くのがベストです。そうしておけば、そのディレクトリには Dockerfile が構築に必要なファイルだけ追加します。

Dockerfile のベストプラクティス — Docker-docs-ja 1.9.0b ドキュメント

あと、goのアプリケーションからdbに接続することを想定するからmysqlのコンテナも作る。postgresはあまり好きじゃないんだ・・・

Nuxtアプリケーションを作成する

buildで指定するDockerfileの場所を変更した。

// frontend/Dockerfile

version: '3'
services:
  nuxt-app:
    # Dockerfileの場所
    build:
      context: ./frontend
      dockerfile: Dockerfile
    working_dir: /frontend
    command: yarn run dev
    # ホストOSとコンテナ内でソースコードを共有する
    volumes:
      - ./frontend:/frontend
    # コンテナ内部の3000を外部から5000でアクセスする
    ports:
      - 5000:3000

別にcontext使わなくても、builddockerfileで動かすことできるけど、contextではGitリポジトリのURLを指定する事ができるから、まあ柔軟にできる感はある。

docs.docker.jp

書いたら、nuxtのアプリケーションをcreate-nuxt-appで作る。

$ docker-compose run --rm nuxt-app npx create-nuxt-app

色々聞かれるから、適当に回答しておこう。こんな感じにしておいた。

create-nuxt-app v2.15.0
✨  Generating Nuxt.js project in .
? Project name frontend
? Project description My impeccable Nuxt.js project
? Author name 
? Choose programming language TypeScript
? Choose the package manager Yarn
? Choose UI framework None
? Choose custom server framework None (Recommended)
? Choose the runtime for TypeScript @nuxt/typescript-runtime
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection)

作成が終わったら、frontendディレクトリ配下にnuxtのファイル群ができているはず。

goアプリケーションを作成する

Nuxtのアプリケーションの作成は終わったから次はgoのアプリケーションをdocker-composeで書く。

// backend/Dockerfile

# 使用するGolangのイメージを指定する
FROM golang:1.14.2-alpine
ENV GO111MODULE=on

# 必要なパッケージなどなどをインストールする
RUN apk update \
    && apk add --no-cache git

EXPOSE 8080

docker-compose.ymlには新しくgo-appっていうservice名で作る。nuxtアプリケーションの記述の下に書いていく。

// docker-compose.yml

  go-app:
    build:
      context: ./backend
      dockerfile: Dockerfile
    working_dir: /backend
    # docker-compose run実行時に実行される
    command: go run main.go
    volumes:
      - ./backend:/backend
    ports:
      - "8080:8080"

docker-compose upで起動したらgoのアプリケーションを動かしたいからcommandにgo run main.goを指定。なので、main.goをbackendディレクトリ配下に作る。

// main.go

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World")
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

これで、docker-compose upしよう。serviceを作る時にgo-appという名称で作ったから、go-app指定。

$ docker-compose up go-app

Hello, Worldが出てきますね。

f:id:utr066:20200412135957p:plain

mysqlをdocker-compose.ymlに記載する。

さっきまででNuxtとgoのアプリケーションはdocker-compose.ymlに書いたけど、データベースは作っていない。goからはデータベース使う想定だから、mysqlのコンテナを作ろう。docker-compose.ymlに追加する。

// docker-compose.yml

  db:
    image: mysql:5.7
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: mysql_dev
      MYSQL_USER: U
      MYSQL_PASSWORD: passw0rd
    ports:
      - "3306:3306"

起動してみよう。

$ docker-compose up db

起動したらmysqlに接続できるか確かめる。

$ docker-compose exec db bash
$ mysql -uroot -p
パスワードが聞かれるからMYSQL_ROOT_PASSWORDで指定したrootと入力

$ show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| mysql_dev          |
| performance_schema |
| sys                |
+--------------------+

入れました。

goからmysqlに接続できるようにする

必要なコンテナは揃ったから、次はgoのアプリケーションからmysqlに接続できるようにする。 docker-compose.ymlのgo-appにdepends_on: -dbを追加。

  go-app:
    build:
      context: ./backend
      dockerfile: Dockerfile
    depends_on:
      - db
    working_dir: /backend
    # docker-compose run実行時に実行される
    command: go run main.go
    volumes:
      - ./backend:/backend
    ports:
      - "8080:8080"

コードをいじる前に、適当にテーブルとレコードを作っておく。とりあえずusersテーブルにnameカラム作って入れておこう。

$ docker-compose exec db bash
$ mysql -uroot -p
パスワード入力
$ use mysql_dev
$ create table users (id int, name varchar(255));
$ insert into users (id, name) values (1, "U");
$ select * from users;

ここで作ったレコードをgoでselectするようにする。

mysqlに接続するためのパッケージをimportするけど、今回はgo mod使うからbackendディレクトリで以下コマンドでファイルを作る。

$ go mod init backend

そしたら、main.goファイルをいじろう。mysqlにつなげるか確認するだけのコードを書く。

package main

import (
    "fmt"
    "net/http"
    "database/sql"
    "log"
  
    _ "github.com/go-sql-driver/mysql"
)

func handler(w http.ResponseWriter, r *http.Request) {
    db, err := sql.Open("mysql", "root:root@tcp(db:3306)/mysql_dev")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    var name string
    err = db.QueryRow("SELECT name FROM users WHERE id = ?", 1).Scan(&name)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Fprintf(w, name)
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

これでdocker-compose upで立ち上げてさっきmysqlにinsertしたレコードのnameが表示されればいいですね。

$ docker-compose up go-app

f:id:utr066:20200412145812p:plain

OK。

Nuxtとつなげる

goのアプリケーションからmysqlにつないでnameを取得できたけど、まだnuxtからは確認できない。Nuxtとgoをつないでnuxtアプリケーションの画面上にnameが表示されるようにしよう。Nuxtではjsonで情報を受け取りたいから、goでjsonを返すようにする。

package main

import (
    "net/http"
    "database/sql"
    "log"
    "encoding/json"
    _ "github.com/go-sql-driver/mysql"
)

type User struct{
    Name string `json:"name"`
}

func handler(w http.ResponseWriter, r *http.Request) {
    db, err := sql.Open("mysql", "root:root@tcp(db:3306)/mysql_dev")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    var name string
    err = db.QueryRow("SELECT name FROM users WHERE id = ?", 1).Scan(&name)
    if err != nil {
        log.Fatal(err)
    }

    user := &User{
        Name: name,
    }
    res, err := json.Marshal(user)

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

    w.Header().Set("Content-Type", "application/json")
    w.Write(res)
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

確認するとjson形式。

f:id:utr066:20200412151652p:plain

これをNuxtで受け取れるようにする。

$ yarn add @nuxtjs/axios

nuxt.config.jsのmodulesにaxiosを追加。

  modules: [
    '@nuxtjs/axios'
  ],

index.vueをいじる。modeにuniversalを指定しているからSSRでやる。async asyncData内でaxiosを使ってapiを呼ぶかな。画面上には取ってきたnameを表示させる。

<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">
        {{ user.name }}
      </h1>
      <h2 class="subtitle">
        My impeccable Nuxt.js project
      </h2>
      <div class="links">
        <a
          href="https://nuxtjs.org/"
          target="_blank"
          class="button--green"
        >
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey"
        >
          GitHub
        </a>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import Logo from '~/components/Logo.vue'

export default Vue.extend({
  components: {
    Logo
  },

  async asyncData({ app }) {
    try {
      const res = await app.$axios.$get("http://go-app:8080")
      console.info(res)
      return {
        user: res
      }
    } catch(error) {
      // エラー処理
    }
  }
})
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.title {
  font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  display: block;
  font-weight: 300;
  font-size: 100px;
  color: #35495e;
  letter-spacing: 1px;
}

.subtitle {
  font-weight: 300;
  font-size: 42px;
  color: #526488;
  word-spacing: 5px;
  padding-bottom: 15px;
}

.links {
  padding-top: 15px;
}
</style>

U表示されてるね。TypeScriptのビルドには失敗するだろうからちゃんとやんないといけないけど、とりあえずの表示はおk。

f:id:utr066:20200412160137p:plain

コード云々は置いておいて、大まかな流れはまあこんな感じできっとできる。