OpenProject を Raspberry pi 4 の Docker で動かす

どうも建築業界向けの機能も対応するにはもっとビルドを頑張らないといけないようである。
※COLLADA2GLTFとかIFCconvertとか。やりたいひとは頑張れ...一応ビルドはできたぞ。長いから書かないけれど。

## 背景

いまさらながら Redmine で GTD をやってみようと 2 週間ほど運用してきた。
しかし、使いやすいようにしようと思うと 10 個近くのプラグインが必要であったり、かゆいところに手が届かない感じが多いように思った。
簡単に調べてみると、やっぱり必要な機能は openproject の方がデフォルトで揃っているように感じた。そのため、一念発起して公式ではサポートされていない arm64 上の docker での実行を試みた。
結果は一応うまくいったので、世界の誰かのためになればと思い、この記事を投稿します。

## 環境構築

ざっくりとした構築方法は以下の通り

* 公式の Dockerfile を元に arm64 で動くように変更を加える
* All in One コンテナではなく、docker-compose を使用するバージョンの arm64 版を目指す
* https を有効にするために letsencrypt を利用する自作の proxy を立てる


### まずは All in One コンテナが動くことを目標とする

#### 公式の GitHub リポジトリをクローンする

```bash
git clone https://github.com/opf/openproject.git
# 真面目な人はこの後、タグをチェックアウトしてね。私は面倒なのでデフォルトブランチで作業します。
```

#### docker/prod/Dockerfile を変更

gosu を arm64 版を使用するように 変更する。
\# clone してきた中身を変えても良い (COPY . . が途中にある)
\# ホストでダウンロードしても良いけど、やり直すことになった時にやり忘れそう

```dockerfile
FROM ruby:2.7.2-buster
MAINTAINER operations@openproject.com

# Allow platform-specific additions. Valid values are: on-prem,saas,bahn
ARG PLATFORM=on-prem
# Use OAuth token in case private gems need to be fetched
ARG GITHUB_OAUTH_TOKEN
ARG DEBIAN_FRONTEND=noninteractive

ARG PGLOADER_BINARY_DOWNLOAD_URL=https://openproject-docker-public.s3-eu-west-1.amazonaws.com/pgloader/bin/pgloader-ccl

ENV NODE_VERSION="12.18.3"
ENV BUNDLER_VERSION="2.1.4"
ENV BUNDLE_PATH__SYSTEM=false
ENV APP_USER=app
ENV APP_PATH=/app
ENV APP_DATA_PATH=/var/openproject/assets
ENV APP_DATA_PATH_LEGACY=/var/db/openproject
ENV PGDATA=/var/openproject/pgdata
ENV PGDATA_LEGACY=/var/lib/postgresql/9.6/main

ENV DATABASE_URL=postgres://openproject:openproject@127.0.0.1/openproject
ENV RAILS_ENV=production
ENV RAILS_CACHE_STORE=memcache
ENV RAILS_GROUPS=production
ENV RAILS_LOG_TO_STDOUT=1
ENV RAILS_SERVE_STATIC_FILES=1
ENV OPENPROJECT_INSTALLATION__TYPE=docker
# Valid values are: standard,bim
ENV OPENPROJECT_EDITION=standard
ENV NEW_RELIC_AGENT_ENABLED=false
ENV ATTACHMENTS_STORAGE_PATH=$APP_DATA_PATH/files
# Set a default key base, ensure to provide a secure value in production environments!
ENV SECRET_KEY_BASE=OVERWRITE_ME

RUN curl ${PGLOADER_BINARY_DOWNLOAD_URL} > /usr/local/bin/pgloader-ccl && chmod +x /usr/local/bin/pgloader-ccl

WORKDIR $APP_PATH

COPY docker/prod/setup ./docker/prod/setup
RUN ./docker/prod/setup/preinstall.sh

COPY Gemfile ./Gemfile
COPY Gemfile.* ./
COPY modules ./modules
COPY vendor ./vendor
# some gemspec files of plugins require files in there, notably OpenProject::Version
COPY lib ./lib

RUN bundle install --quiet --deployment --path vendor/bundle --no-cache \
  --with="$RAILS_GROUPS" --without="test development" --jobs=8 --retry=3 && \
  rm -rf vendor/bundle/ruby/*/cache && rm -rf vendor/bundle/ruby/*/gems/*/spec && rm -rf vendor/bundle/ruby/*/gems/*/test

# Finally, copy over the whole thing
COPY . .

# Replace gosu with gosu-arm64
RUN wget -O ./docker/prod/gosu https://github.com/tianon/gosu/releases/download/1.12/gosu-arm64

RUN ./docker/prod/setup/postinstall.sh

# Expose ports for apache and postgres
EXPOSE 8080 5432

# Expose the postgres data directory and OpenProject data directory as volumes
VOLUME ["$PGDATA", "$APP_DATA_PATH"]

# Set a custom entrypoint to allow for privilege dropping and one-off commands
ENTRYPOINT ["./docker/prod/entrypoint.sh"]

# Set default command to launch the all-in-one configuration supervised by supervisord
CMD ["./docker/prod/supervisord"]
```

