顧客がブラウザにapp.theircustomer.comを入力して待つ。その3秒間の裏側で、インフラストラクチャはDNS検索、ワイルドカード証明書の検証、リバースプロキシのルーティング、そして彼らのブランド化されたダッシュボードの配信を調整している — 実際のサーバーIPを知られることなく。私はこのカスタムドメインパターンを異なる4つのスタックで実装してきた。そして実装のたびに、製品スペックには現れなかった落とし穴が浮上する: 10番目の顧客のオンボーディングをブロックするLet's Encryptレート制限、特定のDNSプロバイダーで破損するCNAMEフラット化の動作、ローンチウィークのサポートチケット急増につながる伝播遅延。このアーキテクチャを2スプリントのプロジェクトだと思い込んで実装した後、SSL自動化を週末のCloudflareAPIデバッグと証明書チェーン検証の地獄に変える落とし穴を発見することになる。

この記事は、私が持っていたらよかったプレイブックだ。マルチテナントアプリケーション向けのワイルドカードサブドメイン、Let's Encryptと代替手段を使った自動SSL供給、顧客が自分のドメインを持ち込むカスタムバニティドメイン、そして計画を立てないと午前2時に噛みつく運用上の懸念事項をカバーする。

目次

SaaS向けカスタムドメイン: ワイルドカードサブドメイン & 自動SSL

SaaS向けカスタムドメインが重要な理由

カスタムドメインは単なる見栄えの機能ではない。信頼のシグナルだ。顧客のクライアントがacmecorp.your-saas.ioではなくportal.acmecorp.comにアクセスすると、顧客のブランドが強化される。摩擦が減る。そして多くのエンタープライズディールにおいて、これは必須要件だ — 調達チームは従業員をサードパーティドメインに強制するソフトウェアは承認しない。

SEOの側面もある。パブリックに面したページを含むSaaS(Shopifyストア、ランディングページビルダー、ナレッジベース、クライアントポータルなど)を構築している場合、顧客はドメインオーソリティを構築するために自分たちのドメインが必要だ。誰かのプラットフォームのサブドメインではそれはできない。まあ、できるけど、最適ではない。

最も一般的な3つのパターン:

  1. プラットフォームサブドメインcustomer.your-app.com(最も簡単)
  2. カスタムサブドメインapp.customer.com(中程度の複雑さ)
  3. Apexカスタムドメインcustomer.com(最も難しい。ApexでCNAMEレコードが機能しないため)

ほとんどの成熟したSaaSプロダクトは3つすべてをサポートする。

アーキテクチャ概要: 3つのアプローチ

実装の詳細に入る前に、3つの主なアーキテクチャアプローチと、それぞれが最適な場合を見てみよう。

アプローチ 複雑さ 最適な用途 SSLメソッド DNS要件
ワイルドカードサブドメイン プラットフォーム制御のサブドメイン 単一ワイルドカード証明書 ワイルドカードDNS A/AAAAレコード
SNI搭載リバースプロキシ カスタムドメイン(中程度スケール) ACMEによるドメインごとの証明書 顧客CNAME またはAレコード
CDN/Edgeによるカスタムドメイン 低-中 高スケール、グローバル配信 CDNプロバイダーが管理 顧客CNAME またはAレコード
テナントごとの専用ロードバランサー エンタープライズ隔離要件 テナントごとの証明書 顧客DNS委任

ほとんどのSaaSアプリケーションではワイルドカードサブドメインから始まり、やがてリバースプロキシベースのカスタムドメイン対応を追加する。それぞれ掘り下げてみよう。

マルチテナンシー向けワイルドカードサブドメイン

これが出発点だ。すべてのテナントは{slug}.yourapp.comを取得する。適切に設定する方法は以下の通り。

DNS構成

ワイルドカードDNSレコードが必要:

*.yourapp.com.  300  IN  A      203.0.113.10
*.yourapp.com.  300  IN  AAAA   2001:db8::1

