Reverse Proxy
Expose Penombre behind a reverse proxy with HTTPS.
Penombre listens on port 3000 by default. In production you should place it behind a reverse proxy to handle HTTPS termination and forward traffic to the container.
Set the ORIGIN environment variable to your public URL (e.g.
https://cloud.example.com). This is required for OAuth callbacks, email
links, and passkey registration to work correctly.
Requirements
Your reverse proxy must:
- Forward the
Host,X-Real-IP,X-Forwarded-For, andX-Forwarded-Protoheaders so Penombre can resolve client addresses and detect HTTPS. - Allow large request bodies — Penombre sets
BODY_SIZE_LIMIT=Infinityfor file uploads, so limit enforcement should happen at the proxy level if needed. - Not buffer responses — streaming is used for file downloads.
Examples
Caddy handles HTTPS automatically via Let's Encrypt.
cloud.example.com {
reverse_proxy localhost:3000
}That's it — Caddy provisions and renews TLS certificates automatically.
server {
listen 80;
server_name cloud.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name cloud.example.com;
ssl_certificate /etc/letsencrypt/live/cloud.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cloud.example.com/privkey.pem;
client_max_body_size 0; # No upload size limit
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
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;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}Use Certbot to provision certificates:
sudo certbot --nginx -d cloud.example.comAdd labels to the app service in your compose.yaml:
services:
app:
image: orochibraru/penombre:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.penombre.rule=Host(`cloud.example.com`)"
- "traefik.http.routers.penombre.entrypoints=websecure"
- "traefik.http.routers.penombre.tls.certresolver=letsencrypt"
- "traefik.http.services.penombre.loadbalancer.server.port=3000"
# Remove the ports mapping — Traefik handles routing
# ports:
# - 3000:3000Make sure Traefik is configured with an ACME certificate resolver named letsencrypt and an entrypoint named websecure on port 443.
Docker Compose with a proxy network
When running both your reverse proxy and Penombre in Docker, place them on a shared network so the proxy can reach the app container by service name:
services:
app:
image: orochibraru/penombre:latest
depends_on:
db:
condition: service_healthy
restart: unless-stopped
volumes:
- storage_data:/data
env_file: .env
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/penombre
networks:
- default
- proxy
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=${POSTGRES_USER-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD-postgres}
- POSTGRES_DB=${POSTGRES_DB-penombre}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test:
[
CMD-SHELL,
"sh -c 'pg_isready -U ${POSTGRES_USER-postgres} -d ${POSTGRES_DB-penombre}'",
]
interval: 1s
timeout: 2s
retries: 10
start_period: 3s
volumes:
postgres_data:
storage_data:
networks:
proxy:
external: trueWith this setup, remove the ports mapping from the app service — the reverse proxy accesses it via http://app:3000 on the shared proxy network. The database port is also no longer exposed to the host.
Found an issue or want to contribute? Edit this page on GitHub