payment-gateway.app Docs
Deployment

Caddy Reverse Proxy

Configuring the Caddy reverse proxy with MongoDB-backed storage for automatic TLS, custom domains, and routing.

Caddy Reverse Proxy

The payment-gateway-reverseproxy module ships a customized Caddy build with a MongoDB storage plugin that stores TLS certificates and ACME challenge data in your existing MongoDB instance — eliminating the need for a shared filesystem between Caddy instances.

Canonical configuration: default routing, rewrites, TLS storage, and security headers are defined only in payment-gateway-reverseproxy/Caddyfile at the root of that repository. This page summarizes that file; when in doubt, follow the Caddyfile.

Hosted reference hostnames (dashboard, api, secure, webhook under payment-gateway.app) and self-hosted naming patterns are listed in Hostnames & DNS conventions.


Why Caddy with MongoDB Storage

Standard Caddy stores TLS certificates on the local filesystem. When running multiple Caddy instances (for HA) or containers that may be replaced, this causes certificate loss or ACME rate limit issues. The MongoDB storage backend:

  • Persists certificates to MongoDB (replicated, backed up automatically)
  • Allows any Caddy instance to serve any domain's certificate
  • Supports on-demand TLS for dynamically registered custom domains when the merchant hostname reaches Caddy directly

Default Caddyfile layout

Placeholders such as {$ADMIN_BACKEND_DOMAIN} are expanded from the environment when the container starts (see your deploy stack). Specific server blocks must appear before the catch-all :443 block — the order in payment-gateway-reverseproxy/Caddyfile is intentional.

Global block

  • on_demand_tls.ask: http://{$ADMIN_BACKEND_SERVICE}:{$ADMIN_BACKEND_SERVICE_PORT}/api/v1/domains/verify — internal service URL is preferred so the proxy does not depend on public DNS to reach the Admin Backend.
  • storage mongodb: {$MONGODB_URI}, {$MONGODB_DATABASE}, {$MONGODB_COLLECTION} (and related options) persist certificates for HA and container replacement.
  • servers: trusted_proxies include Cloudflare CIDRs and private_ranges; client_ip_headers CF-Connecting-IP X-Forwarded-For.

Per-hostname routing (summary)

