Dialupアプリケーションの本番デプロイ方法。
リリースビルド
1. releases設定
mix.exs に追加:
def project do
[
app: :my_app,
version: "0.1.0",
elixir: "~> 1.19",
start_permanent: Mix.env() == :prod,
deps: deps(),
releases: [
my_app: [
include_executables_for: [:unix],
steps: [:assemble, :tar]
]
]
]
end2. ビルド
# 依存取得とコンパイル
MIX_ENV=prod mix deps.get
MIX_ENV=prod mix compile
# リリース作成
MIX_ENV=prod mix release
tar ステップを含めている場合、_build/prod/rel/my_app/my_app-0.1.0.tar.gz が生成される。
3. サーバーでの展開
# サーバーに転送
scp _build/prod/rel/my_app/my_app-0.1.0.tar.gz user@server:/opt/
# サーバーで展開
ssh user@server
cd /opt
mkdir -p my_app
tar -xzf my_app-0.1.0.tar.gz -C my_app
4. 起動
/opt/my_app/bin/my_app start
# またはデーモン化
/opt/my_app/bin/my_app daemon
# 停止
/opt/my_app/bin/my_app stop
環境変数
runtime.exs で環境変数を読み込む:
# config/runtime.exs
import Config
config :my_app, :port, String.to_integer(System.get_env("PORT", "4000"))my_app.ex で使用:
port = Application.get_env(:my_app, :port, 4000)
{Dialup, app: __MODULE__, port: port}systemd設定(Linux)
/etc/systemd/system/my_app.service:
[Unit]
Description=MyApp Dialup Server
After=network.target
[Service]
Type=simple
User=app
WorkingDirectory=/opt/my_app
ExecStart=/opt/my_app/bin/my_app start
ExecStop=/opt/my_app/bin/my_app stop
Restart=on-failure
[Install]
WantedBy=multi-user.target有効化と起動:
sudo systemctl enable my_app
sudo systemctl start my_app
sudo systemctl status my_app
Docker
Dialup 公式サイト(site/)はリポジトリルートの
Dockerfile
で mix release ベースのマルチステージイメージをビルドする。
docker build -t dialup-site .
docker run -p 4001:4001 -e PORT=4001 dialup-site
ローカル検証は docker compose up -d --build でも可能(docker-compose.yml)。
ランタイムイメージには OTP release のみが含まれ、bin/dialup_site start で起動する。
Dockerfile(mix release・マルチステージ)
# build ステージ: モノレポ全体をコピーし site/ で mix release
FROM elixir:1.19-alpine AS build
# ... framework + site ソース ...
RUN mix release
# runtime ステージ: release のみ(build ツール・ソースなし)
FROM alpine:3.23 AS runtime
RUN apk add --no-cache libstdc++ openssl ncurses-libs
COPY --from=build /build/site/_build/prod/rel/dialup_site ./
CMD ["bin/dialup_site", "start"]site/mix.exs の path 依存 {:dialup, path: ".."} はビルドステージでモノレポレイアウトを維持することで release に同梱される。
旧: 開発向け単一ステージ(非推奨)
FROM hexpm/elixir:1.19-erlang-26-alpine-3.18
WORKDIR /app
# ビルド依存
RUN apk add --no-cache build-base git
# 依存インストール
COPY mix.exs mix.lock ./
RUN mix deps.get
# ソースコピーとビルド
COPY . .
RUN MIX_ENV=prod mix compile
EXPOSE 4000
CMD ["mix", "run", "--no-halt"]ビルドと実行:
docker build -t my_app .
docker run -p 4000:4000 my_app
Fly.io(推奨)
1. インストール
brew install flyctl
fly auth login
2. アプリ作成
cd my_app
fly launch
3. デプロイ
fly deploy
fly.toml 例:
app = "my-app"
[http_service]
internal_port = 4000
force_https = true
[env]
MIX_ENV = "prod"HTTPS(リバースプロキシ)
Dialupは直接TLSを終端しないため、Nginx等のリバースプロキシでHTTPSを処理する。WebSocketのUpgradeヘッダー転送が必要。
Nginx設定例
upstream dialup {
server 127.0.0.1:4000;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://dialup;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s;
}
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}proxy_read_timeout を長めに設定しないと、Nginx がアイドル状態のWebSocket接続を切断する。
Caddy設定例(自動HTTPS)
example.com {
reverse_proxy localhost:4000
}CaddyはWebSocketの転送とLet's Encrypt証明書の自動取得をデフォルトでサポートする。
WebSocket オリジン検証
本番環境では、悪意あるサイトからの cross-origin WebSocket 接続を防ぐために check_origin を明示的に設定することを推奨する。
use Dialup,
app_dir: __DIR__ <> "/app",
check_origin: ["https://example.com", "https://www.example.com"]| 設定値 | 動作 |
|---|---|
:conn(デフォルト) | リクエストの host と Origin ヘッダのホストを比較。開発環境では自動スキップ |
["https://..."] | 許可するオリジンを明示的にリストで指定(本番推奨) |
false | チェックを無効化(非推奨) |
リバースプロキシ使用時の注意:
:connモードではconn.hostはリバースプロキシが転送するHostヘッダの値を使用する。Nginx/Caddy の設定でproxy_set_header Host $hostが正しく設定されていることを確認すること。本番では明示的なリストを指定する方が確実。
静的ファイル配信
priv/static/ に配置したファイルは / パスから配信される。
priv/static/
├── images/logo.png → /images/logo.png
├── favicon.ico → /favicon.ico
└── robots.txt → /robots.txt本番リリース時は priv/static/ も含めてビルドされる。
注意点
- PORT環境変数: 多くのPaaSではPORTが動的に割り当てられる
- WebSocket: ロードバランサーがWebSocketをサポートしているか確認(
Upgradeヘッダーの転送が必要) - ファイルアップロード:
/tmpなど一時ディレクトリの書き込み権限が必要 - セッション永続化:
session_store: :etsはノード再起動で消える。永続化が必要な場合は外部ストアの検討を
Dialup 公式サイト本番運用(dialup-framework.org)
モノレポ(site/ + ルート Dockerfile)を UGREEN NAS 上の Coolify VM でホストし、Cloudflare Tunnel で公開する。
現行構成: Coolify の GitHub 連携(Deploy Key / GitHub App)は環境上うまく動かなかったため、GitHub Actions → GHCR → Coolify(イメージ pull) でデプロイする。VM 上での Elixir コンパイルは行わない。
アーキテクチャ
GitHub (master push)
│
└─► GitHub Actions ──► ghcr.io/<owner>/dialup:latest
│
└─► Coolify (UGREEN NAS VM) ──► dialup_site :4001
▲
cloudflared (docker-compose.tunnel.yml)
│
Cloudflare Tunnel
│
https://dialup-framework.orgGitHub Actions が master push のたびに GHCR へイメージを publish する。Coolify は GHCR から pull してコンテナを起動する。Coolify 側に GitHub webhook はないため、GHCR への push 後は Coolify 管理 UI から手動で再デプロイする(下記「再デプロイ」参照)。
Phase 1: Coolify VM セットアップ(Issue #4)
UGREEN NAS で VM を作成し、Coolify をインストールする。NAS 本体への直載せではなく VM 上に構築する。
| 項目 | 推奨値 |
|---|---|
| RAM | 3〜4 GB |
| ディスク | 20 GB 以上 |
| OS | Ubuntu 22.04 / 24.04 LTS |
| ネットワーク | ブリッジ(同一 LAN から管理 UI に到達可能) |
VM に SSH したうえで、リポジトリ付属スクリプトを実行できる:
git clone https://github.com/SouichiroTsujimoto/Dialup.git
cd Dialup
./scripts/vm-setup-coolify.sh
手動で行う場合:
- Coolify 公式インストールに従い Docker + Coolify をセットアップ
- 同一 LAN から Coolify 管理 UI にアクセスできることを確認
- VM スナップショットを取得(ロールバック用)
Phase 2: Coolify + GHCR で site/ をデプロイ(Issue #5 / #8)
GitHub Actions でイメージをビルドし GHCR に publish する。Coolify はリポジトリを clone して Dockerfile ビルドするのではなく、既存イメージ pull でデプロイする。
ワークフロー: .github/workflows/docker-publish.yml
masterpush(およびworkflow_dispatch)でghcr.io/<owner>/dialupにlatestと commit SHA タグで publish- イメージ例:
ghcr.io/souichirotsujimoto/dialup:latest
Coolify アプリ設定
| 項目 | 値 |
|---|---|
| デプロイ方式 | Docker Image(既存イメージ pull) |
| Image | ghcr.io/souichirotsujimoto/dialup:latest |
| Port | 4001 |
| 環境変数 | PORT=4001, MIX_ENV=prod |
| GHCR 認証 | GitHub PAT(read:packages)または Deploy Token |
GitHub 連携(Deploy Key / GitHub App)と Auto Deploy は使わない。Coolify からリポジトリへ直接アクセスする必要がない。
初回セットアップ手順
masterに merge し、Actions の Docker Publish が成功し GHCR にイメージがあることを確認- Coolify に上記のとおり Docker Image アプリを登録し、GHCR 認証を設定
- 手動デプロイで pull・起動を確認
- VM 内から
http://127.0.0.1:4001で agent demo / docs ページを表示確認
再デプロイ
master への merge だけでは Coolify 上のコンテナは自動更新されない。
masterに merge(または workflow_dispatch で手動実行)- GitHub Actions の Docker Publish が完了するまで待つ
- Coolify 管理 UI で対象アプリの Redeploy(または Pull latest image & restart)を実行
特定コミットを pin したい場合は、Coolify の Image タグを SHA タグ(例: ghcr.io/souichirotsujimoto/dialup:abc1234)に変更してからデプロイする。
代替: Coolify GitHub 連携 + Dockerfile ビルド(未採用)
Coolify の GitHub 連携を有効にし、リポジトリから Dockerfile をビルドする方式も想定していたが、本番 VM では連携が確立できなかった。NAS RAM 7.5 GB・VM 3〜4 GB 割当では VM 上での Elixir コンパイルは OOM や極端な遅延のリスクもあり、GHCR 経由を採用している。
Phase 3: Cloudflare Tunnel 切替(Issue #6)
Coolify 上のアプリが VM 内で安定稼働していることを確認してから、トンネルの向き先を切り替える。
リポジトリにはアプリとトンネルを分離済み:
- アプリ:
docker-compose.yml(ローカル検証用) - トンネル:
docker-compose.tunnel.yml(network_mode: host)
推奨: VM 上で docker-compose.tunnel.yml を別管理(Coolify アプリネットワークと疎結合)。
- Cloudflare Zero Trust でトンネルのパブリックホスト名を
http://127.0.0.1:4001に設定 - VM 上でトンネル起動:
export CLOUDFLARE_TUNNEL_TOKEN="<token>"
./scripts/start-cloudflare-tunnel.sh
- 本番確認:
https://dialup-framework.orgの表示- WebSocket
/ws - agent MCP
POST /agent/:token
- 旧 NAS compose の app コンテナを停止してもサイトが稼働することを確認
ロールバック
- 旧 compose は 1 週間は停止のみ(削除しない)で保持
- Cloudflare トンネル設定のスクリーンショット / メモを残す
Phase 4: 旧構成撤去(Issue #7)
トンネル切替完了後:
- NAS 上の旧 2 リポジトリ clone / 旧 compose プロセスを停止
- 1 週間経過後: 旧 compose ファイル・
.envを削除 - ローカルに旧
dialup_site/ディレクトリが残っていれば削除(任意)
ローカル再現手順:
git clone https://github.com/SouichiroTsujimoto/Dialup.git
cd Dialup
docker compose up -d --build
# トンネルは docker-compose.tunnel.yml で別管理
チェックリスト(Issue 対応表)
| Issue | 完了条件 |
|---|---|
| #4 | Coolify ダッシュボードが VM 上で安定、LAN から管理 UI に到達可能 |
| #5 | GHCR イメージで Coolify デプロイ、VM 上でサイト表示 |
| #6 | dialup-framework.org が Coolify 先を指す、旧 app 停止後も稼働 |
| #7 | 旧デプロイプロセスなし、本ドキュメントだけで再デプロイ可能 |
| #8 | VM でコンパイルなし(Actions → GHCR → Coolify pull)、再デプロイ手順が明文化されている |
| #9 | ランタイムイメージに build ツール・ソースなし、bin/dialup_site start で起動 |