これはyourapp.comあらゆるサブドメインがサーバーに解決されることを意味する。アプリケーションはHostヘッダーを読み取ることで、どのテナントを配信するかを判定する。

アプリケーションレベルのルーティング

Next.jsアプリでは(Social Animalで多く構築)、ミドルウェアでこれを処理する:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const hostname = request.headers.get('host') || '';
  const subdomain = hostname.split('.')[0];
  
  // Skip for main domain and known subdomains
  if (['www', 'app', 'api'].includes(subdomain)) {
    return NextResponse.next();
  }
  
  // Rewrite to tenant-specific path
  const url = request.nextUrl.clone();
  url.pathname = `/tenant/${subdomain}${url.pathname}`;
  return NextResponse.rewrite(url);
}

export const config = {
  matcher: ['/((?!_next|api|static).*)']
};

Astroベースのサイト(別のフレームワークで我々も多く使用)の場合、サーバーミドルウェアまたはエッジでこれを処理する。

ワイルドカードSSL証明書

単一のワイルドカード証明書がすべてのサブドメインをカバー:

certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d 'yourapp.com' \
  -d '*.yourapp.com'

注: Let's EncryptのワイルドカードSSL証明書にはDNS-01チャレンジ検証が必要。HTTP-01はワイルドカードに使用できない。これはDNSプロバイダーへのAPIアクセスが必要ということ。CertbotはCloudflare、Route53、Google Cloud DNSなど、ほとんどの主要プロバイダーのプラグインを備えている。

Let's EncryptのワイルドカードSSL証明書は90日間有効なので、更新を自動化しよう。本気で。更新に失敗した場合、監視アラートを設定してほしい — 500の顧客サイトを落とす人になるなよ、証明書の有効期限切れのせいで。

SaaS向けカスタムドメイン: ワイルドカードサブドメイン & 自動SSL - アーキテクチャ

Let's Encryptを使った自動SSL

顧客が自分たちのドメインを持ち込む場合、ドメインごとの証明書が必要だ。ここが興味深くなるところだ。

ACMEプロトコル

Let's EncryptはACME (Automatic Certificate Management Environment)プロトコルを使用している。気になるチャレンジは2つ:

  • HTTP-01: ドメイン所有権を証明するために、特定のファイルをhttp://yourdomain.com/.well-known/acme-challenge/{token}に配信することで証明。個別ドメインに最も簡単に自動化でき、使用可能。
  • DNS-01: TXTレコードを作成することで所有権を証明。ワイルドカードに必須、顧客ドメインの自動化が難しい(DNSを制御していない)。

カスタムドメインの場合、ほぼ常にHTTP-01を使用する。フローは以下のようになる:

  1. 顧客がCNAMEを追加してドメインをプラットフォームにポイント
  2. システムはDNSが正しく解決していることを検知
  3. ACME証明書リクエストを開始
  4. Let's EncryptがHTTP-01チャレンジを送信
  5. サーバーが正しいチャレンジトークンで応答
  6. 証明書が発行されて保存
  7. リバースプロキシがそのドメイン向けにHTTPSの配信を開始

Caddy: 怠け者の(賢い)選択肢

正直なところ、すでにnginxに専念していないなら、Caddyを使おう。ボックスから自動HTTPSを処理してくれる。不明なドメインのオンデマンドTLSも含めて:

{
  on_demand_tls {
    ask http://localhost:5555/check-domain
    interval 2m
    burst 5
  }
}

https:// {
  tls {
    on_demand
  }
  reverse_proxy localhost:3000
}

askエンドポイントは重要 — Caddyが証明書をリクエストする前に、ドメインが実際に有効な顧客ドメインかどうかをアプリケーションに確認する場所。これがないと、誰でも自分のドメインをIPにポイントして証明書リクエストをトリガーできて、Let's Encryptのレート制限をゴリゴリに燃やす可能性がある。

