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.
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
- Keep the server on its default
127.0.0.1:5180and put a reverse proxy on the same machine in front of it. The proxy terminates TLS. The server itself never does. - In
draughtsman.yaml: list the proxy inserver.knownProxies, name your public host inserver.allowedHosts, and oncehttps://works setserver.forceHttps: true. - Open only 443 (and 80 if the proxy fetches certificates). Never expose 5180 directly.
- Leave the data folder owner-only. Never
chmod 777. - Schedule
draughtsman backupand copy the result off the machine.
What the server does by default
| Setting | Default | What it means |
|---|---|---|
| Bind address | 127.0.0.1:5180 | Reachable from this machine only. The Docker image binds every interface so a published port works. |
| Transport | plain HTTP | Anything that travels un-proxied over a network, passwords included, is in clear. |
| Host header | localhost names only while bound to loopback with no proxy listed | A request under another name is refused with 400 (a DNS-rebinding guard). |
| Cookies | ds_session; __Host-ds_session with Secure once the server sees HTTPS | HTTPS is seen when the request arrived over TLS, a trusted proxy said so, or server.forceHttps is on. |
| Security headers | always sent | Content-Security-Policy, nosniff, referrer policy, frame denial, permissions policy; no-store on API responses; HSTS only under forceHttps. |
| Sign-in | every route except health, the theme and sign-in | First 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
knownProxieslists the addresses whoseX-Forwarded-ForandX-Forwarded-Protothe 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::1if it connects over IPv6.allowedHostsis the Host values the server answers to (*.example.commatches 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 gets400 Bad Request - Invalid Hostname.forceHttpsmakes every cookieSecureand sends HSTS (max-age=2592000, 30 days). Turn it on oncehttps://works; on plain HTTP it makes sign-in unusable.
What any proxy must do
| Requirement | Why |
|---|---|
Terminate TLS and forward to http://127.0.0.1:5180 | The server has no TLS. |
Pass the original Host | Cookies, the host allow-list and the __Host- prefix depend on it. |
Send X-Forwarded-For and X-Forwarded-Proto | The client address and scheme. |
| Allow request bodies of about 12 MB | The 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 minutes | Generating 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 /mcp | MCP 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-cmdand 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, files0600), tightens what an earlier run left behind, and warns when the folder is open to other users. Act on that warning withchmod 700. It never loosens, and you should neverchmod 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.