20210119のdockerに関する記事は12件です。

gym.render() でDISPLAYのエラーが出たときの対処法

困ったこと

dockerコンテナでOpen AI Gymのサンプルコードを実行しようとしたら、次のようなエラーメッセージが出てしまいました。

ターミナル
Traceback (most recent call last):
  File "hello_gym.py", line 8, in <module>
    env.render()
  File "/usr/local/lib/python3.6/dist-packages/gym/core.py", line 240, in render
    return self.env.render(mode, **kwargs)
  File "/usr/local/lib/python3.6/dist-packages/gym/envs/classic_control/cartpole.py", line 174, in render
    from gym.envs.classic_control import rendering
  File "/usr/local/lib/python3.6/dist-packages/gym/envs/classic_control/rendering.py", line 25, in <module>
    from pyglet.gl import *
  File "/usr/local/lib/python3.6/dist-packages/pyglet/gl/__init__.py", line 244, in <module>
    import pyglet.window
  File "/usr/local/lib/python3.6/dist-packages/pyglet/window/__init__.py", line 1880, in <module>
    gl._create_shadow_window()
  File "/usr/local/lib/python3.6/dist-packages/pyglet/gl/__init__.py", line 220, in _create_shadow_window
    _shadow_window = Window(width=1, height=1, visible=False)
  File "/usr/local/lib/python3.6/dist-packages/pyglet/window/xlib/__init__.py", line 165, in __init__
    super(XlibWindow, self).__init__(*args, **kwargs)
  File "/usr/local/lib/python3.6/dist-packages/pyglet/window/__init__.py", line 570, in __init__
    display = pyglet.canvas.get_display()
  File "/usr/local/lib/python3.6/dist-packages/pyglet/canvas/__init__.py", line 94, in get_display
    return Display()
  File "/usr/local/lib/python3.6/dist-packages/pyglet/canvas/xlib.py", line 123, in __init__
    raise NoSuchDisplayException('Cannot connect to "%s"' % name)
pyglet.canvas.xlib.NoSuchDisplayException: Cannot connect to "None"

実行環境

実行環境は次のとおりです。

バージョン
OS Ubuntu18.04
python 3.6
gym 0.18.0
pyglet 1.5.0
xvfb 2:1.19.6
x11vnc 0.9.13

Ubintu18.04ベースのdockerコンテナにvncを設定して、render()の出力結果をブラウザで確認しようとしています。

考えたこと

  • VNCの設定ができていない
  • X displayの設定ができていない
  • gymサンプルコードのエラー(render周り)
  • pygletのバージョンが合っていない(pygletは最新を入れていました)

gymのサンプルコードは次のとおりです。

sample.py
import gym
env = gym.make('CartPole-v0')
env.reset()
for _ in range(1000):
    env.render()
    env.step(env.action_space.sample()) # take a random action
env.close()

(実はこのコードは、別の理由でエラーが起こるのですが、それはまた別の記事で…)

原因

DISPLAYの設定ができていませんでした。

ターミナルで環境変数を確認します。

ターミナル
$ printenv

一覧がずらずらっと表示されて

ターミナル
LANG=C.UTF-8
OLDPWD=/home/user
VISIBLE=now
USER=user
PWD=/home/user/workspace
HOME=/home/user
略

DISPLAY=の表示がありません。設定したはずなのに…。

DISPLAY 環境変数の設定 (うまくいかなかった例)

次の3つの方法は試しましたが、うまくいきませんでした。
解決策を早く知りたい方はジャンプ

設定法1

もともとはDockerfileの中で設定していました(できているつもりでした)

Dockerfile
ENV DISPLAY=:0

でも、コンテナ内の環境変数は設定できていませんでした。

設定法2 docker runのオプションで設定する

次に試したのがこの方法。

ターミナル
docker run -e DISPLAY=$DISPLAY 以下略

ちなみに、-e--envに変えても、だめでした笑

設定法3 startup.shで設定する

startup.shは、ざっくり言うとコンテナを起動する際に、実行させることのできるシェルスクリプトです。
(よくわかっていないだけw)

Dockerfileの最後にCMD命令を記述できますが、複数のコマンドを実行させたい場合に、
それらをstartup.shの中に記述することで実行させます。

Dockerfile
CMD ["/startup.sh"]

なぜstartup.shを使ったかというと、sshvncsupervisordによって起動させたかったので、/usr/bin/supervisord -c /etc/supervisor/supervisord.confは少なくともbashで実行する必要があったからです。
そのため、コマンドを2行記述しました。

startup.sh
#!/bin/bash
/usr/bin/supervisord -c /etc/supervisor/supervisord.conf
export DISPLAY=:0

結局この方法でも、エラーは解決しませんでした。

解決策

Dockerfileの中で次のように記述します。

RUN echo "export DISPLAY=:0"  >> /etc/profile

これで、コンテナ内の環境変数にDISPLAYを設定することができました。

まとめ

dockerコンテナ内でgymを実行した際にpyglet.canvas.xlib.NoSuchDisplayException: Cannot connect to "None"というエラーが表示されたら、
DISPLAYの環境変数が設定できていないかもしれません。

Dockerfile内でRUN echo "export DISPLAY=:0" >> /etc/profileと実行すると設定できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rust公式ドキュメントをDockerコンテナ上で進めてみる(2. 数当てゲームをプログラムする)

はじめに

タイトルの通りです。
前回:Rust公式ドキュメントをDockerコンテナ上で進めてみる(1. 事始め)

参考にしたもの

Rust公式ドキュメント(TheRustProgrammingLanguage日本語版):https://doc.rust-jp.rs/book-ja/title-page.html
DokerHub(Rust):https://hub.docker.com/_/rust

2.数当てゲームをプログラムする

公式ドキュメント:https://doc.rust-jp.rs/book-ja/ch02-00-guessing-game-tutorial.html

まず初めに全体を確認します。
以下ファイルの修正が必要そうなので、ローカルで作成したものをCOPYする方式をとります。

  • src/main.rc
  • Cargo.toml

コンテナにコピーするmain.rcとCargo.tomlを作成します。
なお、ローカルで作成するもののフォルダ構成。

guessing_game
 - src/main.rc
 - Cargo.toml
guessing_game/src/main.rc
extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}
guessing_game/Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
authors = ["root"]
edition = "2018"

[dependencies]
rand = "0.3.14"

Dockerfileを作成します。一部、DockerHub上のサンプルに合わせています。
cargo init を使っているのは既にあるディレクトリでcargo newは使えないため。

Dockerfile
FROM rust:latest
WORKDIR /usr/src/projects
COPY . .

# 2.数当てゲームをプログラミングする
RUN cargo init guessing_game --bin && \
    cd /usr/src/projects/guessing_game && \
    cargo build
WORKDIR /usr/src/projects/guessing_game
CMD ["cargo", "run"]

ビルドしたところcargoのエラーになりました

$ docker build -t my-rust-app .
(中略)
 ---> Running in 13aa45df5d36
error: `cargo init` cannot be run on existing Cargo packages
The command '/bin/sh -c cargo init guessing_game --bin &&     cd /usr/src/projects/guessing_game &&     cargo build' returned a non-zero code: 101

どうもcargo initは既存のディレクトリやソースを使うことはできてもCargo.tomlがあるとダメみたいです。
大人しく、cargo newしたあと、COPYで上書きする形にします。

修正後のDockerfile
FROM rust:latest
WORKDIR /usr/src/projects

