Reverse proxy
Use Nginx, Apache, or Caddy to expose Markon through a public domain.
Access control
Markon never treats source IP as identity. Browsers are collaborators unless they hold an explicit admin session, collaborator codes apply to every non-admin browser, and each request must match an exact Host allowlist. Register the public origin with --entry https://docs.example.com or --trusted-host https://docs.example.com; unknown Hosts return 421.
A collaborator code is application-level access control, not transport encryption. Public deployments should terminate TLS and preferably add gateway authentication such as Basic Auth, oauth2-proxy, Cloudflare Access, Tailscale, or WireGuard.
Markon configuration
markon --host 127.0.0.1 -p 6419 \
-b https://docs.example.com \
--entry https://docs.example.com--host keeps the service local, -b opens the public URL, and --entry controls advertised/QR URLs, the Host allowlist, and Secure cookies for HTTPS origins. The proxy supplies TLS.
Nginx
server {
listen 443 ssl http2;
server_name docs.example.com;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
location / {
proxy_pass http://127.0.0.1:6419;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
}WebSocket upgrade is required for Shared and Live.
Apache
sudo a2enmod proxy proxy_http proxy_wstunnel rewrite ssl<VirtualHost *:443>
ServerName docs.example.com
SSLEngine on
SSLCertificateFile /path/to/fullchain.pem
SSLCertificateKeyFile /path/to/privkey.pem
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:6419/$1" [P,L]
ProxyPass / http://127.0.0.1:6419/
ProxyPassReverse / http://127.0.0.1:6419/
</VirtualHost>Caddy
docs.example.com {
reverse_proxy 127.0.0.1:6419
}Caddy handles HTTPS and WebSocket forwarding automatically.
systemd
markon initializes/connects to markond and then exits, so use a oneshot unit:
[Service]
Type=oneshot
RemainAfterExit=yes
User=markon
WorkingDirectory=/srv/docs
ExecStart=/usr/local/bin/markon /srv/docs --host 127.0.0.1 -p 6419 --entry https://docs.example.com
ExecStop=/usr/local/bin/markon shutdown
Environment="MARKON_SQLITE_PATH=/var/lib/markon/annotation.sqlite"Reserved path and troubleshooting
/_/ is reserved for internal CSS, JavaScript, and WebSockets; do not create a workspace-root directory named _. If WebSockets fail, verify Upgrade/Connection headers and proxy timeouts. If QR uses a private IP, correct --entry. For a blank page, check curl http://127.0.0.1:6419 and the proxy error log.