DockerでGhost + Nginx + Varnish ブログサイトを構築する

ども、takiponeです。

このブログ(takipone.com)の構成をリニューアルしたので、それを紹介してみたいと思います。

構成

このブログは、GCEのContainer-optimized Instance上で以下3つのソフトウェアをDockerコンテナで実行しています。

  • Ghost : Node.js製ブログプラットフォーム
  • Nginx : HTTPSと静的ファイル(画像)のホスト
  • Varnish : HTTPキャッシュ

設定は、こちらのブログ記事を参考にしました。
それぞれの設定ファイルは以下のconfigsディレクトリにまとめ、開発環境は手元のMBAにFigで構築しています。

$ tree configs
configs
├── ghost
│   ├── config.js
│   ├── content
|   |      :(中略)
│   └── docker
│       ├── Dockerfile
│       ├── LICENSE
│       ├── README.md
│       └── start.bash
├── nginx
│   ├── conf
│   │   ├── cert.pem
│   │   └── nginx.conf
│   └── logs
└── varnish
    ├── conf
    │   └── default.vcl
    └── docker
        ├── Dockerfile
        ├── LICENSE
        └── README.md

30 directories, 195 files

Ghostコンテナの設定

Ghostの設定は、メール通知用のAmazon SES以外は既定のままです。

config.js

var path = require('path'),
    config;

config = {
    production: {
        url: 'http://takipone.com',
        mail: {
          transport: 'SES',
          options: {
            AWSAccessKeyID: "AKIAXXXXXXXXXXXX",
            AWSSecretKey: "XXXXXXXXXXXXXXXXXXX"
          }
        },
        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(__dirname, '/content/data/ghost.db')
            },
            debug: false
        },
        server: {
            host: '0.0.0.0',
            port: '2368'
        },
        forceAdminSSL: true
    }
};

// Export config
module.exports = config;

Dockerイメージは、dockerfile/ghostをフォークし、takipone/ghostとしてDocker HubのAutomated Buildを利用しています。Dockerfileは特に変更せず、最新バージョンを利用するためにビルドのタイミングを自分で制御したかったためです。

Nginxコンテナの設定

Nginxの設定としては、HTTPのポートを8080番にしてproxy_passをDockerホストのIP(172.17.42.1)にしているところ、HTTPSのバーチャルホストで管理画面(/ghost)を意識してキャッシュヘッダを無効にしているところがポイントです。

nginx.conf

worker_processes 1;
daemon off;

events {
    worker_connections 1024;
}

http {
    server_tokens off;
    keepalive_timeout 65;
    gzip on;

    set_real_ip_from   172.17.0.0/16;
    real_ip_header     X-Forwarded-For;

    server {
        listen 8080;
        server_name takipone.com;

        location / {
            proxy_pass http://172.17.42.1:2368/;
            proxy_set_header X-Forwarded-For   $remote_addr;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_redirect off;
            expires 1d;
        }
        location /content/images {
            root /ghost-override;
            expires max;
        }
    }

    server {
        listen 443 ssl;
        server_name takipone.com;

        ssl_certificate      /nginx/cert.pem;
        ssl_certificate_key  /nginx/cert.pem;

        location / {
            proxy_pass http://172.17.42.1:2368/;
            proxy_set_header X-Forwarded-For   $remote_addr;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_redirect off;
            expires -1;
        }
    }
}

Dockerイメージは、dockerfile/nginxをそのまま使っています。

Varnishの設定

Varnishの設定は、こちらもリバースプロキシとしてDockerホストのNginxのポートを向けています。あと、Dockerホストからのキャッシュインバリデーションの許可設定を追加しました。

default.vcl

# Marker to tell the VCL compiler that this VCL has been adapted to the
# new 4.0 format.
vcl 4.0;

acl purge {
  "172.17.0.0/16";
}

backend default {
  .host = "172.17.42.1";
  .port = "8080";
}

sub vcl_recv {
  if (!(req.url ~ "ghost")) {
    unset req.http.cookie;
  }
  if (req.url == "/") {
    return (pass);
  }

  if (req.method == "PURGE") {
    if (!client.ip ~ purge) {
      return(synth(405,"Not allowed."));
    }
    return (purge);
  }
}

sub vcl_backend_response {
  if (!(bereq.url ~ "ghost")) {
    unset beresp.http.set-cookie;
  }
}

sub vcl_deliver {
  # Happens when we have all the pieces we need, and are about to send the
  # response to the client.
  #
  # You can do accounting or modifying the final object here.
}

Dockerイメージは良さそうなものがなかったので、Dockerfileを自作しました。Varnishのドキュメントを参考にし、takipone/varnishとしてDocker HubのAutomated Buildで設定しました。

Dockerfile

#
# Varnish Dockerfile
#
# https://github.com/otaki-ryuta/docker-varnish
#