# 2.数当てゲームをプログラミングする
RUN cargo new guessing_game --bin
COPY . .
RUN cd /usr/src/projects/guessing_game && \
    cargo build
WORKDIR /usr/src/projects/guessing_game
CMD ["cargo", "run"]

ビルドし問題なくプログラムが稼働することを確認

$ docker build -t my-rust-app .
$ docker run -it --rm --name my-running-app my-rust-app
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
0
You guessed: 0
Too small!
Please input your guess.
100
You guessed: 100
Too big!
Please input your guess.
97
You guessed: 97
You win!

おわりに

単にRustの学習環境を作るだけなら、マウントでいいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【メモ】dockerまとめ

コマンド

起動〜削除まで

  • docker pull [imege名]
    • image(設計図)を落としてくる
  • docker build -t [image名] [ファイルの場所]
    • image作成
    • -tでイメージ名を指定できる
  • docker create -it [imege名]
    • コンテナ生成
    • -itでターミナルからコンテナ内部の操作できる
  • docker start [コンテナ識別子]
    • コンテナ起動
  • docker stop [コンテナ識別子]
    • コンテナ停止
  • docker rm [コンテナ識別子]
    • コンテナ削除
  • ☆docker run [image名]
    • pull create start が含まれている
    • -dをつけるとバックグラウンド実行
    • -p 80:80をつけると80でローカルホストアクセスがきたら80でコンテナ通す
    • --name OO で名前つける
    • ruby:2.5 などでバージョン指定
    • -v $(pwd):/var/www/html
    • -v ホスト側のローカルディレクトリ:コンテナの場所
    • これでローカルを参照できる

よく使いそう

  • docker image ls
    • 今までpullしたimageが見える
  • docker container ls -a
    • コンテナの状況が見える
    • -aで停止中のものも見える
  • docker exec -it コンテナ名 bash
    • コンテナの中に入る
    • ctrl+p+qでコンテナ抜ける

dockerファイル

  • FROM
    • 元にするimage
  • LABEL
    • 作成したユーザの情報
  • RUN
    • buildするときに実行される
  • CMD
    • runするときに実行される
  • EXPOSE
    • ポートを指定、エクスポート
  • WORKDIR
    • 作業ディレクトリ指定
  • ENV
    • 環境変数の指定
  • ADD
    • ファイル追加
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go(Echo) Go Modules × Dockerで開発環境構築

はじめに

フレームワークにEcho
ライブラリ管理にgo mod
ホットリロードにfresh
を使用したGoの開発環境をDockerを使って構築したいと思います。
GoのバージョンはGo 1.15です。

この記事ではDockerのインストール方法や細かい解説等はしません。

最終的なディレクトリ構成

.
├── app
│   ├── Dockerfile
│   ├── go.mod
│   ├── go.sum
│   └── main.go
└── docker-compose.yml

GitHub

急いでいる人向け

ここからコードをダウンロードしてdocker-compose upすればできます。

Dockerfile作成

まず最初に適当なディレクトリを作成(私の場合go-dockerというディレクトリを作成)
上記のディレクトリ構成を参考にappというディレクトリを作成し、
そしてappディレクトリにDockerfileという名前でファイルを作成します。

Dockerfile
FROM golang:1.15-alpine

WORKDIR /go/src/app
ADD ./app /go/src/app

RUN apk update && \
    apk add --no-cache git && \
    go get github.com/labstack/echo/... && \
    go get github.com/pilu/fresh

EXPOSE 8080

CMD ["fresh"]

main.go作成

次はappディレクトリにmain.goというファイルを作成します。

main.go
package main

import (
    "net/http"
    "github.com/labstack/echo"
)

func main() {
    e := echo.New()
    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello world")
    })
    e.Logger.Fatal(e.Start(":8080"))
}

docker-compose.yml作成

次にdocker-compose.ymlという名前でファイルを作成します。
フォルダ構成を参考にファイルを作成する場所に気をつけてください。

docker-compose.yml
version: "3"

services:
  app:
    build:
      context: .
      dockerfile: app/Dockerfile #Dockerfileの場所
    volumes:
      - ./app:/go/src/app
    ports:
      - "8080:8080"
    tty: true #コンテナ永続化

build

次にymlファイルがある場所と同じ階層でdocker-compose buildコマンドを実行します。

$ docker-compose build
Building app
Step 1/6 : FROM golang:1.15-alpine
 ---> b3bc898ad092
Step 2/6 : WORKDIR /go/src/app
 ---> Running in 55f4bfc0b0e5
Removing intermediate container 55f4bfc0b0e5
 ---> bb957624bc5e
Step 3/6 : ADD ./app /go/src/app
 ---> 94a4c0aeb52e
