[Docker] Pixelfedインスタンスを作って慣れるDocker入門

# はじめに

写真共有SNSの一つ、「[Flickr](https://www.flickr.com/)」の無料プランを使っています。しかし最近は枚数が増えてきて、無料プランの上限1000枚に達しそうな状況になりました。Proに課金する手もあるわけですが、SNSとしてはあまり活用していませんし、Proの価格も年々値上げされている[^pro_price]状況で、今後を考えると躊躇してしまいます。

[^pro_price]: 2023年8月現在、毎月払いで月額8.49ドル（約1200円）、2年一括払いで月当たり5.99ドル（約850円）

そこで、自分で画像管理サーバを立てることを考え始めました。移植性・再現性を考えてDockerで作ろうとしたのですが、Docker自体ほとんど触ったことがないうえに、今回扱うPixelfedにも日本語資料が少なく、全容がつかめないまま試行錯誤してしまいました。ここではそのメモをまとめてみます。試行錯誤を通じてDockerに慣れてきたところもあるので、Dockerの入門ガイド（っぽいもの）にもなるかと思います。

## Pixelfedについて

画像管理サーバを実現するためのWebアプリとして候補に上がったのが「[Pixelfed](https://pixelfed.org/)」です。以下のような特徴があります。

- 画像を中心とする分散型SNS。機能的にはInstagramに近いものになる
    - X (Twitter) やInstagramが、唯一のサーバにすべてのデータが集中する中央集権型SNSであるのに対して、複数のインスタンス（サーバ）が存在し、それらが相互につながっているという特徴がある。誰でも自分でインスタンスを立てることが可能
- ActivityPubを実装しており、Fediverseに参加できる（MastodonやMisskeyの各インスタンスに所属するアカウントと相互にフォローできる）
- コレクション機能が使える（Flickrでいえばアルバムに相当）
- ブログパーツが提供されている
- ハッシュタグをつけられる（検索できる）

これだけの機能があれば、私個人の利用目的には十分事足ります。Fediverseに関しては優先度は高くありませんが、MastodonやMisskeyなどのアカウントからフォローしていただけるのは便利そうです。
既存のPixelfedインスタンスにアカウントを作ってもよいのですが、ここは一つ、お一人様インスタンスを立ててみるのも面白いのではないかと思い、Dockerを使ってPixelfedインスタンスを作る研究を始めました。自分で撮影した写真を自分の管理下に置けるというメリットもあります。

## docker-compose.yml は存在するけれど

Pixelfedのソースコードには ``docker-compose.yml`` が入っているので、これを使えばすぐにDockerで構築できるかと思いきや、コアのコンテナイメージが見つからず、コンテナの作成に失敗します。

```bash:Terminal
$ git clone https://github.com/pixelfed/pixelfed
$ cd pixelfed
$ docker compose up -d
Error response from daemon: manifest for pixelfed/pixelfed:latest not found: manifest unknown: manifest unknown
```

サードパーティ製のコンテナイメージもありますが、中身も分からずに使うのもどうかと思うので、やはり自分で作ってみようと思います。

## おことわり

- 用語などの厳密性はあまり追求しません。
- あれこれ作ったり消したりしながらスクリーンショットを取っているので、コンテナIDの表示などが急に変わったりします。
- ローカルのDocker環境で一通り動きそうなことを確認しましたが、まだFediverseにつないだ実績はありません。
- テスト環境を前提とした設定が多く（セキュリティ面や各種チューニング）、公開用サーバにするには設定の見直しが必要です。

# Docker環境の準備

今回の検証環境は以下です。
- Windows 10 Home 22H2
- Ubuntu 22.04 (WSL2)
- Docker Desktop 4.21.1

WSL2およびDocker Desktopのインストールについては、日本語の記事がたくさんあるのでここでは省略します。
- [WSL2のインストールを分かりやすく解説【Windows10/11】 | チグサウェブ](https://chigusa-web.com/blog/wsl2-win11/)
- [【Docker Desktop】Windowsにインストール（WSL2） | チグサウェブ](https://chigusa-web.com/blog/windows%E3%81%ABdocker%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%97%E3%81%A6python%E7%92%B0%E5%A2%83%E3%82%92%E6%A7%8B%E7%AF%89/)

Docker Desktopがインストールできたら、まずはハンズオン形式のチュートリアルで操作に慣れておくとよいでしょう。Docker Desktopの「Learning Center」から入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/f47584ce-c3ca-0c52-eb33-4364fbbb53cb.png)

必要に応じて、WSL2でインストールしたUbuntu環境にテキストエディタなどをインストールしておきます。

```bash:Terminal
$ sudo apt-get install vim
$ sudo apt-get install emacs
# etc...
```

# Pixelfedインスタンスの全体像

PixelfedのコードはPHPで書かれています。公式サイトの[Run your own Pixelfed website](https://docs.pixelfed.org/running-pixelfed/)に従って、環境を整えます。

## サーバ構成

Webサーバとしてnginxを使うことにすると、以下のような4種類のサーバから構成されることになります。それぞれがDockerのコンテナに対応します。（1つのコンテナに複数のサーバを入れることも可能ですが、今回は分かりやすさのためにコンテナを分けます）
![Pixelfedインスタンスの全体像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/34a52094-edb1-12ed-d379-ded53cc7a341.png)

- nginx: HTTPサーバ。ユーザーからのリクエストを受けて静的コンテンツを返す。PHPスクリプトについては自分では実行する機能を持たず、FPMサーバにリクエスト（○○のスクリプトを実行しろ）を転送し、その応答をユーザに返す（いわゆるリバースプロキシ）
- FPM (FastCGI Process Manager): PHPスクリプトを実行するサーバ。FPMにはスクリプト実行時の負荷軽減や高速化機能が含まれる。Pixelfedの大部分の処理はここで行われる
- MySQL: データベース (DB) サーバ
- Redis: Key-Valueストア。DBクエリ結果のキャッシュやセッション管理に使われている（おそらく）

ここで、Webから直接アクセスされるのはnginxサーバのみです。例えばこれらを1台のVPSや物理サーバで動かす場合は、80番と443番のポートだけを開けておけばよいことになります。
また、PHPを実行するのはFPMサーバのみですので、PHPの環境や実行に必要なモジュールはすべてFPMサーバにインストールすることになります。

## ストレージ

前述の通り、静的コンテンツ（画像ファイルなど）はnginx、動的コンテンツ（PHPスクリプト）はFPMと別のサーバで処理されるわけですが、PHPスクリプトだけをFPMサーバ側に分離して設置するのはメンテナンス面で大変ですし、PHPスクリプトが生成した画像（静的コンテンツ）はnginxサーバ側になければならないので、その転送をするのも一苦労です。

というわけで、現実的には両者で共通のストレージを参照することになるでしょう。Dockerを使う場合はボリュームの設定をすればよいです。この記事では、Pixelfedのインストール先を  ``/opt/pixelfed`` としています。

さらに、MySQLやRedisのデータについてもデータを永続化しておきます。Dockerの場合は、デフォルトではコンテナが消滅すると中のデータもすべて消えてしまいます。コンテナイメージとコンテナを作り直した場合も同様です。ボリュームを設定しておけば、このような場合でもデータを消さずにすみます。

以上を踏まえて、以下のようなサーバ・ストレージの構成としましょう。この図では、円柱記号はDockerのボリュームを表しています。

![Pixelfedインスタンスの全体像（＋ストレージ）](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/9bc6ad8a-c327-17ab-5dc3-6264a289136c.png)

以降の説明は、このサーバ構成を前提に進めていきます。

# コンテナイメージの作成

以降は断りがない限り、すべてWSL2のターミナル・ファイルシステム上での操作となります。

## 使用するミドルウェアのバージョン

- PHP 8.2.8
- nginx 1.25.1
- MariaDB 11.0.2
- Redis 7.0.12

## ディレクトリ構成

```
pixelfed/  : 作業ディレクトリの起点
├─ fpm/
│  ├─ configs/
│  │  ├─ cron-pixelfed
│  │  ├─ php.ini
│  │  ├─ php-fpm.conf
│  │  ├─ pixelfed.env
│  │  ├─ supervisord.conf
│  │  ├─ supervisor-cron.conf
│  │  └─ supervisor-pixelfed-horizon.conf
│  ├─ scripts/
│  │  └─ docker-entrypoint.sh
│  └─ Dockerfile
├─ web/
│  ├─ configs/
│  │  ├─ ssl/
│  │  │  ├─ server.crt
│  │  │  └─ server.key
│  │  └─ nginx.conf
│  └─ Dockerfile
└─ compose.yaml
```

## nginxコンテナ

``pixelfed/web`` ディレクトリの中にDockerfileを作成します。

```docker:pixelfed/web/Dockerfile
# ベースとなるコンテナイメージ
FROM nginx:1.25

# メインとなるアプリケーションディレクトリ
WORKDIR /opt/pixelfed

# OSのユーザをシステムアカウントとして追加
RUN useradd -rU -s /bin/bash pixelfed

# 設定ファイルとSSL証明書のコピー
COPY ./configs/nginx.conf /etc/nginx/nginx.conf
COPY ./configs/ssl /etc/nginx/ssl

# ポート開放（コンテナ側で開放しているポートを指定）
EXPOSE 80
EXPOSE 443
```

``pixelfed/web`` の下に ``configs`` というディレクトリを作成し、その中に設定ファイルを置きます。
[Generic installation guide | Pixelfed Documentation](https://docs.pixelfed.org/running-pixelfed/installation/#nginx) の内容をベースに、nginx 1.25.1で動くように書き換えたものです。

```text:pixelfed/web/configs/nginx.conf
user pixelfed;

events {}
http {
  server {
    listen 443 ssl;       # IPv4用
    listen [::]:443 ssl;  # IPv6用
    http2 on;             # HTTP/2対応 (v1.25.1以降)
    server_name pixelfed.internal;  # ホスト名（実運用時は自分で取った独自ドメイン名を指定）
    root /opt/pixelfed/public;      # DocumentRoot

    ssl_certificate /etc/nginx/ssl/server.crt;     # SSL証明書（後述）
    ssl_certificate_key /etc/nginx/ssl/server.key;

    ssl_protocols TLSv1.2;
    ssl_ciphers EECDH+AESGCM:EECDH+CHACHA20:EECDH+AES;
    ssl_prefer_server_ciphers on;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    index index.php;

    charset utf-8;
    client_max_body_size 32M; # 一度にPOSTできるデータ量（デフォルト値より増やしました）

    location / {
      try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
      fastcgi_pass    pixelfed-fpm:9000;  # このpixelfed-fpmはFPMコンテナの名前
      fastcgi_param   SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
      fastcgi_param   QUERY_STRING        $query_string;
      fastcgi_param   REQUEST_METHOD      $request_method;
      fastcgi_param   CONTENT_TYPE        $content_type;
      fastcgi_param   CONTENT_LENGTH      $content_length;
      fastcgi_param   SCRIPT_NAME     $fastcgi_script_name;
      fastcgi_param   REQUEST_URI     $request_uri;
      fastcgi_param   DOCUMENT_URI        $document_uri;
      fastcgi_param   DOCUMENT_ROOT       $document_root;
      fastcgi_param   SERVER_PROTOCOL     $server_protocol;
      fastcgi_param   GATEWAY_INTERFACE   CGI/1.1;
      fastcgi_param   SERVER_SOFTWARE     nginx/$nginx_version;
      fastcgi_param   REMOTE_ADDR     $remote_addr;
      fastcgi_param   REMOTE_PORT     $remote_port;
      fastcgi_param   SERVER_ADDR     $server_addr;
      fastcgi_param   SERVER_PORT     $server_port;
      fastcgi_param   SERVER_NAME     $server_name;
      fastcgi_param   HTTPS           $https if_not_empty;
      fastcgi_param   REDIRECT_STATUS     200;
      fastcgi_param   HTTP_PROXY  "";
    }

    location ~ /\.(?!well-known).* {
      deny all;
    }
  }

  server {                               # HTTPアクセスをHTTPSにリダイレクトする設定
    server_name pixelfed.internal;       # 最初の方に書いたホスト名と同じ
    listen 80;
    listen [::]:80;
    return 301 https://$host$request_uri;
  }
}
```

さらに、テスト用のSSL証明書をあらかじめ作成し、``pixelfed/web/configs/ssl`` ディレクトリに置いておきます。
```bash:Terminal
$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout pixelfed/web/configs/ssl/server.key -out pixelfed/web/configs/ssl/server.crt
```

この状態で、``pixelfed/web`` ディレクトリに移動し
```bash:Terminal
$ docker build -t pixelfed-web .
```
を実行し、コンテナイメージができればOKです（数分かかります）。うまくいけばDocker Desktopの [Images] のタブに pixelfed-web という項目が現れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/cc08a2a5-6f79-3341-7ff3-90e5b989eb0a.png)

## FPMコンテナ

PHP-FPMサーバーとして動作するコンテナイメージがPHP公式から提供されているので、これをベースとしてコンテナイメージを作ります。長いので節を区切りながら説明します。以下、``pixelfed/fpm`` ディレクトリの中を操作します。

### Dockerfile

以下がDockerfileの一例です。

```docker:pixelfed/fpm/Dockerfile
# ベースとなるコンテナイメージ
FROM php:8.2-fpm

# メインとなるアプリケーションディレクトリ（nginxと同じにする）
WORKDIR /opt/pixelfed

# OSのユーザをシステムアカウントとして追加
RUN useradd -rU -s /bin/bash pixelfed

# 必要なパッケージを追加
RUN apt-get update \
    && apt-get -y install cron libicu-dev libjpeg62-turbo-dev libpng-dev libwebp-dev libzip-dev supervisor \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# composerをインストール
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
    && php -r "if (hash_file('sha384', 'composer-setup.php') === 'e21205b207c3ff031906575712edab6f13eb0b361f2085f1f1237b7126d785e826a450292b6cfd1d64d92e6563bbde02') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" \
    && php composer-setup.php \
    && php -r "unlink('composer-setup.php');" \
    && mv composer.phar /usr/local/bin/composer

# 必要なPHP拡張モジュールをインストール
RUN docker-php-ext-configure gd --with-jpeg --with-webp \
    && docker-php-ext-install bcmath exif gd intl mysqli pcntl pdo_mysql zip \
    && pecl install redis && docker-php-ext-enable redis

# 設定ファイルのコピー
COPY ./configs/pixelfed.env /.env-customized
COPY ./configs/php.ini /usr/local/etc/php
COPY ./configs/php-fpm.conf /usr/local/etc/php-fpm.d/www.conf
COPY ./configs/supervisord.conf /etc/supervisor
COPY ./configs/supervisor-pixelfed-horizon.conf ./configs/supervisor-cron.conf /etc/supervisor/conf.d
COPY ./configs/cron-pixelfed /etc/cron.d/pixelfed

# エントリーポイントのコピー
COPY ./scripts/docker-entrypoint.sh /

# 後述
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["php-fpm"]
```

ここで ENTRYPOINT, CMD の組み合わせにより、コンテナを起動した時の処理を ``docker-entrypoint.sh`` に書くことができます。

ENTRYPOINTとCMDの関係については、例えば以下にまとめられています。
[ENTRYPOINTは「必ず実行」、CMDは「（デフォルトの）引数」 ‣ Pocketstudio.Net](https://pocketstudio.net/2020/01/31/cmd-and-entrypoint/)

### エンドポイント

``docker-entrypoint.sh`` の内容は、以下のようにします。**※実行権限をつけるのを忘れずに！**

```bash:pixelfed/fpm/scripts/docker-entrypoint.sh
#!/bin/bash -e

if [ ! -f /opt/pixelfed/.env ]
then
    composer create-project pixelfed/pixelfed=0.11.8 /opt/pixelfed
    chown -R pixelfed:pixelfed /opt/pixelfed

    cd /opt/pixelfed
    cp /.env-customized .env

    php artisan key:generate
    php artisan storage:link
    php artisan migrate --force
    php artisan import:cities
    php artisan passport:keys
    php artisan horizon:install
    php artisan horizon:publish

    php artisan route:cache
    php artisan view:cache
    php artisan config:cache
fi

# Run supervisor
supervisord -c /etc/supervisor/supervisord.conf

# execute a command given by CMD
exec "$@"
```

このスクリプトでは、主に2つのことをしています。

1. コンテナを初めて実行した時 (より正確には、Pixelfedがインストールされていない時) だけ、初期化処理をする
1. 必要なサービス（デーモン）を実行する

if文の処理が1に相当します。初めて実行された時だけ、サーバを起動する前にPixelfedのインストールを行います。初めて実行されたのかどうかは ``.env`` ファイルの有無で判断します[^2]。コンテナ起動時に実行されるコマンドは ``/docker-entrypoint.sh php-fpm`` となり、``exec "$@"`` は ``exec php-fpm`` に展開されますので、この方法でサーバ (``php-fpm``) が起動する前に処理をフックすることが可能です[^3]。

[^2]: そのため、``.env`` ファイルはコンテナイメージの作成時には所定の場所とは別の場所にコピーしておき、インストール時に所定の場所にコピーされるようにします。

[^3]: このページの内容を参考にしています。[Dockerfile のベストプラクティス — Docker-docs-ja 1.9.0b ドキュメント](https://docs.docker.jp/engine/articles/dockerfile_best-practice.html#entrypoint)


2については、バックグラウンドで実行させておく必要のあるサービス（デーモン）を ``supervisord`` を用いて実行させます。何らかの原因でそのサービスが停止した時でも、それを検知して自動でサービスを再起動させてくれます。この設定については後述します。

### PHPの設定ファイル

続いてPHPの設定ファイルを作ります。まずはベースとなる設定ファイルを用意しましょう。一度、ベースとなるイメージからコンテナを作り、そこからデータを吸い出します。WSL2のターミナルから

```bash:Terminal
$ docker pull php:8.2-fpm
```

と入力すると、コンテナイメージがダウンロードされます (GUI操作も)。Docker Desktopの [Images] タブに php という項目が現れますので、三角ボタン [Run] をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/e4c168db-b9ce-8511-9066-9faae318cfce.png)

ダイアログで Optional settings をクリックして展開し、[Container Name] に好きな名前を入力し [Run] をクリックします。空欄でも動きますが、コンテナ名がランダムな英単語がついた文字列になって後からわかりにくくなるので、明示的に書くのがよいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/c7d0be15-7fa6-15b8-db40-def41f1dffd1.png)

これで（オリジナルの）FPMサーバが起動しましたので、データを吸い出します。以下はコンテナ名が **fpm-test** の場合です。

```bash:Terminal
$ docker cp fpm-test:/usr/local/etc/php/php.ini-development pixelfed/fpm/configs/php.ini
$ docker cp fpm-test:/usr/local/etc/php-fpm.d/www.conf pixelfed/fpm/configs/php-fpm.conf
```

吸い出した設定ファイルを書き換えていきます。vimやemacsなどをapt-getでインストールして使ってもよいですし、Windowsのエクスプローラからネットワークドライブとして参照できるので、Windows側のテキストエディタで編集するのも手です。ファイルが長いので、変更点だけを示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/75dcc377-610d-a21f-312c-54900837ab7b.png)

```text:pixelfed/fpm/configs/php.ini
398c398
< expose_php = On
---
> expose_php = Off
433c433
< memory_limit = 128M
---
> memory_limit = 512M
595c595
< ;error_log = syslog
---
> error_log = syslog
701c701
< post_max_size = 8M
---
> post_max_size = 32M
853c853
< upload_max_filesize = 2M
---
> upload_max_filesize = 32M
977c977
< ;date.timezone =
---
> date.timezone = Asia/Tokyo
1654c1654
< ;mbstring.language = Japanese
---
> mbstring.language = Japanese
1687c1687
< ;mbstring.encoding_translation = Off
---
> mbstring.encoding_translation = Off
```

```pixelfed/fpm/configs/php-fpm.conf
28,29c28,29
< user = www-data
< group = www-data
---
> user = pixelfed
> group = pixelfed
```

### Pixelfedの設定ファイル

Pixelfedの設定は以下のようにしておきます。``.env.testing`` をベースに書き換えたものです。

```bash:pixelfed/fpm/configs/pixelfed.env
APP_NAME="Pixelfed Test" # 好きな名前に変更
APP_ENV=local
APP_KEY=
APP_DEBUG=true

APP_URL=https://pixelfed.internal  # ここから4項目のドメイン名を変更、nginxの設定に合わせる
APP_DOMAIN="pixelfed.internal"
ADMIN_DOMAIN="pixelfed.internal"
SESSION_DOMAIN="pixelfed.internal"
TRUST_PROXIES="*"

LOG_CHANNEL=stack

DB_CONNECTION=mysql # DB_で始まる設定を変更、テストなのでとりあえずrootで
DB_HOST=pixelfed-mariadb
DB_PORT=3306
DB_DATABASE=pixelfed
DB_USERNAME=root
DB_PASSWORD=password

BROADCAST_DRIVER=redis  # すべてredisに変更
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_DRIVER=redis

REDIS_SCHEME=tcp
REDIS_HOST=pixelfed-redis  # この名前を変更
REDIS_PASSWORD=null
REDIS_PORT=6379

HORIZON_PREFIX="horizon-"

MAIL_DRIVER=log
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="pixelfed@example.com"
MAIL_FROM_NAME="Pixelfed"

OPEN_REGISTRATION=true
ENFORCE_EMAIL_VERIFICATION=false
PF_MAX_USERS=1000

MAX_PHOTO_SIZE=32768 # アップロードサイズ上限 (MB単位)
MAX_CAPTION_LENGTH=150
MAX_ALBUM_LENGTH=4

ACTIVITY_PUB=false
REMOTE_FOLLOW=false
ACTIVITYPUB_INBOX=false
ACTIVITYPUB_SHAREDINBOX=false
# Set these "true" to enable federation.
# You might need to also run:
#   php artisan cache:clear
#   php artisan optimize:clear
#   php artisan optimize

PF_COSTAR_ENABLED=true
CS_BLOCKED_DOMAINS='example.org,example.net,example.com'
CS_CW_DOMAINS='example.org,example.net,example.com'
CS_UNLISTED_DOMAINS='example.org,example.net,example.com'

## Optional 
#HORIZON_DARKMODE=false  # Horizon theme darkmode
HORIZON_EMBED=false  # Single Docker Container mode 

ENABLE_CONFIG_CACHE=false
```

### サービス実行設定

まずは ``supervisor`` 全体の設定です。``pixelfed/fpm/configs/supervisord.conf`` を作成します。

```ini:pixelfed/fpm/configs/supervisord.conf
; supervisor config file

[unix_http_server]
file=/var/run/supervisor.sock   ; (the path to the socket file)
chmod=0700                       ; sockef file mode (default 0700)

[supervisord]
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
childlogdir=/var/log/supervisor            ; ('AUTO' child log dir, default $TEMP)
#nodaemon=true
#user=root

; the below section must remain in the config file for RPC
; (supervisorctl/web interface) to work, additional interfaces may be
; added by defining them in separate rpcinterface: sections
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL  for a unix socket

; The [include] section can just contain the "files" setting.  This
; setting can list multiple files (separated by whitespace or
; newlines).  It can also contain wildcards.  The filenames are
; interpreted as relative to this file.  Included files *cannot*
; include files themselves.

[include]
files = /etc/supervisor/conf.d/*.conf
```

続いて個別のサービスの設定をしましょう。設定するサービスは2つ、cron と Horizon です。

cronは、Linuxを触ったことのある方ならおなじみ、タスクを定期的に自動実行するサービスです。``pixelfed/fpm/configs/supervisor-cron.conf`` に以下のように書いておきます。

```ini:pixelfed/fpm/configs/supervisor-cron.conf
[program:cron]
process_name=%(program_name)s_%(process_num)02d
command=cron -f
autostart=true
autorestart=true
user=root
numprocs=1
redirect_stderr=true
stdout_logfile=/var/log/supervisor/cron.log
```

cronのタスクとしては、以下のファイルを用意します。

```text:pixelfed/fpm/configs/cron-pixelfed
* * * * * pixelfed /usr/local/bin/php /opt/pixelfed/artisan schedule:run
```

もう一つのHorizonは、画像の最適化・サムネイル作成などのタスクをキューで管理するものです。

```ini:pixelfed/fpm/configs/supervisor-horizon.conf
[program:pixelfed-horizon]
process_name=%(program_name)s_%(process_num)02d
command=php /opt/pixelfed/artisan horizon
autostart=true
autorestart=true
user=pixelfed
numprocs=1
redirect_stderr=true
stdout_logfile=/var/log/supervisor/pixelfed-horizon.log
```

### コンテナイメージ作成


それではFPMサーバのコンテナイメージを作りましょう。pixelfed/fpm ディレクトリに移動し
```bash:Terminal
$ docker build -t pixelfed-fpm .
```
を実行します（数分かかります）。

設定ファイルを吸い出すのに使ったコンテナは、削除してしまってかまいません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/8efe52c3-1fb3-fbdc-d2ce-90d0fb8c7f41.png)

## Redisコンテナ, MySQLコンテナ

こちらは、ひとまず既存のイメージをそのまま使ってコンテナを作ります（調整の余地はありますが、テスト環境としてまずは動く状態に持っていきます）ので、特に何もすることはありません。

# compose

さて、いよいよこれまで作ったものを合わせて一つのPixelfedインスタンスとして動かします。
起点ディレクトリ (pixelfedディレクトリ) に ``compose.yaml`` というファイルを作ります。

```yaml:pixelfed/compose.yaml
version: "3.8"

services:

  web:
    container_name: pixelfed-web # コンテナ名を用いて、TCPなどでコンテナ間通信ができる
    build:
      context: ./web
      dockerfile: Dockerfile
    volumes:
      - app:/opt/pixelfed  # appはボリューム名。このファイルの末尾で定義。/opt/pixelfedはマウントポイント
    ports:  # ポート開放（コンテナ外部側のポート番号:コンテナ内部側のポート番号）
      - 80:80
      - 443:443

  fpm:
    container_name: pixelfed-fpm
    build:
      context: ./fpm
      dockerfile: Dockerfile
    volumes:
      - app:/opt/pixelfed  # webとfpmで、同じストレージを同じ場所にマウントするのがポイント

  redis:
    container_name: pixelfed-redis
    image: redis:7.0    # 既存のコンテナイメージ
    volumes:
      - redis:/data

  mariadb:
    container_name: pixelfed-mariadb
    image: mariadb:11.0 # 既存のコンテナイメージ
    volumes:
      - db:/var/lib/mysql
    environment:
      - MARIADB_ROOT_PASSWORD=password  # DBの設定

# ボリューム定義
volumes:
  app:
    name: pixelfed-app
  db:
    name: pixelfed-db
  redis:
    name: pixelfed-redis
```

同じディレクトリでコマンドを実行します。

```bash:Terminal
$ docker compose up -d
```

これでうまくいけば4つのコンテナが起動します。Docker Desktopで [Container] の中に pixelfed と、配下に4つのコンテナが現れ、すべてのステータスが [Running] になっていれば成功です。初回は fpm の起動に時間がかかることが見込まれます（Pixelfedのインストールが走るため。FPMの起動が完了するまでにブラウザでアクセスすると 502 Bad Gateway エラーが返ります）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/8d6a9b51-e0e6-42dd-d501-c2ae1185d47a.png)

ここで、コンテナ間ではコンテナ名を用いてTCPなどで通信することができます。nginxの設定や .env に pixelfed-fpm などと出てきたのは、他のコンテナに接続させる意図だったのでした。

# ブラウザからアクセス

Windows側でhostsを編集します（管理者権限が必要です）。

```text:C:\Windows\System32\drivers\etc\hosts
127.0.0.1 pixelfed.internal  # この行を追記
```

ブラウザから https://pixelfed.internal/ にアクセスしてみましょう。初回のみ証明書エラーが出ますが、テスト環境なので無視して続行します。うまくいけば以下のような画面が現れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/5934bb97-11a7-a735-e94a-2a47a8b1259c.png)

右上のボタンから Sign up を行うと、ホーム画面が現れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/df96ce61-d873-ddb0-6eb7-fb661413aaf2.png)
「Create New Post」から画像の投稿が可能です。さらに別のアカウントを作成して、フォローの実験をしてみるのも面白いでしょう。色々触ってみてください。