Caddyfile site addressProxied toNotable paths
{$ADMIN_FRONTEND_DOMAIN}:{$ADMIN_FRONTEND_DOMAIN_PORT}Admin Backend for /api/*; otherwise Admin FrontendDashboard uses /api/* to reach the same Admin Backend as the dedicated API host.
{$MONGO_EXPRESS_DOMAIN}:{$MONGO_EXPRESS_DOMAIN_PORT}Mongo ExpressIP allowlist via {$MONGO_EXPRESS_IP_WHITELIST}.
{$MAIN_FRONTEND_DOMAIN}:{$MAIN_FRONTEND_DOMAIN_PORT}Main FrontendHosted checkout UI and customer portal surfaces.
{$ADMIN_BACKEND_DOMAIN}:{$ADMIN_BACKEND_DOMAIN_PORT}Admin Backend/api/* pass-through; /v1/*/api/v1/* then proxy; /health.
{$MAIN_BACKEND_DOMAIN}:{$MAIN_BACKEND_DOMAIN_PORT}Main Backend/api/*, /hooks/*, Apple Pay domain association, /health.
:443 (catch-all)Main Backend or Main FrontendOn-demand TLS; backend path matcher then default to Main Frontend.

The Main Backend block is documented in-file as handling checkout retrieval (for the main frontend), transaction flows, and provider hooks — not Admin API management or POST …/checkouts/{siteId}/create.

Example hostnames (your values come from env): api.yourcompany.com{$ADMIN_BACKEND_DOMAIN}; webhook.yourcompany.com{$MAIN_BACKEND_DOMAIN} — see Hostnames & DNS conventions for the hosted *.payment-gateway.app layout.


Admin API hostname and /v1 prefix

On {$ADMIN_BACKEND_DOMAIN}, the default Caddyfile passes /api/* straight to the Admin Backend and rewrites /v1/* so the backend receives /api/v1/*:

	# Direct /api/* pass-through – used by external API consumers
	handle /api/* {
		reverse_proxy {
			to {$ADMIN_BACKEND_SERVICE}:{$ADMIN_BACKEND_SERVICE_PORT}
			header_up X-Request-ID {http.request.uuid}
		}
	}

	# Rewrite /v1/* to /api/v1/* so backend receives expected paths
	# This allows clean external API paths while keeping internal routing consistent
	handle /v1/* {
		rewrite * /api{uri}
		reverse_proxy {
			to {$ADMIN_BACKEND_SERVICE}:{$ADMIN_BACKEND_SERVICE_PORT}
			header_up X-Request-ID {http.request.uuid}
		}
	}

(Excerpt from payment-gateway-reverseproxy/Caddyfile — see the file for /health, headers, and {$CUSTOM_ADMIN_BACKEND_DOMAIN_OPTIONS}.)

Merchant integrations may call either https://api.yourcompany.com/v1/... or https://api.yourcompany.com/api/v1/... on {$ADMIN_BACKEND_DOMAIN} (replace with your real API host).


On-Demand TLS for Custom Merchant Domains

Merchants can map their own domains (e.g., checkout.merchantshop.com) to their payment portal. Caddy issues TLS certificates on first request using the on_demand_tls feature.

[!IMPORTANT] This flow works when the merchant hostname reaches Caddy directly. It does not by itself solve the case where the merchant hostname points to a Cloudflare-proxied target hostname and Cloudflare terminates TLS first. In that model, Cloudflare needs a certificate for the merchant hostname at the edge before any request can reach origin.

The ask URL must return 200 OK for valid domains and any other status for invalid ones. The Admin Backend's GET /api/v1/domains/verify endpoint serves this purpose — it is a public, no-auth endpoint dedicated to Caddy's on-demand TLS check.

Implementation details:

  • The global ask URL in the default Caddyfile is http://{$ADMIN_BACKEND_SERVICE}:{$ADMIN_BACKEND_SERVICE_PORT}/api/v1/domains/verify; Caddy calls it with a query param: GET /api/v1/domains/verify?domain=<hostname>
  • The handler returns a bare 200 or 403 (no JSON body), matching the Caddy ask protocol

[!NOTE] This is distinct from GET /api/v1/domains/verify-cname, which is the authenticated endpoint used by operators to manually trigger CNAME verification from the Admin Panel.

Merchant Domain Registration Flow

  1. Merchant adds their domain in Admin Panel → Sites → Custom Domain.
  2. Merchant creates a CNAME record to your main frontend hostname (the value of MAIN_FRONTEND_DOMAIN — e.g. secure.yourcompany.com, pay.yourcompany.com, or hosted secure.payment-gateway.app): checkout.merchantshop.com → <main-frontend-host>.
  3. Admin Panel verifies the CNAME via Verify Domain.
  4. On the next request to checkout.merchantshop.com, Caddy calls the ask endpoint, receives 200, and issues a certificate via ACME (Let's Encrypt).

This registration flow assumes the target hostname is reachable at origin level. If the target hostname is itself proxied by Cloudflare, use one of these approaches instead:

  • Keep the merchant-facing target hostname DNS only so requests reach Caddy directly.
  • Or use Cloudflare for SaaS / Custom Hostnames so Cloudflare manages the merchant-domain certificate at the edge before forwarding traffic to origin.

Reference: Create custom hostnames.


TLS Certificate Management

Caddy handles certificate issuance and renewal automatically via ACME (Let's Encrypt by default). No manual certificate management is required.

Requirements:

  • The server must be reachable on ports 80 and 443 from the internet.
  • The domain must have a valid DNS A/CNAME record pointing to the server.
  • Let's Encrypt rate limits apply: 50 certificates per registered domain per week.

Using a Custom ACME Server

For private deployments or staging:

{
  acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

Security Headers (optional)

The default Caddyfile already sets HSTS, X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, and X-Request-ID on the shipped site blocks. The snippet below is an additional pattern if you customize a host — it is not a copy of the default file.

Add or extend security headers on a site block:

pay.yourcompany.com {
  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains"
    X-Frame-Options "SAMEORIGIN"
    X-Content-Type-Options "nosniff"
    Referrer-Policy "strict-origin-when-cross-origin"
  }
  reverse_proxy payment-gateway-main-frontend:6000
}

Rate limiting at the proxy layer (optional)

Not present in the default Caddyfile. Caddy's rate_limit module (available in the extended build) can add a coarse rate limit before requests reach the backend:

webhook.yourcompany.com {
  rate_limit {
    zone api_zone {
      key {remote_host}
      events 100
      window 1m
    }
  }
  reverse_proxy payment-gateway-main-backend:8081
}

Health check routing (optional)

Not present in the default Caddyfile. The default config responds 403 on /internal/health for the admin and main frontend hosts only. To restrict other paths or hosts, you can add matchers such as:

webhook.yourcompany.com {
  @blocked {
    path /internal/*
    not remote_ip 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
  }
  respond @blocked 403
  reverse_proxy payment-gateway-main-backend:6001
}

This blocks /internal/health from public access while allowing it from private network ranges where your monitoring system runs.

On this page