// /check-domain endpoint
app.get('/check-domain', async (req, res) => {
  const domain = req.query.domain;
  const isValid = await db.customDomains.findOne({ 
    domain, 
    verified: true,
    active: true 
  });
  
  if (isValid) {
    res.status(200).send('OK');
  } else {
    res.status(404).send('Not found');
  }
});

Nginx + Certbotアプローチ

すでにnginxを運用している場合、webroot プラグインでcertbotを使用できる:

certbot certonly --webroot -w /var/www/certbot \
  -d customer-domain.com \
  --non-interactive --agree-tos \
  --email ssl@yourapp.com

nginx構成を動的に更新してリロードする必要がある:

server {
    listen 443 ssl;
    server_name customer-domain.com;
    
    ssl_certificate /etc/letsencrypt/live/customer-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/customer-domain.com/privkey.pem;
    
    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

これは動くが、スケール時に管理が苦痛。新規ドメインのたびに構成を生成して、証明書をリクエストして、nginxをリロード。この自動化を構築したくなる。正直、CaddyのオンデマンドTLSはもっと簡単だ。

Let's Encryptレート制限(2025)

これらは重要で、周囲の計画が必要:

制限 説明
登録ドメインごとの証明書 50/週 ルートドメインごと
重複証明書 5/週 ホスト名の同じセット
検証失敗 5/時間(アカウント、ホスト名ごと) 素早くブロック可能
新規オーダー 300/3時間 アカウント全体
保留中の認可 300/アカウント 古いものをクリーンアップ

1週間で50証明書なら、1日当たり7以上のカスタムドメインをオンボーディングしている場合、このことについて考える必要がある。オプション:

  • 別のACME CA (ZeroSSL、BuyPass) をフォールバックとして使用
  • Let's Encryptレート制限の引き上げを申請(正当なSaaS用途では付与される)
  • 証明書発行を試みる前にDNSを事前検証
  • 指数バックオフを使った再試行ロジックを実装

カスタムバニティドメイン: 完全なフロー

複数回この流れを構築してきたので、私が推奨する完全なユーザージャーニーはこれだ。

ステップ1: 顧客がダッシュボードにドメインを入力

明確な指示を提供。彼らが作成する必要があるDNSレコードを正確に表示:

Type: CNAME
Host: portal (またはどのサブドメインでも)
Value: custom.yourapp.com
TTL: 300

Apexドメイン(裸のcustomer.com)の場合、IPをポイントするAレコード、またはCNAMEフラット化をサポートするDNSプロバイダー(Cloudflare、Route53 ALIASレコード等)が必要。

ステップ2: DNS検証

1回だけ確認しない。DNS伝播に数分から数時間かかる可能性がある。ポーリング機構を実装:

async function verifyDomain(domain: string, expectedTarget: string): Promise<boolean> {
  try {
    const records = await dns.promises.resolveCname(domain);
    return records.some(r => r === expectedTarget);
  } catch (err) {
    // For A records (apex domains)
    try {
      const aRecords = await dns.promises.resolve4(domain);
      return aRecords.some(r => EXPECTED_IPS.includes(r));
    } catch {
      return false;
    }
  }
}

// Poll every 30 seconds for up to 24 hours
async function waitForDNS(domain: string) {
  const maxAttempts = 2880; // 24 hours at 30s intervals
  for (let i = 0; i < maxAttempts; i++) {
    if (await verifyDomain(domain, 'custom.yourapp.com')) {
      return true;
    }
    await sleep(30000);
  }
  return false;
}

ステップ3: 証明書の供給

DNSが検証されたら、証明書をリクエスト。Caddyで、これは最初のリクエストで自動的に起こる。Nginx/Certbotで、プログラムによるトリガー。

ステップ4: 継続的なモニタリング

ドメインは壊れることがある。顧客が誤ってDNSレコードを変更。更新に失敗した場合は証明書の有効期限切れ。以下をモニタリング:

  • DNS解決がまだ利用者をポイント(毎日確認)
  • 証明書の有効期限(14日でアラート、7日で重大)
  • カスタムドメインでのSSLハンドシェイク成功(合成モニタリング)
  • HTTPレスポンスコード

プラットフォーム別のインフラストラクチャパターン

ホスティング場所によってアプローチは大きく異なる。

Vercel

Vercelは組み込みのカスタムドメイン対応を備えている。マルチテナントNext.jsアプリでは、Domains APIを使用:

curl -X POST "https://api.vercel.com/v10/projects/{projectId}/domains" \
  -H "Authorization: Bearer $VERCEL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "portal.customer.com"}'

Vercelは自動的にSSLを処理。主な制限: 彼らの価格設定に従う。Proプラン($20/チームメンバー/月)で、プロジェクトあたり50ドメイン。エンタープライズではもっと増える。カスタムドメイン付きで数千の顧客がいる場合、コストが積み上がる。

Cloudflare for SaaS (SSL for SaaS)

これはおそらく2025年でのスケール時の最適なオプション。Cloudflareの「SSL for SaaS」製品(以前は「Custom Hostnames」)はすべてを処理:

  • 自動証明書発行と更新
  • グローバルCDNとDDoS保護
  • 組み込みドメイン検証
  • プログラマティック管理向けAPI
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/custom_hostnames" \
  -H "Authorization: Bearer $CF_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hostname": "portal.customer.com",
    "ssl": {
      "method": "http",
      "type": "dv"
    }
  }'

