はじめに

本エントリーのゴールは以下のような状態です。

  • (ほぼ)Dockerだけしかインストールされていないサーバーが1つある
  • そのサーバーのDockerで複数のWebアプリがホストされている
  • 各WebアプリはDocker Composeで構成管理されていて、アプリごとに docker-compose up するだけで動く
  • docker-compose up した各Webアプリには、指定したサブドメインが自動で割り当てられて、Let’s Encrypt で取得したSSL証明書が適用される

この状態になるまでに必要な作業をできるだけ丁寧に解説していきます。やや長くなりますが、ぜひ最後までお付き合いください。

目次

  1. Ubuntu 16.04のサーバーを用意する
  2. サーバーにDockerをインストールする
    1. 一般ユーザーを作る
    2. Dockerをインストール
    3. Docker Composeをインストール
  3. リバースプロキシを構築
  4. サーバーへのDockerのインストールからリバースプロキシの構築までをAnsibleで自動化
  5. Webアプリを作る
  6. 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できる必要があるということなので、

  1. nginx-proxy コンテナでボリュームを作成
  2. volumes_fromnginx-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領域の設定の仕方は以下の記事が参考になりました。

How To Add Swap on Ubuntu 14.04 | DigitalOcean

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

こんな感じで、nginxphp というサービスをそれぞれ 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 でマウントしているのでこれで問題ないですが、もし nginxphp でマウント先のパスが違っている場合は、

        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 と合わせて使っていただければ、今すぐにでも動作確認ができると思います。もしよろしければここだけでも一緒に手を動かしてみてください :muscle:

では実際に試してみましょう。

6-1. 実際にDigitalOceanでDropletを作る

$5/moプランなら1時間使っても0.8円ぐらいなので、気軽に作っちゃいましょうw

image

6-2. Ansibleでプロビジョニングする

まずサーバー側にpythonをインストールして、

image

ローカル側からプロビジョニングを実行します。

image

そしてしばらく待ちます。

6-3. サーバーにログインしてリバースプロキシが起動していることを確認

完了したら再度(今度は ubuntu ユーザーで)ログインして、docker ps でリバースプロキシのコンテナが起動していることを確認してみましょう。

image

OKですね。

6-4. アプリをデプロイ&ビルド

ここでは先ほど作ったGitHubリポジトリをcloneする形でデプロイしてみます。

image

Dockerfileのビルドに初回は多少時間がかかるので、ここでもしばらく待ちます。ビルドが完了したら docker ps でコンテナの起動を確認してみましょう。

image

OKですね。

6-5. 本物のサブドメインを設定

このままだとアプリのホスト名が myapp.mydomain.com という嘘のドメインになっていてSSL証明書の取得ができないので、手持ちのドメインでサブドメインを発行して実際に確認してみましょう。

今回は myapp.ttskch.com というサブドメインのAレコードをサーバーのアドレス 128.199.131.35 に設定しました。

docker-compose.ymlmyapp.mydomain.commyapp.ttskch.com に変更して、コンテナを再起動しましょう。

image

しばらくすると、~/nginx-proxy/certs に証明書のファイルがダウンロードされていることが確認できると思います。

image

ドメインを持っていない人は、SSL対応の確認はできませんが、/etc/hosts を書き換えることでとりあえずWebアプリの動作だけは確認できるのでやってみましょう。 サーバーの /etc/hosts127.0.0.1 myapp.mydomain.com を、ローカルの /etc/hosts128.199.131.35 myapp.mydomain.com をそれぞれ追記すれば、ローカルのブラウザから myapp.domain.com にアクセスすれば表示が確認できます。

image

6-6. ブラウザで確認

image

デデーン!バッチリ動いてますね!SSLも有効になっています。

6-7. DigitalOceanのDroplotを削除

今回はただの動作確認なので、不要になったDropletは忘れずに削除しましょう。(もし忘れても$5/moなのでダメージは少ないですが)

おわりに

タイトルに「複数のWebアプリを」とあったのに本文では1アプリしか作りませんでしたが、この環境にアプリを追加するのがとても簡単だということはお分かりいただけると思います。

リバースプロキシは常に起動しっぱなしにしておいて、今回作った myapp のような単独のWebアプリをどんどん作って(同じDocker Network内で)Dockerコンテナとして起動すれば、ホストするWebアプリはポンポン増やせます。

まだまだ業務でDockerを使う機会はないという方も多いかもしれませんが、試しに趣味プロジェクトをこんな感じで簡易にDocker化してみるのも面白いのではないでしょうか。

以上、長文にお付き合いいただきありがとうござました :pray: