はじめに
本エントリーのゴールは以下のような状態です。
- (ほぼ)Dockerだけしかインストールされていないサーバーが1つある
- そのサーバーのDockerで複数のWebアプリがホストされている
- 各WebアプリはDocker Composeで構成管理されていて、アプリごとに
docker-compose up
するだけで動く -
docker-compose up
した各Webアプリには、指定したサブドメインが自動で割り当てられて、Let’s Encrypt で取得したSSL証明書が適用される
この状態になるまでに必要な作業をできるだけ丁寧に解説していきます。やや長くなりますが、ぜひ最後までお付き合いください。
目次
- Ubuntu 16.04のサーバーを用意する
- サーバーにDockerをインストールする
- リバースプロキシを構築
- サーバーへのDockerのインストールからリバースプロキシの構築までをAnsibleで自動化
- Webアプリを作る
- Webアプリを実際にデプロイしてみる
1. Ubuntu 16.04のサーバーを用意する
まずサーバーを1つ用意しましょう。 僕は DigitalOcean の$5/moプランでUbuntu 16.04をよく借りて使うので、ここではUbuntu 16.04を前提に話を進めます。
2. サーバーにDockerをインストールする
サーバーが用意できたら、DockerおよびDocker Composeをインストールします。 しかしその前に、DigitalOceanの場合、インスタンス(Droplet)作成直後はrootしかユーザーがいないので、まずは一般ユーザーを作成しましょう。
2-1. 一般ユーザーを作成する
sudoできる一般ユーザーを作ります。ここでは例としてユーザー名を ubuntu
とします。
% adduser ubuntu
% adduser ubuntu sudo
さらに、ローカルのssh公開鍵を /home/ubuntu/.ssh/authorized_keys
に追加してsshログインできるようにしておきましょう。
ssh-copy-id
コマンドを使うと楽です。
Macなら
$ brew install ssh-copy-id $ ssh-copy-id -i ~/.ssh/id_rsa.pub ubuntu@<host>
これ以降はサーバー上の作業は ubuntu
ユーザーで行う前提で解説していきます。
2-2. Dockerをインストール
公式のドキュメント のとおりに作業すれば簡単にインストールできます。
実際に打つコマンド
$ sudo apt-get remove docker docker-engine
$ sudo apt-get update
$ sudo apt-get install \
linux-image-extra-$(uname -r) \
linux-image-extra-virtual
$ sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo apt-key fingerprint 0EBFCD88
# Key fingerprint = 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88 が出力されることを目視で確認
$ sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
$ sudo apt-get update
$ sudo apt-get install docker-ce
$ sudo docker run hello-world
# ちゃんと動作することを確認
sudo不要にする&ブート時にdockerサービスを自動起動する
このままだと sudo
でしか docker
コマンドが使えなくて不便なので、公式ドキュメント(続き) を参考に、ubuntu
ユーザーを docker
グループに追加しましょう。
$ sudo groupadd docker
$ sudo usermod -aG docker $USER
これだけで、sudo
しなくても docker
コマンドが使えるようになります。グループを反映させるために、一度 ubuntu
からログアウトして再度ログインし直してください。
$ docker run hello-world
して使えることを確認してみましょう。
また、サーバーのブート時にdockerサービスが自動で起動するようにしておきたいので、こちらも 公式ドキュメント(続き) を参考に、
$ sudo systemctl enable docker
これだけやっておきましょう。
2-3. Docker Composeをインストール
同じく 公式のドキュメント のとおりに作業すれば簡単にインストールできます。
実際に打つコマンド
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.12.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# 1.12.0は記事公開時点での最新バージョン
$ sudo chmod +x /usr/local/bin/docker-compose
$ docker-compose --version
# ちゃんと動作することを確認
3. リバースプロキシを構築
無事にDockerのインストールが完了したので、続いてDockerコンテナ上で動作するリバースプロキシを構築しましょう。 このリバースプロキシが、後から追加でデプロイする各Webアプリにサブドメインを割り当ててSSL証明書を適用してくれます。
ここでは以下の素晴らしいDockerイメージを活用します。
jwilder/nginx-proxy
は、同じDocker Network内で VIRTUAL_HOST
環境変数を持ったコンテナが作成されると自動でリバースプロキシしてくれるというもので、jrcs/letsencrypt-nginx-proxy-companion/
は jwilder/nginx-proxy
と協調してプロキシされる各コンテナのバーチャルホストに対して自動でLet’s EncryptのSSL証明書を取得して適用してくれるというものです。
この2つを立ち上げておくだけで、ほとんど何も考えなくてもSSL対応のサブドメイン運用がとても簡単に実現できてしまいます。
Docker Composeを使ってこの2つのコンテナを使ったリバースプロキシを構築しましょう。
例として ~/nginx-proxy
というディレクトリに構築することにします。(Dockerコンテナとして使うだけなので物理パスはどこでも大丈夫です)
$ mkdir -p ~/nginx-proxy/certs
$ vi ~/nginx-proxy/docker-compose.yml
# ~/nginx-proxy/docker-compose.yml
version: '2'
services:
nginx-proxy:
image: jwilder/nginx-proxy
container_name: nginx-proxy
ports:
- 80:80
- 443:443
volumes:
- ./certs:/etc/nginx/certs:ro
- /etc/nginx/vhost.d
- /usr/share/nginx/html
- /var/run/docker.sock:/tmp/docker.sock:ro
restart: always
letsencrypt-nginx-proxy-companion:
image: jrcs/letsencrypt-nginx-proxy-companion
container_name: nginx-letsencrypt
volumes:
- ./certs:/etc/nginx/certs
- /var/run/docker.sock:/var/run/docker.sock:ro
volumes_from:
- nginx-proxy
restart: always
このような docker-compose.yml
を作って
$ cd ~/nginx-proxy
$ docker-compose up -d
とすればリバースプロキシの構築は完了です。
docker-compose.yml の内容について
基本的にはREADME(jwilder/nginx-proxy#readme / jrcs/letsencrypt-nginx-proxy-companion#readme)で説明されているとおりに書き起こしただけで特別なことはしていません。
ここにあるとおり、nginx-letsencrypt
コンテナから nginx-proxy
コンテナの
/etc/nginx/certs
/et/nginx/vhost.d
/usr/share/nginx/html
の3つのボリュームに対してRead/Writeできる必要があるということなので、
-
nginx-proxy
コンテナでボリュームを作成 -
volumes_from
でnginx-proxy
コンテナのボリュームをnginx-letsencrypt
コンテナにすべてマウント
しています。
/etc/nginx/certs
に関しては、両方のコンテナでホストの ~/nginx-proxy/certs
ディレクトリをマウントして、nginx-letsencrypt
コンテナにのみWriteを許可しています。
jwilder/nginx-proxy
は同じDocker Network内で新しいコンテナが立ち上がったときにそれを自動検知してバーチャルホストを割り当ててくれるわけですが、ホストの /var/run/docker.sock
をコンテナにマウントすることでこの自動検知が実現されているようです。
なお、マウントする先は nginx-proxy
コンテナと nginx-letsencrypt
コンテナで微妙に違っているので注意が必要です。
/var/run/docker.sock:/tmp/docker.sock:ro
/var/run/docker.sock:/var/run/docker.sock:ro
また、両サービスとも restart: always
を設定して、Docker再起動時に自動的にコンテナが起動するようにしています。
すでにマシンブート時にdockerサービスが自動起動するようには設定したので、これでホストマシン自体の再起動後もDockerコンテナが自動で起動してくれます。
4. サーバーへのDockerのインストールからリバースプロキシの構築までをAnsibleで自動化
実はここまでの作業はすべてAnsibleで自動化しました。
https://github.com/ttskch/ansible-docker
これを使えば上記の作業をコマンド一発で終わらせられます。
$ ANSIBLE_USER_PASSWORD=<some password> ansible-playbook -i hosts -u root playbook.yml
なお、このAnsible Playbookでは、上記で説明した手順に加えて swap領域を設定する というのをやっています。 DigitalOceanの$5/moプランだとメモリが512MBしかなくてたまにメモリ不足になるので。
swap領域の設定の仕方は以下の記事が参考になりました。
docker-machineでプロビジョニング
ちなみに、Dockerホストのプロビジョニングは docker-machine
コマンドを使って行うという手もあります。(こちらの方が簡単かもしれません)
詳細は以下のドキュメントをご参照ください。
5. Webアプリを作る
ではいよいよ、ここまでに作ったサーバーでホストするWebアプリを作りましょう。 僕はPHPerなので今回はNginx+PHP-FPMという構成で作ります。
例として以下のようなファイル構成で作ってみましょう。
$ tree .
.
├── docker
│ ├── nginx
│ │ ├── Dockerfile
│ │ └── default.conf
│ └── php
│ ├── Dockerfile
│ └── php.ini
├── docker-compose.yml
└── index.php
3 directories, 6 files
アプリのソース
アプリ自体の内容は今回はどうでもいいので以下のとおりですw
<?php
// ./index.php
phpinfo();
docker-compose.yml
問題はDockerの構成のほうですが、原則に従って1コンテナ1プロセスにしたいので、アプリの構成にもDocker Composeを使うことにします。
# ./docker-compose.yml
version: '2'
services:
nginx:
build: ./docker/nginx
container_name: myapp-nginx
depends_on:
- php
volumes:
- .:/var/www
environment:
VIRTUAL_HOST: myapp.mydomain.com
LETSENCRYPT_HOST: myapp.mydomain.com
LETSENCRYPT_EMAIL: your.real@email.com
restart: always
php:
build: ./docker/php
container_name: myapp-php
volumes:
- .:/var/www
restart: always
networks:
default:
external:
name: nginxproxy_default
こんな感じで、nginx
と php
というサービスをそれぞれ myapp-nginx
myapp-php
というコンテナ名で作成し、各コンテナはそれぞれ docker/nginx/Dockerfile
docker/php/Dockerfile
をビルドして作ることにします。
nginx
サービスは php
サービスに依存していて、nginx
php
ともホストのカレントディレクトリをコンテナの /var/www
にマウントしています。マウント先のパスは同じである必要はないですが、分かりやすさのためにこうしています。
そして、nginx
サービスに対して以下の3つの環境変数をセットしています。
VIRTUAL_HOST: myapp.mydomain.com
LETSENCRYPT_HOST: myapp.mydomain.com
LETSENCRYPT_EMAIL: your.real@email.com
1つ目の VIRTUAL_HOST
は、冒頭でも触れたように、nginx-proxy
によってリバースプロキシしてもらうために必要なものです。
2つ目と3つ目の LETSENCRYPT_HOST
LETSENCRYPT_EMAIL
は、READMEにあるように Let’s Encryptの証明書を取得するのに必要な情報です。
当然ながら、myapp.mydomain.com
のAレコードにはサーバーのIPアドレスがセットされている必要がありますので、実際にお持ちのドメインで事前にサブドメインを設定しておいてください。
また、特筆すべきは最後のこれです。
networks:
default:
external:
name: nginxproxy_default
nginx-proxy
にリバースプロキシしてもらうためには、プロキシされるコンテナが nginx-proxy
と同じDocker Network内にある必要があることは前述のとおりです。
READMEにある例のように nginx-proxy
とプロキシされるコンテナを同じDocker Composeで構成する場合は問題ないのですが、今回のように nginx-proxy
とプロキシされるコンテナをそれぞれ別のDocker Composeで構成する場合は、そのままだとそれぞれのDocker Composeごとに別々のデフォルトネットワークが作成されてしまうため、明示的に同じネットワークに所属させる必要があります。
この件については、以下の記事がとても参考になりました。
docker-compose で別の docker-compose.yml で作ったコンテナとリンクする (ネットワークを繋げる)
記事で紹介されているように共通リンク用に新しくネットワークを作成して nginx-proxy
とプロキシされるサーバーの両方をそのネットワークに所属させる、という方法でもよかったのですが、今回はちょっと手抜きして、~/nginx-proxy/docker-compose.yml
によって作成されるデフォルトネットワーク nginxproxy_default
に所属させる形にしました。
Dockerfile
最後に、それぞれのコンテナの構成をDockerfileに書きましょう。
Nginx
# ./docker/nginx/Dockerfile
FROM nginx
COPY default.conf /etc/nginx/conf.d/
RUN mkdir -p /var/www
WORKDIR /var/www
# ./docker/nginx/default.conf
server {
listen 80;
server_name localhost;
root /var/www;
index index.php;
location ~ \.php$ {
fastcgi_pass myapp-php:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
ほぼ公式イメージをそのまま使っています。
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
ここでPHPスクリプトのパスの指定にNginxの $document_root
を使っている点に注意です。
今回は nginx
php
両サービスとも .:/var/www
でマウントしているのでこれで問題ないですが、もし nginx
と php
でマウント先のパスが違っている場合は、
fastcgi_param SCRIPT_FILENAME /マウント先のパス/fastcgi_script_name;
といった具合に指定する必要があります。
また、
fastcgi_pass myapp-php:9000;
ここでPHP-FPMのホスト名として、サービス名である php
ではなくコンテナ名である myapp-php
を使っている点にも注意してください。
これは、後々同じDocker Network内に複数のWebアプリをデプロイしていったときに、同じ php
というサービス名を使っているコンテナがあるとどっちに接続されるか確定的でなくなってしまうためです。(実際にやってみたところ、リロードするたびに表示されるアプリが変わるという変態仕様になってしまいましたw)
もちろんコンテナ名も他のアプリと被ってしまえば同じことが起こりますので、アプリ一意な名前を付けるようにしましょう。
PHP-FPM
# ./docker/php/Dockerfile
FROM php:7-fpm
RUN apt-get update \
&& apt-get install -y libicu-dev libmcrypt-dev libxml2-dev \
&& docker-php-ext-install -j$(nproc) intl mcrypt xml mbstring opcache \
&& mkdir -p /var/www
COPY php.ini /usr/local/etc/php/
WORKDIR /var/www
# ./docker/php/php.ini
date.timezone = "Asia/Tokyo"
mbstring.language = "Japanese"
mbstring.internal_encoding = "UTF-8"
memory_limit = 128M
こちらもほとんど公式イメージのままで、いくつかextensionを追加インストールして(今回のアプリでは全く不要ですがw) php.ini
を配置したのみです。
6. Webアプリを実際にデプロイしてみる
さて、ここまでですべての準備が整いました。あとは作ったソース一式をサーバーにデプロイして docker-compose up
すればWebアプリが期待どおりにホスティングできるはずです。
一応、ここまでで書いたコードをGitHubに上げておきました。
https://github.com/ttskch/docker-nginx-proxy-sample
前述した、Docker環境一式をプロビジョンしてくれる ttskch/ansible-docker と合わせて使っていただければ、今すぐにでも動作確認ができると思います。もしよろしければここだけでも一緒に手を動かしてみてください
では実際に試してみましょう。
6-1. 実際にDigitalOceanでDropletを作る
$5/moプランなら1時間使っても0.8円ぐらいなので、気軽に作っちゃいましょうw
6-2. Ansibleでプロビジョニングする
まずサーバー側にpythonをインストールして、
ローカル側からプロビジョニングを実行します。
そしてしばらく待ちます。
6-3. サーバーにログインしてリバースプロキシが起動していることを確認
完了したら再度(今度は ubuntu
ユーザーで)ログインして、docker ps
でリバースプロキシのコンテナが起動していることを確認してみましょう。
OKですね。
6-4. アプリをデプロイ&ビルド
ここでは先ほど作ったGitHubリポジトリをcloneする形でデプロイしてみます。
Dockerfileのビルドに初回は多少時間がかかるので、ここでもしばらく待ちます。ビルドが完了したら docker ps
でコンテナの起動を確認してみましょう。
OKですね。
6-5. 本物のサブドメインを設定
このままだとアプリのホスト名が myapp.mydomain.com
という嘘のドメインになっていてSSL証明書の取得ができないので、手持ちのドメインでサブドメインを発行して実際に確認してみましょう。
今回は myapp.ttskch.com
というサブドメインのAレコードをサーバーのアドレス 128.199.131.35
に設定しました。
docker-compose.yml
の myapp.mydomain.com
を myapp.ttskch.com
に変更して、コンテナを再起動しましょう。
しばらくすると、~/nginx-proxy/certs
に証明書のファイルがダウンロードされていることが確認できると思います。
ドメインを持っていない人は、SSL対応の確認はできませんが、
/etc/hosts
を書き換えることでとりあえずWebアプリの動作だけは確認できるのでやってみましょう。 サーバーの/etc/hosts
に127.0.0.1 myapp.mydomain.com
を、ローカルの/etc/hosts
に128.199.131.35 myapp.mydomain.com
をそれぞれ追記すれば、ローカルのブラウザからmyapp.domain.com
にアクセスすれば表示が確認できます。
6-6. ブラウザで確認
デデーン!バッチリ動いてますね!SSLも有効になっています。
6-7. DigitalOceanのDroplotを削除
今回はただの動作確認なので、不要になったDropletは忘れずに削除しましょう。(もし忘れても$5/moなのでダメージは少ないですが)
おわりに
タイトルに「複数のWebアプリを」とあったのに本文では1アプリしか作りませんでしたが、この環境にアプリを追加するのがとても簡単だということはお分かりいただけると思います。
リバースプロキシは常に起動しっぱなしにしておいて、今回作った myapp
のような単独のWebアプリをどんどん作って(同じDocker Network内で)Dockerコンテナとして起動すれば、ホストするWebアプリはポンポン増やせます。
まだまだ業務でDockerを使う機会はないという方も多いかもしれませんが、試しに趣味プロジェクトをこんな感じで簡易にDocker化してみるのも面白いのではないでしょうか。
以上、長文にお付き合いいただきありがとうござました