Files
pvemanager/compose.prod.yml
Dilshod Rasulov 0662dda6ae
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 11s
fix(compose): actually drop the published ports in production
compose.prod.yml has always intended to take the app and frontend off the
host interface and let nginx front them — "remove port exposure (nginx will
handle it)". It did not work: Compose concatenates list-valued fields when
merging -f files, so `ports: []` contributed nothing and 8000:8000 and
3001:80 survived from compose.yml.

The effect was that an SSL deployment still answered on both ports over
plain HTTP, bypassing TLS, HSTS, the security headers and the rate limits —
confirmed against the live host from off-box, where the API returned its
health payload unencrypted on :8000 and the panel loaded on :3001.

Use the !reset tag so the inherited value is dropped rather than extended.
Verified in place: the merged config now publishes only 80 and 443, both
former ports refuse connections from outside, and the panel still serves
over HTTPS.
2026-08-11 21:05:42 +05:00

86 lines
2.9 KiB
YAML

# Production Docker Compose with NGINX and SSL
# Usage: docker compose -f compose.yml -f compose.prod.yml up -d
# For SSL certificate renewal: docker compose -f compose.yml -f compose.prod.yml --profile ssl up -d
services:
nginx:
image: nginx:alpine
container_name: pvemanager-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- ./nginx/certbot/conf:/etc/letsencrypt:ro
- ./nginx/certbot/www:/var/www/certbot:rw
# nginx reads the certificate once at startup, so a certificate renewed by
# the certbot service below would keep being served in its old form until
# the container restarts — i.e. the site would start serving an expired
# certificate roughly 30 days after the first successful renewal.
# Reload every 6h so a renewed certificate is picked up in time.
command: /bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g "daemon off;"'
depends_on:
app:
condition: service_healthy
frontend:
condition: service_started
networks:
- pvemanager-network
restart: unless-stopped
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 30s
timeout: 3s
retries: 3
# nginx.conf asks for 2048 worker_connections, but Docker's default file
# descriptor limit is 1024 — so the real ceiling was a quarter of the
# configured one (each proxied connection needs an fd downstream and
# upstream), and nginx warned about it on every config test.
ulimits:
nofile:
soft: 8192
hard: 8192
deploy:
resources:
limits:
cpus: '0.5'
memory: 128M
certbot:
image: certbot/certbot:latest
container_name: pvemanager-certbot
volumes:
- ./nginx/certbot/conf:/etc/letsencrypt
- ./nginx/certbot/www:/var/www/certbot
# Renewal loop - only runs if certificates exist
entrypoint: "/bin/sh -c 'if [ -d /etc/letsencrypt/live ]; then trap exit TERM; while :; do certbot renew --quiet; sleep 12h & wait $${!}; done; else echo \"No certificates found. Exiting.\"; exit 0; fi'"
depends_on:
- nginx
networks:
- pvemanager-network
restart: "no"
profiles:
- ssl
# Override app service - remove port exposure (nginx will handle it).
# `ports: []` does NOT do that: compose concatenates list fields across -f
# files, so an empty list contributes nothing and 8000:8000 survived from
# compose.yml — leaving the API on the public interface over plain HTTP.
# !reset drops the inherited value outright.
app:
ports: !reset []
expose:
- "8000"
volumes:
- ~/.ssh:/root/.ssh:ro
- ./logs:/app/logs
- /etc/localtime:/etc/localtime:ro
# Same here: 3001:80 was still published despite `ports: []`.
frontend:
ports: !reset []
expose:
- "80"