# コンテナを作り直してみる

さて、ボリュームを作ることでコンテナを消してもデータが消えなくなると書きましたが、最後にこれを検証してみましょう。

## ボリュームを残してコンテナだけを作り直す

[Container] タブの pixelfed にチェックを入れ、右上の赤い [Delete] ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/149951f7-722e-9272-a1c4-e5c021d30053.png)
確認画面が出ますが [Delete forever] をクリックします。

これでコンテナは綺麗さっぱり消えてしまいました。もちろん、ブラウザをリロードすると繋がらなくなっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/a5b1c335-ae57-311c-5c8f-63a662a5d04e.png)

この状態で、pixelfedディレクトリに移動して再度composeをします。
```bash:Terminal
$ docker compose up -d
```

これで4つのコンテナが再び作成され、起動します。しかし、コンテナの再作成は言うなればマシンをリプレースするようなもの。本当にデータは残っているのでしょうか？

ブラウザをリロードすると、ログイン状態も含め、何事もなかったかのように元通りになっています（今回はPixelfedのインストールが不要なので、すぐにブラウザからアクセスできるはずです）。ざっくり言えば、コンテナの動きが怪しければ再作成すればよいし、Dockerが動いているサーバの調子が悪ければ、新しいサーバを用意してDockerをセットアップし、その上でコンテナを動かせば元通りになるということです（ボリュームの実体が外部のストレージに置いてあればという前提ですが）。AWSで使う場合、Auto Scalingとも相性が良いでしょう。