価格: 最初の100に後続する$0.10/カスタムホスト名/月(エンタープライズプランでは最初の100は無料)。1,000のカスタムドメイン付きSaaS向けに、月約$90。非常にリーズナブル。

AWS (ALB + ACM + Route53)

AWSを使っている場合:

  • ALB ルーティング(SNIベースの証明書選択)
  • ACM (AWS Certificate Manager) 無料証明書用
  • Route53 DNS検証用

落とし穴: ACM証明書はAWSサービス(ALB、CloudFront、API Gateway)でのみ使用可能。ALBはロードバランサーあたり最大25証明書の制限(100に増やすことが可能)。本当のスケールでは、CloudFrontを前面に置いて配布あたり100証明書の制限。

これは高くて複雑になる。正直、特定のAWS要件がない限り、このアプローチよりCloudflare for SaaSをお勧めする。

Fly.io

Fly.ioはカスタムドメイン追加向けの素敵なfly certs addコマンドとAPIを備えている:

fly certs add portal.customer.com

Let's Encryptを自動的に処理。小〜中スケールで動く。

DNS構成と検証

DNS部分はこの機能の他の部分よりもチームをトリップさせる。ここで知る必要があることだ。

ApexでのCNAME問題

顧客は必然的にベアドメイン(customer.com, サブドメインなし)を使いたい。DNS仕様ではCNAMEレコードをゾーンの頂点で許さない。CNAMEレコードは他のレコード型と共存できない(排他的でなければならない)が、頂点には常にSOAとNSレコードがある。

解決策:

  1. CNAMEフラット化 (Cloudflare) — DNS レベルでCNAMEを解決し、Aレコードを返す
  2. ALIASレコード (Route53、DNSimple) — Apexで機能するプロプライエタリレコード型
  3. ANAMEレコード (一部プロバイダー) — ALIASと似ている
  4. Aレコード — IPアドレスをシンプルに提供

オプション4(Aレコード)は最も普遍的だが、最も柔軟でない。サーバーIPをいつ変更しても、Aレコードを使う顧客がDNSを更新する必要。CNAMEの場合、CNAMEターゲットが何に解決するかを更新するだけ。

推奨: 両方をサポート。顧客に告げて、サブドメインはCNAMEを、Apexドメインはaレコード(またはプロバイダーがサポートしている場合ALIAS/ANAME)を使用。

