研究室に最強の wiki と噂されている Crowi を Docker/CoreOS で構築してみた

研究室に [Crowi](http://site.crowi.wiki/) を導入してみたのでメモ。

# 経緯
- 研究室の情報共有を行いたい
    - フロー的な情報管理ツールに手を出してみたものの、なんだかんだでストック情報が多いので wiki が良い
- puki-wiki 使っているものの、
    - ユーザー管理が微妙
    - 編集しながら完成物が見たい
    - ディレクトリ構造を変えるとリンクが切れる

## どんな事がしたいか
- wiki で階層的に情報を整理したい
    - でも、階層構造は途中で変えたくなる事が多いから、階層構造変えてもリンク切れて欲しくない
- 気軽に編集したい
    - 完成品を見ながら編集したい
    - でも、本文は使いまわしたり、Google Document みんなで同時に編集した結果を貼りつけたり、ツールから吐き出させた整形済みテキストを貼り付けたりさせたいので、WYSIWYG 的なエディタより、プレーンテキストで管理したい
    - できれば、markdown
- 編集に対する貢献を見やすくして、編集に対するモチベーションを上げたい
    - たくさんページを編集している人が目立つ
    - 作ったページへのレスポンスが分かる

## Crowi
- 良いところ
    - 上記のしたい事が全て実装されている
- イマイチなところ
    - 本文検索できない (ver 1.5 から対応された)
    - 画像以外のファイルを添付できない

参考

- [Markdownで書ける、よさげなWikiサーバーソフトまとめ - Qiita](http://qiita.com/Bakudankun/items/740ac8ba1395fe4e40a6)
- [Crowi のいいとこ、今後に期待するところ](http://www.kunitake.org/chalow/2016-05-13-2.html)

# 構成
- Core OS / Docker を使って、セットアップをできるだけ自動化させる。
- 複数の crowi (や、他のサーバー類) を同居させるために、proxy サーバーを立てる
- HTTPS 通信させる
- データのバックアップを実施する

## セットアップ内容

![構成図.png](https://qiita-image-store.s3.amazonaws.com/0/107797/7fc62f81-1292-2ac5-7291-925808781058.png)


- docker-compose のインストール
    - スクリプトを `/opt/docker/install-docker-compose.sh` に配置
    - systemd で起動時に実行 (`install-docker-compose.service`)
- docker コンテナの起動
    - コンテナ起動スクリプトを `/opt/docker/{container}/start.sh` に配置
    - systemd でサービス登録 (`docker-{container}.service`)
    - 設定ファイルなどは `/opt/docker/{container}` に配置
    - コンテナ内のボリュームは `/opt/docker/{container}/data` などにマッピング
- バックアップ
    - 起動スクリプトを `/opt/backup/run_backup.sh` に配置
        - `/opt/backup/backup.d` 以下の `.sh` ファイルを実行
    - コンテナごとにバックアップスクリプトを作成し、`/opt/backup/backup.d` に配置
    - 起動スクリプトは、systemd timer で定時実施 (`backup.timer`)
    - 外部ストレージは、`/media/backup` へマウント

## 使用している docker コンテナ
- [nginx](https://hub.docker.com/_/nginx/)
    - proxy サーバーに使用
- [bakudankun/crowi](https://hub.docker.com/r/bakudankun/crowi/)
    - crowi のコンテナ
- [mongo](https://hub.docker.com/_/mongo/), [redis](https://hub.docker.com/_/redis/)
    - crowi の DB に使用
- [ars096/mongo-backup](https://hub.docker.com/r/ars096/mongo-backup/)
    - 定期的に mongo-db にアクセスして mongodump する cron が走っているコンテナ


# 手順

## 1. インストールメディアの準備

- Core OS の [ISO image](https://coreos.com/os/docs/latest/booting-with-iso.html) をダウンロード
- CD-R に焼く

## 2. cloud-config.yml の準備
```cloud-config.yml
#cloud-config

# ===============
# Common settings
# ===============

# -----------------
# OS Configurations
# -----------------

# User Configurations
# - - - - - - - - - -
users: 
  - 
    name: core  # << 運用するユーザー名に変更する
    passwd: ****  # << `openssl passwd -1` で生成されるハッシュを書き込む
    groups:
      - sudo
      - docker

# System Configurations
# - - - - - - - - - - -
hostname: core  # << 運用するホスト名に変更する

coreos:
  update:
    reboot-strategy: off

  units:
    - 
      name: coreos-setup-environment.service
      command: restart
    - 
      name: systemd-networkd.service
      command: restart
    - 
      name: docker.service
      command: start

    -  # << ネットワーク設定を適切に変更する
      name: 10-static.network
      runtime: no
      content: |
        [Match]
        Name=eno1
        [Network]
        Address=192.168.0.2/24
        Gateway=192.168.0.1
        DNS=192.168.0.1

# ----------------------
# Install docker-compose
# ----------------------
coreos:
  units:
    - 
      name: install-docker-compose.service
      command: start
      content: |
        [Unit]
        Description=Install docker-compose command
        
        [Service]
        Type=oneshot
        RemainAfterExit=yes
        ExecStart=/usr/bin/sh /opt/docker/install-docker-compose.sh

write_files:
  - # << 最新の docker-compose のバージョンを確認後、修正する
    path: "/opt/docker/install-docker-compose.sh"
    permissions: "0755"
    owner: "root:root"
    content: |
      if [ ! -e /opt/bin/docker-compose || ! -s /opt/bin/docker-compose ]; then
        mkdir -p /opt/bin
        curl -L https://github.com/docker/compose/releases/download/1.8.0/docker-compose-`uname -s`-`uname -m` > /opt/bin/docker-compose
        chmod +x /opt/bin/docker-compose
      fi

# ---------------------
# Backup Configurations
# ---------------------
coreos:
  units:
    - # << バックアップ先を外付けメディアにしない場合、不要
      name: media-backup.mount
      command: start
      content: |
        [Mount]
        What=/dev/sdb1
        Where=/media/backup
        Type=ext4
    - # << バックアップを定期実行しない場合、不要
      name: backup.service
      content: |
        [Unit]
        Description=Run backup script /opt/backup/run_backup.sh
        
        [Service]
        Type=oneshot
        ExecStart=/opt/backup/run_backup.sh
    - # << バックアップを定期実行しない場合、不要
      # << バックアップ時刻を適切に変更 : default UTC 19:00 (JST 04:00) 
      name: backup.timer
      command: start
      content: |
        [Unit]
        Description=Run backup.service every day
        
        [Timer]
        OnCalendar=*-*-* 19:00:00

write_files:
  - # << バックアップを定期実行しない場合、不要
    path: "/opt/backup/run_backup.sh"
    permissions: "0755"
    owner: "root:root"
    content: |
      for file in `\find /opt/backup/backup.d -name '*.sh'`; do
          echo "$(date --utc '+%Y-%m-%d %H:%M:%S'), /usr/bin/sh $file" >> /var/log/backup.log 2>&1
          /usr/bin/sh $file
      done



# =================
# Docker Containers
# =================

# ------------
# proxy server
# ------------

# Systemd Configurations
# - - - - - - - - - - - -
coreos:
  units:
    - 
      name: docker-proxy.service
      command: start
      content: |
        [Unit]
        Description=Start proxy container
        Requires=docker.service
        After=docker.service
        
        [Service]
        Restart=always
        ExecStart=/usr/bin/sh /opt/docker/proxy/start.sh
        ExecStop=/usr/bin/sh /opt/docker/proxy/stop.sh

write_files:
  - # << HTTPS 接続させない場合、ssl.key, ssl.cer の docker cp をコメントアウトする
    path: "/opt/docker/proxy/start.sh"
    permissions: "0755"
    owner: "root:root"
    content: |
      /usr/bin/docker rm -f proxy
      /usr/bin/docker run -d --name proxy -v /opt/docker/proxy/log:/var/log/nginx -p 80:80 -p 443:443 nginx:alpine
      
      /usr/bin/docker exec -d proxy rm /etc/nginx/conf.d/default.conf
      /usr/bin/docker cp /opt/docker/proxy/nginx.conf proxy:/etc/nginx
      /usr/bin/docker cp /opt/docker/proxy/ssl.key proxy:/etc/nginx
      /usr/bin/docker cp /opt/docker/proxy/ssl.cer proxy:/etc/nginx
      /usr/bin/docker cp /opt/docker/proxy/proxy-crowi.conf proxy:/etc/nginx/conf.d
      
      /usr/bin/docker restart proxy
      /usr/bin/docker attach proxy
  - 
    path: "/opt/docker/proxy/stop.sh"
    permissions: "0755"
    owner: "root:root"
    content: |
      /usr/bin/docker stop proxy
      /usr/bin/docker rm proxy

# Backup Configurations
# - - - - - - - - - - - -
write_files:
  - # << バックアップを定期実行しない場合、不要
    path: "/opt/backup/backup.d/backup_proxy.sh"
    permissions: "0755"
    owner: "root:root"
    content: |
      rsync -av /opt/docker/proxy /media/backup/proxy > /media/backup/proxy.log 2>&1

# nginx conf file
# - - - - - - - -
write_files:
  - 
    path: "/opt/docker/proxy/nginx.conf"
    permissions: "0755"
    owner: "root:root"
    content: |
      user  nginx;
      worker_processes  1;
      
      error_log  /var/log/nginx/error.log warn;
      pid        /var/run/nginx.pid;
      
      
      events {
          worker_connections  1024;
      }
      
      
      http {
          include       /etc/nginx/mime.types;
          default_type  application/octet-stream;
          
          server_tokens off;
          
          log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                            '$status $body_bytes_sent "$http_referer" '
                            '"$http_user_agent" "$http_x_forwarded_for"';          
          access_log  /var/log/nginx/access.log  main;
          
          sendfile        on;
          keepalive_timeout  65;
          
          include /etc/nginx/conf.d/*.conf;
      }
  - 
    path: "/opt/docker/proxy/proxy-crowi.conf"
    permissions: "0755"
    owner: "root:root"
    content: |
      server {  # << HTTPS で運用しない場合、削除
          listen 80;
          server_name crowi.example.jp;  # << 運用するホスト名に変更する
          return 301 https://$host$request_uri;
      }
      
      server { 
          listen 443 ssl;  # << HTTPS で運用しない場合、listen 80; に変更
          server_name crowi.example.jp;  # << 運用するホスト名に変更する
          
          ssl_certificate ssl.cer;  # << HTTPS で運用しない場合、削除
          ssl_certificate_key ssl.key;  # << HTTPS で運用しない場合、削除
             
          access_log  /var/log/nginx/crowi.access.log  main;
          error_log  /var/log/nginx/crowi.error.log warn;
          
          location / {
              proxy_pass http://crowi.example.jp:8000/;  # 運用するホスト名、crowi ポートに変更
              proxy_set_header Host $host;
              proxy_set_header X-Forwarded-Host $host;
              client_max_body_size 1g;  # アップロードできるファイルサイズの最大値
          }
      
          error_page   500 502 503 504  /50x.html;
          location = /50x.html {
              root   /usr/share/nginx/html;
          }
      }

     
# ------------
# crowi server
# ------------

# Systemd Configurations
# - - - - - - - - - - - -
coreos:
  units:
    - 
      name: docker-crowi.service
      command: start
      content: |
        [Unit]
        Description=Start crowi container
        Requires=docker.service
        After=docker.service
        
        [Service]
        Restart=always
        ExecStart=/usr/bin/sh /opt/docker/crowi/start.sh
        ExecStop=/usr/bin/sh /opt/docker/crowi/stop.sh

write_files:
  - 
    path: "/opt/docker/crowi/start.sh"
    permissions: "0755"
    owner: "root:root"
    content: |
      /opt/bin/docker-compose -f /opt/docker/crowi/docker-compose.yml stop
      /opt/bin/docker-compose -f /opt/docker/crowi/docker-compose.yml rm -f
      /opt/bin/docker-compose -f /opt/docker/crowi/docker-compose.yml up
  - 
    path: "/opt/docker/crowi/stop.sh"
    permissions: "0755"
    owner: "root:root"
    content: |
      /opt/bin/docker-compose -f /opt/docker/crowi/docker-compose.yml stop
      /opt/bin/docker-compose -f /opt/docker/crowi/docker-compose.yml rm -f

# Backup Configurations
# - - - - - - - - - - - -
write_files:
  - # << バックアップを定期実行しない場合、不要
    path: "/opt/backup/backup.d/backup_crowi.sh"
    permissions: "0755"
    owner: "root:root"
    content: |
      FROM_DIR="/opt/docker/crowi"
      TO_DIR="/media/backup/crowi"
      
      YEAR=$(date --utc '+%Y')
      MONTH=$(date --utc '+%m')
      DAY=$(date --utc '+%d')
      
      NEW_DIR=$TO_DIR/$YEAR/$MONTH/$DAY
      
      mkdir -p $NEW_DIR
      rsync -av $FROM_DIR $NEW_DIR > $TO_DIR.log 2>&1
      
      ## remove old month daily backup
      if [ $DAY -eq "01" ]; then
        RM_YEAR=$(date --utc '+%Y' --date '2 months ago')
        RM_MONTH=$(date --utc '+%m' --date '2 months ago')
      
        for i in {2..31}; do
          RM_PATH=$TO_DIR/$RM_YEAR/$RM_MONTH/$(printf "%02d" i)
          if [ -e $RM_PATH ]; then
              echo "rm -rf --preserve-root $RM_PATH" >> $TO_DIR.log 2>&1
              rm -rf --preserve-root $RM_PATH
          fi
        done
      fi


# docker-compose file
# - - - - - - - - - - 
write_files:
  - 
    path: "/opt/docker/crowi/docker-compose.yml"
    permissions: "0755"
    owner: "root:root"
    content: |
      version: '2'
      
      services:
        crowi:
          container_name: crowi
          image: bakudankun/crowi:1.4.0
          links:
            - mongo:db
            - redis:redis
          ports:
            - 8000:8000  # << 外部からアクセスさせる port を設定
          volumes:
            - /opt/docker/crowi/data/crowi:/data
          environment:
            - PASSWORD_SEED=****  # << 内緒の文字列を設定する
            - SECRET_TOKEN=****   # << 内緒の文字列を設定する
            - PORT=8000
            - FILE_UPLOAD=local
      
        mongo:
          container_name: crowi-mongo
          image: mongo
          volumes:
            - /opt/docker/crowi/data/db:/data/db
        
        redis:
          container_name: crowi-redis
          image: redis:alpine
      
        mongo-backup:
          container_name: crowi-dbbackup
          image: ars096/mongo-backup
          links:
            - mongo:db
          volumes:
            - /opt/docker/crowi/data/backup:/data/backup
            - /opt/docker/crowi/log/mongo-backup:/var/log
```

## 3. Core OS のインストール

### 3.1. インストール用に Core OS を起動
- 手順 1. で作成した CD-R を使い Core OS で PC を起動
    - 自動的に core ユーザーでログインされる

### 3.2. cloud-config.yml をコピー
- 手順 2. で作成した `cloud-config.yml` を USB メモリに保存し、PC に接続
- デバイス名を確認し、マウントしてからファイルをコピー

```
デバイス名を確認。/dev/sdb1 として認識されていた。
$ ls /dev/sd*
/dev/sda   /dev/sda2  /dev/sda4  /dev/sda7  /dev/sdb   /dev/sdc
/dev/sda1  /dev/sda3  /dev/sda6  /dev/sda9  /dev/sdb1  

適当なディレクトリを作ってマウント
$ sudo mkdir /media/data
$ sudo mount /dev/sdb1 /media/data

ファイルをコピー
$ cp /media/data/cloud-config.yml ~/
```

### 3.3. インストールを実行
- DHCP のネットワークに接続しておく
- coreos-install コマンドを実行

```
$ sudo coreos-install -d /dev/sda -C stable -c cloud-config.yml -v
...
...
Success! CoreOS stable 899.13.0 is installed on /dev/sda
```

- 再起動
    - CD は取り出しておく

```
$ sudo reboot
```

- OS と Docker を最新に更新

```
$ sudo update_engine_client -update
$ sudo reboot

再起動後
$ cat /etc/os-release
NAME=CoreOS
ID=coreos
VERSION=1122.2.0
VERSION_ID=1122.2.0
BUILD_ID=2016-09-06-1449
PRETTY_NAME="CoreOS 1122.2.0 (MoreOS)"
ANSI_COLOR="1;32"
HOME_URL="https://coreos.com/"
BUG_REPORT_URL="https://github.com/coreos/bugs/issues"

$ docker -v
Docker version 1.10.3, build 1f8f545

$ docker-compose -v
docker-compose version 1.8.0, build f3628c7
```

## 4. HTTPS 設定
- サーバー証明書を取得する
    - docker を自動実行させるために、秘密鍵のパスフレーズは解除しておく
- サーバー証明書 (ssl.cer) と秘密鍵 (ssl.key) を、 `/opt/docker/proxy` に設置
    - 無いと、docker cp が失敗し、nginx コンテナの起動に失敗する


## 5. 動作確認
PC 起動後に、自動で各種 docker コンテナと、バックアップ timer が起動しているはずなので確認する。

```
$ docker ps
CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS              PORTS                                      NAMES
a183c7c2d5ba        bakudankun/crowi:1.4.0   "/entrypoint.sh npm s"   22 minutes ago      Up 22 minutes       0.0.0.0:8000->8000/tcp                     crowi
c007eb75a3b8        ars096/mongo-backup      "/entrypoint.sh /bin/"   22 minutes ago      Up 22 minutes       27017/tcp                                  crowi-dbbackup
e4f2cded4aa2        mongo                    "/entrypoint.sh mongo"   22 minutes ago      Up 22 minutes       27017/tcp                                  crowi-mongo
42d1309e9010        redis:alpine             "docker-entrypoint.sh"   22 minutes ago      Up 22 minutes       6379/tcp                                   crowi-redis
410d52e95a3e        nginx:alpine             "nginx -g 'daemon off"   26 minutes ago      Up 26 minutes       0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp   proxy

$ systemctl status docker-proxy
● docker-proxy.service - Start proxy container
   Loaded: loaded (/etc/systemd/system/docker-proxy.service; static; vendor preset: disabled)
   Active: active (running) since Thu 2016-09-15 02:02:15 UTC; 28min ago
 Main PID: 1479 (sh)
    Tasks: 9
   Memory: 11.0M
      CPU: 735ms
   CGroup: /system.slice/docker-proxy.service
           ├─1479 /usr/bin/sh /opt/docker/proxy/start.sh
           └─1770 /usr/bin/docker attach proxy

$ systemctl status docker-crowi
● docker-crowi.service - Start crowi container
   Loaded: loaded (/etc/systemd/system/docker-crowi.service; static; vendor preset: disabled)
   Active: active (running) since Thu 2016-09-15 02:06:33 UTC; 24min ago
 Main PID: 2882 (sh)
    Tasks: 8
   Memory: 41.5M
      CPU: 2.115s
   CGroup: /system.slice/docker-crowi.service
           ├─2882 /usr/bin/sh /opt/docker/crowi/start.sh
           ├─2907 /opt/bin/docker-compose -f /opt/docker/crowi/docker-compose.yml up
           └─2908 /opt/bin/docker-compose -f /opt/docker/crowi/docker-compose.yml up

 $ systemctl list-timers
NEXT                         LEFT     LAST                         PASSED       UNIT                         ACTIVATES
Thu 2016-09-15 14:02:08 UTC  11h left Thu 2016-09-15 02:02:08 UTC  29min ago    rkt-gc.timer                 rkt-gc.service
Thu 2016-09-15 19:00:00 UTC  16h left n/a                          n/a          backup.timer                 backup.service
Fri 2016-09-16 00:00:00 UTC  21h left Thu 2016-09-15 01:11:56 UTC  1h 19min ago logrotate.timer              logrotate.service
Fri 2016-09-16 02:17:15 UTC  23h left Thu 2016-09-15 02:17:15 UTC  14min ago    systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service

4 timers listed.
Pass --all to see loaded but inactive timers, too.

バックアップスクリプトを試しに流してみる
$ sudo sh /opt/backup/run_backup.sh

$ cat /var/log/backup.log 
2016-09-15 02:42:31, /usr/bin/sh /opt/backup/backup.d/backup_proxy.sh
2016-09-15 02:42:31, /usr/bin/sh /opt/backup/backup.d/backup_crowi.sh

$ ls /media/backup/
crowi  crowi.log  lost+found  proxy  proxy.log

$ cat /media/backup/crowi.log 
sending incremental file list
crowi/
crowi/docker-compose.yml
crowi/start.sh
crowi/stop.sh
crowi/data/
crowi/data/backup/
...
crowi/log/
crowi/log/mongo-backup/

sent 430,666,128 bytes  received 3,591 bytes  287,113,146.00 bytes/sec
total size is 430,546,946  speedup is 1.00
```

ブラウザからアクセスすれば、Crowi の install 画面が見えるはず。


## 6. Shipyard を使ってコンテナ管理
そのうち試したい。

- [shipyard &middot; shipyard](http://shipyard-project.com/)
- [dockerをWebで管理できるshipyardが想像以上に便利だった件 - Qiita](http://qiita.com/mitsugogo/items/5ce1ebb3dcb8468c3793)