Reverse Proxy
Your web server sits in front of everything: it serves your published site as plain files and forwards the few dynamic paths (forms, documents, the admin UI) to the CMS daemon. This page explains the shipped example configurations, how ports and languages map, and how TLS certificates are handled.
The shipped examples
The project’s deploy folder contains ready-to-copy configurations:
- Caddy (recommended): a complete example with automatic HTTPS: the product site, the admin domain, and per-language static vhosts built from a reusable snippet.
- nginx (full): the two-vhost pattern, one per-language static vhost plus the admin UI proxy, with caching and security headers.
- nginx (minimal): a single-domain template with placeholders (domain, output path, CMS port) covering the common layout.
All three encode the same rules; the differences are the server binary and how much they hold your hand.
Ports and where traffic flows
- Each CMS instance listens on its own loopback port: the convention is 3001 for instance
1, 3002 for instance2, and so on (the installer assigns them; you can override). - The public site vhost serves the static output directory directly from disk. It is just a file server plus three small proxy rules (below).
- The admin UI (e.g.
admin.example.com) is a full reverse proxy to the instance’s port; every request goes to the CMS.
Three proxy rules you may need
Copy these into the public-site vhost(s); each is required by a specific feature, and the shipped examples all warn about them:
/forms/: every form on your site posts to/forms/<slug>, which no static file server can handle. Without this block, every form submission fails. (Required if you use contact or newsletter forms at all.)/documents/: document downloads (with tracking and access control) are served live by the CMS. Required only if you use the Documents feature, with the caveat that one of the two shipped nginx examples predates it; if your example file lacks the/documents/block, add it when you use documents.- Scanner honeypots: a short list of known probe URLs (
/.env,/wp-login.php,wp-admin,phpmyadmin, …) is proxied to the CMS so those hits are logged and slowed down instead of answered by a plain static 404.
The admin vhost proxies everything; that’s the point: the admin interface is the CMS.
Loopback is a security decision
- Bind the CMS to
127.0.0.1:<port>and let the proxy face the internet. The CMS trusts forwarded client IPs only from loopback connections: bind it to a public interface and your rate limits/login lockouts can be spoofed. - If your proxy reaches the CMS over a non-loopback network (like Docker), add that network via
CHINASKI_TRUSTED_PROXIESin the instance’s env file. - Header behavior: examples pass
X-Real-IP/X-Forwarded-For/X-Forwarded-Protoconsistently. Caddy v2 manages them itself; nginx examples set them explicitly on every proxy block.
Caching (what the examples set)
- Pages (HTML): cached 5 minutes: deliberately short so builds reach readers quickly.
- Service worker: never cached; a stale copy would block site updates.
- Fonts: content-hashed, cache a year (they never change names).
- Other assets (CSS/JS/images): one day.
- The app itself already sends correct headers; the proxy rules are belt-and-braces so wrong responses can’t sneak through even from stale error pages.
Security headers
The admin vhost examples add frame protection, content-type sniffing protection, and referrer policy at the proxy layer, explicitly as a backup in case a static error page is served before the CMS can respond. The CMS reproduces each of them itself on every response, so the two layers agree; nginx’s minimal example also adds HSTS at the proxy.
TLS with Let’s Encrypt
- Caddy: does everything automatically; request and renew certificates as soon as your DNS points at the host and ports 80/443 are reachable. No configuration lines in the example; that’s the point.
- nginx: the templates ship HTTP-only and expect one command (
certbot --nginx -d YOUR_DOMAIN) which injects thelisten 443and certificate lines for you. - No internet? The offline (“EOTW”) mode intentionally runs plain HTTP with no certificates to renew: the documented alternative when Let’s Encrypt isn’t reachable. Use a VPN or firewall rules to protect the admin in that mode.
- TLS is terminated at the proxy; the CMS always sees encrypted traffic terminating there and emits one-year HSTS.
Multi-language sites
Caddy and nginx full examples both give one vhost per language (each rooted at that language’s output/<lang> directory). If your permalink language mode changes later, the deploy script re-syncs the web server’s document roots automatically; the proxy config never needs hand-editing for that switch.
Subdomain language mode pairs with one language host each; the canonical URLs are then built per-subdomain, matching the layout.
A checklist before going live
- Two proxy blocks exist:
/forms/(if forms are used) and/documents/(if documents are used). - The admin daemon binds loopback; only the proxy is exposed.
nginx -t(or Caddy validation) passes and the service reloaded cleanly.- Certificates issued (or EOTW mode chosen deliberately).
- A quick pass with an external scanner hits the honeypot block, not a bare 404, visible in the admin logs.