所有権検証

ドメインがインフラストラクチャに解決されることを確認する以上に、ダッシュボードでドメインを構成する人が実際に所有していることを検証したいかもしれない。一般的なアプローチはTXTレコードを要求:

_verification.customer.com  TXT  "yourapp-verify=abc123unique"

これは所有していないドメインを指すことで、プラットフォームに請求することを防止。

本番環境対応とモニタリング

このセクションはデモと本番システムの違いだ。

証明書ストレージ

複数インスタンスを実行している場合、ディスク上に証明書を保存しない。分散ストアを使用:

  • Caddy: Redis、Consul、S3、PostgreSQLのストレージバックエンドをサポート
  • カスタム: 暗号化されたデータベースまたはシークレットマネージャー(AWS Secrets Manager、HashiCorp Vault)に証明書を保存

グレースフルフォールバック

顧客のドメイン設定が不適切な場合はどうなる? SSLエラーを表示しない。代わりに:

  1. 失敗したSSLハンドシェイクを検知
  2. 問題を説明するステータスページにリダイレクト
  3. 顧客に通知を送信
  4. 証明書供給を自動的に再試行

ヘルスチェック

定期的にすべてのカスタムドメインをチェックするバックグラウンドジョブを構築:

async function healthCheckDomains() {
  const domains = await db.customDomains.find({ active: true });
  
  for (const domain of domains) {
    const checks = {
      dnsResolves: await checkDNS(domain.hostname),
      sslValid: await checkSSL(domain.hostname),
      httpOk: await checkHTTP(domain.hostname),
    };
    
    if (!checks.dnsResolves) {
      await alertCustomer(domain, 'DNS no longer points to our platform');
      await markDomain(domain, 'dns_error');
    } else if (!checks.sslValid) {
      await triggerCertRenewal(domain);
    }
    
    await db.domainHealthChecks.insert({
      domainId: domain.id,
      ...checks,
      checkedAt: new Date(),
    });
  }
}

毎時間実行。10,000ドメインでも、正しく並列化すれば数分でチェック完了。

スケール時のコスト分析

2025年の実際の数字を話そう。

ソリューション 100ドメイン 1,000ドメイン 10,000ドメイン メモ
Caddy + Let's Encrypt (自己管理) ~$50/月 サーバー ~$200/月 サーバー ~$1,000/月 サーバー 運用負担
Cloudflare for SaaS 無料(エンタープライズ) ~$90/月 ~$990/月 スケール時の最良値
Vercel (Pro) 含まれる 追加$0 エンタープライズ必須 Pro時点で50/プロジェクトに制限
AWS CloudFront + ACM ~$100/月 ~$300/月 ~$2,000/月 CDN転送コスト含む
Fastly ~$150/月 ~$500/月 カスタム価格 Fastlyをすでに使用している場合に良い

ほとんどのSaaSプロダクトでは、Cloudflare for SaaSが甘い場所に当たる。月当たりドメイン10セント以下のグローバルCDN、DDoS保護、自動証明書を取得。それを打つのは難しい。

ヘッドレスCMSアーキテクチャで構築している場合 — 私たちが多くやる — Cloudflare for SaaSは、分離されたフロントエンドの恩恵を受けるエッジキャッシングを既に扱っているため、特に適切にペアリング。

FAQ

DNS設定後、カスタムドメインが有効になるまでどのくらいかかる?

DNS伝播には通常5〜30分かかり、稀な場合は最大48時間。Let's EncryptでのSSL供給は、DNS解決に成功すると追加で30〜60秒かかる。実際には、ほとんどのカスタムドメインは15分以内に完全に有効になる。DNS30秒ごとにポーリングして、解決に成功するとすぐに証明書供給をトリガーすることをお勧めする。

SaaS上の数千のカスタムドメイン向けにLet's Encryptを使用できる?