Step 4/6 : RUN apk update &&     apk add --no-cache git &&     go get github.com/labstack/echo/... &&     go get github.com/pilu/fresh
 ---> Running in 2e16203c8eac
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz     
v3.12.3-65-g746e0b7bc7 [http://dl-cdn.alpinelinux.org/alpine/v3.12/main]
v3.12.3-62-gebf75fec7d [http://dl-cdn.alpinelinux.org/alpine/v3.12/community]
OK: 12756 distinct packages available
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz     
(1/5) Installing nghttp2-libs (1.41.0-r0)
(2/5) Installing libcurl (7.69.1-r3)
(3/5) Installing expat (2.2.9-r1)
(4/5) Installing pcre2 (10.35-r0)
(5/5) Installing git (2.26.2-r0)
Executing busybox-1.31.1-r16.trigger
OK: 22 MiB in 20 packages
Removing intermediate container 2e16203c8eac
 ---> dfceb18e2ebd
Step 5/6 : EXPOSE 8080
 ---> Running in 8353095ba65e
Removing intermediate container 8353095ba65e
 ---> fa4b48a798c4
Step 6/6 : CMD ["fresh"]
 ---> Running in a8c85cdb33ba
Removing intermediate container a8c85cdb33ba
 ---> e121e6032342

Successfully built e121e6032342
Successfully tagged go-docker_app:latest

go mod init

次にdocker-compose run --rm app go mod initというコマンドを実行します。
するとappディレクトリにgo.modが作成されます。

$ docker-compose run --rm app go mod init
Creating network "go-docker_default" with the default driver
Creating go-docker_app_run ... done
go: creating new go.mod: module app

docker-compose up

最後にddocker-compose upというコマンドを実行します。
そしてgo.sumも作られコンテナが立ち上がります。

$ docker-compose up
Creating go-docker_app_1 ... done
Attaching to go-docker_app_1
app_1  | 9:27:18 runner      | InitFolders
app_1  | 9:27:18 runner      | mkdir ./tmp
app_1  | 9:27:18 watcher     | Watching .
app_1  | 9:27:18 main        | Waiting (loop 1)...
app_1  | 9:27:18 main        | receiving first event /
app_1  | 9:27:18 main        | sleeping for 600 milliseconds
app_1  | 9:27:18 main        | flushing events
app_1  | 9:27:18 main        | Started! (5 Goroutines)
app_1  | 9:27:18 main        | remove tmp/runner-build-errors.log: no such file or directory
app_1  | 9:27:18 build       | Building...
app_1  | 9:27:34 runner      | Running...
app_1  | 9:27:34 main        | --------------------
app_1  | 9:27:34 main        | Waiting (loop 2)...
app_1  | 9:27:34 app         | 
app_1  |    ____    __
app_1  |   / __/___/ /  ___
app_1  |  / _// __/ _ \/ _ \
app_1  | /___/\__/_//_/\___/ v3.3.10-dev
app_1  | High performance, minimalist Go web framework
app_1  | https://echo.labstack.com
app_1  | ____________________________________O/_______
app_1  |                                     O\
app_1  | 9:27:34 app         | ⇨ http server started on [::]:8080

この状態になったら下記にアクセスしてHello worldと表示されていたら成功です。
http://localhost:8080/

さいごに

次はMySQLの構築とその接続を時間があればやってみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker-composeのMySQLのパスワードなどを変えたくなったとき

表題の通り、こんな感じでmysqlのパスワードを設定しているときの設定値の変え方です。

docker-compose.yml
  mysql:
    image: mysql:5.7.32
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root_pass
      MYSQL_DATABASE: db_name
      MYSQL_USER: user1
      MYSQL_PASSWORD: user1_pass

volumes:
  mysql_data:

普通にdocker-compose.ymlを変更したのち、次のコマンドを実行すると設定値が反映されます。

docker-compose down --volumes
docker-compose up -d

普通にdownしてupしただけだと、パスワードが設定済みのMySQLのデータが使われてしまうのでうまくいきません。
そこで前述のコマンドのようにボリュームを削除させる必要があります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Node.js]docker-compose upで気楽に開発環境構築

前提

  • 筆者はMac環境(試してないがWindowsでも動くと思う)
  • dockerコマンドが使える状態
  • npmコマンドが使える状態
  • nodeコマンドが使える状態
  • Dockerの基本的知識(イメージやコンテナの概念)を有する

対象

  • 環境構築を手っ取り早くしたい人
  • Node.jsをdockerで動かしたい人

環境

bash
$ docker -v
Docker version 20.10.2, build 2291f61

$ npm -v
6.14.5

$ node -v
v13.11.0

$ pwd
~/{project_name}

ファイル構成

project
{project_name}
 ├─ node_modules
 |   └─ ...
 ├─ src
 |   └─ index.js
 └─ docker-compose.yml
 └─ Dockerfile
 └─ package.json
 └─ package-lock.json

node_modulesの中身は割愛

Dockerfileの設定

基本的にはこちらの公式ドキュメント通りにやれば良いが、楽に開発したいたので修正する

{project_name}/Dockerfile
FROM node:12

# アプリケーションディレクトリを作成する
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Dockerfileが置かれているディレクトリ以下をコピー
ADD . /usr/src/app

# アプリケーションの依存関係をインストールする
COPY package*.json /usr/src/app
RUN npm install
# 本番用にコードを作成している場合
# RUN npm install --only=production

# アプリケーションのソースをバンドルする
COPY . /usr/src/app
  • FROM node:1212の部分はこちらを参考に自身の好きなバージョンで!
  • WORKDIR /usr/src/app:dockerコンテナの作業ディレクトリを指定
    (変更する場合はDockerfileと後に紹介するdocker-compose.ymlに書かれた/usr/src/appを全て変更する必要あり)

docker-compose.ymlの設定

{project_name}/docker-compose.yml
version: '3'
services:
  app:
    build: .
    command: bash -c 'node src/index.js'
    image: node_test
    volumes: .:/usr/src/app
    ports: "8080:8080"
    tty: true
  • command: bash -c 'node src/index.js'docker-compose upコマンドでコンテナ起動の際に呼ばれるコマンド
  • image: node_test:image名は任意で変更(分かりやすいプロジェクト名とか)
  • volumes: .:/usr/src/app:ローカルのファイルとdockerコンテナ内のファイルを同期させる
  • ports: "8080:8080":ローカルとコンテナ両方とも8080番ポートを使用
  • tty: truedocker-compose upでコンテナが終了しないようにする

(index.jsの設定)

基本的に自身の環境による
以下はこちらの雛形

'use strict';
const express = require('express');

// Constants
const PORT = 8080;
const HOST = '0.0.0.0';

// App
const app = express();
app.get('/', (req, res) => {
  res.send('Hello World');
});

app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

起動

bash
$ docker-compose up

Have fun !!

※ イメージができてない場合、コンテナ起動までに時間がかかる

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go + MySQL + nginxの開発環境をDocker(docker-compose)で作る

やりたいこと

  • Go、MySQL、nginxの開発環境をDocker(docker-compose)で作る
  • Goの外部パッケージはGo Modulesで管理
  • Goのプロジェクトを実践的なものにする
  • DBのテーブル管理はマイグレーションを使う
  • testはテスト用のDBを使う

こんな人におすすめ

  • GoとMySQLでAPIサーバーの開発がしたい
  • 環境構築はDockerで手っ取り早く済ませたい
  • 拡張しやすいGoのプロジェクトが欲しい

使用するフレームワーク、バージョン

バージョン
Go 1.15
MySQL 5.7
nginx 1.19

GoのフレームワークはGin、ORMにGORMを使用。

ディレクトリ構成

├── docker-compose.yml
├── Dockerfile
├── app
│   ├── cmd
│   │   ├── migrate
│   │   │   └── main.go
│   │   └── server
│   │       └── main.go
│   ├── db
│   │   └── migrations
│   │       ├── 1_create_users.down.sql
│   │       └── 1_create_users.up.sql
│   ├── go.mod
│   ├── go.sum
│   └── pkg
│       ├── connecter
│       │   └── connecter.go
│       ├── controller
│       │   ├── router.go
│       │   └── users.go
│       └── model
│           ├── main_test.go
│           ├── user.go
│           └── user_test.go
├── mysql
│   ├── Dockerfile
│   ├── docker-entrypoint-initdb.d
│   │   └── init.sql
│   └── my.cnf
└── nginx
    ├── Dockerfile
    └── default.conf

ルートディレクトリにあるDockerfileがGoのコンテナ用です。

使い方

GitHubレポジトリはこちらにあります。
https://github.com/fuhiz/docker-go-sample

まずはdocker-compose.ymlがあるディレクトリでコンテナを立ち上げます。

$ docker-compose up -d --build

Goのコンテナに入ります。

$ docker-compose exec web bash

Goのコンテナの/appでマイグレーションを実行します。
実行されるSQLはapp/db/migrationsのファイルです。
usersとマイグレーション管理のためのschema_migrationsが作られます。

$ go run cmd/migrate/main.go -exec up

usersには名前(name)、年齢(age)、日時カラムを用意しました。

/db/migrations/1_create_users.up.sql
CREATE TABLE `users` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `age` int NOT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  `deleted_at` datetime,
  PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8mb4;

サーバーを起動します。

$ go run cmd/server/main.go

これでlocalhost:8082でGoのAPIにつながるようになります。

APIを使ってみる

DBにはusersテーブルがあって、ユーザーのCRUD機能が使えるようになっているので、curlで確認します。

  • ユーザー作成
$ curl localhost:8082/api/v1/users -X POST -H "Content-Type: application/json" -d '{"name": "test", "age":30}'
  • ユーザー一覧
$ curl localhost:8082/api/v1/users
{"users":[{"ID":1,"CreatedAt":"2021-01-09T11:09:31+09:00","UpdatedAt":"2021-01-09T11:09:31+09:00","DeletedAt":null,"name":"test","age":30}]}%

先ほど作ったユーザーが取得できます。

  • ユーザー更新
$ curl localhost:8082/api/v1/users/1 -X PATCH -H "Content-Type: application/json" -d '{"name": "update", "age":31}'
  • ユーザー削除
$ curl localhost:8082/api/v1/users/1 -X DELETE

docker-compose.yml

ここから環境構築の細かいところを見ていきます。

docker-compose.ymlの基本的な書き方には触れないので、参考にされる方はこちらを。
https://qiita.com/zembutsu/items/9e9d80e05e36e882caaa

それぞれのserviceについてはこのようになっております。

db

  • MySQLコンテナ
  • ユーザー名やパスワードなどを環境変数で定義。
  • docker-entrypoint-initdb.dをマウントして、コンテナ起動時にdocker-entrypoint-initdb.d/init.sqlが実行されるようにします。init.sqlでgo_sampleとgo_sample_testというデータベースを作ります。 /docker-entrypoint-initdb.dはMySQLのDockerイメージに備わっているディレクトリで初期データを作ることができます。
  • ホスト側のポートが3310なのはローカルで動かすMySQLと被らないようにするためです。
  • Sequel Proで接続するときはこうなります。

スクリーンショット 2021-01-03 11.24.13 (1).png
※パスワードはlocalpass。データベースは空でも構わないです。

web

  • Goコンテナ
  • 起動後すぐにコンテナが閉じてしまわないようにtty: trueでコンテナを永続化します。 (サーバー起動をDockerfileに書かず、コンテナの中で手動で実行するためです)
  • Goプロジェクト内で使う環境変数を定義。Goのコードでos.Getenv("DB_PASSWORD")とすればこの値が読み込めます。DB_HOSTのdbはMySQLコンテナのサービス名です。GORMでDB接続するときにこのサービス名で接続できます。
  • Goプロジェクトがある./app(ホスト)を/app(コンテナ)にマウントします。コンテナ内の/appはDockerfileのWORKDIRで指定したときに作成されます。

proxy

  • nginxはリバースプロキシによってURLを転送します。ここではhttp://localhostがGoのAPIになるように設定する目的で使います。
  • ホスト側のportは8082を指定しました。
docker-compose.yml
version: "3"

services:
  db:
    build: ./mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: localuser
      MYSQL_PASSWORD: localpass
      TZ: Asia/Tokyo
    volumes:
      - ./mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    ports:
      - "3310:3306"

  web:
    build: .
    tty: true
    environment:
      APP_MODE: local
      DB_PASSWORD: localpass
    volumes:
      - "./go:/app"
    depends_on:
      - db

  proxy:
    build: ./nginx
    ports:
      - 8082:80
    depends_on:
      - web

MySQLコンテナ

MySQLのDockerfileはこれらの一般的な設定です。

タイムゾーンをAsia/Tokyoにする。
設定ファイルのmy.cnfをコピーする。
起動時に実行するinit.sqlをコピーする。

mysql/Dockerfile
FROM mysql:5.7

ENV TZ Asia/Tokyo
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && chown -R mysql:root /var/lib/mysql/

COPY my.cnf /etc/mysql/conf.d/my.cnf
COPY docker-entrypoint-initdb.d/init.sql /docker-entrypoint-initdb.d/

CMD ["mysqld"]

EXPOSE 3306

init.sqlではgo_sampleとテスト用のgo_sample_testを作ってユーザーの権限設定をします。

mysql/docker-entrypoint-initdb.d/init.sql
CREATE DATABASE IF NOT EXISTS `go_sample` COLLATE 'utf8mb4_general_ci' ;
CREATE DATABASE IF NOT EXISTS `go_sample_test` COLLATE 'utf8mb4_general_ci' ;

GRANT ALL ON `go_sample`.* TO 'localuser'@'%' ;
GRANT ALL ON `go_sample_test`.* TO 'localuser'@'%' ;

FLUSH PRIVILEGES ;

Goコンテナ

内容はコメントの通りで、外部パッケージをダウンロードするためにgo.modとgo.sumを事前にコピーしています。

Dockerfile
FROM golang:1.15

## 作業ディレクトリ
WORKDIR /app

# モジュール管理のファイルをコピー
COPY go/go.mod .
COPY go/go.sum .

# 外部パッケージのダウンロード
RUN go mod download

EXPOSE 9000

nginxコンテナ

nginx.confで読み込むdefault.confをコピーします。

nginx/Dockerfile
FROM nginx:1.19-alpine

COPY ./default.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
nginx/default.conf
server {
    listen 80;
    server_name localhost;

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

nginxの設定ファイルである/etc/nginx/nginx.confで/etc/nginx/conf.d/配下の*.confを読み込むようになっているので、読み込まれる部分だけを作っています。

nginxの設定はこちらが参考になります。
https://qiita.com/morrr/items/7c97f0d2e46f7a8ec967

大事なのはproxy_pass http://web:9000;の部分で、ここでhttp://localhosthttp://web:9000に置き換えています。
webはdocker-compose.ymlで定義したGoコンテナのサービス名です。
docker-composeはサービス間でネットワーク通信できるので、このような指定ができます。
Goプロジェクトはポートを9000でサーバーを立ち上げているのでポートはそれに合わせます。

また、docker-compose.ymlのnginxコンテナでポートを8082:80としているので、ホストからはhttp://localhost:8082でアクセスします。

ややこしいですが、とどのつまりはhttp://localhost:8082でGoのAPIが叩けることになります。

Goのプロジェクト概要

Goのコードはなるべく実践的に使えるものを意識して作りました。
ディレクトリ構造はこちらを参考にしています。
https://qiita.com/sueken/items/87093e5941bfbc09bea8

cmd
アプリケーションのエントリーポイント。
サーバー起動とマイグレーション機能を配置。

db
マイグレーションで実行したいsqlファイルを配置。

pkg
アプリケーションの挙動に関わる部分。
モデル(model)、コントローラー(controller)、接続(connecter)を作成。

マイグレーション

マイグレーション周りはこちらを参考にさせていただきました。
https://qiita.com/tanden/items/7b4fb1686a61dd5f580d

golang-migrateを使用して、db/migrationsにあるsqlファイルでDBを管理します。

ファイル名のルールは{version}を昇順にすれば、番号でもタイムスタンプでも問題ありません。
https://github.com/golang-migrate/migrate/blob/master/MIGRATIONS.md

{version}_{title}.up.{extension}
{version}_{title}.down.{extension}

ここではusersテーブルを作成する1_create_users.up.sqlとテーブル削除用の1_create_users.down.sqlを作成しました。

マイグレーション管理のファイルはcmd/migrate/main.goにあります。内容は参考サイトのほぼコピペになります。

このファイルを実行すれば追加した分の*.up.sqlだけが走ります。

$ go run cmd/migrate/main.go -exec up

戻したいときはオプションをdownにすれば、全ての*.down.sqlが実行されます。

$ go run cmd/migrate/main.go -exec down

test用のデータベースに接続したいときはAPP_MODE=testで環境変数つきで実行します。

$ APP_MODE=test go run cmd/migrate/main.go -exec up

cmd/migrate/main.goのinit()でAPP_MODEがtestなら、データベースはDB_NAME_TESTを使うようにしてます。

cmd/migrate/main.go
func init() {
    // database name decide by APP_MODE
    dbName := os.Getenv("DB_NAME")
    if os.Getenv("APP_MODE") == "test"{
        dbName = os.Getenv("DB_NAME_TEST")
    }

    Database = fmt.Sprintf("mysql://%s:%s@tcp(%s:%s)/%s",
        os.Getenv("DB_USER"),
        os.Getenv("DB_PASSWORD"),
        os.Getenv("DB_HOST"),
        os.Getenv("DB_PORT"),
        dbName)
}

GoのAPIの処理の流れ

エントリーポイントとなるファイルはcmd/server/main.go。
ginを使って、ポート9000でサ-バーを立ち上げています。

cmd/server/main.go
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"

    "github.com/fuhiz/docker-go-sample/app/pkg/connecter"
    "github.com/fuhiz/docker-go-sample/app/pkg/controller"
)

func main() {
    // gormのDB接続
    connecter.Setup()

    router := gin.Default()

    // apiの疎通確認用
    router.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "Response OK")
    })

    // routing
    r := router.Group("/api/v1")
    controller.Setup(r)

    router.Run(":9000")
}