## ボリュームを消してからコンテナを作り直す

再び実験です。先ほどと同じように、コンテナを削除します。今度は、データを保存しているボリュームも削除してみます。
Docker Desktopの [Volume] タブに移動すると、3つのボリュームが現れます。これは最初の方で図示した3つのボリュームに対応します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/bc7ddeec-84a4-e975-04d0-df7c0f78fce9.png)

これらを一つずつ、ゴミ箱マークをクリックして削除していきます。これで、データも綺麗さっぱり消えてしまいました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/53a0d9a3-7828-90c2-f37b-a7e61e8cc8b0.png)

そして、みたびcomposeを実行します。すると先ほど同様にコンテナが4つ起動します。
```bash:Terminal
$ docker compose up -d
```

今回はブラウザからアクセスできるようになるのにしばらく時間を要します（Pixelfedのインストールからやり直すためです）。数分待ってからブラウザのリロードボタンを押すと、確かにデータは無くなっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204084/ccf9b3c0-f299-1673-8755-b96bc425a6ff.png)

というわけで、確かにボリュームにデータが保存されていて、コンテナを削除してもデータは保持されるものの、ボリュームを削除してしまうとデータも消えてしまうことが確認できました。

# 付録: トラブルシューティング

上の手順を作る間にハマったポイントをまとめます。
以下の解決策は、すべて上記の設定に反映済みです。

