STGYのインストール

本記事では、STGYのシステム全体を、開発環境とデモ運用環境の双方にインストールする方法について説明する。Dockerを使った運用とネイティブサービスを使った運用の双方についても説明する。ドメイン名やIPアドレスは自分のものに読み替えられたい。

Mac OS上での開発環境の構築

STGYの開発作業はDockerが動く環境であればどこでもできるが、ここではMac OSでの環境構築方法について述べる。Linuxについてはデモ環境の構築方法と一緒に後述する。Mac上では、作業環境は自宅等のホームネットワーク環境を想定し、外部からの接続を受け付けることは想定しない。Dockerを使って各種の依存サービスを立てた上で、自ら開発するSTGYのバックエンドサービスとフロントエンドサービスをローカルホストで動かしながら開発を進める。検証時のために、バックエンドサービスとフロントエンドサービスも含めて全てDockerで動くようにもする。

まずは、Homebrewを入れる。カスタマイズ性の高いMacPortsなどよりも、通り一遍で再現性の高い手順となるHomebrewの方が管理しやすい。

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

Homebrewで、その他の必要パッケージを入れる。Docker VMであるColima、Dockerクライアント、composeプラグイン、Node.js、jq、Git、GitHub CLIである。

brew install colima docker docker-compose node jq git gh

Colimaを起動する。初回の起動ではリソースの使用量をオプションで指定する。

colima start --cpu 4 --memory 8 --disk 100

DockerクライアントがColimaを使うように設定する。

docker context use colima

~/.docker/config.jsonに以下のような記述をして、ColimaとComposeプラグインを利用可能にする。Colima等の既存の設定があればそのまま残す。

{
  "auths": {},
  "currentContext": "colima",
  "cliPluginsExtraDirs": ["/opt/homebrew/lib/docker/cli-plugins"]
}

Dockerクライアントの動作確認をする。それぞれ、「Docker Compose version 2.39.3」的な文字列と「Hello from Docker!」が表示されれば成功。

docker compose version
docker run --rm hello-world

Node.jsの動作確認をする。それぞれ、「v24.4.0」と「11.4.2」的な文字列が表示されれば成功。

node -v
npm -v

GitHubにログインする。アクセスキーを聞かれるので、事前にGitHub上でアクセスキーを作っておくこと。

NO_COLOR=1 gh auth login

開発作業用のディレクトリを作り、stgyのリポジトリのクローンを作成する。以後、stgyディレクトリで作業を行う。

mkdir ~/dev
cd ~/dev
git clone git@github.com:estraier/stgy.git
cd stgy

リポジトリ構成

STGYの関連パッケージは全て単一のリポジトリに収められている。いわゆるmonorepo構成である。主要なディレクトリ構造とファイルを以下に示す。

  • package.json : NPMでパッケージ管理をするための設定ファイル
  • package-lock.json : 依存パッケージのバージョンを固定するための設定ファイル
  • docker-compose.yml : 依存サービスをDockerで管理するための設定ファイル
  • docker-compose.vps.yml : VPSのデモ環境用に上記設定を上書きするためのファイル
  • env.dev.txt : 開発環境用の環境変数設定ファイル
  • env.vps.txt : 全サービスをDockerで動かすVPS環境用の環境変数設定ファイル
  • env.native.txt : 全サービスをネイティブで動かすVPS環境用の環境変数設定ファイル
  • env.hybrid.txt : STGY独自サービスだけをDockerで動かすVPS環境用の環境変数設定ファイル
  • caddy/ : Caddyの設定と独自イメージを置くディレクトリ
    • Dockerfile : レート制限モジュールを組み込んだCaddyイメージの設定ファイル
    • Caddyfile : 開発環境用のCaddy設定ファイル
    • Caddyfile.vps : VPSのデモ環境用のCaddy設定ファイル
    • extra-sites/ : Caddyに追加で読み込ませるホスト固有設定を置くディレクトリ
  • packages/ : 独自の依存パッケージ群のディレクトリ
    • markdown/ : Markdownの解釈やHTMLへの変換
    • track/ : 経路データの解釈やTrackJSONへの変換とそのレンダリング
    • geocoder/ : ジオコーディングのデータ生成と検索
  • ttts/ : 全文検索サービスのソースコードや設定ファイル一式のディレクトリ
  • backend/ : バックエンドサービスのソースコードや設定ファイル一式のディレクトリ
  • frontend/ : フロントエンドサービスのソースコードや設定ファイル一式のディレクトリ
  • scripts/ : 管理用の便利スクリプト一式のディレクトリ
  • tests/ : テスト用のコードやファイル一式のディレクトリ
  • postgres/ : PostgreSQLの設定一式のディレクトリ
  • minio/ : MinIOの設定一式のディレクトリ
  • postfix/ : Dockerで動かすPostfixの設定一式のディレクトリ
  • seeder/ : 初期登録データ一式のディレクトリ
    • users-core-<ID>.txt, users-test-<ID>.txt : ユーザ
    • posts-core-<ID>.txt, posts-test-<ID>.txt, posts-after-actions-<ID>.txt : 投稿
    • agreements-core-<ID>.json : 利用規約
    • actions/ : follow、block、likeなどの投入データ