細かい処理は/pkgのcontrollerなどを使っていきます。

gormのDB接続はpkg/connecter/connecter.goで行います。
変数dbに*gorm.DBを格納して、DB()で呼び出せる形になっています。

接続の仕方は公式を見れば大体把握できます。
https://gorm.io/docs/connecting_to_the_database.html

データベースの各パラメータはdocker-compose.ymlで定めた環境変数から取得しています。
ここでもAPP_MODEがtestならDB_NAME_TESTを使います。

pkg/connecter/connecter.go
package connecter

import (
    "fmt"
    "os"

    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var db *gorm.DB

func Setup() {
    // APP_MODEからデータベース名を決める
    dbName := os.Getenv("DB_NAME")
    if os.Getenv("APP_MODE") == "test"{
        dbName = os.Getenv("DB_NAME_TEST")
    }

    // DB接続 (https://gorm.io/docs/connecting_to_the_database.html)
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=%s",
        os.Getenv("DB_USER"),
        os.Getenv("DB_PASSWORD"),
        os.Getenv("DB_HOST"),
        os.Getenv("DB_PORT"),
        dbName,
        os.Getenv("DB_LOC"))
    gormDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

    if err != nil {
        panic(err)
    }

    db = gormDB
}

func DB() *gorm.DB {
    return db
}