# based on 14.04(Trusty Tahr)
FROM dockerfile/ubuntu

# Install Varnish.
RUN \
  apt-get update && \
  apt-get install -y apt-transport-https && \
  curl https://repo.varnish-cache.org/ubuntu/GPG-key.txt | apt-key add - && \
  echo "deb https://repo.varnish-cache.org/ubuntu/ trusty varnish-4.0" >> /etc/apt/sources.list.d/varnish-cache.list && \
  apt-get update && \
  apt-get install -y varnish && \
  rm -rf /var/lib/apt/lists/*

# Define mountable directories.
VOLUME ["/etc/varnish"]

# Define working directory.
WORKDIR /etc/varnish

# Expose ports.
EXPOSE 80

# Define default command.
CMD /usr/sbin/varnishd -F -f /etc/varnish/default.vcl -a :80 -s malloc,128m

開発環境

手元のMBAでデバッグ用に利用している、Figの設定ファイルです。

fig.yml

ghost:
  image: takipone/ghost:latest
  volumes:
    - ./configs/ghost:/ghost-override
  ports:
    - "2368:2368"
nginx:
  image: dockerfile/nginx:latest
  volumes:
    - ./configs/nginx/conf:/nginx
    - ./configs/nginx/logs:/nginx-log
  volumes_from:
    - ghost
  ports:
    - "8080:8080"
    - "443:443"
  command: nginx -c /nginx/nginx.conf
varnish:
  image: takipone/varnish:latest
  volumes:
    - ./configs/varnish/conf:/etc/varnish
  ports:
    - "80:80"

本番環境

Container-optimized Instanceでは、実行するDockerコンテナの構成を、VMインスタンス作成時に読み込んで自動構成することができます。最初のversionがコロコロ変わるので、実行するVMイメージと対応しているか確認しましょう。あと、commandfig.ymlと異なり厳密な配列で記述しなければいけないところがわからず、苦戦しました。

kubelet.manifest

version: v1beta2
containers:
  - name: ghost
    image: takipone/ghost:latest
    ports:
      - name: ghost
        containerPort: 2368
        hostPort: 2368
    volumeMounts:
      - name: ghost-override
        mountPath: /ghost-override
  - name: nginx
    image: dockerfile/nginx:latest
    command: [ 'nginx', '-c', '/nginx/nginx.conf' ]
    ports:
      - name: http-proxy
        containerPort: 8080
        hostPort: 8080
      - name: https
        containerPort: 443
        hostPort: 443
    volumeMounts:
      - name: nginx
        mountPath: /nginx
      - name: nginx-log
        mountPath: /nginx-log
      - name: ghost-override
        mountPath: /ghost-override
  - name: varnish
    image: takipone/varnish:latest
    ports:
      - name: http
        containerPort: 80
        hostPort: 80
    volumeMounts:
      - name: varnish
        mountPath: /etc/varnish
volumes:
  - name: ghost-override
    source:
      hostDir:
        path: /mnt/pd1/ghost
  - name: nginx
    source:
      hostDir:
        path: /mnt/pd1/nginx/conf
  - name: nginx-log
    source:
      hostDir:
        path: /mnt/pd1/nginx/logs
  - name: varnish
    source:
      hostDir:
        path: /mnt/pd1/varnish/conf

これに加えて、前述した構成ファイルをアーカイブファイルconfigs.tgzにまとめてGoogle Cloud StorageにアップロードしVMインスタンス起動時にダウンロードするようにしました。VMインスタンス起動時のスタートアップスクリプトについてはこちらの記事が詳しいです。

startup-script.bash

#!/bin/bash

if [ ! -d /mnt/pd1 ]; then
  mkdir /mnt/pd1
fi
gsutil cp gs://<Bucket名>/configs.tgz /tmp
tar zxf /tmp/configs.tgz -C /mnt/pd1

で、それぞれの設定をVMインスタンス作成時のコマンドラインで指定します。長くなるので、シェルスクリプトでまとめました。

create_containerinstance.bash

#!/bin/bash

if [ -s $1 ]; then
  echo "Usage: $0 <VM_NAME>"
  exit 0
fi

gcloud compute instances create $1 \
    --image container-vm \
    --zone asia-east1-a \
    --machine-type f1-micro \
    --scope storage-rw \
    --tags http-server https-server \
    --address test-takipone-com \
    --metadata-from-file \
      google-container-manifest=kubelet.manifest \
      startup-script=startup-script.bash

スタートアップスクリプトの実行後、DockerエージェントであるKubeletがDockerの構成を読み取るのでうまく動作するのですが、
厳密な順序制御はされないようでスタートアップスクリプトの実行に時間がかかると、並行してDockerコンテナが実行されるケースもあると思います。この辺り、ちゃんと順番が制御できる仕組みがあると嬉しいですね。