docker と systemd と ghost blog platform

[2016/11/04 不備があったので修正・追記]

VPS環境のお引越しの関係で、(バージョンアップしたばかりですが、)Docker コンテナ上にghost によるblog環境を構築したので備忘録。

移行先のVPSについてはまた別記事で。

Docker コンテナ用のghostイメージが提供されているので、これを利用することでバージョンアップが劇的に楽になるのと、VPSの移行作業が簡単になるというメリットがあります。 もちろん、ホストOS側にコンテンツデータを保存するが前提です。

ネタ元:Running Ghost blogging platform via Docker


目次は下記のとおり。

前提

環境は以下のとおり。

  • Ubuntu 16.04 LTS (x86_64)
  • systemd 229
  • Docker 1.12.3
  • ghost 0.11.2

英語用と日本語用にghostのインスタンスをDockerでラップして合計2つ起動します。

基本方針としてホストOS側でNginxを動作させ、コンテナ上で動作するghostとはUnix Domain Socketで接続。外部からコンテナ上のプロセスとは直接通信させません。 docker volume機能でホストOS側のディレクトリをコンテナ内の/var/lib/ghostにマウントさせます。

OS自体の設定は完了しているものとします。

Docker のインストール

Docker 自体の解説はこの記事がわかりやすいかと思います。

ディストリビューション側のバイナリではなく、開発元の新しいバージョンを使うので、公式ドキュメントに従って作業します。

Installation on Ubuntu - Docker

準備

(1). システムを最新の状態に。

$ sudo apt update
$ sudo apt upgrade

下記のパッケージをインストール(おそらく最初からインストールされている)。

$ sudo apt-get install apt-transport-https ca-certificates

(2). パッケージの署名を検証するためのGPG鍵を追加。

$ sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D

(3). aptリポジトリの追加(公式ドキュメントの表を確認して使用中のバージョンに合わせて"deb ..." の行を変更)。

$ echo "deb https://apt.dockerproject.org/repo ubuntu-xenial main" | sudo tee /etc/apt/sources.list.d/docker.list

(4). 追加されたリポジトリを取り込むためにパッケージデータベースの最新化。

$ sudo apt-get update

(5). ここまでの作業の確認

$ apt-cache policy docker-engine

(6). airs を利用するための kernel モジュールのインストール

$ sudo apt-get install linux-image-extra-$(uname -r) linux-image-extra-virtual

(7). docker のインストール

$ sudo apt update
$ sudo apt-get install docker-engine

(8). docker プロセスの起動

$ sudo service docker start

動作確認

お約束のhello-worldです。

$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
c04b14da8d14: Pull complete
Digest: sha256:0256e8a36e2070f7bf2d0b0763dbabdd67798512411de4cdcf9431a1feb60fd9
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker Hub account:
 https://hub.docker.com

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

いつの間にかメッセージが増えてますね。

各種設定

iptables 自動設定機能の無効化

ghostのコンテナイメージは初めからTCPポート2368番を外部に公開する設定になっているようです。この状態ではコンテナにポートをマップした時点でDocker側でiptablesが自動設定されます。

どういうことかというと、ufwなどのファイアーウォール関連ツールによる設定とは独立してポートが解放されるということです。セキュリティ的に非常によろしくないので無効化します。

参考:Viktor's ramblings • The dangers of UFW + Docker

iptablesとの連携機能を無効化してしまうとDockerfileを利用してコンテナを使う際にコンテナ内部からの通信に支障をきたす場合があります。よって他の手段で行きます。

考えられる対応方法は、以下のような感じ。

  1. ローカルホストのIPアドレスとポートを明示して、接続元を限定する(ファイアーウォールに穴は空くが、実際には通信できない)
  2. 公式のイメージを使う代わりに、Dockerfileを利用してコンテナのイメージをカスタマイズする
  3. Unix Domain Socketを使う前提でghost用のコンテナの通信機能を無効化する(docker runコマンドのオプションに--net=noneを指定する)
  4. docker network createで隔離用のネットワークを用意する


c.d.の場合はコンテナ内部から外部にメールが送信できないというデメリットがあります。常識に考えたらb.ですが、楽がしたいのでc.の方式で。

docker グループ

$ sudo groupadd docker
$ sudo usermod -aG docker $USER

一旦ログアウトするか再起動。

起動オプション

docker infoコマンドの、WARNING: No swap limit supportという警告を消すた目の設定。

$ vi /etc/defaults/grub

GRUB_CMDLINE_LINUXという行を探し出し、下記のようにcgroup_enable=memory swapaccount=1を追記(元からあるのはそのまま)。

GRUB_CMDLINE_LINUX="consoleblank=0 cgroup_enable=memory swapaccount=1"
$ sudo update-grub

正常に完了したら再起動。