ルーティングはpkg/controllers/router.goに書きます。
それぞれpkg/controllers/users.goのfuncを呼びます。

pkg/controllers/router.go
package controller

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

func Setup(r *gin.RouterGroup) {
    users := r.Group("/users")
    {
        u := UserController{}
        users.GET("", u.Index)
        users.GET("/:id", u.GetUser)
        users.POST("", u.CreateUser)
        users.PATCH("/:id", u.UpdateUser)
        users.DELETE("/:id", u.DeleteUser)
    }
}

pkg/controllers/users.goでは処理に応じて/pkg/model/user.goのfuncを呼びます。

pkg/controllers/users.go
package controller

import (
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"

    "github.com/fuhiz/docker-go-sample/app/pkg/connecter"
    "github.com/fuhiz/docker-go-sample/app/pkg/model"
)

type UserController struct{}

type UserParam struct {
    Name string `json:"name" binding:"required,min=1,max=50"`
    Age  int    `json:"age" binding:"required,number"`
}

// ユーザー取得
func (self *UserController) GetUser(c *gin.Context) {
    ID := c.Params.ByName("id")
    userID, _ := strconv.Atoi(ID)
    user, err := model.GetUserById(connecter.DB(), userID)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user not found"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"user": user})
}

// ユーザー一覧
func (self *UserController) Index(c *gin.Context) {
    users, err := model.GetUsers(connecter.DB())

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user search failed"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"users": users})
}

// ユーザー作成
func (self *UserController) CreateUser(c *gin.Context) {
    var param UserParam
    if err := c.BindJSON(&param); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    newUser := model.NewUser(param.Name, param.Age)
    user, err := model.CreateUser(connecter.DB(), newUser)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user create failed"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"user": user})
}

// ユーザー更新
func (self *UserController) UpdateUser(c *gin.Context) {
    ID := c.Params.ByName("id")
    userID, _ := strconv.Atoi(ID)
    user, err := model.GetUserById(connecter.DB(), userID)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user not found"})
        return
    }

    var param UserParam
    if err := c.BindJSON(&param); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    updateParam := map[string]interface{}{
        "name": param.Name,
        "age":  param.Age,
    }

    _, err = user.Update(connecter.DB(), updateParam)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user update failed"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"user": user})
}

// ユーザー削除
func (self *UserController) DeleteUser(c *gin.Context) {
    ID := c.Params.ByName("id")
    userID, _ := strconv.Atoi(ID)
    user, err := model.GetUserById(connecter.DB(), userID)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user not found"})
        return
    }

    _, err = user.Delete(connecter.DB())

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user delete failed"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"deleted": true})
}
/pkg/model/user.go
package model

import (
    "gorm.io/gorm"
)

