Skip to content

Production Deployment

Writ edited this page Jul 28, 2026 · 2 revisions

Production deployment

The defaults are tuned for a local trial: both containers publish on loopback only. Before exposing this to a network, work through this page.

1. Put TLS in front

Terminate HTTPS at nginx, Caddy or Traefik and forward to 127.0.0.1:8000. Do not publish port 8000 on a public interface.

The container keeps listening on plain HTTP inside the network — that is expected and correct when your proxy terminates TLS.

server {
    listen 443 ssl http2;
    server_name writ.example.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;      # agents connect over WebSocket
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

The Upgrade/Connection headers are not optional — without them agents cannot establish their WebSocket and will retry forever.

2. Set the deployment identity

WRIT_PUBLIC_URL=https://writ.example.com
ALLOWED_HOSTS=writ.example.com
CORS_ORIGINS=https://writ.example.com
ENVIRONMENT=production

WRIT_PUBLIC_URL is what agents dial back on. If it is wrong, agents fail with Connection refused and nothing explains why.

3. Trust only your proxy for forwarded IPs

FORWARDED_ALLOW_IPS=127.0.0.1     # or your proxy's address

Never *. With a wildcard, any client can set X-Forwarded-For and bypass every per-IP rate limit and ban.

4. Reach the document extractor from remote agents

If agents run on other machines, DOC_EXTRACT_URL's loopback default is unreachable and non-HTML content is skipped silently. Give the service a route — usually a second server block — and set DOC_EXTRACT_URL to it. Keep DOC_EXTRACT_SECRET; it is the only thing in front of it.

5. Turn on admin MFA

Enrol a second factor first, then:

REQUIRE_ADMIN_MFA=true

The coordinator warns at every boot while this is off. Enabling it before enrolling locks you out.

6. Review the safety defaults

Setting Keep it at Why
ALLOW_PRIVATE_TARGETS false It is the SSRF guard. Only enable if you deliberately monitor an internal host.
WRIT_EXPOSE_OPENAPI unset Keeps the route map off a public host.
ENVIRONMENT production Enforces strong secrets, host allowlist, non-wildcard CORS.

What the container already does

The shipped compose is hardened, and you do not need to add these:

  • read_only: true — only /data is writable, so a file-write primitive cannot modify application code or persist across a restart
  • /tmp as noexec,nosuid tmpfs
  • cap_drop: ALL, no-new-privileges:true
  • non-root user; application code is world-readable and writable by nobody

One worker, always

The coordinator refuses to boot with more than one web worker. Its JWT blacklist, rate limits, agent presence and scheduler live in in-process fakeredis on a single SQLite file — a second worker gets its own empty keyspace, so token revocation silently stops working and schedules double-fire.

Scale by adding agents, not web workers.

Then

Clone this wiki locally