#### docker/prod/setup/preinstall-common.sh を変更

x64 版のバイナリを指定している箇所を arm64 版を使うように変更する。

```bash
#!/bin/bash

set -e
set -o pipefail

# install node + npm
curl -s https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-arm64.tar.gz | tar xzf - -C /usr/local --strip-components=1

wget --quiet -O- https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
echo "deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main" > /etc/apt/sources.list.d/pgdg.list

apt-get update -qq
apt-get install -y \
	apt-transport-https \
	pandoc \
	poppler-utils \
	unrtf \
	tesseract-ocr \
	catdoc \
	postgresql-9.6 \
	postgresql-client-9.6 \
	imagemagick

rm -rf "$PGDATA_LEGACY"

# Specifics for BIM edition
curl -SL -o dotnet.tar.gz https://dotnetcli.blob.core.windows.net/dotnet/Runtime/master/dotnet-runtime-latest-linux-arm64.tar.gz
mkdir -p /usr/share/dotnet
tar -zxf dotnet.tar.gz -C /usr/share/dotnet
ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet

tmpdir=$(mktemp -d)
cd $tmpdir

# Install XKT converter
npm install @xeokit/xeokit-gltf-to-xkt@0.0.3 -g

# Install COLLADA2GLTF
wget --quiet https://github.com/KhronosGroup/COLLADA2GLTF/releases/download/v2.1.5/COLLADA2GLTF-v2.1.5-linux.zip
unzip -q COLLADA2GLTF-v2.1.5-linux.zip
mv COLLADA2GLTF-bin "/usr/local/bin/COLLADA2GLTF"

# IFCconvert
wget --quiet https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.6.0-9bcd932-linux64.zip
unzip -q IfcConvert-v0.6.0-9bcd932-linux64.zip
mv IfcConvert "/usr/local/bin/IfcConvert"

wget --quiet https://github.com/bimspot/xeokit-metadata/releases/download/1.0.0/xeokit-metadata-linux-arm.tar.gz
tar -zxvf xeokit-metadata-linux-arm.tar.gz
chmod +x xeokit-metadata-linux-arm/xeokit-metadata
cp -r xeokit-metadata-linux-arm/ "/usr/lib/xeokit-metadata"
ln -s /usr/lib/xeokit-metadata/xeokit-metadata /usr/local/bin/xeokit-metadata

cd /
rm -rf $tmpdir

gem install bundler --version "$BUNDLER_VERSION" --no-document

useradd -d /home/$APP_USER -m $APP_USER

```

#### build する (20 分ほど掛かる)

```bash
docker build -t openproject/community:11-arm64 -f docker/prod/Dockerfile ./
```

#### 疎通 (スモーク) テストを行う


```bash
docker run --rm -it -p 8080:80 -e SECRET_KEY_BASE=secret openproject/community:11-arm64
```


### docker-compose で動かす


#### 公式の `docker-compose.yml` を取得する

```bash
git clone https://github.com/opf/openproject-deploy --depth=1 --branch=stable/11 openproject
cd openproject/compose
nano docker-compose.yml
```

####  `docker-compose.yml` を変更する

* コンテナネーム `app` のイメージを自作のイメージに変更する
* `volumes` のホスト側のマウントポイントをカレントディレクトリにする

```yaml
version: "3.7"

networks:
  frontend:
  backend:

volumes:
  pgdata:
  opdata:

x-op-restart-policy: &restart_policy
  restart: unless-stopped
x-op-image: &image
  image: openproject/community:11-arm64
x-op-app: &app
  <<: *image
  <<: *restart_policy
  environment:
    RAILS_CACHE_STORE: "memcache"
    OPENPROJECT_CACHE__MEMCACHE__SERVER: "cache:11211"
    OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
    DATABASE_URL: "postgres://postgres:p4ssw0rd@db/openproject"
    USE_PUMA: "true"
    # set to true to enable the email receiving feature. See ./docker/cron for more options
    IMAP_ENABLED: "${IMAP_ENABLED:-false}"
  volumes:
    - "./opdata:/var/openproject/assets"

services:
  db:
    image: postgres:10
    <<: *restart_policy
    stop_grace_period: "3s"
    volumes:
      - "./pgdata:/var/lib/postgresql/data"
    environment:
      POSTGRES_PASSWORD: p4ssw0rd
      POSTGRES_DB: openproject
    networks:
      - backend

  cache:
    image: memcached
    <<: *restart_policy
    networks:
      - backend

  proxy:
    <<: *image
    <<: *restart_policy
    command: "./docker/prod/proxy"
    ports:
      - "${PORT:-8080}:80"
    environment:
      APP_HOST: web
      OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
    depends_on:
      - web
    networks:
      - frontend

  web:
    <<: *app
    command: "./docker/prod/web"
    networks:
      - frontend
      - backend
    depends_on:
      - db
      - cache
      - seeder

  worker:
    <<: *app
    command: "./docker/prod/worker"
    networks:
      - backend
    depends_on:
      - db
      - cache
      - seeder

  cron:
    <<: *app
    command: "./docker/prod/cron"
    networks:
      - backend
    depends_on:
      - db
      - cache
      - seeder

  seeder:
    <<: *app
    command: "./docker/prod/seeder"
    restart: on-failure
    networks:
      - backend
```