$ sudo reboot


以下、参考。

$ docker version
Client:
 Version:      1.12.3
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   6b644ec
 Built:        Wed Oct 26 22:01:48 2016
 OS/Arch:      linux/amd64

Server:
 Version:      1.12.3
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   6b644ec
 Built:        Wed Oct 26 22:01:48 2016
 OS/Arch:      linux/amd64


$ docker info
Containers: 3
 Running: 2
 Paused: 0
 Stopped: 1
Images: 2
Server Version: 1.12.3
Storage Driver: aufs
 Root Dir: /var/lib/docker/aufs
 Backing Filesystem: extfs
 Dirs: 17
 Dirperm1 Supported: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: host bridge null overlay
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Security Options: apparmor seccomp
Kernel Version: 4.4.0-45-generic
Operating System: Ubuntu 16.04.1 LTS
OSType: linux
Architecture: x86_64
CPUs: 1
Total Memory: 740.4 MiB
Name: hawkeye
ID: QJKY:6QWY:CNPM:3XGW:OFEU:FG5L:C2PJ:T66X:PD4Q:5ZCB:MT76:E4HI
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
WARNING: bridge-nf-call-iptables is disabled
WARNING: bridge-nf-call-ip6tables is disabled
Insecure Registries:
 127.0.0.0/8

二項目ほどWARNINGが出ているが、たぶんネットワークブリッジ関連。/etc/sysctl.confあたりに何か設定すればいいだろうと思うが気にしない。

関連:Getting WARNING: bridge-nf-call-iptables is disabled with overlay storage driver · Issue #24809 · docker/docker · GitHub

再起動する前は出ていなかったような気もする。

Docker Compose のインストール([2016/11/04 追記])

他のコンテナの設定に使うので。

Ubuntu 環境において、Dockerの公式リポジトリを利用した場合は一緒にはインストールされないようです。Ubuntu側のリポジトリにあるバイナリは古いのでGitHubにある説明に従ってインストール。

$ sudo curl -L https://github.com/docker/compose/releases/download/1.8.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose

公式サイト)を使うように変更しました。

コマンド自体に実行権限を付与。

$ sudo chmod +x /usr/local/bin/docker-compose

ghost 環境の構築

ネタ元は下記のサイトの記事ですが、Unix Domain Socket経由で接続するように変更します。

Running Ghost blogging platform via Docker

準備

まずは公式のイメージをpull。

$ docker pull ghost

念のために起動するか確認しておく。

$ docker run ghost

データの保存先ディレクトリを作成

$ sudo mkdir /srv/docker/ghost_{en, ja}

ユニットファイルの作成

冒頭のリンク先をベースにカスタマイズする。

$  cat /etc/systemd/system/ghost_en.service
[Service]
ExecStartPre=-/usr/bin/docker kill ghost_blog_en
ExecStartPre=-/usr/bin/docker rm ghost_blog_en
ExecStartPre=-/usr/bin/docker pull ghost
ExecStart=/usr/bin/docker run -e 'NODE_ENV=production'  --name ghost_support_en --net=none --volume /srv/docker/ghost_en/:/var/lib/ghost ghost
ExecStop=/usr/bin/docker stop ghost_blog_en

[Install]
WantedBy=multi-user.target

英語用と日本語用の2つそれぞれghost_enghost_jaとして作成する。2つ目の方は ghost_blog_enの部分と、ホスト側のポート番号を--publish 2468:2368のようにずらしておく。

コンテナの実行オプションとして--net=noneを指定することで外部ネットワークから切り離す。

本当はコンテナの名称を変数で置き換えたかったがよくわからなかったので無難にベタ書き。

あとでログを見たところdocker killの終了ステータスがよろしくないのでどこかにsleep 1とか入れた方がいいかもしれない。

変更を反映するためにdaemon-reloadを実行。

$ sudo  systemctl daemon-reload

コンテナの起動。

エラーになるが、設定ファイルの雛型を生成するために一度は実行する。

$ sudo systemctl start ghost_en

systemctl status ghost_enとするとexitedか何かで起動に失敗する。実行ログを確認すると、概ね以下のようなエラーになっているはず。

ERROR: Unable to access Ghost's content path:
  EACCES: permission denied, open '/usr/src/ghost/content/apps/f5ee8a7e6371c4a1'

Check that the content path exists and file system permissions are correct.
Help and documentation can be found at http://support.ghost.org.

npm info ghost@0.11.2 Failed to exec start script
npm ERR! Linux 4.4.24-moby
npm ERR! argv "/usr/local/bin/node" "/usr/local/bin/npm" "start"
npm ERR! node v4.6.1
npm ERR! npm  v2.15.9
npm ERR! code ELIFECYCLE
npm ERR! ghost@0.11.2 start: `node index`
npm ERR! Exit status 235
npm ERR!