はい。ただしレート制限を計画する。主な制約は登録ドメイン当たり週50証明書(プラットフォーム自体のサブドメイン)と3時間当たり300新規オーダー。顧客カスタムドメイン向けに、各ドメインが別の登録ドメインなので、ドメインごとの制限は同じ方法では適用されない。正当な必要を示すことで、Let's Encryptレート制限引き上げを申請することもできる。ZeroSSLは固い代替ACME プロバイダー。

ワイルドカードサブドメインとカスタムドメイン間の違いは?

ワイルドカードサブドメイン(*.yourapp.com)は完全にあなたに管理されている — 1つのDNSレコード、1つのSSL証明書、シンプルなルーティング。カスタムドメイン(portal.customer.com)はあなたの顧客が管理するドメイン。DNS設定と個別SSL証明書供給を要求。ほとんどのSaaSプロダクトはワイルドカードサブドメインで始まり、顧客が要求する際に後でカスタムドメイン対応を追加(これはプレミアム機能)。

Apex(ベア)カスタムドメインをどう処理する?

Apexドメイン(wwwなしのcustomer.com)はDNS仕様ごとCNAMEレコードを使用できない。オプション: 顧客にIPをポイントするAレコードを作成させる、ALIAS/ANAMEレコードをサポートするDNSプロバイダーを使用、またはCloudflareのようなCNAMEフラット化をしているプロバイダーを推奨。常にApexとサブドメイン構成をサポート — 顧客は両方を要求する。

自動SSLにはCaddyかnginxを使うべき?

ゼロから始める場合、Caddyを使用。そのオンデマンドTLS機能はマルチテナントカスタムドメイン用途向けに文字通り構築。スタックに既にnginxがある場合は問題ないが、証明書ライフサイクル管理向けcertbot周囲にもっと自動化を構築する必要。Caddyは発行、更新、ストレージ、OCSPステープル自動処理。

カスタムドメイン機能の悪用をどう防ぐ?

3つのレイヤー: 第一に、カスタムドメイン受け入れ前にTXTレコードでドメイン所有権を検証。第二に、証明書リクエスト前にドメインを検証する「ask」エンドポイント(CaddyのオンデマンドTLS ask)を実装。第三に、アカウントごとのドメイン追加をレート制限。これらなしに、誰かが数千のドメインをIPにポイントして、証明書レート制限を燃やすか、ドメインフロンティングにインフラを使用できる。

顧客がDNSレコードを削除する場合?

プラットフォームは通常のヘルスチェック内数時間でこれを検知。DOMが機能停止してインフラストラクチャに解決されなくなると、ドメイン不健康としてマーク、顧客に通知、証明書更新試行を停止。完全に削除しない — 顧客は移行中にDNS一時的に破壊。完全に非アクティブ化前に30日間のグレース期間をお勧め。

Cloudflare for SaaSは自己管理の証明書と比較してそれだけの価値がある?

ほとんどのSaaSプロダクトではそう。月当たりホスト名$0.10で、証明書自動化構築と保守、エッジケース処理、監視更新、レート制限処理に費やすエンジニア時間と比較すれば些細。グローバルCDNとDDoS保護も含む。唯一自己管理する理由はインフラの完全制御が必要か、特定のコンプライアンス要件でサードパーティプロキシ使用が禁止されている場合。あなたの特定プロダクト向けに正しいアプローチについて知りたい場合、お問い合わせ — 両パターン実装。

カスタムドメインは悪魔が本当に細部にいるSaaS機能の1つ。ハッピーパスはストレートだが、本番対応は、DNS伝播遅延、証明書失敗、顧客設定誤り、スケール時のモニタリング処理を意味する。ワイルドカードサブドメインで始まり、顧客が要求する際にカスタムドメイン対応を追加(そして彼らは要求)。CaddyとCloudflare for SaaSのようなツールが存在する場合、スクラッチからすべてを構築しようとしない。未来のオンコール自身が感謝する。