## データベースの初回migrationに失敗する

Pixelfedの構成ファイルには ``.env.testing`` がありますが、これを ``.env`` にリネーム or コピーして使うと、``php artisan migrate --force`` が途中で失敗するようです。
``.env.testing`` ではデータベースとしてSQLiteを使う設定になっていますが、これが良くないようで、``DB_CONNECTION=mysql`` に変えて接続先情報も指定するとうまくいきました。

（``testing`` というのはテスト環境ではなく、ユニットテストなどを想定している？）

## ユーザー登録しようとすると "419 Page Expired" エラーになる

``.env`` ファイルを ``SESSION_DRIVER=redis`` と設定することで解決しました。
これも一つ前の問題と同様、``.env.testing`` の初期値 (``SESSION_DRIVER=array``) を信用して使ってしまってハマりました。``array`` はセッション情報を全く保持しない設定のようですね。

## 数MB以上の大きな画像をアップロードするとエラー（"413 Request Too Large" エラーなど）が出る。あるいは、画像投稿時にエフェクトやフィルターを掛けようとすると、永遠に処理中のアイコンが回る（実際にはリクエストが失敗している）。

PHP, nginx, Pixelfedのファイルサイズ制限を見直します。似たような設定が3箇所あって、1つでも制限に引っかかるとエラーになるので注意が必要です。