type User struct {
    gorm.Model
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func NewUser(name string, age int) *User {
    return &User{
        Name: name,
        Age:  age}
}

func CreateUser(db *gorm.DB, user *User) (*User, error) {
    result := db.Create(&user)

    return user, result.Error
}

func GetUsers(db *gorm.DB) ([]*User, error) {
    users := []*User{}
    result := db.Find(&users)

    return users, result.Error
}

func GetUserById(db *gorm.DB, ID int) (*User, error) {
    user := User{}
    result := db.First(&user, ID)

    return &user, result.Error
}

func (user *User) Update(db *gorm.DB, param map[string]interface{}) (*User, error) {
    result := db.Model(&user).Updates(param)

    return user, result.Error
}

func (user *User) Delete(db *gorm.DB) (*User, error) {
    result := db.Delete(&user)

    return user, result.Error
}

テスト

テストはGoのコンテナ内でAPP_MODE=testをつけて実行します。
以下手順。

マイグレーション(up)でgo_sample_testにテーブルを作成。

$ APP_MODE=test go run cmd/migrate/main.go -exec up

/pkgをテスト。

$ APP_MODE=test go test -v ./pkg/...

次のテストのためにgo_sample_testを戻す。

$ APP_MODE=test go run cmd/migrate/main.go -exec down

テストファイルは/pkg/modelにmain_test.goとuser_test.goがあります。
TestMainが最初に実行されるので、そこでDB接続しときます。

/pkg/model/main_test.go
package model_test

import (
    "testing"

    "github.com/fuhiz/docker-go-sample/app/pkg/connecter"
)

func TestMain(m *testing.M) {
    connecter.Setup()
    m.Run()
}

ユーザー作成のテスト。

/pkg/model/user_test.go
package model_test

import (
    "testing"

    "github.com/fuhiz/docker-go-sample/app/pkg/connecter"
    "github.com/fuhiz/docker-go-sample/app/pkg/model"
)

func TestCreateUser(t *testing.T) {
    newUser := model.NewUser("test_user", 30)
    user, _ := model.CreateUser(connecter.DB(), newUser)

    if user.Name != "test_user" {
        t.Fatal("model.CreateUser Failed")
    }
}

まとめ

ローカル環境としてはそれなりに使える環境が整えられたと思います。
自動テストやデプロイにも対応できるかは今後検証していきたいです。

Goはまだまだベストプレクティスが確立されていないようでテスト環境の切り分けは苦労しました。
改めてLaravelやRailsのような全部入りのフレームワークの偉大さも感じました。

長めの記事でしたが参考にしてもらえたらありがたいです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6[APIモード]+ MySQL5.7 を Docker で環境構築

はじめに

自分用です
Rails6 APIモード + MySQL5.7 を Docker(docker compose) で環境構築

Dockerそのものの導入は省略

はじめに環境構築に必要なファイルを作成

  • 以下のファイルを作成する
    • アプリ用のトップレベルディレクトリ
      • Dockerfile
      • docker-compose.yml
      • Gemfile
      • Gemfile.lock

アプリ用のトップレベルディレクトリ作成&移動

$ cd

$ mkdir sample_app

$ cd sample_app


Dockerfile, docker-compose.yml, Gemfile, Gemfile.lock作成

sample_app$ touch {Dockerfile,docker-compose.yml,Gemfile,Gemfile.lock}

sample_app$ ls
Dockerfile docker-compose.yml Gemfile Gemfile.lock


ファイルの中身書いていく

sample_app/Dockerfileファイル

Dockerfile
FROM ruby:2.6.5

# 必要なパッケージのインストール(Rails6からWebpackerがいるので、yarnをインストールする)
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
        && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
        && apt-get update -qq \
        && apt-get install -y build-essential libpq-dev nodejs yarn

# 作業ディレクトリの作成
RUN mkdir /myapp
WORKDIR /myapp

# ホスト側(ローカル)(左側)のGemfileを、コンテナ側(右側)のGemfileへ追加
ADD ./Gemfile /myapp/Gemfile
ADD ./Gemfile.lock /myapp/Gemfile.lock

# Gemfileのbundle install
RUN bundle install
ADD . /myapp


sample_app/docker-compose.yml

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: root
    ports:
    - "3306:3306"

  web:
    build: .
    command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3001 -b '0.0.0.0'"
    tty: true
    stdin_open: true
    depends_on:
      - db
    ports:
      - "3001:3001"
    volumes:
      - .:/myapp


sample_app/Gemfile

Gemfile
source 'https://rubygems.org'

gem 'rails', '~> 6.0.3'


Gemfile.lockは空のまま

docker-compose run コマンドで Rails アプリを作成

  • APIモードなので--apiオプション付与
  • バージョン6以降なので、--webpackerオプション付与
$ docker-compose run web rails new . --force --database=mysql --skip-bundle --api --webpacker


database.yml ファイルを修正

  • sample_app/config/database.yml ファイルに、コンテナに作成されたDB情報を記述する
database.yml
default: &dafault
    adapter: mysql2
    encoding: utf8
    pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
    username: root
    password: password  # docker-compose.ymlの MYSQL_ROOT_PASSWORD
    host: db    # docker-compose.ymlの service名


Dockerコンテナを起動する

  • コンテナの実行
$ docker-compose build


  • コンテナを起動
    • -d オプション付与でバックグラウンド実行をする。これを実行すると、コンテナを起動したままプロンプト画面へ戻る
$ docker-compose up -d


DBを作成する

  • まだコンテナを起動していない場合はしておく
$ docker-compose up -d


  • コンテナID確認
$ docker ps -a


  • 確認したIDより、コンテナに入る
$ docker exec -it <コンテナのID> /bin/bash


  • DB作成
$ rails db:create
$ rails db:migrate


  • コンテナから出る
$ exit


  • コンテナに入らず、ローカルから実行する場合。コンテナ起動後に、docker-compose run コマンドを実行する
$ docker-compose run web rails db:create
$ docker-compose run web rails db:migrate



構築は以上。
localhost:3001で開くようになった。



その他

サーバーを止める場合

  • Ctrl + C で止めないこと。コンテナが残って次回起動時にエラーが出る
    • もしやってしまった場合、tmp/pids/server.pid を削除して、再びdocker-compose upで再起動する
$ docker-compose down


Dockerfileやdocker-compose.ymlの変更を反映、railsサーバー再起動

$ docker-compose up --build


bundle install などのコマンドを実行したい場合

#  docker-compose run { サービス名 } { 任意のコマンド }
$ docker-compose run web bundle install


ローカルからMySQLコンテナに接続

  • コンテナ起動してない場合は起動
$ docker-compose up -d


  • mysqlのidを確認
$ docker ps


  • MySQLコンテナにログイン
$ docker exec -it <MySQLのコンテナのID> /bin/bash


$ mysql -u root -p -h 0.0.0.0 -P 3306 --protocol=tcp

mysql>

// 脱出
mysql> quit



以上。



参考にさせて頂いた記事

丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)
【Rails】Rails 6.0 x Docker x MySQLで環境構築

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker in LXD

Docker in LXD

Docker をあれこれ使ってたら、Docker Engine をたくさん立ち上げる羽目になったりしませんか? 少なくとも私はそうです。

方法はいろいろとあるのですが、Docker in LXD が昔と違って楽に構築できるので最近は Docker Engine をインストールするときは LXD コンテナーの上に構築しています。

LXD のセットアップ

Ubuntu 20.04 LTS はインストール時に Snap で LXD がインストール済みです。でも、LXD を使えるようにするために lxd init で初期化する必要があります。

$ lxd init

Would you like to use LXD clustering? (yes/no) [default=no]:

...

なんか色々聞かれます。基本的にデフォルトでいいのですが、ストレージバックエンドは必ず btrfs を選びます。(デフォルトが zfs なことが多いようで、明示的に btrfs にしてください)

...

Name of the storage backend to use (ceph, btrfs, dir, lvm) [default=btrfs]: btrfs

...

動作確認のために、LXD コンテナーを作成し起動して、そのまま終了します。

$ lxc launch ubuntu:20.04 ubuntu
Creating ubuntu
Starting ubuntu

$ lxc exec ubuntu bash
root@ubuntu:~# exit

動作確認が終わったのでコンテナーを削除します。

$ lxc rm -f ubuntu

Docker in LXD

LXD の素のコンテナーでは Docker Engine はインストールできるかもしれませんが、Docker コンテナーは起動しません。

LXD のドキュメントの LXD コンテナー内で docker を実行できますか?によると、security.nestingtrue にすればよいようです。

$ lxc launch -c security.nesting=true ubuntu:20.04 docker
Creating docker
Starting docker

念のため、設定されているかを調べます。

$ lxc config show docker
config:
...

  security.nesting: "true"

...

問題ないので、Docker Engine をインストールします。

$ lxc exec docker bash
root@docker:~# curl https://get.docker.com | sh

インストールが終わったので Docker コンテナーを起動します。

root@docker:~# docker run --rm hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

動きました!

ストレージドライバーの選定

LXD や Docker Engine はストレージドライバーを選べます。ですが、起動する組み合わせとなると少ないようです。今回の手順で構築した場合は、LXD はも Docker Engine も btrfs で動作しています。

交換可能なストレージ・ドライバ構造 にある表によると、よく使われる Docker Engine のストレージドライバーの overlay2 はホスト側のストレージが ext4xfs でなければなりません。

動きそうな組み合わせは以下の通りです。

LXD のストレージドライバー Docker Engine のストレージドライバー
btrfs btrfs
zfs zfs
lvm devicemapper

手元でうまくいったのは btrfs のみです。zfs は LXD コンテナー上で zfsutils-linux をインストールするのに失敗しました。lvm は面倒なので試していません。

Docker Engine の btrfs ストレージドライバーはそれほど性能がよくなさそうなのですが、動くだけでもありがたいです。

なお、Docker Engine のストレージドライバーには vfs というのもありまして、こいつなら LXD のストレージドライバーにかかわらず動くっぽいのですが、開発用で性能最悪らしいです。

security.privileged=true

ググると LXD コンテナーの設定で security.privilegedtrue にしているのも見かけます。

起動する Docker コンテナーによっては必要になるっぽいのですが、これを有効にするとコンテナーからホスト側へいろいろできてしまうので不要ならやらない方がよさそうです。

