HTTPS proxy
MarkStack has no built-in TLS server. A host/reverse proxy supplies HTTPS, certificates/renewal and reachability controls. Use separate reader/editor hosts where both exist. Serve only reader output publicly; keep the optional editor on its intended private perimeter and upstream loopback.
The nginx blocks below are templates for a host already equipped with nginx, actual certificates, DNS and an approved service process. They do not issue certificates, configure firewall, grant filesystem access or authenticate private readers. Validate final adapted configuration on staging before reload.
Boundary map
| Layer | Responsibility |
|---|---|
| Static reader | Output bytes, route, index, media correctness |
| nginx reader | Document root, MIME, 404, TLS, cache, reader gate |
| nginx editor | HTTPS, intended audience, safe headers, upstream |
| Node editor | Accounts, section grants, Host, origin, CSRF and limits |
| Operator | Service, paths, certificate renewal, backups and recovery |
Do not serve the project root. Accounts/settings/history/publication state belong outside output. Editor authentication protects writing, not a separately served reader. If reading is private, the gate must cover HTML, search JSON, fonts/scripts/images/downloads, not only home.
Static-reader template
server {
listen 443 ssl;
server_name docs.example.com;
ssl_certificate /etc/ssl/example/docs-fullchain.pem;
ssl_certificate_key /etc/ssl/example/docs-key.pem;
root /srv/field-guide/app/dist;
index index.html;
location / {
try_files $uri $uri/ =404;
}
error_page 404 /404.html;
}
try_files checks selected filesystem paths before returning 404. error_page internally serves the custom file while retaining error behavior unless you deliberately use a status override. Do not turn it into =200 or blanket home fallback. Verify unknown route’s actual status/content.
This root-site template needs your installed normal MIME configuration for CSS/JS/JSON/SVG/fonts and readable output/ancestors. A subpath site requires adapted path/root/alias handling and matching generated/authored URLs. Do not assume this root block is a complete subpath example.
Stock generated 404 has a literal / home link. Verify it, deep directory URLs, assets and search at actual hosted prefix. Review symlinks/permissions and directory-listing policy; unlike bundled preview’s containment tests, nginx’s final filesystem access follows its own settings.
Editor template
server {
listen 443 ssl;
server_name editor.example.com;
ssl_certificate /etc/ssl/example/editor-fullchain.pem;
ssl_certificate_key /etc/ssl/example/editor-key.pem;
client_max_body_size 3m;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
This illustrates a single front proxy directly receiving the client. The forwarded address is replaced with connection address, not concatenated with an arbitrary client-supplied header. If another trusted proxy/CDN is in front, $remote_addr may be that proxy; review the entire trusted address/sanitization chain rather than changing to an unreviewed append recipe.
| Editor setting | Must match |
|---|---|
| Bind | Local upstream 127.0.0.1 and selected port |
| Allowed hosts | Actual forwarded browser hostname, port |
| Secure cookie | Enabled for actual browser HTTPS |
| Trusted peers | nginx’s actual connecting address |
| Body limit | Node limit still applies independently |
Exact values belong in service configuration. MARKSTACK_EDITOR_COOKIE_SECURE=1 selects Secure host-prefixed cookie mode. MARKSTACK_TRUSTED_PROXIES=loopback only suits a real local connecting peer. Pin trusts exact addresses/loopback, not CIDR ranges. It walks forwarded addresses from the right, skipping configured trusted peers; do not let an untrusted source choose client identity.
nginx defaults can change Host upstream; explicit proxy_set_header Host is important. $host suits this conventional 443 hostname example. If exposing a nonstandard external port, review/preserve the full expected hostname:port and add its exact value to allowed Hosts. X-Forwarded-Host is not a substitute for required Host.
The Node default JSON-body limit is 2 MiB; proxy 3m limit does not enlarge it. A tighter proxy cap may reject first. Align intended payload limits without disabling server safeguards. Do not change Node defaults based on one failed request before checking its actual content/size.
TLS, DNS and exposure
| Requirement | Check |
|---|---|
| Name | DNS points to intended reachable host |
| Certificate | Covers actual hostname, valid chain, key handling |
| Renewal | Supported automated process and monitored failures |
| HTTP | Intended redirect, challenge policy |
| Editor access | Team network, access gate, no bypass upstream |
| Reader access | Public decision or full separate private gate |
Secure cookie flag does not create TLS. Do not sign in over public HTTP or disable Secure just to get a cookie stored. The upstream can be loopback HTTP while browser connection is HTTPS. HTTP-to-HTTPS and ACME challenge locations depend on your selected certificate process; see DNS setup.
Validate and reload
Read actual existing sites/service state before replacement. Preserve rollback config and check that paths/certificates/ports do not conflict. After adapting the template:
sudo nginx -t
Only after success and intended maintenance approval:
sudo systemctl reload nginx
Syntax validation is not a successful live TLS/auth test. Inspect reload/service logs, actual listener and public/private URLs. A command exit can coexist with wrong document root or blocked resources.
Staging acceptance
| Check | Required result |
|---|---|
| Reader | Current home, deep, resources, search, diagram |
| Missing route | 404 content, status, not soft 200 home |
| TLS, cookie | Correct certificate and Secure browser cookie |
| Unauthenticated API | Protected access rejected |
| Unknown Host, origin | Server perimeter rejects request |
| Role, grant | Synthetic outside-section writes denied |
| Publish | Correct staged output, swap and actual served reader |
| Private files | No accounts, runtime, server bytes accessible |
Use synthetic accounts/content. Do not repeat completed mutations when logging fails; check original target. Restarting editor clears sessions/current job state; it does not stop separate reader serving.
nginx, runtime, DNS, certificates, firewall and production publication were not deployed by these documentation steps. Directive semantics were checked against official sources, not proven on your host. Caddy and Apache examples offers alternatives, not software to stack with this proxy blindly.
Official sources
Checked 12 October 2026: