背景
これまでローカル/Railsサーバーの開発環境で開発を進めていたが、本番環境(EC2)との差分により環境依存が発生。そこで、Dockerを用いてより本番環境に近い開発環境を構築することで環境依存に取られる時間を削減しようと考え実装しました。
開発環境
Ruby 2.7.2
Rails 6.1
MySQL 5.7
Docker for Mac
Nginx
Puma
Supervisor
docker開発環境
$ mkdir docker/dev
まず、アプリのroot配下にdockerフォルダ、その配下にdevフォルダを作ります。
このdev配下にdocker関連のファイルを作成していきます。
root
├ docker
└ dev
├ app
├ nginx
├ dev.ドメイン名.conf
└ nginx.conf
├ supervisor
└ app.conf
└ Dockerfile
├ mysql
├ Dockerfile
└ mysql.cnf
└ docker-compose.yml
├ config ─ dev_puma.rb
└ .env
最終的には上記のようなディレクトリ構造になります。
(アプリ自体のディレクトリは省略)
appコンテナ
Nginx
upstream puma {
server unix:/root/tmp/puma.sock;
}
server {
listen 80;
server_name dev.ドメイン名;
access_log /var/log/nginx/dev.access.log;
error_log /var/log/nginx/dev.error.log;
root /var/www/app/public;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Client-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60;
proxy_read_timeout 60;
proxy_send_timeout 60;
send_timeout 60;
proxy_pass http://puma;
}
client_max_body_size 100m;
error_page 404 /404.html;
error_page 505 502 503 504 /500.html;
try_files $uri/index.html $uri @app;
keepalive_timeout 5;
}
user root;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '[nginx]\t'
'time:$time_iso8601\t'
'server_addr:$server_addr\t'
'host:$remote_addr\t'
'method:$request_method\t'
'reqsize:$request_length\t'
'uri:$request_uri\t'
'query:$query_string\t'
'status:$status\t'
'size:$body_bytes_sent\t'
'referer:$http_referer\t'
'ua:$http_user_agent\t'
'forwardedfor:$http_x_forwarded_for\t'
'reqtime:$request_time\t'
'apptime:$upstream_response_time\t';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
gzip on;
include /etc/nginx/conf.d/*.conf;
}
Nginxのファイルです。
内容については以下の記事をご覧ください。
参考:Nginx設定ファイル
logについてはわかりやすいように「dev.」を頭につけたファイルにしています。
開発環境ではソケットファイルをroot配下に置いています。マウントすると権限等による不具合発生とMacはファイルシステムがLinuxと比較すると違うため、遅延を防ぐためです。
Supervisor
[supervisord]
nodaemon=true
[program:app]
command=bundle exec puma -C config/dev_puma.rb -e development
autorestart=true
stopsignal=TERM
user=root
directory=/var/www/app/
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
stopsignal=TERM
user=root
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Supervisorはプロセス管理、Daemon化、永続化を役割としています。
SupervisorはDockerでのプロセス管理する上でベストプラクティスとしてDocker社の公式で紹介されています。今回はpumaやNginxの起動を担当。
したがって、コンテナ内でpumaやNginxを再起動したい場合重宝するので必須になるかと思います。
Dockerfile/appコンテナ
FROM ruby:2.7.2
ENV APP_ROOT /var/www/app
WORKDIR $APP_ROOT
RUN mkdir -p /root/tmp
RUN curl https://deb.nodesource.com/setup_12.x | bash
RUN curl https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
locales \
vim \
nginx \
supervisor \
nodejs \
yarn \
mariadb-client && \
rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/*
# setting locales
RUN localedef -f UTF-8 -i en_US en_US.UTF-8
# Setup UTC+9
RUN cp -p /etc/localtime /etc/localtime.UTC \
&& cp -p /usr/share/zoneinfo/Japan /etc/localtime
RUN gem install bundler
# yarn install Preparation
RUN npm install n -g
RUN n 12.18.4
## nginx
RUN groupadd nginx
RUN useradd -g nginx nginx
ADD nginx/nginx.conf /etc/nginx/nginx.conf
ADD nginx/dev.ドメイン名.conf /etc/nginx/conf.d/dev.ドメイン名.conf
## supervisor
RUN mkdir -p /var/log/supervisor
ADD supervisor/app.conf /etc/supervisor/conf.d/app.conf
EXPOSE 80
CMD ["/usr/bin/supervisord"]
インストラクション | 説明 |
---|---|
FROM | ベースとなるイメージ。今回はruby:2.7.2とmysql:5.7 |
ENV | 環境変数。 ENV 変数名=値 で定義。今回は=を省略した書き方。 |
WORKDIR | この命令文以下の命令文はWORKDIRで指定したディレクトリで実行される。今回はENVで定義されたAPP_ROOT(=/var/www/app) |
RUN | 実行コマンド。RUN <コマンド> やRUN <実行ファイル> の形で書く。 |
ADD | ADDはファイルを追加する命令。ADD <追加元> <追加先> やADD ["<追加元>",... "<追加先>"] という形で書く。 |
EXPOSE | コンテナ実行時にリッスンするポートやプロトコルをDockerに通知する命令。EXPOSE 80/TCP という形で記述。プロトコルは省略するとデフォルトでTCPになる。 |
CMD | コンテナの初期設定の命令。CMD命令はDockerfileの中で1つだけ指定できる。仮に複数書いた場合は最後のものが採用される。今回はsupervisordを指定することで、管理対象のファイル一緒に実行している。 |
余談だが、Docker imageにはLayerという概念があり、Layerが増えるとimage(コンテナの容量)が大きくなり作業に影響を与えてしまう。Layerを作るコマンドはRUN, COPY, ADDなのでこれらを使う場合は命令文を&&
で繋げたり、バックスラッシュで改行することでLayerの節約に繋がる。
MySQLコンテナ
FROM mysql:5.7
RUN apt-get update && \
apt-get install -y apt-utils \
locales && \
rm -rf /var/lib/apt/lists/* && \
echo "ja_JP.UTF-8 UTF-8" > /etc/locale.gen && \
locale-gen ja_JP.UTF-8
ENV LC_ALL ja_JP.UTF-8
ADD mysql.cnf /etc/mysql/mysql.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
[client]
default-character-set=utf8mb4
命令文は割愛するが、DockerfileについてはAPPコンテナと同じ要領で書く。
docker-compose.yml
version: '2'
services:
app:
container_name: アプリ名-app
build: ./app/
image: アプリ名-app
ports:
- '80:80'
volumes:
- ~/git/github/アプリディレクトリ名:/var/www/app/
tty: true
depends_on:
- db
db:
container_name: アプリ名-db
build: ./mysql/
image: アプリ名-db
ports:
- '3306:3306'
environment:
MYSQL_DATABASE: アプリ名_dev
MYSQL_ROOT_PASSWORD: password
TZ: "Asia/Tokyo"
volumes:
- ./mysql/db_data:/var/lib/mysql
設定 | 説明 |
---|---|
version | バージョンは2もしくは3を選択しましょう。詳しくは公式ドキュメントを確認。 |
services | サービスの指定。ネストさせてサービス名を指定。今回はappとdb。 |
container_name | コンテナ名(何でも良いです。わかりやすいのにしましょう)今回はapp側は「アプリ名-app」、db側は「アプリ名-db」としました。 |
build | servicesをビルドしたりリビルドしたりする。 |
image | イメージ名 |
ports | ポートのマッピング設定。'ホスト側:コンテナ側'という形で指定し、コンテナ側のポートをホスト側のポートにマッピングする。 |
environment | 環境変数 |
volumes | *1 |
tty | コンテナを起動し続ける |
depends_on | サービス間の依存関係を設定。今回はサービスdbに依存させている。 |
*1
volumes - volume(データの永続化領域)の定義
volume とは、コンテナのライフサイクルが終了した後でもデータを保管しておけるデータ領域です。特徴は以下の通りです。
データの永続化を目的とした機能のため、コンテナが削除されても volume が明示的に破棄されない限り、volume 中のデータは保持される。
volume は、特定のコンテナ専用の volume だけでなく、複数のコンテナ間から参照できる volume も作成できる。
ホスト側のディレクトリを volume としてコンテナ内にマウントできる。
本機能はホストとコンテナ間でファイルを受け渡すときに利用できる。
引用:さわって理解する Docker 入門
volumes: - ./mysql/db_data:/var/lib/mysql
こちらはホスト側./mysql/db_dataにデータを保存しています。起動した後にローカルのdocker/deb/mysql配下を見るとわかると思いますが、docker-compose down
してもMySQLコンテナのデータは削除されません。なのでデータ全体を削除したい場合は上記のディレクトリを削除する必要があります。
ちなみにgitignore
に上記のディレクトリも追加しないとgitで管理されてしまうので必ずgitignoreに記載してください。
その他
Puma設定ファイル
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count
worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
bind "unix:///root/tmp/puma.sock"
environment ENV.fetch("RAILS_ENV") { "development" }
pidfile ENV.fetch("PIDFILE") { "/root/tmp/server.pid" }
plugin :tmp_restart
ここは本番環境と形は同じです。各コードが何を実行しているのか、確認したい場合は以下の記事に記載していますのでご確認ください。
参考: puma設定ファイル
先程Nginx設定ファイルでも説明しましたが、開発環境では権限等による不具合の発生、Macのファイルシステムによる遅延の防止の為、ソケットファイルやpidファイルをroot配下に置いています。
環境変数
GOOGLE_MAP_API_KEY=XXXXXXXXXXXXXXX
AMAZON_S3_ACCESS_KEY_ID=XXXXXXXXXXXXXXX
AMAZON_S3_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXX
AMAZON_S3_BUCKET=XXXXXXXXXXXXXXX
DATABASE_HOST=db
DATABASE_NAME=アプリ名_dev
DATABASE_USER=root
DATABASE_PASSWORD=password
DATABASE_SOCKET=/tmp/mysql.sock
私のポートフォリオではGoogleMapsAPIやS3を使っていたため、環境変数に入れています。ご自身のポートフォリオで使っているAPIキーは入れておきましょう。
今回、使っているのがMySQLなので必要な情報を入れています。
HOST名をdbにしておりますが、ここには本来MySQLコンテナのIPアドレスが指定されます。dbとしているのは、docker-compose.ymlでサービス名をdbとしているため、名前解決されMySQLコンテナのIPアドレスを参照して実行できます。
Docker起動
それでは、dockerを起動して開発環境を構築しましょう。
- git clone (もしまだやっていなかったら)
$ mkdir -p ~/git/github
$ cd ~/git/github
$ git clone リモートリポジトリ
- add localhost /etc/hosts
$ sudo vi /etc/hosts
以下に修正
127.0.0.1 dev.ドメイン名
- 環境変数(もしくは自分で環境変数作ってもOKです。)
$ cd ~/git/github/Tamari-Ba
$ cp .env.sample .env
- docker run
$ cd docker/dev
$ docker-compose up -d
- app deploy
$ docker exec -it appコンテナ名 bash
$ yarn install
$ bundle install
$ rails db:migrate
$ /usr/bin/supervisorctl restart app
- Access
http://dev.ドメイン名/
にアクセスしてブラウザにアプリ画面が表示されていれば成功です!
あとは開発できるか一通り確認しておきましょう。
- DB login
ローカルと同じようにmysqlにログインすることも可能です。
docker exec -it appコンテナ名 bash
mysql -u root -h db -p
注意点
-
docker rm
やdocker rmi
でコンテナ/イメージを削除してもpidは残ります。なので、新たにコンテナを立ち上げたときにmigrate等を失敗すると思います。これは、pidファイルを削除するか、個人ポートフォリオレベルであれば一度レポジトリごと削除してしまえば解消可能です。 -
以下のエラー、もしどこを直しても解消できない場合は右上のdockerアイコンを確認して、起動しているか確認してください。私は全然気が付かず再起動することで解消しましたw
ERROR: for コンテナ名 UnixHTTPConnectionPool(host='localhost', port=None): Read timed out. (read timeout=60)
ERROR: for app UnixHTTPConnectionPool(host='localhost', port=None): Read timed out. (read timeout=60)
ERROR: An HTTP request took too long to complete. Retry with --verbose to obtain debug information.
If you encounter this issue regularly because of slow network conditions, consider setting COMPOSE_HTTP_TIMEOUT to a higher value (current value: 60).
まとめ
- Docker環境構築は本番環境とできるだけ環境を近づけることができ、「環境依存」を防げるメリットがある。
- 扱うにはLinux, MySQL, Nginx等のサーバー周りの知識が必要。
- Dockerの公式ドキュメントはわかりやすい