security.nesting=true の安全性

LXD の公式ドキュメントで security.nesting=true が安全かどうか (コンテナーからホスト側にアクセスできないか) についての記述が見つかりませんでした。

ググって調べてみても情報がなくて分かりませんでした。このあたり詳しい人がいたら教えてほしいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker 簡単インストール手順(CentOS)

本記事の内容

  • Linux(CentOS)にDockerを簡単にインストールする。
  • curl | shでインストールする際の注意点を理解する。

※本手順はインストールするDockerのバージョンや依存パッケージを指定できないため、本番環境での使用は推奨されない。

前提条件

  • CentOS 7 または 8
  • Dockerがインストールされていない。

AWS Lightsail の CentOS バージョン 7.9.2009 で動作を確認。

※インストールスクリプトはDebian、Fedora、Raspbian、RHEL、Ubuntuにも対応しているが、コマンドyumcurlsystemctlの部分は適宜読み替える必要がある。
※RHELはコマンドが同一のため、本手順がそのまま使える想定。

インストール手順

1. 未インストール確認

$ yum list installed | grep docker

何も表示されなければ、Dockerがインストールされていないので作業を進める。

2. 管理者(root)への切替

$ sudo su -

3. インストール

公式サイトより、curlでインストールスクリプトを取得し、shで実行する。

# curl -fsSL https://get.docker.com | sh

スクリプトはLinuxの種類を自動判別し、必要なコマンドを実行する。
CentOSの場合の主な内容は以下の通り。

  1. 「yum-utils」のインストール。(2. の実行に必要)
  2. 「yum-config-manager」で、Dockerの取得先リポジトリhttps://download.docker.com/linux/centos/docker-ce.repoを追加。
  3. 「docker-ce」と依存パッケージをインストール。

スクリプトの内容はブラウザからも確認できる。
https://get.docker.com

このようなインストール方法には注意点もある。
提供スクリプトに悪意がある場合はもちろんだが、ブラウザから確認した際には正しいスクリプトを返し、curlなどコマンドから要求した際には不正なスクリプトを返すことも、技術的には可能。
curl -fsSL https://get.docker.comのように、| shを除けばスクリプトを実行せずに内容を確認できるので、心配な場合は確認する。

また、スクリプトを取得する際に一部が欠落し、意図しないコマンドが実行される可能性もある。
get.docker.comは、インストール処理を関数「do_install」として定義し、最終行で実行する対策がされている。

4. インストールされたパッケージの確認

$ yum list installed | grep docker
containerd.io.x86_64                1.4.3-3.1.el7              @docker-ce-stable
docker-ce.x86_64                    3:20.10.2-3.el7            @docker-ce-stable
docker-ce-cli.x86_64                1:20.10.2-3.el7            @docker-ce-stable
docker-ce-rootless-extras.x86_64    20.10.2-3.el7              @docker-ce-stable

5. 起動・OS起動時の自動起動設定

# systemctl start docker && systemctl enable docker
Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.

6. バージョン確認

# docker version
Client: Docker Engine - Community
 Version:           20.10.2
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        2291f61
 Built:             Mon Dec 28 16:17:48 2020
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.2
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       8891c58
  Built:            Mon Dec 28 16:16:13 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.3
  GitCommit:        269548fa27e0089a8b8278fc4fc781d7f65a939b
 runc:
  Version:          1.0.0-rc92
  GitCommit:        ff819c7e9184c13b7c2607fe6c30ae19403a7aff
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

7. 動作確認(hello-world)

# docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete 
Digest: sha256:31b9c7d48790f0d8c50ab433d9c3b7e17666d6993084c002c2ff1ca09b96391d
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

参考

公式サイト
Install Docker Engine on CentOS | Docker Documentation

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

M1 MAC(Apple Silicon) でmysqlイメージを動かす

docker-compose.yml
に以下の様にplatformを設定する

services:
  db:
    platform: linux/x86_64
    image: mysql:5.7
    ...

なんでこれで動くのか不思議だったが、
https://www.publickey1.jp/blog/20/apple_m1docker_desktopcpux86docker.html
によると、
「Docker Desktop for Mac」はマルチCPUアーキテクチャ対応しており、上記の指定だと、
linux/x86_64用のイメージをエミュレーションして動作させることになるっぽい。
エミュレーションするため遅くはなるかもしれない

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

既存Rails6アプリのDocker環境への移行

はじめに

ローカル環境でrailsを使用し開発を進めていましたが、今後のデプロイを考慮し、環境をDockerによるコンテナで管理することとしました。
初めてでかなり時間がかかってしまったため、備忘録として残します。
同様の環境構築が必要な方の参考になれば幸いです。

環境構築にあたっての目標

  • ローカルの環境をコンテナ化し、本番環境構築時に容易にしたい。
  • Dockerのイメージは、効率化を考慮し、できるだけ軽量化したい。
  • bundleやyarnのモジュールを永続化することにより、起動にかかる時間を減らしておきたい。
  • DBはsqliteを使用していたので、ついでにMysqlにしたい。

構築した環境

コンテナの構成

  • db
  • web
  • webpacker←webpack_dev_server実行用

バージョン

  • ruby 2.7.2
  • Rails 6.0.3
  • Mysql 8.0.22

一覧

今回変更した箇所のみ記載しています。
ルートディレクトリはrailsアプリのルートディレクトリとしています。

.
├── config
│   ├── database.yml    #更新
│   └── webpacker.yml   #更新
├── docker
│   └── rails
│       └── Dockerfile  #新規作成
├── docker-compose.yml  #新規作成
├── .env                #新規作成
└── Gemfile             #更新

Dockerfile

Dockerfile
#軽量化のため、alpineを使用。
FROM ruby:2.7.2-alpine3.12

ENV TZ="Asia/Tokyo" \
    LANG="C.UTF-8" \
    APP_ROOT="/app" \
    ENTRYKIT_VERSION="0.4.0"

WORKDIR $APP_ROOT

#ENTRY KITの導入
RUN wget https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSION}/entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
    && tar -xvzf entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
    && rm entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
    && mv entrykit /bin/entrykit \
    && chmod +x /bin/entrykit \
    && entrykit --symlink

RUN apk update \
&&  apk add --no-cache \
    alpine-sdk \
    bash \
    build-base \
    mysql-client \
    mysql-dev \
    nodejs \
    tzdata \
    yarn

COPY . $APP_ROOT

ENTRYPOINT [ \
  "prehook", "bundle install -j4 --quiet", "--", \
  "prehook", "yarn install --check-files --ignore-optional", "--"]

イメージの軽量化

今後の作業の効率化のため、イメージの軽量化するために、alpineを使用しています。
約1.6GB→約400MBまで容量が減りました。

マルチステージビルドを使用し、nodejsのインストールを別にすることで、さらにイメージの軽量化ができそうです。
ある程度軽量化できたので、今回はここまでとしました。

ENTRY KITの導入

ENTRY KITを使用し、コンテナ起動時にyarn installbundle installを実行するようにしています。
調べた記事では、Dockerfile内で実行しているものが多かったですが、以下の問題があるため今回は見送りました。

  • gem等の追加が必要となった時は、コンテナのbuildからやり直す必要がある。
  • コンテナをdownすると、installしたgem等が保持されない。

そのためコンテナ起動時の実行かつ、モジュールを永続化することで、上記の問題を解決する構成としました。

docker-compose.yml

docker-compose.yml
version: '3'

