Skip to content

HTTPS with Reverse Proxy

cv4pve-admin runs on HTTP by default. For HTTPS, place a reverse proxy in front of it.

How it works: The Docker Compose setup uses two networks:

  • appnet: backend network that connects cv4pve-admin to its companion services (postgres, watchtower and, in the Enterprise Edition, apprise)
  • publicnet: network meant for the reverse proxy. It connects cv4pve-admin, the reverse proxy and, while it runs, the optional pgweb

Both are ordinary bridge networks: neither is declared internal. What is reachable from outside depends only on the published ports. By default the compose file publishes one port, the web UI: the mapping ${CV4PVE_ADMIN_PORT}:8080 of the cv4pve-admin service. The optional PgWeb tool publishes PGWEB_PORT only while it is started.

The reverse proxy joins publicnet and reaches cv4pve-admin via the hostname cv4pve-admin:8080. 8080 is the fixed container port. CV4PVE_ADMIN_PORT in .env is the host port. The companion services keep communicating via appnet and are not affected.

Forwarded headers: The application reads X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host, by default from any address. It therefore handles HTTPS termination at the proxy correctly, with no extra configuration. On the requests that arrive as HTTPS, the application also sends the HSTS header. To accept these headers only from your proxy, list its address in TrustedProxies: a client that reaches the application without the proxy can no longer change the address and the scheme the application sees.

Security headers: The pages and the other answers of the application carry X-Frame-Options: SAMEORIGIN, Content-Security-Policy: frame-ancestors 'self', X-Content-Type-Options: nosniff and Referrer-Policy: strict-origin-when-cross-origin. The pages of cv4pve-admin therefore open inside a frame only on cv4pve-admin itself, not on another site. A proxy that removes or replaces these headers takes this protection away.

Public address: Set PublicUrl to the address users open, for example https://admin.example.com/. The links in the emails (email confirmation, password reset) are built from it. Without it they take the address from the request, and the log shows a warning at startup.


The simplest option: automatic HTTPS with Let’s Encrypt, minimal configuration.

Add a caddy service to your docker-compose.yaml:

services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- publicnet # same network as cv4pve-admin
volumes:
caddy_data:
caddy_config:

Create a Caddyfile in the same directory:

admin.example.com {
reverse_proxy cv4pve-admin:8080
}

Replace admin.example.com with your domain. Caddy handles certificates automatically.


For environments already using Traefik, add it to publicnet and add labels to the cv4pve-admin service:

services:
traefik:
image: traefik:v3
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik.yml:/traefik.yml
networks:
- publicnet
cv4pve-admin:
# ... existing config ...
labels:
traefik.enable: "true"
traefik.http.routers.cv4pve-admin.entrypoints: "websecure"
traefik.http.routers.cv4pve-admin.rule: "Host(`admin.example.com`)"
traefik.http.routers.cv4pve-admin.tls: "true"
traefik.http.routers.cv4pve-admin.tls.certresolver: "letsencrypt"
traefik.http.services.cv4pve-admin.loadbalancer.server.port: "8080"
traefik.docker.network: "<project>_publicnet"

Network name: Docker Compose prefixes network names with the project name. By default the project name is the directory name: an installer setup gives cv4pve-admin-docker_publicnet. Use the full name in traefik.docker.network, or uncomment name: at the top of docker-compose.yaml to pin a fixed project name.


When Nginx runs outside Docker (directly on the host), it cannot use the Docker network name. Point it to localhost on the host port: CV4PVE_ADMIN_PORT in .env, 8080 by default. Keep the host port mapping in docker-compose.yaml.

server {
listen 443 ssl;
server_name admin.example.com;
ssl_certificate /etc/ssl/certs/admin.example.com.crt;
ssl_certificate_key /etc/ssl/private/admin.example.com.key;
location / {
proxy_pass http://localhost:8080;
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-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name admin.example.com;
return 301 https://$host$request_uri;
}