docker と systemd と ghost blog platform
スポンサーリンク
[2016/11/04 不備があったので修正・追記]
VPS環境のお引越しの関係で、(バージョンアップしたばかりですが、)Docker コンテナ上にghost によるblog環境を構築したので備忘録。
移行先のVPSについてはまた別記事で。
Docker コンテナ用のghostイメージが提供されているので、これを利用することでバージョンアップが劇的に楽になるのと、VPSの移行作業が簡単になるというメリットがあります。 もちろん、ホストOS側にコンテンツデータを保存するが前提です。
ネタ元:Running Ghost blogging platform via Docker
目次は下記のとおり。
前提
環境は以下のとおり。
英語用と日本語用に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を利用してコンテナを使う際にコンテナ内部からの通信に支障をきたす場合があります。よって他の手段で行きます。
考えられる対応方法は、以下のような感じ。
- ローカルホストのIPアドレスとポートを明示して、接続元を限定する(ファイアーウォールに穴は空くが、実際には通信できない)
- 公式のイメージを使う代わりに、Dockerfileを利用してコンテナのイメージをカスタマイズする
- Unix Domain Socketを使う前提で
ghost
用のコンテナの通信機能を無効化する(docker run
コマンドのオプションに--net=none
を指定する) 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
あたりに何か設定すればいいだろうと思うが気にしない。
再起動する前は出ていなかったような気もする。
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_en
、ghost_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.js
のproduction
環境の箇所のみ抜粋。ポイントはソケットファイルのパーミッションを設定するところ。デフォルトではパーミッション設定が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)を組み合わせるとしょぼいサーバーでもかなりアクセス捌けるはず。
まとめ
もっと楽ができるかと思ったら意外にすんなりとはいかなかったという話。
これで次からは楽ができるはず(と信じている)。