パッケージ管理はNPMで行っており、その設定はpackage.jsonに書いてある。Dockerの管理はそこにあるスクリプトコマンドを叩いて行う。ワークスペース構成を取っていて、packages/*とtttsとbackendとfrontendの下にあるpackage.jsonはその管理下にある。それぞれのサブパッケージには、以下の共通したファイルやディレクトリが格納されている。

  • package.json : NPMでパッケージ管理をするための設定ファイル
  • tsconfig.json : TypeScriptのビルド設定ファイル
  • tsconfig.eslint.json : eslintが使うビルド設定ファイル
  • eslint.config.mjs : eslintによるコード解析の設定ファイル
  • jest.config.js : jestによるユニットテストの設定ファイル
  • src/ : ソースコード一式

依存パッケージのインストールはプロジェクトルート直下に作られるnode_modulesディレクトリに格納され、それらのバージョンはpackage-lock.jsonで固定される。以下のコマンドで依存関係を処理する。

  • npm ci : 固定されたバージョンの依存パッケージをインストールする
  • npm install : package.jsonの記述に基づき、新しいバージョンの依存パッケージをインストールする
  • npm run clean : 依存パッケージを消す

tttsとbackendとfrontendにはDockerfileがあり、それぞれのサービスのDockerインスタンスの設定が書いてある。それ以外の依存サービスの設定はdocker-compose.ymlに直接書いてある。docker composeや管理スクリプトは.envファイルから環境変数を読み込むが、.envはリポジトリには含まれない。利用する構成に応じてenv.dev.txt、env.vps.txt、env.native.txt、env.hybrid.txtのいずれかを.envにコピーしてから、それを環境に合わせて編集して使う。.envにはパスワードなどの秘密情報が書かれるので、Gitの管理対象にすべきではない。

scriptsの下には、システムの管理に必要なスクリプトが入っている。多くのものはnpmを介して起動される。.envの環境変数から設定を読み込むので、.envを事前に作っておく必要がある。

  • reset-data.sh : PostgreSQLのデータベースを初期化し、seederの初期データを投入する
  • reset-minio-data.sh : MinIOのデータベースを初期化する。空になる
  • reset-cache.sh : Redisのキャッシュデータを初期化する。空になる
  • reset-ttts-data.sh : TTTSのインデックスを初期化する。空になる
  • reset-service-data.sh : サービス全体のデータ(PostgreSQL、Redis、TTTS)を初期化し、初期データを投入する。
  • edit-users.py : ユーザを作成または更新する
  • edit-posts.py : 投稿を作成または更新する
  • user-actions.py : 個々のユーザでログインしてイイネやフォローやブロックを行う
  • make-volume-test.py : 検証用に大量のユーザ、投稿、フォロー、イイネ、通知を作成する。特定ユーザの大量投稿と特定投稿への大量返信も作り、複合インデックスを実行計画で確認できるようにする
  • run-local-ttts.sh : 全文検索サービスをローカルホスト上で起動する
  • run-local-backend.sh : バックエンドサービスをローカルホスト上で起動する
  • run-local-frontend.sh : フロントエンドサービスをローカルホスト上で起動する
  • run-databaseUtil.sh : データベース管理用のユーティリティ
  • run-storageUtil.sh : メディアストレージ管理用のユーティリティ
  • run-searchUtil.sh : 全文検索サービス管理用のユーティリティ
  • deploy-backend.sh : デモ環境にてバックエンドをデプロイする
  • deploy-frontend.sh : デモ環境にてフロントエンドをデプロイする
  • import-archive.py : エクスポートしたユーザデータをインポートする。
  • upload-embedded-images.py : ローカルの画像を一括アップロードして記事を書く。

postgresの下にはデータベースのスキーマ定義を行うSQLファイルが置いてある。minioの下にはオブジェクトストレージのバケットの初期設定を行うスクリプトが置いてある。

開発環境でのサービスの起動と終了

Macの開発環境においては、以下のコマンドでDockerホストVNの起動と終了を行う。

  • colima start
    • Colimaを起動
  • colima stop
    • Colimaを停止する
  • colima restart
    • Colimaを再起動する

リポジトリルートにて、開発環境の設定が書いてあるenv-dev.txtを.envとしてコピーする。これは忘れがちなので注意。その上で.envの内容を編集する。AI機能を有効化するならSTGY_OPENAI_API_KEYを設定し、メール認証を簡略化するならSTGY_TEST_SIGNUP_CODEを設定する。

cp env.dev.txt .env

以下のコマンドでDocker上のサービスの起動と終了を行う。

  • npm run docker:init
    • ホスト全体を初期化。他プロジェクトも含めてすべてのコンテナとイメージとボリュームを強制削除する。データも消える。
  • npm run docker:reset
    • Dockerイメージを削除して再作成する。
  • npm run docker:build
    • Dockerイメージを作成する。
  • npm run docker:destroy
    • Dockerイメージを削除する。
  • npm run docker:create
    • Dockerコンテナを作成する。必要ならネットワークやボリュームも作成。イメージのpullも行う。
  • npm run docker:remove
    • Dockerコンテナを削除する。
  • npm run docker:up
    • Dockerイメージの作成、Dockerコンテナの作成、Dockerコンテナの起動を一気に行う。
  • npm run docker:down
    • Dockerコンテナの停止と削除を一気に行う。
  • npm run docker:start
    • Dockerコンテナを起動する。
  • npm run docker:stop
    • Dockerコンテナを停止する。

Docker関係のコマンドを利用目的別に整理する。

  • 最も簡単に全サービスの起動と終了を行う。
    • npm run docker:up
    • npm run docker:stop
  • まっさらな状態から逐次的にサービス起動状態までたどり着く
    • npm run docker:build
    • npm run docker:create
    • npm run docker:start
  • サービス起動状態から逐次的にまっさらな状態に戻す。
    • npm run docker:stop
    • npm run docker:remove
    • npm run docker:destroy
  • ビルド設定やTypeScriptコードの変更を強制反映してサービスを再起動する。
    • npm run docker:reset
    • npm run docker:start
  • ビルド設定やTypeScriptコードの変更を差分反映してサービスを再起動する。
    • npm run docker:up
  • Docker環境全体とデータを完全初期化する。ストレージ逼迫の場合もこれを使う。
    • npm run docker:init

開発中には、backendとfrontendはローカルホストで起動し、その他の依存サービスのみをDockerで起動するのが楽である。以下の手順で行う。

  • npm ci
    • ローカル環境に外部依存パッケージをインストールする。
  • npm run packages:build
    • ローカル環境で内部依存パッケージをビルドして準備する。
  • npm run docker:start-dev
    • backendとfrontend以外の依存サービスのDockerコンテナを起動する。
    • 全文検索サーバも一緒にDocker起動するので、止めたいならdocker compose stop tttsを実行。
    • 全部止める時はnpm run docker:stopを実行する。
  • ./scripts/run-local-backend.sh
    • backendサービスと各種ワーカをローカルホストで起動する。
    • --startオプションをつけると、ビルド結果を実行する。
    • --openai-api-keyオプションでOpenAIのAPIキーを上書きできる。
    • --multi-workerをつけると全ワーカを個別プロセスで起動し、--no-workerをつけるとワーカを起動しない。
    • 止める時はCtrl-Cで落とす。ワーカもサーバと一緒に停止する。
  • ./scripts/run-local-frontend.sh
    • frontendサービスをローカルホストで起動する。
    • --startオプションをつけると、ビルド結果を実行する。
    • 止める時はCtrl-Cで落とす。
  • npm run reset-data
    • DBとオブジェクトストレージを初期化して、初期データを投入する。
  • npm run reset-data-test
    • DBとオブジェクトストレージを初期化して、機能テスト用のデータを投入する。
  • npm run reset-data-volume-test
    • DBとオブジェクトストレージを初期化して、性能テスト用の大量のデータを投入し、PostgreSQLのANALYZEを実行する。
  • npm run reset-cache
    • Redisのキャッシュを初期化する。
  • npm run reset-search
    • 全文検索サービスのインデクスを初期化して、全ユーザと全投稿を投入する。

バックエンドやフロントエンドのTypeScriptコードを変更すると、それぞれの起動中のサービスは勝手に再起動して変更を取り込む。ただし、tscやlintのチェックが甘いので、そのままだとDockerイメージにした時に動く保証がない。よって、以下のコマンド群で品質管理をする。frontendやbackendの中で実行しても良い。

  • npm run test
    • *.test.tsに書かれたユニットテストを実行する。
    • backendやfrontendの中でnpm run testとしてもよい。
  • npm run lint
    • eslintで静的なコードの検証を行う。
  • npm run build
    • tscの厳格なルールで本番用にビルドする。
  • npm run fmt
    • prettierでコードを整形する。

全てのサービスを立ち上げたら、ブラウザで以下のURLを開くと、STGYのサービスが使い始められる。

  • https://localhost:8080/ : ユーザが使うサイト
    • デフォルトでは、「admin」ユーザのパスワードが「stgystgy」になっている。
  • http://localhost:8080/site-test : caddy/extra-sites/test.caddyの読み込み確認
    • 「test.caddy is OK」と表示されれば成功。
  • https://s3-console.stgy.jp/ : MinIOのコンソール
    • アップロードされた画像データはここで確認できる。
  • https://mail.localhost:8080/ : 確認メールの保存先
    • サインアップやメアド変更の際の確認メールはここに送られる。

Linux上での開発環境の構築

Linux上でSTGYの稼働と開発を行うための環境設定の手順を記す。まずは、Macの時と同様に、Dockerを使ってサービスを立ち上げる方法を示す。バックエンドとフロントエンドをローカルで動かすことも当然できる。データセンター等の環境を想定し、インターネットからの接続を受け付ける。ドメインを取得し、VPSのホストが利用できることを前提とする。ここでは、stgy.jpというドメインを取得し、VPS上のUbuntu Linux環境の上でシステムを構築する。IPアドレスは153.127.47.206が割り振られている。

事前にドメイン取得サービスのDNS設定で、該当ホストのレコードを設定する。主たるドメインと、画像配信やその他のサブドメインを同じIPアドレスで登録する。

  • サブドメイン=(空文字列)、種別=A、値=153.127.47.206
  • サブドメイン=s3、種別=A、値=153.127.47.206
  • サブドメイン=s3-console、種別=A、値=153.127.47.206
  • サブドメイン=www、種別=A、値=153.127.47.206
  • サブドメイン=mail、種別=A、値=153.127.47.206
  • サブドメイン=(空文字列)、種別=TXT、値=v=spf1 ip4:153.127.47.206 -all

上記設定が伝搬していることを確認する。「Name: stgy.jp Address: 153.127.47.206」的な文字列が表示されれば成功。

nslookup stgy.jp

VPSサービスのコントロールパネルで、逆引き設定をする。メールが弾かれないことを優先するので、mail.stgy.jpとして登録する。設定後、以下のコマンドで伝搬を確認する。

dig +trace -x 153.127.47.206

VPSサービスのコントロールパネルで、Ubuntuの最新のLTS版をインストールし、コンソールからログインして初期設定の作業を行う。インストール時に作成した初期ユーザでログインするが、それはスーパーユーザではないので、sudoして管理作業を行う。インストール後の初期設定が終わったら作業用のprodユーザを作り、以後はそのユーザからsudoして作業を行う。

  • ソフトウェアリストの更新: sudo apt update
  • ソフトウェアの更新: sudo apt upgrade
  • ロケールのインストールと有効化:
    • sudo apt install locales
    • /etc/locale.genを開いて必要なロケール(en_US.UTF-8 UTF-8とja_JP.UTF-8 UTF-8)を戻す
    • sudo locale-gen
  • ホスト名の設定: sudo hostnamectl set-hostname setagaya
  • 通常ユーザ作成: sudo adduser prod
  • sudo権限付与: sudo gpasswd -a prod sudo

sshdの設定をする。/etc/ssh/sshd_configを開き、AcceptEnvの行をコメントアウトして、クライアントから環境変数を受け取らないようにする。

ファイアウォールを設定する。SSHの22、HTTPSの443、HTTPの80、メールリレーの587は外部公開する。Dockerコンテナからホスト上のネイティブサービスへの通信は、STGY専用のDockerブリッジであるbr-stgyから入る通信として一括で許可する。これにより、ネイティブサービスの追加やポート変更のたびにUFWの許可規則を追加する必要はない。この許可はbr-stgy以外の外部インターフェースから入る通信には適用されない。br-stgyはdocker-compose.ymlで定義されている。

sudo ufw default deny incoming
sudo ufw default allow outgoing

sudo ufw allow ssh
sudo ufw allow https
sudo ufw allow http
sudo ufw allow 587/tcp
sudo ufw allow in on br-stgy

sudo ufw enable
sudo ufw status

再起動する。

sudo shutdown -r now

prodユーザでログインして、環境設定を行う。

  • SSHの鍵設定:
    • .ssh/id_rsaと.ssh/id_rsa.pubと.ssh/authorized_keysを既存環境からコピー。
    • パーミッション設定: chmod 700 .ssh ; chmod 600 .ssh/*
  • 最低限の環境設定
    • .bash_profileと.bashrcを既存環境からコピー
  • 任意のエディタの設定。Emacs(GUIなし)の場合は以下
    • sudo apt install emacs-nox
    • .emacsを既存環境からコピー

Docker関連ツールを入れる。標準ではaptがDockerの公式リポジトリを見ないので、設定を加える必要がある。

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Dockerサービスを有効化する。また、dockerグループにprodユーザを入れて利用可能にする。

sudo systemctl enable --now docker
sudo usermod -aG docker prod
newgrp docker  # 現在のセッションにdockerを加える。ログインし直しても良い。

Dockerクライアントの動作確認をする。それぞれ、「Docker version 29.6.2」「Docker Compose version 5.3.1」的な文字列と「Hello from Docker!」が表示されれば成功。

docker --version
docker compose version
docker run --rm hello-world

Node.jsとjqをインストールする。Ubuntuのデフォルト設定で入るNode.jsは古いので、NodeSourceのリポジトリからNode 24系を導入する。NodeSourceのnodejsパッケージにはnpmも含まれるため、Ubuntu標準のnpmパッケージを別途指定する必要はない。

curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install nodejs jq

Node.jsの動作確認をする。それぞれ、「v22.23.1」と「10.9.8」的な文字列が表示されれば成功。

node -v
npm -v

GitとGitHub CLIをインストールする。

sudo apt install git gh

GitHubにログインする。アクセスキーを聞かれるので、事前にGitHub上でアクセスキーを作っておくこと。SSHのキーとアクセスキーの対応が取れている必要がある。

NO_COLOR=1 gh auth login

開発作業用のディレクトリを作り、stgyのリポジトリのクローンを作成する。以後、stgyディレクトリで作業を行う。

git clone git@github.com:estraier/stgy.git
cd stgy

運用環境の選択

STGYはAWS上でRDSとS3を使って構築できるシステムアーキテクチャになっているのだが、多数のマネージドサービスを使うと、デモで使うにはランニングコストが高くなりすぎる。デプロイも面倒くさい。よって、デモ用途では1台構成のVPSで動かす方が手軽で良いと判断した。さくらVPSの4GBプランなら月4000円以下で運用できるし、ストレージが400GBまで使えるので、ユーザ数がかなり増えても大丈夫だろう。何より、固定額なのが安心だ。1円の収益にもなっていない趣味のサービスが物量攻撃されて何十万円も払う羽目になったら心がくじけてしまうだろう。デモ運用しているうちは、従量課金で際限なく金がかかるよりは、サービスが落ちる方がマシだ。

VPS上で全ての依存サービスを自前で運用するとして、それらをネイティブサービスとして動かすか、Dockerコンテナとして動かすかは、選択できる。ネイティブサービスとして動かす方法だと、性能上は最善で、ホストが単一なので手動での設定作業も楽だが、自動化しづらいので何度もやるのは厳しい。Dockerコンテナとして動かす方法だと、最初のコンテナの設定はややこしいが、一度やっておけば自動デプロイが簡単にできるようになる。Docker上で動かすとCPU効率とメモリ効率とI/O効率がほんの少し落ちる可能性はあるが、LinuxのDocker層のオーバーヘッドはほぼ無視できる程度に小さいので、性能に関しては気にする必要はない。よって、とりあえずはDockerで動くようにしよう。開発環境とほぼ同じ設定で、VPS上でDockerを動かして、そこにデータベースもオブジェクトストレージもバックエンドサーバもフロントエンドサーバも乗せてしまう。

Docker完結方式

Docker完結の前提で、アーキテクチャと設定を整理してみる。コンテナ間の接続は主に環境変数で管理することになるが、各コンテナが互いを認識するためにどの環境変数を使っているかを洗い出し、その適切な値を決めるのだ。内側(インターネット側)と外側(Dockerサブネット)で名前が違うのと、リバースプロクシが読み替えを行うのがややこしい。理解が甘いと後で確実に混乱するので、予め整理しておくべきだ。

  • リバースプロクシ(Caddy)
    • Node.jsのサービスはHTTPしか喋れないので、SSL化するのに必要。
      • STGY本体の設定はcaddy/Caddyfileまたはcaddy/Caddyfile.vpsに書き、STGY以外のホスト固有設定はcaddy/extra-sites/*.caddyから追加で読み込む。
    • https://stgy.jp/backend をDockerサブネットの http://backend:3100 であるバックエンドサーバに中継する。
    • https://stgy.jp のそれ以外をDockerサブネットの http://frontend:3000 であるフロントエンドサーバに中継する。
    • https://s3.stgy.jp をDockerサブネットの http://minio:9000 であるS3サーバに中継する。
    • 証明書はCaddy自身が自動的にLet's Encryptから取ってきてくれる。
      • 7日間で5回以上取得するとレート制限がかかるので、down -vでボリュームを消さないように注意。
    • Dockerで立てるWebサービスのうち、インターネットから直接見えるのはこいつだけ。Docker版PostfixのSMTPポートはホストには公開しない。
  • フロントエンド(Node.js + Next.js)
    • Dockerサブネットでfrontend:3000をlistenし、主にJavaScriptを配信。表にはhttps://stgy.jpとして露出。
      • PORT=3000
    • フロントエンドをhttps://stgy.jpとして認識し、UI上の相対パスを絶対パスに解決する。
    • バックエンドをhttps://stgy.jp/backendとして認識し、その配下にAPIリクエストを送る。
      • NEXT_PUBLIC_BACKEND_API_BASE_URL=https://stgy.jp/backend
      • CSRのfetchは相対URLで動くが、SSRのfetchは絶対URLでないと動かないので注意。
    • S3サーバをhttps://s3.stgy.jpとして認識し、画像の配信とpresigned-POSTのベースURLにそれを用いる。
      • NEXT_PUBLIC_STORAGE_S3_BUCKET_PREFIX=stgy
      • NEXT_PUBLIC_STORAGE_S3_PUBLIC_URL_PREFIX=https://s3.stgy.jp/{bucket}/
  • バックエンド(Node.js + Express)
    • Dockerサブネットでbackend:3100をlistenし、サービスAPIのエンドポイントを担う。表にはhttps://stgy.jp/backendとして露出。
    • フロントエンドをhttps://stgy.jpとして認識し、1ホップまでのプロクシ応答のX-Forwarded-Forを信じる。
    • S3サーバのエンドポイントをhttp://minio:9000として認識し、Dockerサブネットで通信。
    • ただし、オブジェクトの所在はhttps://s3.stgy.jp/{bucket_name} の配下としてクライアントに通達。
    • DBサーバをpostgres:5432として認識し、Dockerサブネットで通信。
      • STGY_DATABASE_HOST=postgres
      • STGY_DATABASE_PORT=5432
    • キャッシュサーバをredis:6379として認識し、Dockerサブネットで通信。
      • STGY_REDIS_HOST=redis
      • STGY_REDIS_PORT=6379
    • 全文検索サーバをhttp://ttts:3200として認識し、Dockerサブネットで通信。
  • メールワーカ(Node.jsのCLI。実運用では統合ワーカ内で起動)
    • キャッシュサーバをredis:6379として認識し、Dockerサブネットで通信。
      • STGY_REDIS_HOST=redis
      • STGY_REDIS_PORT=6379
    • SMTPサーバをpostfix:25として認識し、Dockerサブネットで通信。
      • STGY_SMTP_HOST=postfix
      • STGY_SMTP_PORT=25
  • メディアワーカ(Node.jsのCLI。実運用では統合ワーカ内で起動)
    • キャッシュサーバをredis:6379として認識し、Dockerサブネットで通信。
      • STGY_REDIS_HOST=redis
      • STGY_REDIS_PORT=6379
    • S3サーバのエンドポイントをhttp://minio:9000として認識し、Dockerサブネットで通信。
  • 通知ワーカ(Node.jsのCLI。実運用では統合ワーカ内で起動)
    • キャッシュサーバをredis:6379として認識し、Dockerサブネットで通信。
      • STGY_REDIS_HOST=redis
      • STGY_REDIS_PORT=6379
    • DBサーバをpostgres:5432として認識し、Dockerサブネットで通信。
      • STGY_DATABASE_HOST=postgres
      • STGY_DATABASE_PORT=5432
  • 検索インデックスワーカ(Node.jsのCLI。実運用では統合ワーカ内で起動)
    • DBサーバをpostgres:5432として認識し、Dockerサブネットで通信。
      • STGY_DATABASE_HOST=postgres
      • STGY_DATABASE_PORT=5432
    • 全文検索サーバをhttp://ttts:3200として認識し、Dockerサブネットで通信。
  • AI要約ワーカ(Node.jsのCLI。実運用では統合ワーカ内で起動)
    • バックエンドをhttp://backend:3100として認識し、Dockerサブネットで通信。
    • キャッシュサーバをredis:6379として認識し、Dockerサブネットで通信。
      • STGY_REDIS_HOST=redis
      • STGY_REDIS_PORT=6379
    • DBサーバをpostgres:5432として認識し、Dockerサブネットで通信。
      • STGY_DATABASE_HOST=postgres
      • STGY_DATABASE_PORT=5432
  • AIユーザワーカ(Node.jsのCLI。実運用では統合ワーカ内で起動)
    • バックエンドをhttp://backend:3100として認識し、Dockerサブネットで通信。
    • キャッシュサーバをredis:6379として認識し、Dockerサブネットで通信。
      • STGY_REDIS_HOST=redis
      • STGY_REDIS_PORT=6379
    • DBサーバをpostgres:5432として認識し、Dockerサブネットで通信。
      • STGY_DATABASE_HOST=postgres
      • STGY_DATABASE_PORT=5432
  • DBサーバ(PostgreSQL)
    • Dockerサブネットでpostgres:5432をlistenし、DBを管理。
      • STGY_DATABASE_PORT=5432
  • S3サーバ(MinIO)
    • Dockerサブネットでminio:9000をlistenし、オブジェクトストレージを管理。表にはhttps://s3.stgy.jpとして露出。
      • STGY_MINIO_PORT=9000
    • Dockerサブネットでminio:9001をlistenし、コンソール画面を表示。表にはhttps://s3-console.stgy.jpとして露出。
      • STGY_MINIO_CONSOLE_PORT=9001
  • キャッシュサーバ(Redis)
    • Dockerサブネットでredis:6379をlistenし、キャッシュを管理。
      • STGY_REDIS_PORT=6379
  • メールサーバ(Postfix)
    • Dockerサブネットでpostfix:25をlistenし、メールワーカからのメールを受け取る。
      • STGY_SMTP_PORT=25
    • ホスト側にはSMTPポートを公開せず、Dockerサブネットからだけ受け付ける。
    • 外部のリレーサーバは使わず、宛先ドメインのMXを参照して直接配送する。
  • 全文検索サーバ(TTTS)
    • Dockerサブネットでttts:3200をlistenし、全文検索用インデックスを管理。
      • STGY_SEARCH_PORT=3200

どのサービスがどのサービスの接続を知っているかという参照関係を図にしておく。これは、実際に接続しているという意味ではなく、その設定を読み込んでいるという意味だ。例えば、バックエンドサーバがCaddyに接続することはないが、バックエンドにはCaddy越しに来るフロントエンドのURLを信用する設定が必要だ。また、バックエンドサーバはフロントエンドにS3サービスのURLを伝える必要があり、そこにはCaddyのアドレスが書いてある。同様にして、フロントエンドはCaddy越しにバックエンドやS3サーバに接続するアドレスを知っておく必要がある。

サーバ間参照

上述の設定は既にenv.vps.txtとdocker-compose.vps.ymlとcaddy/Caddyfile.vpsに書いてあるので、Dockerによる本番運用は、DockerとGitが動くホストさえあれば、5分で始められるようになっている。他のドメイン名を使う場合、それらのファイルのstgy.jpの部分をそのドメイン名に変えれば良い。HTTPSでなくHTTPで運用する場合、docker-compose.vps.ymlとcaddy/Caddyfile.vpsのhttpsの記述をhttpに置換する。ただし、HTTPだとパスワードやセッションIDが平文で転送されるため、検証用途以外で運用するのは止めておいた方がよい。

デモ環境Docker完結方式でのサービスの起動と終了

リポジトリルートにて、デモ環境Docker完結方式の設定が書いてあるenv.vps.txtを.envとしてコピーする。これは忘れがちなので注意。その上で、.envの内容を編集する。必要ならドメイン名やパスワードなどの値を変更する。MinIOのパスワード(STGY_MINIO_ROOT_PASSWORDとSTGY_STORAGE_S3_SECRET_ACCESS_KEYの両方)だけは、必ず初期値から変更すること。そうしないと外部から操作されてしまう。AI機能を有効化するならSTGY_OPENAI_API_KEYを設定する。

cp env.vps.txt .env

手動でDockerコンテナを立ち上げる。npmのスクリプト名に"vps:"が接頭していることに注意。

npm run vps:docker:up

Docker版のCaddyは、IP単位のレート制限モジュールを組み込んだ独自イメージとして自動的にビルドされる。STGY本体のCGI以外のリクエストは、既定でIPあたり10分間100リクエストまでに制限し、env.vps.txtのSTGY_CADDY_RATE_LIMIT_EVENTSとSTGY_CADDY_RATE_LIMIT_WINDOWで変更できる。これとは別に、パスが.cgiで終わるリクエストと.cgiの後ろにPATH_INFOが続くリクエストは、ホスト名と接続元IPアドレスの組み合わせごとに1時間100リクエストまでに制限する。制限超過は以下のログで確認できる。

docker compose -f docker-compose.yml -f docker-compose.vps.yml logs caddy

初期データを投入する。STGY上のadminユーザを変えている場合、環境変数STGY_ADMIN_EMAILとSTGY_ADMIN_PASSWORDに変更後の値を設定してから実行する。

npm run vps:reset-data

そして、以下のURLをブラウザで開けば、もう使えるようになっている。adminユーザのパスワードを変える作業を最初にやること。

  • https://stgy.jp/ : ユーザが使うサイト
    • デフォルトでは、「admin」ユーザのパスワードが「stgystgy」になっているので、それでログインして変える。
  • https://stgy.jp/site-test : caddy/extra-sites/test.caddyの読み込み確認
    • 「test.caddy is OK」と表示されれば成功。
  • https://s3-console.stgy.jp/ : MinIOのコンソール
    • この管理者アカウント情報は、環境設定ファイルのものから変えては駄目。

Docker完結方式のVPSではMailpitを起動せず、メールワーカからDocker内のPostfixへ直接送る。Postfixは外部のリレーサーバを使わず、宛先ドメインのMXへ配送する。PostfixのSMTPポートはホストには公開しない。配送状況は以下で確認できる。

docker compose -f docker-compose.yml -f docker-compose.vps.yml logs postfix
docker compose -f docker-compose.yml -f docker-compose.vps.yml exec postfix postqueue -p

外向きTCP 25番がVPS事業者やネットワークによって遮断されている場合は直接配送できない。また、配送先から信用されるために、mail.stgy.jpの正引き、送信元IPアドレスの逆引き、stgy.jpのSPFを一致させておく必要がある。

Caddyの追加サイト設定

Dockerで動かすCaddyは、STGY本体の設定とは別に、リポジトリのcaddy/extra-sitesディレクトリに置かれたCaddy設定を読み込める。docker-compose.ymlは、このディレクトリをCaddyコンテナ内の/etc/caddy/extra-sitesへマウントする。そのため、Caddyfileから見たインポート先は従来どおりextra-sitesとなる。VPS用のcaddy/Caddyfile.vpsは、extra-sites直下の拡張子が.caddyのファイルを全て読み込む。開発環境用のcaddy/Caddyfileは、VPS固有の仮想ホストをローカル開発環境で有効化しないよう、動作確認用のtest.caddyだけを読み込む。

# caddy/Caddyfile.vps
import extra-sites/*.caddy

# caddy/Caddyfile
import extra-sites/test.caddy

caddy/Caddyfile.vpsには、CGIへのアクセスをホスト名と接続元IPアドレスの組み合わせごとに1時間100リクエストまでに制限するcgi_rate_limitスニペットも定義してある。追加サイトでCGIを公開する場合は、そのサイトブロック内で以下のように読み込む。

import cgi_rate_limit

docker-compose.ymlは、リポジトリのcaddy/extra-sitesをCaddyコンテナ内の/etc/caddy/extra-sitesへ読み取り専用でマウントする。このディレクトリにはCaddyの追加設定だけを置く。追加サイトの静的ファイルやプログラムはCaddyコンテナへマウントせず、STGYとは別に動かすWebサービスから配信し、Caddyはそのサービスへリバースプロクシする。したがって、追加サイトごとにdocker-compose.vps.ymlを編集する必要はない。Docker版Caddyからホスト上のネイティブサービスへ接続する場合は、docker-compose.ymlでホストのゲートウェイに割り当てているhost.docker.internalを接続先に使う。

caddy/extra-sites/test.caddyはインポート機構の動作確認用としてGitで管理されている。test.caddyで定義した設定をSTGY本体のサイト設定から読み込み、/site-testに固定文字列を返す。開発環境ではhttp://localhost:8080/site-test、VPSではhttps://stgy.jp/site-testへアクセスし、「test.caddy is OK」と表示されれば追加設定が読み込まれている。

caddy/Caddyfile.vpsまたはcaddy/extra-sites/*.caddyを書き換えた場合、読み取り専用でマウントされたファイル自体はCaddyコンテナから即座に見えるが、実行中のCaddyは自動では設定を再読み込みしない。以下のコマンドで設定を検証してから再読み込みする。

docker compose -f docker-compose.yml -f docker-compose.vps.yml exec caddy \
  caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
docker compose -f docker-compose.yml -f docker-compose.vps.yml exec caddy \
  caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile

STGY以外の仮想ホストを同じVPSで運用する場合は、caddy/extra-sites直下にそのサイトの.caddyファイルを置く。test.caddyと.gitignore以外はcaddy/extra-sites/.gitignoreによってSTGYのGit管理対象外になるため、ホスト固有の設定をSTGYのリポジトリへコミットせずに利用できる。

別サイトの運用

STGYとは別のWebサイトを同一ホストで運用する場合、STGYのCaddyでデータを配信してもよいが、CGI等の込み入ったことをしたい場合には、lighttpdの別サービスを立てた方が良い。ここでは、ネイティブサービスとしてlighttpdを使うとして、prodユーザで起動して、ポート8123番で待ち受け、/home/prod/dbmx-public以下のデータを配信し、また*.cgiのファイルをCGIスクリプトとして実行する。

lighttpdをインストールする。dbmx.netではCAPTCHAを使うので、そのためのLuaモジュールもここで入れておく。

sudo apt install -y lighttpd lighttpd-modules-lua

パッケージ導入によって起動した標準サービスを停止し、自動起動も無効化する。

sudo systemctl disable --now lighttpd.service

公開ディレクトリを作成する。

mkdir /home/prod/dbmx-public

lighttpdの設定ファイル/etc/lighttpd/dbmx-site.confを作成する。所有者はroot:rootにすること。
内容は以下。

server.modules = (
  "mod_indexfile",
  "mod_access",
  "mod_accesslog",
  "mod_setenv",
  "mod_cgi"
)

server.bind = "0.0.0.0"
server.port = 8123
server.document-root = "/home/prod/dbmx-public"

index-file.names = (
  "index.html", "index.xhtml"
)

include_shell "/usr/share/lighttpd/create-mime.conf.pl"

setenv.add-environment = (
  "PATH" => "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
)

static-file.exclude-extensions = (
  ".cgi"
)

cgi.assign = (
  ".cgi" => ""
)

cgi.execute-x-only = "enable"

server.bindを0.0.0.0にしているのは、ホスト自身のループバックだけでなく、Docker版Caddyからも通常のTCPで接続できるようにするためである。外部インターフェースから8123番への接続はUFWのデフォルト拒否で遮断され、br-stgyからの接続だけが許可される。

設定を検証する。設定検証は、実際の起動ユーザーであるprodとして実行する。エラーが表示されず終了すれば正常である。

/usr/sbin/lighttpd -tt -f /etc/lighttpd/dbmx-site.conf

/etc/systemd/system/dbmx-site.serviceを作成する。所有者はroot:rootにすること。

[Unit]
Description=dbmx.net static and CGI site
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=prod
Group=prod

ExecStartPre=/usr/sbin/lighttpd -tt -f /etc/lighttpd/dbmx-site.conf
ExecStart=/usr/sbin/lighttpd -D -f /etc/lighttpd/dbmx-site.conf

ExecReload=/usr/sbin/lighttpd -tt -f /etc/lighttpd/dbmx-site.conf
ExecReload=/bin/kill -USR1 $MAINPID

Restart=on-failure

[Install]
WantedBy=multi-user.target

systemdへユニットを読み込ませる。

sudo systemctl daemon-reload

OS起動時の自動起動を有効にし、そのまま起動する。

sudo systemctl enable --now dbmx-site.service

状態を確認する。

systemctl --no-pager --full status dbmx-site.service

疎通を確認する。

echo "<h1>hello</h1>" > /home/prod/dbmx-public/index.html
curl -i http://127.0.0.1:8123/

以下のコマンドで、サービスの起動、停止、再起動、設定再読み込みができる。dbmx-site.confを書き換えた後には設定再読み込みをすればよい。

sudo systemctl start dbmx-site.service
sudo systemctl stop dbmx-site.service
sudo systemctl restart dbmx-site.service
sudo systemctl reload dbmx-site.service

dbmx.netのDNSを切り替える前に、旧サーバと新サーバを同時に稼働させたまま新サーバの内容を確認する場合は、caddy/Caddyfile.vpsのhttps://stgy.jpブロック内で、引数なしのhandleディレクティブより前に以下を追加する。handle_pathは/dbmxという接頭辞を取り除いてからlighttpdへ転送するので、https://stgy.jp/dbmx/index.htmlへのアクセスはlighttpdの/index.htmlへ届く。

  redir /dbmx /dbmx/
  handle_path /dbmx/* {
    reverse_proxy http://host.docker.internal:8123 {
      header_up Host {http.request.host}
    }
  }

caddy/Caddyfile.vpsを書き換えた後は、前述のcaddy validateとcaddy reloadを実行し、以下で確認する。

curl -i https://stgy.jp/dbmx/
curl -i https://stgy.jp/dbmx/index.html

DNSでdbmx.netのAレコードを153.127.47.206へ切り替えた後は、https://dbmx.netへのアクセスをlighttpdへ転送できる。caddy/extra-sites/dbmx.caddyを次の内容にする。Caddyがネイティブサービスである場合、接続先はlocalhost:8123にする。DNSを切り替える前にこの仮想ホストを有効にすると、Caddyはまだ旧サーバを指しているdbmx.netの証明書を取得できないため、切り替え後に有効にする。

https://dbmx.net {
  encode zstd gzip

  import cgi_rate_limit

  reverse_proxy http://host.docker.internal:8123
}

Dockerコンテナの自動起動

OSの起動時に自動的に全サービスが立ち上がるようにすべきだ。起動スクリプトを動かして、上記のnpm run vps:docker:upを実行するようにしたい。そのためには、systemdの起動スクリプトを書く。/etc/systemd/system/stgy-docker.serviceに以下の内容を記述する。

[Unit]
Description=STGY stack via Docker Compose
Wants=network-online.target
After=network-online.target docker.service
Requires=docker.service

[Service]
Type=oneshot
User=prod
WorkingDirectory=/home/prod/stgy
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/bin/npm run vps:docker:up
ExecStop=/usr/bin/npm run vps:docker:down
RemainAfterExit=yes
TimeoutStartSec=0
TimeoutStopSec=120

[Install]
WantedBy=multi-user.target

自動起動スクリプトを有効化し、状態を確認する。起動シーケンス(ExecStart)が実行されて、Dockerのサービスが起動するはずだ。

sudo systemctl daemon-reload
sudo systemctl enable --now stgy-docker
systemctl status stgy-docker

自動起動スクリプトを無効化するには、以下のコマンドを実行する。停止シーケンス(ExecStop)が実行されて、Dockerのサービスが停止するはずだ。

sudo systemctl disable --now stgy-docker

ネイティブサービス稼働方式

大規模サイトとしての本番運用を考えると、リバースプロクシとDBサーバとストレージサーバとメールサーバはクラウドのマネージドサービスを使うのが典型的だ。オンプレミスだとしても個々のサービスは個別のホストで稼働させて性能と可用性を向上させる構成にするのが定石だ。DockerによってSNSのサービスをカプセル化できるのは素晴らしいが、デモサイトをそれで運用していると、本番サイトの構成とかけ離れすぎていて、運用の訓練としては理想的ではない。依存サービスを同一ホストで動かすにしても、個別のネイティブサービスとして稼働させた方が本番の構成に近づけられる。

ネイティブサービスを動かすと、個々のサービスを他の用途にも使えるという利点もある。例えば、stgy.jpにSNS以外のサービスも乗っけるなら、単一のリバースプロクシにそれらの中継もさせた方が運用が楽だ。同様に、データベースやオブジェクトストレージを他のサービスと共有して何らかの連携を行う場合も、ネイティブサービスになっている方が管理しやすい。理想論としては、そのような同居や密結合は避けるべきで、SNSと他のサービスはホストも分けて運用すべきなのだが、リソースの制約でそうも言っていられないことがある。

実際のところ、デモ環境の運用に手間と金をかけたくない私としては、自前の様々なオープンソース製品のデモは1台のホストに集めて、1つのリバースプロクシで一括運用したい。デモ用途ならセキュリティやアベイラビリティの優先度は要件は無いようなものなので、運用効率重視の構成を追求できる。ホスト1台に集約しつつコンテナ運用をするのも可能だが、同じ役割のサービスが複数コンテナで起動するとポートや証明書の管理が煩雑になるので、同じ役割のサービスは統合したい。

コンテナ運用のサービスでも複数の用途で使い回せるとの指摘があるだろう。しかし、VPSの単一ホスト上で動かすことを前提とすると、単に面倒になるだけだ。サービスの設定ファイルを書いて、それをコンテナにマウントさせる旨を別の設定ファイルに書くという無駄な作業が発生する。また、用途Aと用途Bの双方から使われるコンテナの設定をどちらのリポジトリで管理するのかという話になる。単機能のコンテナを使い回すことにも利点はあり、スケールアウト戦略で多数のインスタンスを立てる場合には特に有用だ。しかし、その際にはECSなどのコンテナサービスを使うだろう。VPS1台構成の話とは分けて考えるべきだ。

結局のところ、どのサービスをネイティブにして、どのサービスをコンテナ運用にするかは、運用の都合で決まるということになる。いずれにせよ、全てのサービスをネイティブで稼働させる方法をここで洗い出しておくことは有益だ。必要に応じて、以下の手順をなぞればよいだろう。

全ネイティブ構成では、最初にenv.native.txtを.envとしてコピーする。以後、PostgreSQL、MinIO、Redisなどに設定するユーザ名やパスワードを変更した場合は、.envの対応する値も同じ値にする。.envはGitの管理対象外である。統合ワーカからバックエンドへの内部API呼び出しはCaddyを経由させず、STGY_BACKEND_API_BASE_URL=http://127.0.0.1:3100として同一ホスト上のバックエンドへ直接接続する。

cp env.native.txt .env

systemctl

Ubuntu Linuxにおいて、ネイティブサービスすなわちデーモンプロセスの管理はsystemdによって行われる。その設定は/etc/systemd/systemの下に置かれる。サービスの管理はsystemctlコマンドを使って行われる。

  • sudo systemctl start SERVICENAME : 手動で起動
  • sudo systemctl stop SERVICENAME : 手動で停止
  • sudo systemctl restart SERVICENAME : 手動で再起動
  • sudo systemctl enable --now SERVICENAME : 自動起動を有効化
  • sudo systemctl disable --now SERVICENAME : 自動起動を無効化
  • systemctl status SERVICENAME : 起動設定の状態確認

ログを確認する際には、journalctlコマンドを使う。

  • journalctl -u SERVICENAME : ログ全体をページャで見る
  • journalctl -u SERVICENAME -f : 最新ログを表示して端末に表示し続ける

systemdの設定ファイル(*.service、*.socketなど)の変更を反映したい場合、以下のコマンドを実行する。

sudo systemctl daemon-reload

DockerでSTGYを立てている場合にネイティブサービスに運用に変えるには、stgy-dockerサービスをdisableしてから作業を行う。ネイティブサービスを立てている場合にDocker運用に変えるには、後述のcaddy等のサービス全てをdisableしてから作業を行う。

なお、運用方式を変える場合、単に起動方法を切り替えるだけではPostgreSQLやMinIOのデータは移行されないことには注意すべきだ。後述の手順でバックアップデータを作ってから、新しい環境でそれを使ってデータをリストアすることになる。

Caddy

CaddyはGoで書かれた軽量かつ高速のWebサーバである。設定も簡単で、何より証明書の取得を自動でやってくれるのが便利だ。Caddyは以下のようにインストールし、自動起動を有効化できる。

sudo apt install caddy
sudo systemctl enable --now caddy
systemctl status caddy

aptでインストールされるcaddyはプラグインが使えないので、バイナリだけをビルドし直して置き換える必要がある。go言語の最新版もインストールする必要がある。

sudo apt remove golang-go
wget https://go.dev/dl/go1.25.1.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.25.1.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
xcaddy build --with github.com/mholt/caddy-ratelimit@v0.1.0
sudo mv caddy /usr/bin/caddy
sudo chown root:root /usr/bin/caddy
sudo chmod 755 /usr/bin/caddy

Caddyの自動起動スクリプトの本体は/usr/lib/systemd/system/caddy.serviceである。このファイルを編集して、Caddyの実行ユーザをprodにしておく。UMaskの行は追加の設定であり、同一グループのプロセスが証明書などのデータを読めるようにする。

User=prod
Group=prod
UMask=0027

起動スクリプトの変更を反映する。

sudo systemctl daemon-reload

/var/caddyというディレクトリを作り、そこにCaddy関連のデータを置くことにする。その所有者をCaddyの実行ユーザに合わせておく。

sudo mkdir /var/caddy
sudo chown -R prod:prod /var/caddy

Caddy自体の設定は/etc/caddy/Caddyfileで行う。典型的には以下のように書く。証明書はワイルドカード方式にせず、Caddyfileに列挙したホスト名ごとに個別に取得する。mail.stgy.jpはPostfixで使う証明書を取得・更新するために定義し、Webアクセスには404を返す。S3にはホットリンク防止のRefererの制限をかけている。STGYサービスの配信に加えて、/home/www以下のファイルを通常のWebサーバとして配信し、その中のprivate/配下にはBasic認証をかけるものとする。

stgy.jpでは、CGI以外のリクエストに接続元IPアドレスごとに5分間120リクエストのレート制限をかける。Next.jsの静的ファイルである/_next/static/以下は、通常のページ表示で多数のリクエストが発生する一方、アプリケーション処理の負荷にはなりにくいので対象外にする。また、CGIを公開するサイトではcgi_rate_limitスニペットを読み込み、パスが.cgiで終わるリクエストと.cgiの後ろにPATH_INFOが続くリクエストを、ホスト名と接続元IPアドレスの組み合わせごとに1時間100リクエストまでに制限する。制限を超えたリクエストにはHTTP 429とRetry-Afterが返り、Caddyのログには対象IPアドレスが記録される。

{
  storage file_system {
    root /var/caddy
  }
}

(cgi_rate_limit) {
  rate_limit {
    zone cgi_per_ip {
      match {
        path_regexp cgi_rate_limit_path ^.*\.cgi(/.*)?$
      }
      key {http.request.host}:{remote_host}
      events 100
      window 1h
    }
    log_key
  }
}

https://stgy.jp {
  encode zstd gzip

  rate_limit {
    zone stgy_per_ip {
      match {
        not path /_next/static/*
        not path_regexp stgy_cgi_path ^.*\.cgi(/.*)?$
      }
      key {remote_host}
      events 100
      window 10m
    }
    log_key
  }

  redir /backend /backend/
  handle_path /backend/* {
    reverse_proxy http://127.0.0.1:3100 {
      lb_policy first
      health_uri /health
      header_up Host {http.request.host}
    }
  }

  handle {
    reverse_proxy http://127.0.0.1:3000 {
      lb_policy first
      health_uri /
      header_up Host {http.request.host}
    }
  }
}

https://s3.stgy.jp {
  encode zstd gzip

  @okref header_regexp Referer ^https?://([^.]+\.)?stgy\.jp(/|$)
  @noref not header Referer *
  handle @okref {
    reverse_proxy 127.0.0.1:9000 {
      flush_interval -1
    }
  }
  handle @noref {
    reverse_proxy 127.0.0.1:9000 {
      flush_interval -1
    }
  }
  respond 403
}

https://s3-console.stgy.jp {
  encode zstd gzip

  reverse_proxy http://127.0.0.1:9001
}

https://mail.stgy.jp {
  respond 404
}

https://www.stgy.jp {
  encode zstd gzip
  root * /home/prod/www

  import cgi_rate_limit

  @secret path /private*
  basic_auth @secret {
    admin $2a$14$uuicMQhEHEbzlvNfISPcue77In..Te9ZMzAKzppaRilla5wBlXdoa
  }

  file_server {
    index index.xhtml index.html
  }
}

以下のコマンドで、設定ファイルの検証や設定変更の反映をする。

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl restart caddy

Basic認証で使うパスワードのハッシュ値は、以下のコマンドで生成できる。

caddy hash-password --plaintext NEWPASSWORD

STGYには必要ない余談だが、CGIスクリプトを使いたい場合、FastCGIのラッパーであるfcgiwrapを使うと良い。

sudo apt install fcgiwrap
sudo systemctl enable --now fcgiwrap.socket
sudo systemctl status fcgiwrap.socket

fcgiwrapの自動起動スクリプト本体は/usr/lib/systemd/system/fcgiwrap.serviceと/usr/lib/systemd/system/fcgiwrap.socketなので、それぞれを編集して実行ユーザと所有ユーザをprodにしておく。

User=prod
Group=prod
SocketUser=prod
SocketGroup=prod

起動スクリプトの変更を反映する。

sudo systemctl daemon-reload

そのうえで、Caddyfileのwww.stgy.jpのディレクティブの中に以下の設定を書くと、.cgi拡張子のファイルがCGIスクリプトとして実行されるようになる。前掲の設定例ではcgi_rate_limitも読み込んでいるため、これらのリクエストには1時間100回の制限がかかる。ディレクティブは上から評価されるので、file_serverディレクティブより上に置くことが重要である。

  @cgi path_regexp cgi ^(.+\.cgi)(/.*)?$
  handle @cgi {
    reverse_proxy unix//run/fcgiwrap.socket {
      transport fastcgi {
        split .cgi
        env SCRIPT_FILENAME /home/prod/www{http.regexp.cgi.1}
        env DOCUMENT_ROOT /home/prod/www
        env SCRIPT_NAME {http.regexp.cgi.1}
        env PATH_INFO   {http.regexp.cgi.2}
      }
    }
  }

Caddyfileを書き換えたならCaddyを再起動する。

sudo systemctl restart caddy

HTTPSで使うサーバ証明書は、Caddyfileに列挙した各ホスト名ごとにLet's Encryptから自動取得され、/var/caddy/certificates配下に保存される。更新も勝手にやってくれて、その際のACME認証も自身のWebサーバの機能で通せるので、certbotを運用するより楽だ。後述のPostfixの設定では、mail.stgy.jp用にCaddyが取得した証明書を流用する。

$ cd /var/caddy/certificates/acme-v02.api.letsencrypt.org-directory
$ find . -name "*.crt" -o -name "*.key"
./s3-console.stgy.jp/s3-console.stgy.jp.crt
./s3-console.stgy.jp/s3-console.stgy.jp.key
./mail.stgy.jp/mail.stgy.jp.crt
./mail.stgy.jp/mail.stgy.jp.key
./stgy.jp/stgy.jp.key
./stgy.jp/stgy.jp.crt
./www.stgy.jp/www.stgy.jp.key
./www.stgy.jp/www.stgy.jp.crt
./s3.stgy.jp/s3.stgy.jp.key
./s3.stgy.jp/s3.stgy.jp.crt

Postfix

Postfixは定番のメールサーバである。Mailpitの代わりに稼働させることで、ちゃんとしたメールの配送が実現できる。Postfixは以下のようにインストールし、自動起動を有効化できる。インストール時に出るダイアログでは「No configurations」を選ぶ。SASL認証用のモジュールとコマンドも入れる。

sudo apt install postfix
sudo systemctl enable --now postfix
systemctl status postfix
sudo apt install libsasl2-modules sasl2-bin

postfixというユーザが自動的に作られるので、それがcaddyの証明書を読めるようにprodグループに追加する。また、SASL認証のデータベースにアクセスできるように、saslグループにも追加する。

sudo usermod -aG prod postfix
sudo usermod -aG sasl postfix

/etc/postfix/main.cfを編集(新規作成)する。ローカルホストで稼働するSTGYメールワーカから出されたメールは無認証で受け取る。Macで開発している場合にはMailpitのリレー設定でTLS認証で受け取る。

myhostname = mail.stgy.jp
mydomain = stgy.jp
myorigin = $mydomain

mydestination = $myhostname, localhost.$mydomain, localhost
inet_interfaces = all
inet_protocols = ipv4

mynetworks = 127.0.0.0/8, [::1]/128, 172.23.0.0/24

mailbox_size_limit = 0
recipient_delimiter = +
append_dot_mydomain = no
biff = no

smtpd_recipient_restrictions =
  permit_mynetworks,
  permit_sasl_authenticated,
  reject_unauth_destination

smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = $mydomain
smtpd_sasl_path = smtpd
broken_sasl_auth_clients = yes

smtpd_tls_cert_file = /var/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.stgy.jp/mail.stgy.jp.crt
smtpd_tls_key_file = /var/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.stgy.jp/mail.stgy.jp.key
smtpd_use_tls = yes
smtpd_tls_security_level = may
smtp_tls_security_level = may
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache

maillog_file = /var/log/mail.log
message_size_limit = 10485760
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases

/etc/postfix/master.cfを編集する。既存の行を全てコメントアウトした上で以下を加える。すなわち、smtpの行のchrootをnに変え、submissionの行でローカルホストのクライアントとSASL認証を経たクライアントを許可する。

smtp       inet  n       -       n       -       -       smtpd
submission inet  n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject_unauth_destination

SASL認証のためのアカウントを作る必要がある。ここでは postfix@stgy.jp というアカウントを作る。パスワードを聞かれるので、予め決めておいた文字列を入力する。その後の確認コマンドで「postfix@stgy.jp: userPassword」と表示されれば成功。

sudo saslpasswd2 postfix -u stgy.jp
sudo chgrp sasl /etc/sasldb2
sudo sasldblistusers2

Postfixを再起動する。

sudo systemctl restart postfix

Macでswaksコマンドを使うと、疎通が確認できる。パスワードは先ほど決めたものを指定すること。

swaks --to hirarin@gmail.com --server stgy.jp:587 --from noreply@stgy.jp --auth-user postfix@stgy.jp --auth-password SASLPASSWORD --auth PLAIN --header "Subject: test" --body "test1" --tls

SMTPサーバが配信先のSMTPサーバに信用されるためには、自身が名乗るホスト名(設定ファイル内のmyhostnameの値)とIPアドレスの逆引きの値が同じである必要がある。また、DNSのSPF設定で、自身のIPアドレスが登録されている必要がある。SASL認証はメールのリレーに必須の設定ではないが、インターネット越しで認証して安全にメールを受信するには、SSLとSASL認証の組み合わせを使うのが望ましい。

PostgreSQL

PostgreSQLは堅牢性と多機能性に定評のあるデータベースサーバである。STGYのいくつかの機能は標準SQLの範囲を超えてPostgreSQLの独自機能をいくつか使っているので、基本的にはPostgreSQLありきの実装になっている。PostgreSQLは以下のようにインストールし、自動起動を有効化できる。

sudo apt install postgresql postgresql-contrib postgresql-client
sudo systemctl enable --now postgresql
systemctl status postgresql

Ubuntu 26.04の標準パッケージではPostgreSQL 18が導入される。設定パスを特定のメジャーバージョンに固定せず、稼働中のPostgreSQLから実際の設定ディレクトリを取得する。以下のコマンドを実行し、以後の設定作業は同じシェルで行う。

PG_CONF_DIR=$(dirname "$(sudo -u postgres psql -Atqc 'SHOW config_file')")
printf '%s\n' "$PG_CONF_DIR"

まずは、$PG_CONF_DIR/conf.d/90-stgy.confファイルを新規作成する。内容は以下のようにする。メモリ使用量はメモリ4GBのシステムに最適化してある。

listen_addresses = '*'
port = 5432

password_encryption = scram-sha-256

logging_collector = on
log_min_duration_statement = 1000
log_line_prefix = '%m [%p] %u@%d '

shared_buffers = '1GB'
effective_cache_size = '3GB'
work_mem = '16MB'
maintenance_work_mem = '256MB'
checkpoint_completion_target = 0.9
wal_compression = on
max_connections = 100

shared_preload_libraries = 'pg_stat_statements'

$PG_CONF_DIR/pg_hba.confを編集して、ホストベース認証の設定をする。ローカルとDockerブリッジ(172.23.0.*)からの接続のみを受け付け、全てにパスワード認証を要求する。元の内容は全て消してよい。

local  all  postgres                    peer
local  all  all                         scram-sha-256
host   all  all       127.0.0.1/32      scram-sha-256
host   all  all       ::1/128           scram-sha-256
host   all  all       172.23.0.0/24     scram-sha-256

confファイルを書き換えたならPostgreSQLを再起動する。

sudo systemctl restart postgresql

psqlコマンドで接続し、STGY用のデータベースを作成する。

sudo -u postgres psql <<__EOF__
CREATE ROLE admin LOGIN PASSWORD 'stgystgy';
CREATE DATABASE stgy OWNER admin TEMPLATE template0 ENCODING 'UTF8' LC_COLLATE 'C' LC_CTYPE 'C';
GRANT ALL PRIVILEGES ON DATABASE stgy TO admin;
__EOF__

データベースのスキーマ定義や初期データの登録を行う。

ls postgres/init/*.sql | sort | while read file ; do
  PGPASSWORD=stgystgy psql -h 127.0.0.1 -p 5432 -U admin -d stgy < $file
done

データベースの内容を確認する。

PGPASSWORD=stgystgy psql -h 127.0.0.1 -p 5432 -U admin -d stgy

-- ロールの一覧
stgy=> \du
-- データベースの一覧
stgy=> \l
-- テーブルの一覧
stgy=> \dt
-- adminユーザの存在確認
stgy=> SELECT id, nickname, is_admin FROM users;

MinIO

MinIOはS3互換のオブジェクトストレージサービスであり、STGYではメディアデータの管理に使う。MinIOはaptパッケージが無いので、以下のようにバイナリを直接インストールする。

wget -O /tmp/minio https://dl.min.io/server/minio/release/linux-amd64/minio
sudo install -m 0755 /tmp/minio /usr/local/bin/minio
wget -O /tmp/mc https://dl.min.io/client/mc/release/linux-amd64/mc
sudo install -m 0755 /tmp/mc /usr/local/bin/mc

MinIOの実行ユーザを作り、設定とデータの置き場所を作る。

sudo useradd --system --home /var/minio --shell /usr/sbin/nologin minio
sudo mkdir -p /var/minio/data
sudo mkdir -p /etc/minio
sudo chown -R minio:minio /var/minio
sudo chown -R minio:minio /etc/minio

/etc/minio/minio.envを新規作成して設定を書く。MinIOは外部からアクセスされるので、ユーザとパスワードは必ず変更すること。

MINIO_ROOT_USER=admin
MINIO_ROOT_PASSWORD=stgystgy
MINIO_API_CORS_ALLOW_ORIGIN=https://stgy.jp,https://s3.stgy.jp,https://s3-console.stgy.jp
MINIO_BROWSER_REDIRECT_URL=https://s3-console.stgy.jp
MINIO_SERVER_URL=https://s3.stgy.jp

設定ファイルが他のユーザに読まれないように所有権とパーミッションを設定する。

sudo chown minio:minio /etc/minio/minio.env
sudo chmod 600 /etc/minio/minio.env

自動起動のためのsystemdスクリプトを/etc/systemd/system/minio.serviceとして書く。

[Unit]
Description=MinIO Object Storage
Wants=network-online.target
After=network-online.target

[Service]
User=minio
Group=minio
EnvironmentFile=-/etc/minio/minio.env
ExecStart=/usr/local/bin/minio server /var/minio/data \
  --address 0.0.0.0:9000 \
  --console-address 127.0.0.1:9001
Restart=always
RestartSec=5
LimitNOFILE=65536
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

自動起動を有効化する。

sudo systemctl daemon-reload
sudo systemctl enable --now minio
sudo systemctl status minio

STGY用のバケットを作成する。.envに設定したMinIOのユーザ名とパスワードを使う。

set -a
source .env
set +a
mc alias set stgylocal http://127.0.0.1:9000 "$STGY_MINIO_ROOT_USER" "$STGY_MINIO_ROOT_PASSWORD"
mc mb stgylocal/stgy-test
mc anonymous set download stgylocal/stgy-test
mc mb stgylocal/stgy-images
mc anonymous set download stgylocal/stgy-images
mc mb stgylocal/stgy-profiles
mc anonymous set download stgylocal/stgy-profiles
mc mb stgylocal/stgy-tracks
mc anonymous set download stgylocal/stgy-tracks

Redis

Redisはメモリ常駐のキー・バリュー型ストアで、STGYでは主にキャッシュ用途に使う。Redisは以下のようにインストールし、自動起動を有効化できる。

sudo apt install redis-server
sudo systemctl enable --now redis-server
systemctl status redis-server

/etc/redis/redis.confを編集する。内容は以下のようにする。全てのアドレスにバインドするが、パスワード認証を要求する。メモリ使用量は512MBに制限する。元の内容は全て消してよい。

bind 0.0.0.0
port 6379
protected-mode yes

supervised systemd
pidfile /run/redis/redis-server.pid
logfile ""
dir /var/lib/redis

requirepass stgystgy

save ""
appendonly no

maxmemory 512mb
maxmemory-policy allkeys-lru
timeout 0
tcp-keepalive 60

confファイルを書き換えたならRedisを再起動する。

sudo systemctl restart redis-server

CLIで接続確認する。

redis-cli -a stgystgy PING

全文検索サービス

STGYの全文検索には内製の3TS(TTTS: time-tiered text Search)アーキテクチャに基づく独自のサービスを用いる。Gitのリポジトリ内でビルドして、/home/prod/stgy-tttsの配下にデプロイして、そこにあるJavaScriptコードをNode.jsで実行する起動スクリプトをsystemd経由で実行する。

全文検索サーバをデプロイするには、prodユーザが、stgyリポジトリの中で、以下のコマンドを実行する。stgy.jpで全てネイティブサービスを使った場合の環境設定がスクリプト内に書いてある。

./scripts/deploy-ttts.sh

デプロイ用スクリプトを実行すると、/home/prod/stgy-backend配下にJavaScriptがデプロイされる。その中にstart.shというスクリプトがあり、それを実行すればバックエンドサービスが起動する。oneWorkerという、各種の非同期ワーカを統合したプロセスもも同時に起動される。それらを自動起動するsystemdスクリプトを/etc/systemd/system/stgy-backend.serviceとして書く。

[Unit]
Description=STGY TTTS
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=prod
Group=prod
WorkingDirectory=/home/prod/stgy-ttts
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/home/prod/stgy-ttts/start.sh start
ExecStop=/home/prod/stgy-ttts/start.sh stop
ExecReload=/home/prod/stgy-ttts/start.sh restart
Restart=on-failure
RestartSec=2s
TimeoutStopSec=30s
KillMode=process

[Install]
WantedBy=multi-user.target

自動起動スクリプトを有効化し、状態を確認する。

sudo systemctl daemon-reload
sudo systemctl enable --now stgy-ttts
systemctl status stgy-ttts

ネットワークの疎通確認をする。200 OKが返されれば成功。

curl -i http://localhost:3200/health

STGYのバックエンドとフロントエンド

STGYのバックエンドとフロントエンドもネイティブサービスとして稼働させられる。Gitのリポジトリ内でビルドして、/home/prod/stgy-backendおよび/home/prod/stgy-frontendの配下にデプロイして、そこにあるJavaScriptコードをNode.jsで実行する起動スクリプトをsystemd経由で実行する。

バックエンドサーバをデプロイする前に、.envがPostgreSQL、MinIO、Redisなどに実際に設定したユーザ名とパスワードに一致していることを確認する。特に、変更が必須のMinIOのユーザ名とパスワードは、STGY_MINIO_ROOT_USER、STGY_MINIO_ROOT_PASSWORD、STGY_STORAGE_S3_ACCESS_KEY_ID、STGY_STORAGE_S3_SECRET_ACCESS_KEYの対応する値を一致させる。

prodユーザがstgyリポジトリの中で以下を実行する。.envの値をデプロイ時の環境変数として読み込み、deploy-backend.shが生成するstart.shへ焼き込む。

set -a
source .env
set +a
./scripts/deploy-backend.sh

デプロイ時の環境変数が指定されていない項目には、deploy-backend.sh内の全ネイティブ構成向け初期値が使われる。

デプロイ用スクリプトを実行すると、/home/prod/stgy-backend配下にJavaScriptがデプロイされる。その中にstart.shというスクリプトがあり、それを実行すればバックエンドサービスが起動する。oneWorkerという、各種の非同期ワーカを統合したプロセスもも同時に起動される。それらを自動起動するsystemdスクリプトを/etc/systemd/system/stgy-backend.serviceとして書く。

[Unit]
Description=STGY Backend (API + workers)
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=prod
Group=prod
WorkingDirectory=/home/prod/stgy-backend
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/home/prod/stgy-backend/start.sh start
ExecStop=/home/prod/stgy-backend/start.sh stop
ExecReload=/home/prod/stgy-backend/start.sh restart
Restart=on-failure
RestartSec=2s
TimeoutStopSec=30s
KillMode=process

[Install]
WantedBy=multi-user.target

自動起動スクリプトを有効化し、状態を確認する。

sudo systemctl daemon-reload
sudo systemctl enable --now stgy-backend
systemctl status stgy-backend

ネットワークの疎通確認をする。200 OKが返されれば成功。

curl -i http://localhost:3100/health

フロントエンドサーバをデプロイするには、prodユーザが、stgyリポジトリの中で、以下のコマンドを実行する。stgy.jpで全てネイティブサービスを使った場合の環境設定がスクリプト内に書いてある。

./scripts/deploy-frontend.sh

デプロイ用スクリプトを実行すると、/home/prod/stgy-frontend配下にJavaScriptがデプロイされる。その中にstart.shというスクリプトがあり、それを実行すればバックエンドサービスが起動する。それを自動起動するsystemdスクリプトを/etc/systemd/system/stgy-frontend.serviceとして書く。

[Unit]
Description=STGY Frontend
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=prod
Group=prod
WorkingDirectory=/home/prod/stgy-frontend
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/home/prod/stgy-frontend/start.sh start
ExecStop=/home/prod/stgy-frontend/start.sh stop
ExecReload=/home/prod/stgy-frontend/start.sh restart
Restart=on-failure
RestartSec=2s
TimeoutStopSec=30s
KillMode=process

[Install]
WantedBy=multi-user.target

自動起動スクリプトを有効化し、状態を確認する。

sudo systemctl daemon-reload
sudo systemctl enable --now stgy-frontend
systemctl status stgy-frontend

ネットワークの疎通確認をする。200 OKが返されれば成功。

curl -i http://localhost:3000/health

以下のコマンドで初期データの投入を行う。

./scripts/edit-users.py seeder/users-core-*.txt seeder/users-test-*.txt
./scripts/edit-agreement.py seeder/agreements-core-*.json
./scripts/edit-posts.py seeder/posts-core-*.txt seeder/posts-test-*.txt
./scripts/user-actions.py seeder/actions-core.txt seeder/actions-test.txt
./scripts/edit-posts.py seeder/posts-after-actions-*.txt

deploy-backend.shとdeploy-frontend.shが、デプロイ時の環境設定を生成物へ焼き込むself-containedな実装になっているのは重要である。デプロイした時の環境設定で常にサービスが安定稼働するべきだからだ。deploy-backend.shはデプロイ実行時に設定されたSTGY_*環境変数を初期値より優先し、その値をstart.shへ焼き込む。デプロイ後に.envや外部の環境変数を変えても、再デプロイしない限りサービスの挙動は変化しない。二つのスクリプトはCI/CDへの布石である。STGYはOSS製品として頒布するので特定のデプロイ方式を前提としないが、少なくともGitHub Actionsでデプロイしやすいようには設計している。実際、GitHub Actionsのワークフローでリポジトリ内のスクリプトを実行するだけで、CDは達成できる。パスワード等はリポジトリのシークレットからデプロイ時の環境変数として注入する。CIに関しては、単にテスト環境でnpm testを呼ぶワークフローを仕掛ければ良い。そこで呼ばれるテストが統合テストと呼ぶに足るものであるかどうかが品質を左右することになる。

ネイティブサービス運用での初期化

リポジトリ直下で以下のコマンドを実行すると、ネイティブサービスのPostgreSQL、MinIO、TTTSのデータを初期化し、STGYの初期データを投入できる。全ての対象データを問答無用で初期化するので注意すること。.envには、上述のenv.native.txtを元にしてネイティブサービスに設定した接続情報を書いておく。

npm run vps:native:reset-data

Redisも初期化する場合は、モードを明示して以下を実行する。

npm run reset-cache -- --mode native

個別の初期化スクリプトも、--mode dockerまたは--mode nativeで接続先を切り替えられる。

フロントエンド・バックエンド・ワーカーのみDockerで動かす方式

Caddy、PostgreSQL、MinIO、Redis、Postfix、TTTSをネイティブサービスで動かし、STGY独自のフロントエンド、バックエンド、統合ワーカーだけをDockerで動かす場合は、env.hybrid.txtを.envとしてコピーして実際のパスワード等を設定する。

cp env.hybrid.txt .env

ネイティブ版のフロントエンドとバックエンドは停止し、依存サービスとTTTSを有効にする。

sudo systemctl disable --now stgy-frontend
sudo systemctl disable --now stgy-backend
sudo systemctl enable --now caddy postgresql minio redis-server postfix stgy-ttts

Docker側は、既存の依存サービスのコンテナが残っていれば停止・削除した上で、worker、backend、frontendだけを依存サービスなしで起動する。

npm run vps:hybrid:up

初期データを投入する場合は、ネイティブ側のデータサービスを対象にする以下のコマンドを使う。

npm run vps:hybrid:reset-data

Docker側の三サービスだけを停止・削除する場合は以下を実行する。

npm run vps:hybrid:down

ネイティブサービスの全てを無効化するには、以下のコマンドを実行する。全Docker運用に戻したい場合は、一旦全てのネイティブサービスを停止し、.envをenv.vps.txtを元にした全Docker構成用の設定へ切り替えてからDocker側を起動する必要がある。

sudo systemctl disable --now stgy-frontend
sudo systemctl disable --now stgy-backend
sudo systemctl disable --now stgy-ttts
sudo systemctl disable --now postfix
sudo systemctl disable --now redis-server
sudo systemctl disable --now minio
sudo systemctl disable --now postgresql
sudo systemctl disable --now caddy

全Docker構成へ戻す場合は、全Docker構成用の設定を.envへ反映してから起動する。

cp env.vps.txt .env
# .envのパスワード等を実際の値に設定する
npm run vps:docker:up

ネイティブサービスの全てを有効化するには、以下のコマンドを実行する。

sudo systemctl enable --now caddy
sudo systemctl enable --now postgresql
sudo systemctl enable --now minio
sudo systemctl enable --now redis-server
sudo systemctl enable --now postfix
sudo systemctl enable --now stgy-ttts
sudo systemctl enable --now stgy-backend
sudo systemctl enable --now stgy-frontend

バックアップ

実運用中は、データベースとオブジェクトストレージのデータを定期的にバックアップすることが望ましい。Docker運用とネイティブサービス運用のどちらでも、同じ手順でバックアップの作成ができる。

バックアップのタスクを実行する環境は、本番稼働環境のネットワーク上にあるLinuxサーバならば何でも良いが、PostgreSQLとMinIOのクライアントが必要になるので、VPSサーバ以外で動かす場合には両者のクライアントパッケージを入れること。それに加えて、以下のパッケージをインストールしておく。

sudo apt install ca-certificates rsync

バックアップデータの置き場所を作成しておく。

sudo mkdir -p /var/backups/stgy /var/log/stgy
sudo chown -R prod:prod /var/backups/stgy /var/log/stgy

手動でバックアップを取るには、prodユーザでログインして、以下のコマンドを実行する。デーベースサーバとオブジェクトサーバの情報は.envファイルから取得する。ホスト名の解決ができない場合にはlocalhostを使うので、.env内の各種ホスト名はDocker運用の設定のままでも、ネイティブサービス運用のものでも、変更なしで動作させられる。

cd stgy
./scripts/backup-full-site.sh backup

実行すると、/var/backups/stgyの下に、full-site-2026-02-11 のような日付の名前を持つディレクトリが作られ、そこにデータベースとオブジェクトストレージのデータがダンプされる。中身は以下のものがある。既存のディレクトリがある場合には二重には実行されないので、同日にもう一度実行したい場合、手動で該当ディレクトリを消すこと。

  • db : stgy.dumpに全データが入る。
  • objects : バケット毎にサブディレクトリが作られる。

データベースのデータはpg_dumpがgzip圧縮したものを保存しているので、空間効率はかなり高い。オブジェクトデータは、既に圧縮されている画像形式なのでそれ以上の圧縮はしない。ただし、世代間で重複するファイルはハードリンクとして保存して実態を共有することで、何世代分持っていてもデータ容量が1世代と大差ないようになっている。ソフトリンク(シンボリックリンク)と違ってハードリンクはファイルの実体(inode)のリファレンスカウントに参加しているので、ハードリンクが1つ以上あればファイルは消えない。

バックアップデータを使ってサーバ側のデータを復元するには、以下のコマンドを実行する。引数には復元対象のディレクトリ名を指定する。データベース上の現在のデータは消えることに注意すること。

./scripts/backup-full-site.sh restore full-site-2026-02-11

古い世代のバックアップデータを消すには、以下のコマンドを実行する。デフォルトでは最新7世代を保持し、それより古いものを削除する。--retain-generations 3 などとして、保持する世代数を指定できる。

./scripts/backup-full-site.sh prune

VPS上でcronでバックアップを自動実行するには、prodユーザでログインしてcrontab -eで以下の設定を書けば良い。夜中2時15分に古いデータを消し、続いて2時30分にバックアップコマンドが動く。

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

15 2 * * * cd /home/prod/stgy && /usr/bin/flock -n /tmp/stgy-backup.lock ./scripts/backup-full-site.sh prune >> /var/log/stgy/backup-full-site.log 2>&1
30 2 * * * cd /home/prod/stgy && /usr/bin/flock -n /tmp/stgy-backup.lock ./scripts/backup-full-site.sh backup >> /var/log/stgy/backup-full-site.log 2>&1

同一サーバにバックアップデータを置いているとハードウェア障害の際に困るので、rsync等で最新バックアップを別ホストやリモートストレージに退避するのも良い考えだ。退避先にて、月1回、年1回といった区切りのデータは、長期保存の対象にすると良いだろう。

全文検索のインデックス構築

全文検索サーバのインデックスはユーザや投稿の更新に応じて自動的に更新されるので、特にメンテナンスの必要はない。しかし、データベースをリストアした場合や、検索サーバのインデックスの設定を変えた際には、検索用のインデックス全体を再構築するべきだ。以下のコマンドを実行すれば良い。

npm run reset-search

何らかの理由で検索サーバ上のインデックスだけが壊れた場合、以下のコマンドで壊れたインデックスファイルのみを修復し、その担当範囲のユーザや投稿のみを再登録する。

npm run reset-search-restore

検索用インデックスの再構築中にも検索が行える。また、再構築の処理ではユーザや投稿が新しい順に登録され、再構築中に新規登録されたユーザや投稿は最優先して登録される。検索結果は常に新しい順に表示されるので、見かけ上のダウンタイムは小さい。よって、検索用インデックスのバックアップを作るよりは、全体を再構築した方が運用しやすいだろう。

エクスポートとインポート

フロントエンドのエクスポート機能を使うと、個々のユーザの単位でデータをダウンロードできる。それをインポートすると、該当のユーザが作られ、プロファイルとアバター画像と記事と画像と経路が全てアップロードされる。ユーザIDや投稿IDも保たれる。画像や経路は、記事から参照されているもののみを再アップロードして、オブジェクトIDを振り直す。VPS上でエクスポートしたzipファイルを展開してインポートする場合は、公開URLではなくBackendがホストのloopbackに公開しているポートへ直接接続する。こうするとCaddyを経由しないため、インポート処理はCaddyのレートリミットの対象にならない。以下のように --backend-api-base を指定する。

./scripts/import-archive.py --backend-api-base http://127.0.0.1:3100 --data-dir stgy-export-0000000000020101-20260722140653 --admin-email admin@stgy.jp --admin-password stgystgy

既存のユーザの所有リソースとして記事データとそれに付随する画像と経路をインポートしたい場合、--ownerオプションを使う。

./scripts/import-archive.py --backend-api-base http://127.0.0.1:3100 --data-dir stgy-export-0000000000020101-20260722140653 --admin-email admin@stgy.jp --admin-password stgystgy --owner 1234567890ABCDEF

VPS外から公開URL経由で実行する場合は、従来どおり --stgy-base https://stgy.jp/ を指定できる。この場合はCaddyを経由する。返信の記事を除外したい場合、--no-replyオプションをつける。インポートした記事を全て公開したい場合、--publishオプションをつける。

ローカルにある多数の画像を埋め込んだ記事を書きたい場合、ローカルの画像ファイルのパスを埋め込んだ任意のテキストを用意して、アップロードと同時にアップロード後のURLに置換する処理をするスクリプトが便利だ。まず、以下のようなファイルを用意する。

ネズミ:{{image:mouse.jpg}}
ウシ:{{image:cow.jpg}}
トラ:{{image:tiger.jpg}}

以下のようなコマンドを実行する。入力と出力のテキストファイル名と、パスの起点となるディレクトリと、出力ファイルを指定する。

./scripts/upload-embedded-images.py --stgy-base http://localhost:8080/ --data-dir ~/images --input ~/zodiac-raw.txt --output ~/zodiac-resolved.txt --user-email admin@stgy.jp --user-password stgystgy

出力には以下のように書かれる。これを適当に編集して記事として投稿すればよい。

ネズミ:/images/0000000000000001/masters/797392/20cdf97adef7e7dd.webp
ウシ:/images/0000000000000001/masters/797392/20cdf97adef7e7dd.webp
トラ:/images/0000000000000001/masters/797392/20cd706c651d3975.webp

フロントエンドのUIで、管理者専用の画像インポート機能もある。任意のWebサイトの内容を投稿フォームに貼り付けるとMarkdownに変換したデータが追加されるが、その際に画像の外部参照は以下のように絶対URLが指定される。

![](https://cdn-ak.f.st-example.com/images/k/kingtiger/20260712/20260712133258.jpg)

STGY上では絶対URLの画像参照を認めていないので、このままだと画像が表示できない。そこで、外部の画像をダウンロードして画像ストレージにインポートする機能がある。該当行にカーソルを合わせると、メニューバーにダウンロードボタンが現れる。それを押すと、画像データを画像ストレージに保存するダイアログが現れて、保存後にはURLが画像ストレージの参照に変わる。この機能は外部サイトの任意のURLを叩くという潜在的なリスクがあるので、管理者専用となっている。また、SSRFを防ぐために、STGY上サーバと同一ドメインや同一IPアドレスのサーバには通信できなくなっている。

古い日時の記事を投稿するのはUI上からは基本的にはできないようなっているが、管理者専用の裏技がある。新規投稿の際に、記事入力フォームの末尾に #[date=ISO8601時刻表記] の形式の行を置くと、その日時に対応したIDと投稿時刻の記事を投稿できる。

# Hello World
ごにょごにょ
#[date=2026-08-01T22:33:44+09:00]

まとめ

STGYは、リバースプロクシにCaddy、データベースにPostgreSQL、オブジェクトストレージにMinIO、キャッシュにRedisを用い、Node.jsのバックエンドとフロントエンドがそれらを操るアーキテクチャである。メール送信には、開発環境ではMailpit、VPSではPostfixを用いる。開発時にはMac上でDockerを用いて依存サービスを稼働させた上で、バックエンドとフロントエンドをローカルホストで動かす。

デモ運用は、VPSのUbuntu Linuxサーバ1台で行う。Docker運用とネイティブサービス運用の双方が可能である。短期間のデモであればDocker運用が楽だ。長期的に運用し、かつ他のサービスを同一ホストに同居させるなら、ネイティブサービス運用をした方が良い。ネイティブサービス運用の場合、systemdを使って多くのサービスを管理する。また、バックエンドとフロントエンドのデプロイ用のスクリプトも用意してあるので、Dockerほどではないが、それほど手間を書けずに導入できるようになっている。

Next: STGYのデータベース