KeycloakとGitLab Gitea CodiMD Wiki.js GROWI Mattermost rocket.chat Wekan TAIGA PENPOT NextCloudを連携してnginx-proxyで配信するSSOなWeb開発環境をdocker-composeで構築する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63934/8829a096-22c4-2e37-f8c7-b1e52fccf9d5.png)

いつの日かクラウドサービス使用不可案件が降ってくると信じて。[^1]

# 概要

- 選んで使える静的Webサイト/テンプレート開発用ブラウザアプリスイート
- 数人～十数人程度での使用を想定
- KeycloakでSSO化
- nginx-proxyを使ってリバースプロキシの設定もdocker-compose.ymlに持たせる
- SSL対応（自己署名にも対応）
- アプリは原則として日本語対応のもので、できるだけ初期設定も日本語にする
- 既製イメージを利用し自分ではDockerfileを書かない
- イメージは今後も安定してリリースが継続されそうなものを選ぶ
- 極力dockerリソースには名前をつける

## 成果物

https://github.com/macropygia/dc-sso-self-hosted-workspace

- スモークテストのみ実施
- インストール手順等はリポジトリ内の `docs/INSTALL_ja.md` に概ね書いてあるため、本稿では方針や試行錯誤した点を扱う

## 作業環境

- OS: RHEL8互換（今回はOL8）
    - SELinuxは切っておく
    - 作業はrootで行う
    - 一部のシェルスクリプトはRHEL8互換でないディストロでは動かないかも
- CPU: 4コア
- メモリ: 16GB
    - 8GB以上推奨（メモリ消費の多いものを使わないなら少なくても可）
- ストレージ: 500GB (SSD)
    - 全て入れるとイメージだけで16GB強使用する
- dockerとdocker-compose

## 対応アプリケーション

| 名称        | 種別                     |
| ----------- | ------------------------ |
| Keycloak    | SSO                      |
| GitLab      | Gitプラットフォーム      |
| Gitea       | Gitプラットフォーム      |
| CodiMD      | Markdownエディタ         |
| Wiki.js     | wiki                     |
| GROWI       | wiki                     |
| Mattermost  | チャットツール           |
| rocket.chat | チャットツール           |
| Wekan       | カンバン                 |
| TAIGA       | プロジェクト管理         |
| PENPOT      | デザインプロトタイピング |
| NextCloud   | クラウドストレージ       |

- 追加検討中
    - SynapseまたはDendrite+Element: Matrixサーバ+クライアント
    - Mailu: 全部入りメール環境
    - dnsmasq: DNSサーバ（踏み台構築用）
- 選外
    - Zulip: Postgresに専用パッケージを入れる必要がある
    - OpenProject: SSO対応が有償版のみ
    - Restyaboard: SSO対応が有償版のみ, 日本語が微妙

## 全導入イメージ一覧

<details><div>

| 名称          | 種別                     | Port   | Project Name  | Alias | Depends on     | Auth    |
| ------------- | ------------------------ | ------ | ------------- | ----- | -------------- | ------- |
| Keycloak      | SSO                      | Auto   | keycloak      | kc    | pg             | ID/Pass |
| GitLab        | Gitプラットフォーム      | Auto   | gitlab        |       | pg redis       | SAML    |
| Gitea         | Gitプラットフォーム      | Auto   | gitea         |       | pg             | OIDC    |
| CodiMD        | Markdownエディタ         | Auto   | codimd        |       | pg             | SAML    |
| Wiki.js       | wiki                     | Auto   | wikijs        |       | pg es          | SAML    |
| GROWI         | wiki                     | Auto   | growi         |       | mongo redis es | SAML    |
| Mattermost    | チャットツール           | Auto   | mattermost    | mm    | pg             | GitLab  |
| rocket.chat   | チャットツール           | Auto   | rocketchat    | rc    | mongo          | SAML    |
| Wekan         | カンバン                 | Auto   | wekan         |       | mongo          | OIDC    |
| TAIGA         | プロジェクト管理         | Auto   | taiga         |       | pg rmq         | GitLab  |
| PENPOT        | デザインプロトタイピング | Auto   | penpot        |       | pg redis       | GitLab  |
| NextCloud     | クラウドストレージ       | Auto   | nextcloud     | nc    | pg             | SAML    |
| pgAdmin       | PostgreSQL WebUI         | Auto   | pgadmin       | pga   | pg             |         |
| phpMyAdmin    | MariaDB WebUI            | Auto   | phpmyadmin    | pma   | maria          |         |
| mongo-express | MongoDB WebUI            | Auto   | mongo_express | me    | mongo          |         |
| RedisInsight  | Redis WebUI              | Auto   | redisinsight  | ri    | redis          |         |
| ElasticHQ     | Elasticsearch WebUI      | Auto   | elastichq     | eshq  | es             |         |
| PostgreSQL    | RDBMS                    | 5432   | postgres      | pg    |                |         |
| MariaDB       | RDBMS                    | 3306   | mariadb       | maria |                |         |
| MongoDB       | NoSQL                    | 27017  | mongo         |       |                |         |
| Redis         | KVS                      | 6379   | redis         |       |                |         |
| Elasticsearch | 全文検索                 | 9200   | elasticsearch | es    |                |         |
| RabbitMQ      | メッセージキューイング   | 5672   | rabbitmq      | rmq   |                |         |
| nginx-proxy   | Reverse Proxy            | 80,443 | proxy         |       |                |         |
| MailCatcher   | ダミーSMTPサーバ         | Auto   | mailcatcher   | smtp  |                |         |
| Scope         | Docker WebUI             | 4040   | scope         |       |                |         |
| Portainer     | Docker WebUI             | 9000   | portainer     |       |                |         |

