Web サービス (SaaS アプリ)
ToukaCloud のコンテナホスティング — イメージをデプロイして Web アプリを公開する方法
1. 概要
Web サービス (SaaS アプリ) は、ToukaCloud に push したコンテナイメージを、 Kubernetes 上で動かして Web アプリを公開するホスティングサービスです。以下の機能を提供します。
- イメージ指定: レジストリから任意のイメージを選択 (例:
registry.max3584.net/...) - 自動ドメイン割り当て:
{appName}.{tenantId}.tenant.max3584.netを自動生成 - HTTPS 対応: Let's Encrypt による SSL 証明書の自動発行・更新
- スケーリング: レプリカ数の動的調整で複数インスタンスを実行
- 環境変数: アプリに必要な設定値やシークレットを注入
- 永続ストレージ: PersistentVolumeClaim による永続データ保存
2. 前提条件
- 有効なライセンス (S 以上のいずれか) があること — ライセンスページで確認
- コンテナイメージを ToukaCloud レジストリ (registry.max3584.net) に push 済みであること
- アプリが環境変数で PORT を受け取るか、フォーム入力のポート番号でリッスンしていること
3. アプリを作成
新しい SaaS アプリを作成するには、/saas/create ページでフォームを記入して送信します。以下が主要なフィールドです。
3.1 アプリ名
- 使える文字: 小文字英数字 (
a-z 0-9) とハイフン (-) - 英小文字で始まり、末尾は英数字で終わる (2 〜 63 文字)
- ハイフンを連続させない (
my--app不可) - 例:
my-app,web-server - 一度作成すると変更不可(削除して再作成してください)
- ドメイン名として使用:
my-app.{tenantId}.tenant.max3584.net
3.2 Docker イメージ
フルパスでイメージを指定します。レジストリ画面から直接コピーできます。
registry.max3584.net/<tenantId>/<userId>/<projectName>/myapp:latest
または外部イメージ (例: nginx:latest, node:20-alpine) も指定可能ですが、内部レジストリ経由のことが多いです。
3.3 ポート番号
- アプリが内部でリッスンするポート (例:
3000,8080) - 有効範囲: 1 〜 65535
- 複数ポートの場合は、最初のメインポートを指定(複数ポート機能は将来対応予定)
3.4 レプリカ数
並行して実行するアプリインスタンス数です。プラン別の上限は以下のとおりで、 単一の SoT (components/document/PlanLimitsTable.tsx) を ライセンスガイド と共有しています。
| プラン | 最大アプリ数 | 1 アプリあたりのレプリカ上限 |
|---|---|---|
| SAAS_FREE | 1 | 1 |
| SAAS_BASIC | 3 | 1 |
| SAAS_PRO | 10 | 1 |
| SAAS_ENTERPRISE | ∞ | 1 |
| S | 1 | 1 |
| M | 3 | 2 |
| L | 5 | 3 |
| XL | 10 | 5 |
| XXL | 20 | 8 |
| MAX | ∞ | 10 |
レプリカ数を 0 にするとアプリは停止状態になります。 後で変更・再開できます。
3.5 環境変数 (オプション)
アプリに必要な環境変数をカスタマイズで設定できます。
NODE_ENV = production API_KEY = sk-xxxxxxxx DEBUG = false
データベース接続情報を手動で入力するか、データベース連携機能を使用します。
3.6 永続ストレージ (オプション)
アプリがファイルを保存する場合、永続ストレージを有効化できます。 容量はプランに含まれており (S 1GB / M 2GB / L 5GB / XL 10GB / XXL 20GB / MAX 50GB)、超過する場合はアプリストレージアドオンで 追加できます。
- サイズ: 1 〜 10 GB(ライセンス容量内)
- マウントパス: コンテナ内のパス (例:
/data,/var/lib/app) - データはアプリ再起動後も保持されます
3.7 プライベートレジストリの認証 (任意)
ToukaCloud のレジストリ (registry.max3584.net/…) を使うときは アクセストークンの選択ボックスが出ます。一方、Docker Hub の private repo、 ghcr.io の private、独自レジストリなど 外部のプライベートレジストリ を使う場合は、フォームの「プライベートレジストリの認証を使う」チェックを 入れて Username と Password / Token を入力します。
- 入力された認証情報は
kubernetes.io/dockerconfigjsonSecret として作成され、Pod のimagePullSecretsに自動付与されます - anonymous で pull できる public image ではチェック不要
- 既存アプリの認証情報は設定ページで差し替え可能。password 欄を空のまま保存すれば 現在の認証情報をそのまま使い続けます (「認証情報を削除」ボタンで完全クリアも可能)
3.8 作成完了
「アプリを作成」をクリックすると、Kubernetes にリソースがデプロイされます。 数秒〜数分で以下が進行します。
- Namespace とネットワークポリシーの作成
- Deployment 作成 — イメージ pull と Pod 起動
- Service 作成 — クラスタ内通信用
- IngressRoute 作成 — Traefik による HTTP/HTTPS ルーティング
- DNS レコード作成 — 公開ドメイン設定
- SSL 証明書リクエスト — Let's Encrypt から自動発行
4. 公開ドメイン
各アプリには自動的に公開ドメインが割り当てられます。アクセス方法は以下の通りです。
4.1 ドメイン形式
https://{appName}.{tenantId}.tenant.max3584.net
例:
https://my-app.a1b2c3d4.tenant.max3584.net
https://web-server.f9e8d7c6.tenant.max3584.netappName: 作成時に指定したアプリ名tenantId: あなたのテナント ID (自動生成、8 桁の hex)- ベースドメイン:
tenant.max3584.net
4.2 HTTPS による自動保護
すべてのアプリは自動的に HTTPS で保護されます。Let's Encrypt により無料で SSL 証明書が発行されます。
- HTTP からの自動リダイレクト —
http://...にアクセスしても自動的にhttps://...に転送 - 証明書の自動更新 — 有効期限の 30 日前に自動更新
- SAN 対応 — 複数ドメイン対応 (将来のカスタムドメイン対応に備え)
4.3 Let's Encrypt 証明書のタイミング
初回デプロイ時、Traefik から Let's Encrypt への ACME チャレンジが発生します。
- DNS 設定後、約 5 〜 10 分で ACME チャレンジが完了 しブラウザで安全に表示されます
- その間、自己署証明書または警告ページが表示される可能性があります(正常です)
- 完了後は「保護された接続」として表示され、セキュリティ警告は出ません
5. 環境変数 / シークレット
アプリに設定値やシークレットを注入する方法は複数あります。
5.1 作成時に設定
/saas/create ページで「カスタム環境変数」セクションから、KEY=value の形式で入力できます。
NODE_ENV = production DATABASE_URL = postgresql://user:pass@db.internal:5432/mydb API_SECRET = sk-abc123xyz LOG_LEVEL = info
5.2 データベース連携
ToukaCloud のデータベースサービスを連携すると、接続情報がアプリの 環境変数として自動注入されます。クレデンシャルは作成・編集の UI には一切表示されず、デプロイ直前にサーバ側で取得され Pod 環境変数に流し込まれます。 ローテーション時もアプリの再デプロイで自動的に最新値が反映されます。
5.2.1 ロール (権限分離)
1 つのデータベースに対して用途別の MariaDB ユーザーを発行できます。 アプリには必要最小限のロールだけ渡すのが推奨です。
| ロール | 権限 | 用途 |
|---|---|---|
owner | DDL + DML(フル権限) | マイグレーション、Adminer などスキーマ操作 |
app | DML のみ(SELECT/INSERT/UPDATE/DELETE) | 通常のアプリ接続。SQL インジェクション対策として推奨 |
readonly | SELECT のみ | ダッシュボード、レプリカ的な参照系 |
複数ロールを同時に渡すと env 名にロール名がプレフィックスされます (例: APP_DATABASE_URL と READONLY_DATABASE_URL)。 1 つだけ選んだ場合はプレフィックスなし(DATABASE_URL)。
5.2.2 接続フォーマット
アプリが期待する設定の形に合わせて 3 つから選びます。
| フォーマット | 注入される env | 向いてるアプリ |
|---|---|---|
separate | HOST + PORT + DATABASE + USER + PASSWORD(5 個) | Ghost、Rails、Django など個別 env を読むタイプ |
combined | HOST(host:port) + DATABASE + USER + PASSWORD(4 個) | HOST に host:port を期待する古めの Java / PHP 系 |
url | DSN URL 1 本(mysql://user:pass@host:port/db) | Node.js (sequelize, prisma) など DATABASE_URL 1 本で済むやつ。creds が URL に埋め込まれるためロール 1 つ限定 |
5.2.3 env 名プリセット
env 名はプリセットから選ぶか手で書きます。 切替時に既存の env 名が一括で書き換わるので、ベースとして近いものを 選んで足りない所だけ手で直すのが速いです。
| プリセット | env 名の例(app ロール / separate) |
|---|---|
標準 DATABASE_* | DATABASE_HOST / DATABASE_PORT / DATABASE_NAME / DATABASE_USER / DATABASE_PASSWORD |
MySQL MYSQL_* | MYSQL_HOST / MYSQL_PORT / MYSQL_DATABASE / MYSQL_USER / MYSQL_PASSWORD |
短縮 DB_* | DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD |
| カスタム | 任意の名前を手で指定(Ghost の database__connection__host のような特殊形式に使用) |
5.2.4 デプロイ後の変更
アプリ詳細ページの「連携 DB」セクションで、リンク先データベース・ ロール構成・フォーマット・env 名をいつでも変更できます。保存すると 再デプロイされ、新しい接続情報が反映されます。連携を解除したい 場合は DB ドロップダウンで「連携しない」を選びます。
5.3 予約プレースホルダ ${...}
環境変数の 値 側に ${TOKEN} 形式で 書くと、デプロイ直前にアプリ固有の値へ自動置換されます。アプリのホスト名や URL を手で組み立てる必要がなくなり、たとえば Ghost の url や OAuth コールバックの設定が一行で済みます。
| トークン | 解決値(例: appName=blog, tenantId=abc12345, port=2368) |
|---|---|
${APP_NAME} | blog |
${TENANT_ID} | abc12345 |
${APP_HOSTNAME} | blog.abc12345.tenant.max3584.net |
${APP_URL} | https://blog.abc12345.tenant.max3584.net/(末尾 / あり) |
${NAMESPACE} | saas-abc12345 |
${APP_PORT} | 2368 |
使用例(Ghost のデプロイ):
url = ${APP_URL}
NODE_ENV = production
database__client = mysql- 置換対象: 環境変数の値 / イメージ名 / タグ / 永続ストレージのマウントパス
${...}内は UPPER_SNAKE_CASE のみ 認識。 bare$VARや${lowercase}はそのまま渡るので、 bcrypt ハッシュ ($2a$10$...) などは安全。- 未知のトークン(タイポ含む)はエラーにならず、リテラルとして残ります。
5.4 Kubernetes 内での参照
環境変数は Deployment の spec.template.spec.containers[].env に設定され、アプリプロセスから標準的に参照できます。
# Node.js の例
const dbUrl = process.env.DATABASE_URL;
const apiKey = process.env.API_SECRET;
# Python の例
import os
db_url = os.getenv('DATABASE_URL')
api_key = os.getenv('API_SECRET')
# Bash の例
echo "API_SECRET=$API_SECRET"5.5 シークレット管理のベストプラクティス
- パスワード・トークンは環境変数で注入(ソースコードに埋め込まない)
- 本番環境では強力なパスワードを使用
- 定期的に credentials をローテーション
6. レプリカ / リソース設定
アプリの負荷分散やリソース制限の方法について説明します。
6.1 レプリカ数の役割
レプリカ数は、Kubernetes Deployment が維持する同一 Pod インスタンス数です。増やすことで負荷分散と可用性が向上します。
# 例: レプリカ3
Pod-1 (10.0.0.1:3000)
Pod-2 (10.0.0.2:3000)
Pod-3 (10.0.0.3:3000)
↓
Service (cluster-ip:3000)
↓ (ラウンドロビン)
Traefik → どれかに振り分け6.2 CPU / メモリの制限
各アプリのリソースは requests (保証) と limits (上限) の 2 軸で設定します。アプリ作成直後はデフォルトが 入っていて、/saas の アプリ詳細 → 設定タブのスライダーで上限内で動かせます。
- 初期値 (requests):
100m CPU/128Mi メモリ - 初期値 (limits):
500m CPU/512Mi メモリ - 絶対上限 (MAX プラン):
8000m CPU/16384Mi メモリ(8 cores / 16 GiB)
1 アプリあたりの CPU / メモリ上限はプラン別に決まり、サーバー側で保存時に 再クランプされます。スライダーは 50m 刻み (CPU) / 64Mi 刻み (Memory) で動き、requests ≤ limits を自動で維持します。
上限が足りない場合は、上位プランへのアップグレードや アドオンをご検討ください。
6.3 スケーリング戦略
- 垂直スケーリング: 設定ページのスライダーで CPU / メモリ の requests / limits を変更し、再デプロイ
- 水平スケーリング: レプリカ数を増やしてラウンドロビンで分散
- 自動スケーリング: Metrics-based HPA (計画中、CPU/メモリ使用率に応じた自動増減)
7. アプリの管理
作成したアプリを管理・制御する方法です。
7.1 アプリ一覧表示
/saas ページでは、現在のテナント配下のすべてのアプリが表示されます。 各カードに以下の情報が含まれます。
- アプリ名
- ステータスバッジ:「準備中」「作成中」「稼働中」「更新中」「エラー」「削除中」
- 公開ドメイン — クリックで新タブにアプリを開きます
- イメージ名:タグ
- ポート、レプリカ数
- 連携 DB と使用ロール(連携している場合)
- 作成日時
ドメイン以外の領域をクリックするとアプリ詳細ページに遷移します。
7.2 アプリ詳細 / 編集
カード本体をクリックすると /saas/apps/<appName> に遷移し、デプロイ済みアプリの設定を変更できます。
- 設定: イメージ / タグ / ポート / レプリカ / 環境変数 / リソース上限を編集して「更新(再デプロイ)」
- 連携 DB: リンク先データベース、ロール構成、 接続フォーマット、env 名プリセットを変更(詳細は 5.2 データベース連携 を参照)
- 接続情報: hostname / namespace / 連携 DB / 作成・更新時刻の参照
- 危険ゾーン: アプリと関連 K8s リソース(Deployment / Service / IngressRoute / PVC / DNS)の一括削除
デプロイが失敗した状態(ステータス「エラー」)の場合は赤いパネルの 「再デプロイ」ボタンで現在の設定のままもう一度試行できます。原因 (pull 失敗、Quota、TLS 等)を直してから押す形です。
7.3 起動 / 停止
レプリカ数を 0 に設定するとアプリが停止します。 またいつでも起動(レプリカ数を 1 以上に)できます。
- 停止中のアプリにアクセスするとエラー (503 Service Unavailable など)
- 停止中もドメイン・設定は保持されます
- 再起動時に環境変数・イメージは再適用されます
7.4 再起動
アプリを再起動するには、詳細ページから「再起動」を選択するか、 または一度停止 (replicas=0) して再度起動 (replicas=1+) します。
- 再起動中は数秒程度のダウンタイムが発生
- 設定変更 (環境変数、ポート、イメージなど) を反映させたい場合は詳細ページから「更新」を選択
7.5 削除
アプリが不要になった場合、詳細ページから「削除」を選択します。 以下が自動的に削除されます。
- Kubernetes Deployment, Pod, Service
- IngressRoute
- DNS レコード
- SSL 証明書
- PersistentVolumeClaim (永続ストレージが有効な場合)
8. ログとメトリクス
アプリのログとメトリクスを確認する方法です。
8.1 アプリケーションログ
詳細ページの「ログ」タブで、アプリの stdout/stderr を閲覧できます(計画中、現在は以下のコマンドで確認)。
kubectl logs -n saas-{tenantId} -l app={appName} --tail=100 -f8.2 メトリクス
詳細ページの「メトリクス」タブで、CPU/メモリ使用率や リクエスト数などを時系列グラフで表示します(計画中)。
8.3 Kubernetes からの確認
詳しいトラブルシューティングには kubectl コマンドラインを使用できます。
# Pod 一覧
kubectl get pods -n saas-{tenantId}
# Pod の詳細
kubectl describe pod {podName} -n saas-{tenantId}
# Pod のログ(stdout/stderr)
kubectl logs {podName} -n saas-{tenantId}
# 前のインスタンスログ(クラッシュした場合)
kubectl logs {podName} -n saas-{tenantId} --previous9. トラブルシューティング
イメージ pull、Pod 起動、DNS/SSL 設定に数分かかることがあります。以下を確認してください。
- 数分待ってページをリロード
- kubectl describe pod -n saas-{tenantId} でイベントを確認
- 以下の「ImagePullBackOff」や「Crashloop」を参照
ImagePullBackOff エラーイメージ pull に失敗しています。以下の原因を確認してください。
- イメージパスが正しいか — 空白やタイポなし?
- イメージが registry に存在するか — レジストリ画面で確認
- イメージを registry に push したか — docker push で再確認
- タグが指定されているか —
image:tag形式で指定 - 内部レジストリ使用時、トークンが有効か — レジストリで新しいトークンを発行
CrashLoopBackOff — アプリがすぐに終了するPod が起動しても数秒で停止を繰り返しています。アプリのエラーログを確認してください。
- kubectl logs {podName} -n saas-{tenantId} でエラーを確認
- 環境変数が不足していないか — NODE_ENV, DATABASE_URL など
- ポート番号は正しいか — フォームで指定したポートでリッスン中?
- コンテナのリソース上限に達していないか —「リソース設定」を参照
ブラウザで接続できない、タイムアウトする場合、ポート設定を確認してください。
- フォームで指定したポート(例:
3000)が、 コンテナのリッスンポートと一致しているか? - 例: Node.js app が
process.env.PORT || 3000で起動している - Dockerfile の
EXPOSE 3000は参考情報のため、 実際のリッスンポートを フォームで指定してください
初回デプロイ時や DNS 設定直後、Let's Encrypt のチャレンジが完了していない可能性があります。
- 約 10 分待機後、再度アクセスしてください
- ブラウザのキャッシュをクリアして F5 リロード
- kubectl -n traefik-system logs -f でセキュリティ情報を確認
404 Not Foundドメインは正解していますが、アプリが応答していない可能性があります。
- アプリのステータスが「稼働中」か確認 — /saas ページで確認
- kubectl get pods -n saas-{tenantId} で Pod が Running 状態か
- kubectl logs {podName} -n saas-{tenantId}でエラーログを確認
- 404 が返される場合は、アプリ側のルーティング設定を確認
503 Service Unavailableアプリが起動していないか、すべての Pod が停止している可能性があります。
- レプリカ数を確認 — 0 になっていないか?
- 再起動してみる — 詳細ページから「再起動」を選択
- Pod のイベントを確認 — kubectl describe pod ... でエラーを見る