# 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"