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_proxiesinclude Cloudflare CIDRs andprivate_ranges;client_ip_headers CF-Connecting-IP X-Forwarded-For.
Per-hostname routing (summary)
Caddyfile site address | Proxied to | Notable paths |
|---|---|---|
{$ADMIN_FRONTEND_DOMAIN}:{$ADMIN_FRONTEND_DOMAIN_PORT} | Admin Backend for /api/*; otherwise Admin Frontend | Dashboard uses /api/* to reach the same Admin Backend as the dedicated API host. |
{$MONGO_EXPRESS_DOMAIN}:{$MONGO_EXPRESS_DOMAIN_PORT} | Mongo Express | IP allowlist via {$MONGO_EXPRESS_IP_WHITELIST}. |
{$MAIN_FRONTEND_DOMAIN}:{$MAIN_FRONTEND_DOMAIN_PORT} | Main Frontend | Hosted 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 Frontend | On-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
askURL in the default Caddyfile ishttp://{$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
200or403(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
- Merchant adds their domain in Admin Panel → Sites → Custom Domain.
- 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 hostedsecure.payment-gateway.app):checkout.merchantshop.com → <main-frontend-host>. - Admin Panel verifies the CNAME via Verify Domain.
- On the next request to
checkout.merchantshop.com, Caddy calls theaskendpoint, receives200, 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
80and443from 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.