アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

docker環境のWebサーバーでLet's Encryptを使う

こんにちは!!こんにちは!!
インフラエンジニアのyamamotoです。

Let's Encrypt、いつも活用させていただいております!
ただ、証明書を取得するときにサーバー上でいろいろ作業をしなければなりません。さらにdocker環境上ではどうするんだ!?となるかと思います。
そこで、dockerコンテナでもLet's Encryptを簡単に使えるように整備してみました。

既存のイメージでLet's Encrypt組み込み済みのものもありますが、ここではあえて自前で作ってみます。

Dockerfileまわり

dockerイメージは、nginxのオフィシャルイメージを元に、Let's Encryptで必要なプログラムのインストールと、ちょっとひと工夫入れた起動スクリプトを組み込みます。

Dockerfile

FROM nginx:latest

ENV LETSENCRYPT_HOSTS 【ドメイン名 スペース区切りで複数指定可】
ENV LETSENCRYPT_MAIL 【窓口メールアドレス】
ENV LETSENCRYPT_SUBJECT "/C=JP/ST=Tokyo/L=Shinagawa/CN=default"【←仮証明書用の記載内容】

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update \
    && apt-get install --yes --no-install-recommends \
      openssl \
      certbot \
    && rm -rf /var/lib/apt/lists/*

COPY start.sh /start.sh
RUN chmod +x /start.sh
CMD /start.sh

nginxのオフィシャルイメージはDebianベースのものを使っています。apt-getでopensslとcertbot(Let's Encryptの証明書取得プログラム)をインストールしています。

上で指定している「start.sh」というシェルスクリプトが起動スクリプトになります。起動スクリプト内で証明書を自動発行・自動更新するようにしています。

start.sh

#!/bin/bash

for HOST in ${LETSENCRYPT_HOSTS}
do
  if [ ! -d "/etc/letsencrypt/live/${HOST}" ]; then
    mkdir -p /etc/letsencrypt/live/${HOST}
    mkdir -p /var/lib/letsencrypt

    crt_file="/etc/letsencrypt/live/${HOST}/fullchain.pem" &&
    key_file="/etc/letsencrypt/live/${HOST}/privkey.pem" &&
    subject="${LETSENCRYPT_SUBJECT}" &&
    openssl req -new -newkey rsa:2048 -sha256 -x509 -nodes \
      -set_serial 1 \
      -days 3650 \
      -subj "$subject" \
      -out "$crt_file" \
      -keyout "$key_file" &&
    chmod 400 "$key_file"
  fi
done

nginx

for HOST in ${LETSENCRYPT_HOSTS}
do
  if [ ! -e "/etc/letsencrypt/initialize" ]; then
    rm -rf /etc/letsencrypt/live/${HOST}
    certbot certonly -n --keep-until-expiring --agree-tos \
      --webroot --webroot-path /var/lib/letsencrypt \
      -m ${LETSENCRYPT_MAIL} -d ${HOST}
  fi
done

touch /etc/letsencrypt/initialize
certbot renew

nginx -s reload

while true
do
    sleep 7
done

nginxの設定にSSLの設定を入れた状態だと、証明書が無いと起動ができないのでまずオレオレ証明書で起動して、その後Let's Encryptで証明書を取得して再起動します。
証明書の取得は最初だけにして、あとは「certbot renew」で証明書の更新を毎回チェックするようにしています。こうすることで、コンテナを再起動しただけで証明書が更新できます。

また、最後にループを入れています。これは、万が一nginxがエラー終了した場合でもコンテナに入ってチェックできるようするためです。

nginxの設定

nginxの設定は、Let's Encryptの証明書を読み込ませるようにしておきます。他は通常通りで構いません。複数ドメインにも対応可能です。
コンテナに取り込むことを考えて、なるべく一つのファイルで済ませられるようにしています。

nginx.conf

user  nginx;
worker_processes  1;

error_log /var/log/nginx/error.log ;
pid /var/run/nginx.pid;

events {
  worker_connections  1024;
}


http {
  client_max_body_size 50m;

  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  log_format main '$remote_addr - $remote_user [$time_local] "$request" '
            '$status $body_bytes_sent "$http_referer" '
            '"$http_user_agent" "$http_x_forwarded_for"';

  sendfile on;
  keepalive_timeout 15;

  gzip on;
  gzip_disable "msie6";
  gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

  server {
    listen 80 default_server;
    listen 443 ssl default_server;
    root /var/www/html;
    ssl_certificate /etc/letsencrypt/live/test.harmonicom.jp/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/test.harmonicom.jp/privkey.pem;
  }

  # test
  server {
    listen 80;
    server_name 【ドメイン名】;
    root /var/www/html;

    access_log /var/log/nginx/access.log  main;
    error_log /var/log/nginx/error.log;

    # Let's Encrypt証明書の更新に必要なので残しておくこと
    location ^~ /.well-known/acme-challenge {
      alias /var/lib/letsencrypt/.well-known/acme-challenge;
      default_type "text/plain";
      try_files $uri =404;
    }
  }
  server {
    listen 443 ssl http2;
    server_name 【ドメイン名】;
    root /var/www/html;

    access_log /var/log/nginx/access_ssl.log  main;
    error_log /var/log/nginx/error_ssl.log;

    ssl_certificate /etc/letsencrypt/live/【ドメイン名】/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/【ドメイン名】/privkey.pem;
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
    ssl_prefer_server_ciphers on;
  }
}

docker-compose.yml

最後にdocker-composeの設定ファイルです。ここではDockerfileで用意した環境変数を定義しています。Dockerfileの設定を書き換えるより、こちらを書き換えた方がいいと思います。

docker-compose.yml

version: '3'

services:
  nginx:
    container_name: hogehoge-nginx
    build: ./
    environment:
      TZ: Asia/Tokyo
      LETSENCRYPT_HOSTS: 【ドメイン名 スペース区切りで複数指定可】
      LETSENCRYPT_MAIL: 【窓口メールアドレス】
      LETSENCRYPT_SUBJECT: "/C=JP/ST=Tokyo/L=Shinagawa/CN=default"【←仮証明書用の記載内容】
    ports:
      - '80:80'
      - '443:443'
    volumes:
      # nginx.conf
      - ./nginx.conf:/etc/nginx/nginx.conf
      # log directory
      - /var/log/nginx:/var/log/nginx
      # document root directory
      - /var/www/html:/var/www/html

ログディレクトリやドキュメントルートはここで設定可能です。

docker-composeの実行

上記の4つのファイルを全て一つのディレクトリに入れ、そのディレクトリの中でdocker-composeコマンドを実行します。

$ sudo docker-compose -f docker-compose.yml build
$ sudo docker-compose -f docker-compose.yml up -d

イメージをビルドして、そのイメージからコンテナを起動します。
最初の起動時には、オレオレ証明書を作ったうえでnginxを起動し、Webでの認証でLet's Encryptの証明書を取得してnginxをリロードします。
起動後すぐにHTTPSでページの閲覧が可能です。

証明書の更新時はコンテナを再起動します。

$ sudo docker-compose -f docker-compose.yml restart

最後に

注意点としては、テストなどであまり何度も証明書を取得してしまうと取得制限に引っかかってしまうので、あまり繰り返しテストなどはしない方がいいと思います。
再起動する分には問題無いですが、イメージを再ビルドすると取得しなおしてしまいます。 一週間で5回証明書を発行したら、それ以上発行できなくなりますのでご注意を。

アクトインディではdockerやりたい!というインフラエンジニアも募集していますよ!