- [self-sign-cert](https://hub.docker.com/r/tkuni83/self-sign-cert): 自己署名作成用

</div></details>

### 使用バージョン/リポジトリ

<details><div>

| 名称          | Tag                 | Version                   | Override | Repository                                                               | Tags                                                                     |
| ------------- | ------------------- | ------------------------- | -------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| Keycloak      | latest              | 12.0.4                    | config   | [quay.io](https://quay.io/repository/keycloak/keycloak)                  | [Link](https://quay.io/repository/keycloak/keycloak?tab=tags)            |
| GitLab        | latest              | 13.9.3-ce.0               |          | [dockerhub](https://hub.docker.com/r/gitlab/gitlab-ce)                   | [Link](https://hub.docker.com/r/gitlab/gitlab-ce/tags)                   |
| Gitea         | 1                   | 1.13.4                    |          | [dockerhub](https://hub.docker.com/r/gitea/gitea)                        | [Link](https://hub.docker.com/r/gitea/gitea/tags)                        |
| CodiMD        | latest              | 2.3.2                     |          | [dockerhub](https://hub.docker.com/r/hackmdio/hackmd)                    | [Link](https://hub.docker.com/r/hackmdio/hackmd/tags)                    |
| Wiki.js       | 2                   | 2.5.191                   |          | [dockerhub](https://hub.docker.com/r/requarks/wiki)                      | [Link](https://hub.docker.com/r/requarks/wiki/tags)                      |
| GROWI         | latest-nocdn        | 4.2.12-nocdn              |          | [dockerhub](https://hub.docker.com/r/weseek/growi)                       | [Link](https://hub.docker.com/r/weseek/growi/tags)                       |
| Mattermost    | latest              | 5.32.1                    |          | [dockerhub](https://hub.docker.com/r/mattermost/mattermost-team-edition) | [Link](https://hub.docker.com/r/mattermost/mattermost-team-edition/tags) |
| rocket.chat   | latest              | 3.12.1                    |          | [dockerhub](https://hub.docker.com/_/rocket-chat)                        | [Link](https://hub.docker.com/_/rocket-chat?tab=tags)                    |
| Wekan         | latest              | v5.05                     |          | [quay.io](https://quay.io/repository/wekan/wekan)                        | [Link](https://quay.io/repository/wekan/wekan?tab=tags)                  |
| TAIGA         | latest              | 6.x                       | source   | [dockerhub](https://hub.docker.com/u/taigaio)                            |                                                                          |
| PENPOT        | latest              | 1.3.0-alpha-1             | source   | [dockerhub](https://hub.docker.com/u/penpotapp)                          |                                                                          |
| NextCloud     | 21-apache           | 21.0.0-apache (21.0.0.18) | config   | [dockerhub](https://hub.docker.com/_/nextcloud)                          | [Link](https://hub.docker.com/_/nextcloud?tab=tags)                      |
| pgAdmin       | latest              | 5.0                       |          | [dockerhub](https://hub.docker.com/r/dpage/pgadmin4)                     | [Link](https://hub.docker.com/r/dpage/pgadmin4/tags)                     |
| phpMyAdmin    | latest              | 5.1.0-apache              |          | [dockerhub](https://hub.docker.com/_/phpmyadmin)                         | [Link](https://hub.docker.com/_/phpmyadmin?tab=tags)                     |
| mongo-express | latest              | 0.54.0                    |          | [dockerhub](https://hub.docker.com/_/mongo-express)                      | [Link](https://hub.docker.com/_/mongo-express?tab=tags)                  |
| RedisInsight  | latest              | 1.10.0                    |          | [dockerhub](https://hub.docker.com/r/redislabs/redisinsight)             | [Link](https://hub.docker.com/r/redislabs/redisinsight/tags)             |
| ElasticHQ     | latest              | v3.5.6                    |          | [dockerhub](https://hub.docker.com/r/elastichq/elasticsearch-hq)         | [Link](https://hub.docker.com/r/elastichq/elasticsearch-hq/tags)         |
| PostgreSQL    | 13-alpine           | 13.2-alpine               |          | [dockerhub](https://hub.docker.com/_/postgres)                           | [Link](https://hub.docker.com/_/postgres?tab=tags)                       |
| MariaDB       | 10                  | 10.5.9                    |          | [dockerhub](https://hub.docker.com/_/mariadb)                            | [Link](https://hub.docker.com/_/mariadb?tab=tags)                        |
| MongoDB       | 4-bionic            | 4.4.4-bionic              |          | [dockerhub](https://hub.docker.com/_/mongo)                              | [Link](https://hub.docker.com/_/mongo?tab=tags)                          |
| Redis         | 6-alpine            | 6.2.1-alpine              |          | [dockerhub](https://hub.docker.com/_/redis)                              | [Link](https://hub.docker.com/_/redis?tab=tags)                          |
| Elasticsearch | 6.8.14              | 6.8.14                    |          | [elasticsearch](https://www.docker.elastic.co/r/elasticsearch)           | [Link](https://www.docker.elastic.co/r/elasticsearch)                    |
| RabbitMQ      | 3-management-alpine | 3.8.14-management-alpine  |          | [dockerhub](https://hub.docker.com/_/rabbitmq)                           | [Link](https://hub.docker.com/_/rabbitmq?tab=tags)                       |
| nginx-proxy   | alpine              | 0.8.0                     | source   | [dockerhub](https://hub.docker.com/r/jwilder/nginx-proxy)                | [Link](https://hub.docker.com/r/jwilder/nginx-proxy/tags)                |
| MailCatcher   | latest              |                           |          | [dockerhub](https://hub.docker.com/r/sj26/mailcatcher)                   | [Link](https://hub.docker.com/r/sj26/mailcatcher/tags)                   |
| Scope         | latest_release      | 1.13.1                    |          | [dockerhub](https://hub.docker.com/r/weaveworks/scope)                   | [Link](https://hub.docker.com/r/weaveworks/scope/tags)                   |
| Portainer     | alpine              | 1.24.1-alpine             |          | [dockerhub](https://hub.docker.com/r/portainer/portainer)                | [Link](https://hub.docker.com/r/portainer/portainer/tags)                |

</div></details>

# docker-composeプロジェクト構築

## 共通方針


### プロジェクト名・サービス名・コンテナ名

- プロジェクト名は必ず.envの `COMPOSE_PROJECT_NAME` で明示的に指定し、ディレクトリ名と一致させる
    - ハイフン・アンダースコア使用不可
- プロジェクト内のメインとなるコンテナのサービス名とコンテナ名も違和感がなければ合わせる
- container_nameは明示的に指定する
- hostnameに転用する関係でcontainer_nameにはアンダースコア使用不可

### hostname

- 明示的に指定する
- aliasがあるものは `[alias]-server` 、ないものは `[container_name]-server` を原則とする
- アンダースコア使用不可

### ports, expose

- `expose` にコンテナ側のポートを指定する
- コンテナ間ネットワークに繋がったnginx-proyxで配信するため `ports` は使用しない

### volumes

- 一方的にコンテナに送り込むファイル・ディレクトリは `<project_root>/mount` に配置する
    - オーバーライド用の設定ファイルや鍵など
    - git管理内とする
- ホストとコンテナ双方で編集するか、ホストから頻繁に参照するディレクトリは `<project_root>/volumes` に配置する
    - 設定など
    - git管理外とする（雛形などがあるならgit管理内の別の場所に用意しておく）
- 通常閲覧も編集もしないものは `docker volume` で予め作成しておく
    - データベースのデータディレクトリなど
    - 命名は `[project_name]-<purpose>` を原則とする
- 勝手にvolumeが作られる場合は `docker inspect` して `mounts` の内容を確認する

### networks

- 原則として全てのコンテナはbridgeネットワーク `docker.internal` に繋げておく
- 個別に必要な場合は名前をつける
    - ハイフン使用不可

### logging

- 標準では無制限のため理由がなければ予防的措置として全てのサービスに下記を設定しておく

```yaml
logging:
  options:
    max-size: "10m"
    max-file: "3"
```

### extra_hosts

- テレメトリっぽいものや不要な外部アクセスを `192.0.2.0` に向けて遮断
- KeycloakのアドレスをサーバのローカルIPに向ける
    - コンテナ内部からKeycloakへのアクセスを外に出さず、かつSSLでも通るように

## ホスト上でのdocker-composeの操作

専用のMakefileを使用する。詳しくは[MakafileのREADME](https://github.com/macropygia/dc-multiple-project-makefile/blob/main/README_ja.md)参照。
もちろん普通にdocker-composeコマンドを打っても構わない。

https://github.com/macropygia/dc-multiple-project-makefile

コンテナ作成後はScopeやPortainerでブラウザ上から操作。

# カテゴリ毎の方針

## データベース等

PostgreSQL, MariaDB, MongoDB, Redis, RabbitMQ, ElasticSearch

- コンテナ間ネットワークでのみ使用
- rootユーザーはユーザー名 `root` パスワード `root` とする
    - PostgreSQL, MariaDB, MongoDB, RabbitMQで設定
- Mongo, Redis, ElasticSearchは認証なし
- 初期化は `inidb.d` を使用して初回起動時に自動で行う
    - RabbitMQはdocker-composeを使った初期化方法が分からないためWebUIから行う
    - RedisとElasticSearchは初期化不要
    - MongoDBはrocket.chatがレプリケーションを必要とするため独立した初期化処理が必要（シェルスクリプトで対応）


## データベース用WebUI

pgAdmin, phpMyAdmin, mongo-express, RedisInsight, RabbitMQ (Built-in), ElasticHQ

- ローカルネットワークからのアクセスのみ許可する
- 具体的には `127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16` からのアクセスのみ許可
    - environmentに `NETWORK_ACCESS=internal` と書いておくとnginx-proxyが自動でやってくれる

## サーバ

nginx-proxy, MailCatcher

- 特に設定することはない

## モニタリング

Scope, Portainer

- nginx-proxyの管理下には置かない
- ポート `4040` と `9000` をファイアーウォールで適切に制限する
- ブラウザ経由でコンテナにフルアクセスできるため絶対に外部に公開しないこと

## アプリケーション

Keycloak, GitLab, Gitea, CodiMD, Wiki.js, GROWI, Mattermost, rocket.chat, Wekan, TAIGA, PENPOT, NextCloud

- 管理者アカウントはユーザー名 `webmaster` , パスワード `Pa55w@rd` , メールアドレス `webmaster@<DOMAIN>` とする
    - ものによっては `admin` が使えない → 統一しておきたいので `webmaster` としておく

# その他

## SSO

各アプリの認証方式と、「Keycloak上のユーザープロパティ/属性」と「アプリのユーザー情報」のマッピングについては事前調査の記事を参照のこと。

https://qiita.com/macropygia/items/f1cd804b4135096cecf9

アプリケーション毎の設定方法についてはリポジトリの各アプリのディレクトリにある `INSTALL_ja.md` を参照のこと。

### Gitea対策

上記の調査にある「Keycloakのユーザー属性に `nickname` が存在するとGiteaが問答無用で使おうとする問題」については、Giteaのソースを大幅に修正する以外に解決方法がなさそうなため、ユーザー属性には `nickname` を使用しないことにした。
表示名を格納するユーザー属性は `displayName` としてマッピングで対処する。

### TAIGAのソース修正

TAIGAの公式Dockerイメージに同梱されているGitLab OAuth2用の `taiga-contrib-gitlab-auth` プラグインの実装に問題があり、Keycloakと連携するにはソースの修正が必要だった。

具体的にはrequests.postに渡すデータの型が `param` （クエリパラメータ）になっており、これを `data` に修正する。（[該当箇所のソース](https://github.com/taigaio/taiga-contrib-gitlab-auth/blob/dedd7307453f3e09a1d7c1e83eea586dc7453dac/back/taiga_contrib_gitlab_auth/connector.py#L95)）

### PENPOTのソース修正

PENPOTの使用しているhttp実装はhttpステータスコード307によるPOSTリクエストのリダイレクトに対応しておらず、Keycloakコンテナ上のGitLabのエンドポイントに対するアクセスをKeycloakのエンドポイントにリダイレクトすることができないため、ソースの修正が必要だった。

具体的にはハードコーディングされているGitLab OAuth2エンドポイントのパスを、KeycloakのOpenID Connectエンドポイントのものに書き換えればよい。（[該当箇所のソース](https://github.com/penpot/penpot/blob/de394a7d4e7e590047ce7c2647bcbc0da8d38e2a/backend/src/app/http/oauth/gitlab.clj#L39)）

## SSL対応

nginx-proxyが各docker-compose.ymlのenvironmentを見てよしなにやってくれる。

各アプリケーションの[environmentに記述するnginx-proxy用の設定項目](https://github.com/nginx-proxy/nginx-proxy/wiki/List-of-Supported-Environment-Variables)のうち、SSLに関連する設定項目の有無を見て自動で構成してくれる模様。詳しい条件は[conf出力用のテンプレート](https://github.com/nginx-proxy/nginx-proxy/blob/master/nginx.tmpl)を読めば書いてある。

SHWでは `SSL_POLICY` の有無で分岐させている。

## 自己署名認証局

ホスト上で認証局の証明書を然るべきディレクトリに配置し `update-ca-trust` で登録すると、PEMだけでなくJavaやEDK2用のバイナリなど各種フォーマットのCAバンドルが自動で生成される。

生成したCAバンドルを必要に応じてコンテナにマウントすることで、既製イメージ内のOS、Python、Java等に任意の認証局を追加できる。（ `update-ca-certificates` とか `keytool -importcert -trustcacerts -file ca.crt -cacerts -storepass changeit -alias self-sign -noprompt` とかやらなくて済む）

なお、証明書と鍵の作成には下記記事のものを使用した。

https://qiita.com/t-kuni/items/d3d72f429273cf0ee31e

サーバの証明書と秘密鍵はnginx-proxyが使用するため、マウント用ディレクトリにコピーする。
一連の作業はシェルスクリプトで実行できるようにした。詳しくは `setup/*selfsigncert.sh` を参照のこと。

## インストール用シェルスクリプト

サーバのローカルIP・ドメイン・SSLの使用有無・言語・タイムゾーンといった設定項目は関係するファイル全般に反映する必要があるが、対象が――

- docker-compose.yml
- volumesでマウントする各アプリ用の設定ファイルや修正を加えたソース
    - jsonとかphpとかpythonとかclojureとか色々
- nginx-proxy用設定ファイルの**ファイル名**
    - ドメイン名とファイル名を照合して取捨選択する仕組みのため

――と多岐に渡るため、予め設定部分を置換用文字列にしておき、シェルスクリプトで設定ファイルに従って置換する方法を採った。
処理の内訳は `docs/INSTALL_ja.md` に詳しく書いてある。

## コンテナのタイムゾーン統一

イメージによって `tzdata` が入っていたり入っていなかったりするため、environmentで `TZ=Asia/Tokyo` を指定しても日本時間にならないものがある。
全てのコンテナに `TZ=Asia/Tokyo` を設定してdateコマンドを打ってみたところ、全34コンテナ中21個がJST、11個がUTC、2個がAsia（時刻はUTC）となった。

要はtzdataのAsia/Tokyo用のバイナリがコンテナ内に存在していればいい訳で、ホスト上の `/usr/share/zoneinfo/Asia/Tokyo` をそのまま `read-only` でコンテナにマウントすることにした。

```yaml:docker-compose.yml
︙
environment:
  - TZ=Asia/Tokyo
volumes:
  - /usr/share/zoneinfo/Asia/Tokyo:/usr/share/zoneinfo/Asia/Tokyo:ro
︙
```

`Asia/Tokyo` は置換用文字列 `<TZ>` にしておき、インストール用シェルスクリプトの一括置換対象とする。

# ToDo

- アプリのUI上の時刻表示が日本時間になっているか確認する
- docsの整備
- production用に使用するバージョンを固定したものも欲しい

[^1]: Dockerもdocker-composeもなんならLinuxも初学者なので多少の粗はご容赦ください