production環境として起動する際に必要な値がセットされていないのでこける。

よって対処法は、config.jsに設定を追加する。

$vi /srv/docker/ghost_en/config.js

production環境向けの設定箇所を探し、下記のように修正。

修正前

server: {
     host: '0.0.0.0',
     port: '2368'
 }

修正後

server: {
     host: '0.0.0.0',
     port: '2368'
 },
 paths: {
     contentPath: path.join(process.env.GHOST_CONTENT, '/')
 }

データ(ブログ記事など)の保存先を明示的に指定する設定。

Unix Domain Socket 向けの設定

紆余曲折は省略して完成形のみ。

config.jsproduction環境の箇所のみ抜粋。ポイントはソケットファイルのパーミッションを設定するところ。デフォルトではパーミッション設定が0660なのでNginxから接続できない。

   production: {
        url: 'http://support.example.com',
        mail: { from: '"Admin(en)" <support@example.com>' },
        forceAdminSSL: true,
        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(process.env.GHOST_CONTENT, '/data/ghost.db')
            },
            debug: false
        },

        server: {
            socket: {
                path: path.join(process.env.GHOST_CONTENT, '/socket.sock'),
                permissions: '0666'
            }
        },

詳細:Configuring Ghost - Ghost SupportGhost Support

ghost の起動

もう一度起動してみる。

$ sudo systemctl start ghost_en

問題がなければOK。

この時点で勝手にポートが解放されていないかを確認する。

$ sudo iptables -L | grep 2368

何も表示されない(ヒットしない)なら問題なし。

同じことをghost_jaの分も繰り返す。

参考情報

せっかくなので参考まで。

Unix Domain Socket を使用しない場合の例

config.js の、(production環境向け)ネットワーク設定箇所

server: {
     host: '0.0.0.0',
     port: '2368'
 },
 paths: {
     contentPath: path.join(process.env.GHOST_CONTENT, '/')
 }

また、/etc/systemd/system/ghost_en.serviceの修正例:

ExecStart=/usr/bin/docker run -e 'NODE_ENV=production' --name ghost_blog_en --publish 127.0.0.1:2368:2368 --volume /srv/docker/ghost_en/:/var/lib/ghost ghost 

のようにすると、接続元をローカルホストに限定しつつ、TCPで通信をさせることができる。

Nginx の設定

SSLの証明書はLet's Encryptで取得しているものとする。

設定ファイルの作成

Ubuntuのお作法に従う。/etc/nginx/sites-available/ghostを作成し、シンボリックリンク/etc/nginx/sites-enabled/に作成することで有効化。

$ sudo vi /etc/nginx/sites-available/ghost
upstream ghost-en-uds {
    server unix:/srv/docker/ghost_en/socket.sock
    fail_timeout=0;
}
upstream ghost-ja-uds {
    server unix:/srv/docker/ghost_ja/socket.sock
    fail_timeout=0;
}
server {
        listen 80;
        listen 443 ssl http2;
        server_name www.example.com;

ssl_certificate  /etc/letsencrypt/live/www.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem;

        location /.well-known/acme-challenge {
                root /var/lib/letsencrypt;
                default_type "text/plain";
        }

        location / {
                proxy_set_header        X-Real-IP       $remote_addr;
                proxy_set_header        Host    $http_host;
                proxy_pass     http://ghost-en-uds;
        }


        location /ja {
                proxy_set_header        X-Real-IP       $remote_addr;
                proxy_set_header        Host    $http_host;
                proxy_pass      http://ghost-ja-uds;
        }
}

server_nameはホスト名に応じて適宜修正のこと。

また、 location /.well-known/acme-challengeで始まる箇所はLet's EncryptのSSL証明書を更新するための設定なので通常の環境では不要です。location /jaは同じサブドメインで日本語用に起動しているインスタンスのためのもの。


設定の有効化

シンボリックリンクを作成。

$ cd /etc/nginx/sites-enabled/
$ sudo ln -s /etc/nginx/sites-available/ghost ghost

設定ファイルのチェック。

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo systemctl enable nginx
$ sudo systemctl start nginx

ブラウザで"http://ホスト名/ghost"にアクセスしてghostの設定と記事データのインポート(必要なら)。画像は/srv/docker/配下の対応するディレクトリにコピーすれば移行できる。

仕上げ

あとはDNSとかSSLの設定のチェックとか普通のWebサーバー構築の世界へ。

CloudFlare のCDN(とDNS)を組み合わせるとしょぼいサーバーでもかなりアクセス捌けるはず。

www.cloudflare.com

まとめ

もっと楽ができるかと思ったら意外にすんなりとはいかなかったという話。

これで次からは楽ができるはず(と信じている)。

関連URL