services:
  db:
    image: mysql:8.0.22
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
    ports: 
      - '3306:3306'
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_DATABASE: ${DATABASE}
      MYSQL_ROOT_PASSWORD: ${ROOTPASS}
      MYSQL_USER: ${USERNAME}
      MYSQL_PASSWORD: ${USERPASS}

  web: &app_base
    build:
      context: .
      dockerfile: ./docker/rails/Dockerfile
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b 0.0.0.0"
    depends_on:
      - db
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - gem_modules:/vendor/bundle
      - node_modules:/node_modules
    tty: true            #binding.pry実行用
    stdin_open: true     #binding.pry実行用
    environment:
      WEBPACKER_DEV_SERVER_HOST: webpacker    #webpack_dev_server実行用のコンテナを指定
      BUNDLE_APP_CONFIG: ./.bundle
      NODE_ENV: development
      RAILS_ENV: development

  webpacker:             #webpack_dev_server実行用のコンテナ
    <<: *app_base
    command: bash -c "bundle exec bin/webpack-dev-server"
    depends_on:
      - web
    ports:
      - '3035:3035'
    tty: false           #binding.pry不要なのでfalseへ変更
    stdin_open: false    #binding.pry不要なのでfalseへ変更
    environment:
      BUNDLE_APP_CONFIG: ./.bundle
      WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
      NODE_ENV: development
      RAILS_ENV: development

volumes:
  db_data:
  gem_modules:
  node_modules:
  • DBのユーザ情報は別ファイルとしておきたかったので、変数化し.envに記載しています。
  • コンテナをdownさせても、DB、モジュールのデータが失われないよう、永続化しています。

database.yml

database.yml
 default: &default
-  adapter: sqlite3
+  adapter: mysql2
+  username: app
+  password: password
+  host: db    #サービス名を指定

 development:
   <<: *default
-  database: db/development.sqlite3
+  database: mysql_development

webpacker.yml

webpacker.yml
 default: &default
   dev_server:
-    host: localhost
+    host: webpack    #サービス名を指定

Gemfile

Gemfile
-gem 'sqlite3', '~> 1.4'
+gem 'mysql2', '0.5.3'

.env

.env
DATABASE=mysql_development
USERNAME=app
USERPASS=password
ROOTPASS=password

疑問点・解消できなかったこと

コンテナ初回起動時エラー(stack Error: getaddrinfo EAI_AGAIN nodejs.org)

コンテナ初回起動時のyarn install実行中に、エラーが発生することがあります。
調べた結果、原因はDNSで名前解決できていないことだそう。

解決方法と思われるホスト側のDNS変更や、docker-compose.ymlでのDNSの指定等試しましたが、解決しませんでした。
2回目起動時はエラーが発生せず、コンテナの再起動を解決方法としている記事もあったため、このままとしておきます。

error /app/node_modules/node-sass: Command failed.
web_1        | Exit code: 1
web_1        | Command: node scripts/build.js
web_1        | Arguments: 
web_1        | Directory: /app/node_modules/node-sass
web_1        | Output:
web_1        | Building: /usr/bin/node /app/node_modules/node-gyp/bin/node-gyp.js rebuild --verbose --libsass_ext= --libsass_cflags= --libsass_ldflags= --libsass_library=
web_1        | gyp info it worked if it ends with ok
web_1        | gyp verb cli [
web_1        | gyp verb cli   '/usr/bin/node',
web_1        | gyp verb cli   '/app/node_modules/node-gyp/bin/node-gyp.js',
web_1        | gyp verb cli   'rebuild',
web_1        | gyp verb cli   '--verbose',
web_1        | gyp verb cli   '--libsass_ext=',
web_1        | gyp verb cli   '--libsass_cflags=',
web_1        | gyp verb cli   '--libsass_ldflags=',
web_1        | gyp verb cli   '--libsass_library='
web_1        | gyp verb cli ]
web_1        | gyp info using node-gyp@3.8.0
web_1        | gyp info using node@12.20.1 | linux | x64
web_1        | gyp verb command rebuild []
web_1        | gyp verb command clean []
web_1        | gyp verb clean removing "build" directory
web_1        | gyp verb command configure []
web_1        | gyp verb check python checking for Python executable "python2" in the PATH
web_1        | gyp verb `which` succeeded python2 /usr/bin/python2
web_1        | gyp verb check python version `/usr/bin/python2 -c "import sys; print "2.7.18
web_1        | gyp verb check python version .%s.%s" % sys.version_info[:3];"` returned: %j
web_1        | gyp verb get node dir no --target version specified, falling back to host node version: 12.20.1
web_1        | gyp verb command install [ '12.20.1' ]
web_1        | gyp verb install input version string "12.20.1"
web_1        | gyp verb install installing version: 12.20.1
web_1        | gyp verb install --ensure was passed, so won't reinstall if already installed
web_1        | gyp verb install version not already installed, continuing with install 12.20.1
web_1        | gyp verb ensuring nodedir is created /root/.node-gyp/12.20.1
web_1        | gyp verb created nodedir /root/.node-gyp
web_1        | gyp http GET https://nodejs.org/download/release/v12.20.1/node-v12.20.1-headers.tar.gz
web_1        | gyp WARN install got an error, rolling back install
web_1        | gyp verb command remove [ '12.20.1' ]
web_1        | gyp verb remove using node-gyp dir: /root/.node-gyp
web_1        | gyp verb remove removing target version: 12.20.1
web_1        | gyp verb remove removing development files for version: 12.20.1
web_1        | gyp ERR! configure error 
web_1        | gyp ERR! stack Error: getaddrinfo EAI_AGAIN nodejs.org
web_1        | gyp ERR! stack     at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:66:26)
web_1        | gyp ERR! System Linux 4.19.121-linuxkit
web_1        | gyp ERR! command "/usr/bin/node" "/app/node_modules/node-gyp/bin/node-gyp.js" "rebuild" "--verbose" "--libsass_ext=" "--libsass_cflags=" "--libsass_ldflags=" "--libsass_library="
web_1        | gyp ERR! cwd /app/node_modules/node-sass
web_1        | gyp ERR! node -v v12.20.1
web_1        | gyp ERR! node-gyp -v v3.8.0
web_1        | gyp ERR! not ok 

マルチステージビルドでのイメージの軽量化

時間短縮等、更なる効率化が必要となった時に試してみようと思います。

最後に

新規のRailsアプリをDockerで環境構築する記事はあったのですが、既存のRailsアプリをコンテナ化する記事はあまりなかったので苦労しました。
開発環境として一旦構築したので、テスト環境や本番環境の分け方・構築も検討して取り組んでいきます。

他にも以下の内容も取り組んでいきたいので、実施できれば記事にしようかと思います。

  • webサーバとしてnginxの導入
  • CI/CDの導入
  • AWS上での環境構築

参考にさせて頂いた記事

開発しやすいRails on Docker環境の作り方 - Qiita
Rails newからproductionモードで動くようになるまで - Qiita
Rails 6.0 × MySQL8でDocker環境構築(Alpineベース) - Qiita
Docker Composeのvolumesを使ってもっと効率的に - Qiita
【Dockerfile全解説】Rails本番環境のための一番シンプルなDockerイメージを作る - Qiita
【Docker】Rails開発で知っておきたい!gemの永続化による作業効率アップの話 | Enjoy IT Life
docker-composeでの環境構築で留意しておきたいところ - Qiita
Rails + webpacker on Dockerの環境をdocker-composeで構築する - RoadMovie

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む