####  疎通 (スモーク) テスト

```bash
docker-compose up
```

###  HTTPS を有効にする


####  ディレクトリ構造

```
compose/
├── docker-compose.yml #変更する
└── proxy #作成する
    ├── Dockerfile #作成する
    └── default.conf.template #作成する
```

####  proxy を自作のものに変更する

```dockerfile
FROM nginx:latest
ARG CERTBOT_EMAIL=default@default.com
ARG DOMAIN_LIST

# Expose ports.
EXPOSE 80
EXPOSE 443

RUN  apt-get update \
      && apt-get install -y cron certbot \
      && certbot certonly --dry-run --standalone --agree-tos -m "${CERTBOT_EMAIL}" -n -d ${DOMAIN_LIST} \ 
      && certbot certonly  --standalone --agree-tos -m "${CERTBOT_EMAIL}" -n -d ${DOMAIN_LIST} \
      && rm -rf /var/lib/apt/lists/* \
      && echo "@monthly /usr/bin/certbot renew --nginx >> /var/log/cron.log 2>&1" >/etc/cron.d/certbot-renew \
      && crontab /etc/cron.d/certbot-renew 
VOLUME /etc/letsencrypt

CMD [ "sh", "-c", "cron && ./docker-entrypoint.sh nginx -g 'daemon off;'" ]
```

####  `docker-compose.yml` を変更する


```dockerfile

version: "3.7"

networks:
  frontend:
  backend:

volumes:
  pgdata:
  opdata:

x-op-restart-policy: &restart_policy
  restart: unless-stopped
x-op-image: &image
  image: openproject/community:11-arm64
x-op-app: &app
  <<: *image
  <<: *restart_policy
  container_name: app
  environment:
    RAILS_CACHE_STORE: "memcache"
    OPENPROJECT_CACHE__MEMCACHE__SERVER: "cache:11211"
    OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
    DATABASE_URL: "postgres://postgres:p4ssw0rd@db/openproject"
    USE_PUMA: "true"
    # set to true to enable the email receiving feature. See ./docker/cron for more options
    IMAP_ENABLED: "${IMAP_ENABLED:-false}"
  volumes:
    - "./opdata:/var/openproject/assets"

services:
  db:
    container_name: db
    image: postgres:10
    <<: *restart_policy
    stop_grace_period: "3s"
    volumes:
      - "./pgdata:/var/lib/postgresql/data"
    environment:
      POSTGRES_PASSWORD: p4ssw0rd
      POSTGRES_DB: openproject
    networks:
      - backend

  cache:
    container_name: cache
    image: memcached
    <<: *restart_policy
    networks:
      - backend

#  proxy:
#    <<: *image
#    <<: *restart_policy
#    command: "./docker/prod/proxy"
#    ports:
#      - "${PORT:-8080}:80"
#    environment:
#      APP_HOST: web
#      OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"

  proxy:
    <<: *restart_policy
    container_name: proxy
    build:
      context: ./proxy
      network: host
      args:
        - CERTBOT_EMAIL=your-email@mail.com #replace with your own email
        - DOMAIN_LIST=your.hostname.com            #replace with your own domains
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./proxy/default.conf.template:/etc/nginx/templates/default.conf.template
    environment:
      MY_DOMAIN_NAME: your.hostname.com #replace with your own domains
    depends_on:
      - web
    networks:
      - frontend
  web:
    <<: *app
    container_name: web
    command: "./docker/prod/web"
    networks:
      - frontend
      - backend
    depends_on:
      - db
      - cache
      - seeder

  worker:
    <<: *app
    container_name: worker
    command: "./docker/prod/worker"
    networks:
      - backend
    depends_on:
      - db
      - cache
      - seeder

  cron:
    <<: *app
    container_name: cron
    command: "./docker/prod/cron"
    networks:
      - backend
    depends_on:
      - db
      - cache
      - seeder

  seeder:
    <<: *app
    container_name: seeder
    command: "./docker/prod/seeder"
    restart: on-failure
    networks:
      - backend
```


####  `proxy/default.conf.template ` を変更する

```conf
server {
    listen       80;
    server_name  localhost;

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name  localhost;

    ssl_certificate /etc/letsencrypt/live/${MY_DOMAIN_NAME}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/${MY_DOMAIN_NAME}/privkey.pem;

    location / {
        proxy_pass http://web:8080/;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_redirect http:// https://;
    }
    # --- For CertBot ---
    location ^~ /.well-known/acme-challenge/ {
        root /usr/share/nginx/html/;
    }

    location = /.well-known/acme-challenge/ {
        return 404;
    }
}
```

####  疎通 (スモーク) テスト

```bash
docker-compose up
```