``.env`` の ``MAX_PHOTO_SIZE`` の値を変えても設定が反映されない（アップロード画面に表示される上限サイズが変わらない）場合は、

```bash:Terminal
$ docker exec -it pixelfed-fpm bash
# コンテナに入って作業
$ cd /opt/pixelfed
$ php artisan config:clear
$ php artisan config:cache
```

と実行して反映されるか確認します。ダメな場合でも、数十分くらい待てば反映されている気がします。（詳細不明）

## アップロードした画像が読み込めない（サムネイルが×印になったり、No Imageになったりする）

WebサーバとFPMサーバで、実行するユーザ (UID) を揃えてください。具体的には、Webサーバ側は ``nginx.conf`` の ``user pixelfed;`` の行、FPMサーバ側は ``php-fpm.conf (php-fpm.d/www.conf)`` の ``user = pixelfed`` と ``group = pixelfed`` の2行です。

FPMサーバはアップロードされた画像をパーミッション600で保存し、必要な親ディレクトリをパーミッション700で作成します。そのため、ファイルをWebサーバ（ブラウザ）が読み込むためには、Webサーバも同じユーザ (UID) で実行されている必要があります。そうでないと、アップロードは成功するのにブラウザに画像が表示されない（Webサーバにアクセス権がないので）事態に陥ります。

## サムネイルが作成されない（表示サイズが小さいだけで、巨大なオリジナル画像が読み込まれる）

サムネイル作成（画像の最適化）処理はキューで管理されます。前述のHorizonが正しく動作していないと、キューに投入された処理が消化されない、すなわちサムネイルがいつまでたっても作成されず、オリジナルサイズの画像が表示され続けるようです。

私の場合はもう一つ、PHPのGDモジュールにJPEGサポートが入っていなかったためにサムネイルの作成がエラーになっていました。先に ``libjpeg62-turbo-dev`` パッケージをインストールしておき、``--with-jpeg`` オプションをつけてGDモジュールをインストールすれば解決します。

```bash:Terminal
$ apt-get install libjpeg62-turbo-dev # libwebp-dev  # WebPサポートを加えたい場合は追加
$ docker-php-ext-configure gd --with-jpeg # --with-webp
$ docker-php-ext-install gd
```