Administering

Deployment and TLS

The server speaks plain HTTP and listens on loopback. To let other people use it, put a reverse proxy on the same machine in front of it to terminate TLS, and tell the server about the proxy. This page is the summary; the full deployment guide ships in the docs folder of the archive, with complete Caddy, nginx, IIS and Docker Compose recipes.

What was and was not run

The Caddy and nginx recipes were run by Overpass against real servers in containers. The IIS recipe, the Windows firewall command and the Linux firewall commands were not run on those systems. This page says so where it matters.

Set the public address

Behind a proxy, set server.publicUrl in draughtsman.yaml to the address people actually use (for example https://draughtsman.example.com). Reset links in emails are built on it and on nothing else, never on the request's Host header, and Forgot password? is offered only when it is set and outgoing email works. Outgoing email itself is set up in the browser under Admin, Email, or in draughtsman.yaml; see Password recovery and email. If you must block the server's outbound traffic, allow the one mail server you name there.

The short version

  1. Keep the server on its default 127.0.0.1:5180 and put a reverse proxy on the same machine in front of it. The proxy terminates TLS. The server itself never does.
  2. In draughtsman.yaml: list the proxy in server.knownProxies, name your public host in server.allowedHosts, and once https:// works set server.forceHttps: true.
  3. Open only 443 (and 80 if the proxy fetches certificates). Never expose 5180 directly.
  4. Leave the data folder owner-only. Never chmod 777.
  5. Schedule draughtsman backup and copy the result off the machine.

What the server does by default

SettingDefaultWhat it means
Bind address127.0.0.1:5180Reachable from this machine only. The Docker image binds every interface so a published port works.
Transportplain HTTPAnything that travels un-proxied over a network, passwords included, is in clear.
Host headerlocalhost names only while bound to loopback with no proxy listedA request under another name is refused with 400 (a DNS-rebinding guard).
Cookiesds_session; __Host-ds_session with Secure once the server sees HTTPSHTTPS is seen when the request arrived over TLS, a trusted proxy said so, or server.forceHttps is on.
Security headersalways sentContent-Security-Policy, nosniff, referrer policy, frame denial, permissions policy; no-store on API responses; HSTS only under forceHttps.
Sign-inevery route except health, the theme and sign-inFirst run needs the one-time setup code from the log.

The security headers are real: on the documentation instance the health endpoint answered with Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval'; …; frame-ancestors 'none', X-Content-Type-Options: nosniff, Referrer-Policy: same-origin, X-Frame-Options: DENY and a restrictive Permissions-Policy. The 'unsafe-eval' is there because the web app compiles the document schema in the browser; there is no 'unsafe-inline' script and no external script host.

The three proxy settings

server:
  knownProxies: ["127.0.0.1", "::1"]     # the proxy's own addresses or CIDR ranges
  allowedHosts: ["draughtsman.example.com"]
  forceHttps: true
  • knownProxies lists the addresses whose X-Forwarded-For and X-Forwarded-Proto the server believes. An entry is an IP address or a CIDR range; a host name is ignored with a log line. List the proxy and nothing wider: trusting a range that clients can also reach lets any client forge its address and defeats the sign-in throttle and the source address in the security log. A proxy on the same machine connecting over IPv4 loopback is trusted without being listed; list ::1 if it connects over IPv6.
  • allowedHosts is the Host values the server answers to (*.example.com matches subdomains). A proxy on the same machine trips over the loopback default, because it forwards the public name; naming the public host fixes it. Anything else gets 400 Bad Request - Invalid Hostname.
  • forceHttps makes every cookie Secure and sends HSTS (max-age=2592000, 30 days). Turn it on once https:// works; on plain HTTP it makes sign-in unusable.

What any proxy must do

RequirementWhy
Terminate TLS and forward to http://127.0.0.1:5180The server has no TLS.
Pass the original HostCookies, the host allow-list and the __Host- prefix depend on it.
Send X-Forwarded-For and X-Forwarded-ProtoThe client address and scheme.
Allow request bodies of about 12 MBThe server accepts up to 10 MiB. nginx's default is 1 MB, and a 2 MB import through default nginx is refused with 413.
Allow responses that take minutesGenerating a diagram from text waits for the AI model (up to 300 seconds by default). Set the proxy's read timeout to at least 330 seconds if you use AI. nginx defaults to 60, IIS ARR to 30.
Not buffer /mcpMCP over HTTP can stream. It matters for nginx (proxy_buffering off).

Draughtsman needs no WebSocket: the editor polls for changes about once a second, and MCP uses streamable HTTP.

Caddy

Caddy gets and renews a public certificate by itself, sets the forwarding headers and passes the Host. Run it on the same machine as Draughtsman:

draughtsman.example.com {
	reverse_proxy 127.0.0.1:5180
}

with the three settings above. Overpass checked it with Caddy 2 in a container, in front of a server on the host, using Caddy's internal CA and a localhost site: the health check answered, the first administrator was created through it, the session cookie came back as __Host-ds_session, and the security log recorded the client address Caddy saw. Automatic public certificates need ports 80 and 443 reachable from the internet; on a private network use tls internal or supply tls /path/cert.pem /path/key.pem.

nginx

server {
    listen 443 ssl;
    http2 on;
    server_name draughtsman.example.com;
    ssl_certificate     /etc/ssl/draughtsman/fullchain.pem;
    ssl_certificate_key /etc/ssl/draughtsman/privkey.pem;
    client_max_body_size 12m;

    location / {
        proxy_pass http://127.0.0.1:5180;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 330s;
    }
    location /mcp {
        proxy_pass http://127.0.0.1:5180;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;
        proxy_read_timeout 330s;
    }
}
server {
    listen 80;
    server_name draughtsman.example.com;
    return 301 https://$host$request_uri;
}

Checked by Overpass with nginx 1.27 in a container (nginx -t passed, requests through it reached the server). Certificates are yours to arrange (certbot or your organisation's); http2 on; needs nginx 1.25.1 or later.

IIS, Windows, Docker Compose

The IIS with Application Request Routing recipe is in that deployment guide, in the docs folder of the archive, and is unverified: no Windows machine was available when it was written. A Docker Compose file with Caddy in front is in the same guide and was checked end to end by Overpass. In Docker, list the compose network's CIDR range in knownProxies rather than a container address that changes on redeploy.

Firewall and file permissions

  • Allow inbound 443 (and 80 if the proxy fetches certificates); block 5180 from everywhere but the proxy. With the server on loopback and the proxy on the same machine nothing listens on the network at 5180 at all, which is the best firewall. The ufw, firewall-cmd and PowerShell commands in the guide are standard but were not run.
  • Docker bypasses ufw. Docker writes its own iptables rules for published ports. Publish to loopback (-p 127.0.0.1:5180:5180), as shown on Install.
  • The data folder holds every document, the password and token hashes, the key ring and the stored AI key. On Linux and macOS the server sets umask 077 before creating anything (folders 0700, files 0600), tightens what an earlier run left behind, and warns when the folder is open to other users. Act on that warning with chmod 700. It never loosens, and you should never chmod 777.
  • On Windows there are no modes: keep the data folder where only the service account and administrators have access (unverified).
  • Run the server as an unprivileged account. The Docker image runs as uid 10001; the systemd unit uses a dynamic user; the launchd plist runs as root unless you pass --user.

Postgres

With storage.provider: postgres the database lives on your Postgres server and everything else stays in the data folder:

storage:
  provider: postgres
  connectionString: "Host=db.internal;Port=5432;Database=diagrams;Username=drafter;Password=...;SSL Mode=Require"

Use a dedicated database and role, TLS between the two, and do not expose Postgres beyond the two machines. The role needs the right to create tables, since migrations run at start-up. Back up with pg_dump. Overpass migrated an empty Postgres 17 database, created the first administrator and restarted against it. SQL Server is not supported.

Check your deployment

curl -sI https://draughtsman.example.com/api/health          # 200; Strict-Transport-Security present when forceHttps is on
curl -s  https://draughtsman.example.com/api/health          # {"status":"ok"}
curl -sI http://draughtsman.example.com:5180/api/health      # must fail to connect from outside
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: other.example.com' https://draughtsman.example.com/api/health   # 400

Then sign in and open Security: the source address on your own sign-in must be your machine's, not the proxy's. If it is the proxy's, knownProxies does not list the address the server actually sees. In the browser's developer tools the session cookie should be __Host-ds_session with Secure ticked.