← Blog

Put PrintStash behind a reverse proxy with TLS

Use the production Compose file with Caddy, Traefik, or nginx, keep the API off the host network, and preserve uploads and WebSocket status updates.

guidesecuritydeployment

PrintStash is intended for a trusted network. If you need to reach it through a public hostname, use the production Compose file and put a TLS reverse proxy in front of the web container.

The production stack publishes the frontend on 127.0.0.1:3000. It does not publish the API port to the host. The frontend’s nginx instance proxies API and WebSocket traffic over the internal Docker network, so your reverse proxy needs only one upstream.

Start the production stack with:

Terminal window
docker compose -f docker-compose.prod.yml up -d

Before doing that, set a strong VAULT_JWT_SECRET in .env. The production Compose file refuses to start without one.

Caddy

Caddy handles certificates and WebSockets without extra rules:

printstash.example.com {
reverse_proxy 127.0.0.1:3000
}

Point the hostname at the server, make ports 80 and 443 reachable by Caddy, and reload its configuration. Do not publish port 8000.

Traefik

When Traefik runs in Docker, attach it to the same network as the PrintStash frontend and add labels to the frontend service:

labels:
- "traefik.enable=true"
- "traefik.http.routers.printstash.rule=Host(`printstash.example.com`)"
- "traefik.http.routers.printstash.entrypoints=websecure"
- "traefik.http.routers.printstash.tls.certresolver=le"
- "traefik.http.services.printstash.loadbalancer.server.port=3000"

If Traefik reaches the container over Docker networking, remove the frontend host port mapping. Otherwise, leave the localhost mapping in place and configure Traefik to use the host upstream.

nginx

nginx needs upgrade headers for live printer status and a body-size limit large enough for model uploads:

server {
listen 443 ssl;
server_name printstash.example.com;
ssl_certificate /etc/letsencrypt/live/printstash.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/printstash.example.com/privkey.pem;
client_max_body_size 512m;
location / {
proxy_pass http://127.0.0.1:3000;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

Match client_max_body_size to VAULT_MAX_UPLOAD_MB if you change the 512 MB default.

Check the boundary before opening a port

Confirm the host is listening only where expected:

Terminal window
docker compose -f docker-compose.prod.yml ps
curl -I http://127.0.0.1:3000

Port 8000 should not be published. Requests to /api/v1 and the live status WebSocket should travel through the frontend, not through a second public proxy route.

Use strong account passwords and keep PrintStash, the proxy, and the host updated. Proxy basic authentication can add another login prompt, but it does not replace PrintStash authentication or a unique JWT secret.

If only your own devices need access, a VPN such as WireGuard or Tailscale is usually simpler than exposing a public service. You can keep PrintStash on the private network and avoid inbound port forwarding.

The installation guide contains the current deployment examples. The security model explains what the trusted-network boundary means for the rest of the application.