Configure nginx reverse proxy for Podman containers with SSL and load balancing

Intermediate 45 min Apr 18, 2026 181 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up nginx as a reverse proxy for Podman containers with SSL termination, health checks, and load balancing. Includes automated SSL certificate management with Let's Encrypt and container integration.

Prerequisites

  • Root or sudo access
  • Domain name pointing to server
  • Basic understanding of containers

What this solves

Running multiple Podman containers behind a single nginx reverse proxy gives you SSL termination, load balancing, and centralized routing. This configuration handles traffic distribution, health monitoring, and certificate management for containerized applications without exposing individual container ports.

Step-by-step configuration

Update system packages

Start by updating your package manager to ensure you get the latest versions.

sudo apt update && sudo apt upgrade -y
sudo dnf update -y

Install nginx and Podman

Install nginx web server and Podman container runtime with required dependencies.

sudo apt install -y nginx podman certbot python3-certbot-nginx
sudo dnf install -y nginx podman certbot python3-certbot-nginx

Create example application containers

Run three nginx containers on different ports to demonstrate load balancing. These containers will serve as backend applications.

podman run -d --name app1 -p 8081:80 nginx:alpine
podman run -d --name app2 -p 8082:80 nginx:alpine
podman run -d --name app3 -p 8083:80 nginx:alpine

Create custom index pages

Add unique content to each container so you can verify load balancing is working.

podman exec app1 sh -c 'echo "

Application Server 1

" > /usr/share/nginx/html/index.html' podman exec app2 sh -c 'echo "

Application Server 2

" > /usr/share/nginx/html/index.html' podman exec app3 sh -c 'echo "

Application Server 3

" > /usr/share/nginx/html/index.html'

Configure nginx upstream servers

Create the main nginx configuration with upstream server definitions and load balancing.

user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 768;
}

http {
    sendfile on;
    tcp_nopush on;
    types_hash_max_size 2048;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # Logging
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;
    
    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    
    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=general:10m rate=1r/s;
    
    # Upstream servers with health checks
    upstream app_backend {
        least_conn;
        server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
        server 127.0.0.1:8082 max_fails=3 fail_timeout=30s;
        server 127.0.0.1:8083 max_fails=3 fail_timeout=30s;
        keepalive 32;
    }
    
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Create virtual host configuration

Configure the virtual host with SSL termination, security headers, and reverse proxy settings.

server {
    listen 80;
    server_name example.com www.example.com;
    
    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
    
    # SSL configuration (will be managed by certbot)
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    # SSL security settings
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    
    # Security headers
    add_header Strict-Transport-Security "max-age=63072000" always;
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Referrer-Policy "strict-origin-when-cross-origin";
    
    # Rate limiting
    limit_req zone=general burst=5 nodelay;
    
    # Health check endpoint
    location /health {
        access_log off;
        return 200 "healthy";
        add_header Content-Type text/plain;
    }
    
    # API endpoints with stricter rate limiting
    location /api/ {
        limit_req zone=api burst=10 nodelay;
        
        proxy_pass http://app_backend;
        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;
        
        # Proxy timeouts
        proxy_connect_timeout 5s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
        
        # Connection pooling
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
    
    # Main application
    location / {
        proxy_pass http://app_backend;
        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;
        
        # Proxy timeouts
        proxy_connect_timeout 5s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
        
        # Connection pooling
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        
        # Error handling
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
        proxy_next_upstream_tries 3;
        proxy_next_upstream_timeout 10s;
    }
    
    # Static file caching
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2|woff)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        add_header Vary Accept-Encoding;
    }
    
    # Logging with detailed information
    access_log /var/log/nginx/app_access.log combined;
    error_log /var/log/nginx/app_error.log;
}

Enable the virtual host

Create a symbolic link to enable the site configuration and remove the default nginx page.

sudo ln -s /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default

Test nginx configuration

Validate the nginx configuration syntax before starting the service.

sudo nginx -t

Enable and start nginx

Enable nginx to start on boot and start the service.

sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl status nginx

Configure firewall rules

Open the necessary ports for HTTP and HTTPS traffic.

sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

Obtain SSL certificates with Let's Encrypt

Use certbot to automatically obtain and configure SSL certificates. Replace example.com with your actual domain.

sudo certbot --nginx -d example.com -d www.example.com
Note: Your domain must point to this server's IP address for Let's Encrypt verification to work.

Set up automatic certificate renewal

Create a systemd timer for automatic certificate renewal every 12 hours.

[Unit]
Description=Certbot Renewal
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --post-hook "systemctl reload nginx"
User=root
[Unit]
Description=Run certbot twice daily
Requires=certbot-renewal.service

[Timer]
OnCalendar=--* 00,12:00:00
RandomizedDelaySec=3600
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl enable certbot-renewal.timer
sudo systemctl start certbot-renewal.timer

Create health check script

Create a script to monitor backend container health and restart failed containers.

#!/bin/bash

Container health check script

CONTAINERS=("app1:8081" "app2:8082" "app3:8083") LOG_FILE="/var/log/nginx/container-health.log" log_message() { echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE" } for container_info in "${CONTAINERS[@]}"; do IFS=':' read -r container_name port <<< "$container_info" # Check if container is running if ! podman ps --format "{{.Names}}" | grep -q "^$container_name$"; then log_message "WARNING: Container $container_name is not running. Attempting to restart." podman start "$container_name" || log_message "ERROR: Failed to start $container_name" continue fi # Check if container responds to HTTP requests if ! curl -f -s -o /dev/null "http://127.0.0.1:$port" --connect-timeout 5; then log_message "WARNING: Container $container_name (port $port) is not responding. Restarting." podman restart "$container_name" || log_message "ERROR: Failed to restart $container_name" fi done
sudo chmod +x /usr/local/bin/container-health-check.sh

Set up health check automation

Create a systemd service and timer to run health checks every 2 minutes.

[Unit]
Description=Container Health Check
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/container-health-check.sh
User=root
[Unit]
Description=Run container health check every 2 minutes
Requires=container-health-check.service

[Timer]
OnBootSec=2min
OnUnitActiveSec=2min

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable container-health-check.timer
sudo systemctl start container-health-check.timer

Verify your setup

Test that your nginx reverse proxy is correctly load balancing requests to the Podman containers.

# Check nginx status
sudo systemctl status nginx

Verify containers are running

podman ps

Test load balancing (run multiple times to see different backends)

curl -I http://localhost/ curl http://localhost/

Check SSL certificate

openssl s_client -connect example.com:443 -servername example.com

Verify health check is running

sudo systemctl status container-health-check.timer tail -f /var/log/nginx/container-health.log

Test failover behavior

Stop one container to verify nginx automatically routes traffic to healthy backends.

# Stop one container
podman stop app2

Verify requests still work

for i in {1..5}; do curl http://localhost/; sleep 1; done

Check nginx error logs

sudo tail -f /var/log/nginx/app_error.log

Restart the container

podman start app2

Performance optimization

Enable nginx caching

Add response caching to reduce backend load and improve performance.

# Proxy cache configuration
proxy_cache_path /var/cache/nginx/proxy levels=1:2 keys_zone=app_cache:10m max_size=1g inactive=60m use_temp_path=off;

Cache key definition

proxy_cache_key "$scheme$request_method$host$request_uri";

Cache status header

add_header X-Cache-Status $upstream_cache_status;

Update virtual host with caching

Add caching directives to your main location block in /etc/nginx/sites-available/app.conf.

location / {
    # Enable caching
    proxy_cache app_cache;
    proxy_cache_valid 200 10m;
    proxy_cache_valid 404 1m;
    proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
    proxy_cache_bypass $http_pragma $http_authorization;
    
    proxy_pass http://app_backend;
    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;
    
    # Proxy timeouts
    proxy_connect_timeout 5s;
    proxy_send_timeout 30s;
    proxy_read_timeout 30s;
    
    # Connection pooling
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    
    # Error handling
    proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
    proxy_next_upstream_tries 3;
    proxy_next_upstream_timeout 10s;
}

Create cache directory and reload nginx

Create the cache directory with correct permissions and reload nginx.

sudo mkdir -p /var/cache/nginx/proxy
sudo chown www-data:www-data /var/cache/nginx/proxy
sudo chmod 755 /var/cache/nginx/proxy
sudo nginx -t
sudo systemctl reload nginx

Monitoring and logging

Set up log rotation

Configure logrotate to manage nginx and container health check logs.

/var/log/nginx/app_*.log /var/log/nginx/container-health.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    create 644 www-data www-data
    postrotate
        systemctl reload nginx
    endscript
}

Create monitoring script

Set up a script to monitor key metrics and send alerts when thresholds are exceeded.

#!/bin/bash

Nginx monitoring script

LOG_FILE="/var/log/nginx/monitor.log" ERROR_THRESHOLD=100 # Alert if more than 100 errors in last hour RESPONSE_TIME_THRESHOLD=2 # Alert if response time > 2 seconds log_message() { echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE" }

Check nginx is running

if ! systemctl is-active --quiet nginx; then log_message "CRITICAL: nginx service is not running" exit 1 fi

Check error rate

ERROR_COUNT=$(grep "$(date '+%d/%b/%Y:%H')" /var/log/nginx/app_error.log 2>/dev/null | wc -l) if [ "$ERROR_COUNT" -gt "$ERROR_THRESHOLD" ]; then log_message "WARNING: High error rate detected: $ERROR_COUNT errors in the last hour" fi

Check backend connectivity

for port in 8081 8082 8083; do if ! curl -f -s -o /dev/null "http://127.0.0.1:$port" --max-time 5; then log_message "WARNING: Backend on port $port is not responding" fi done

Check disk space for cache

CACHE_USAGE=$(df /var/cache/nginx 2>/dev/null | awk 'NR==2 {print $5}' | sed 's/%//') if [ "$CACHE_USAGE" -gt 90 ]; then log_message "WARNING: Nginx cache directory is $CACHE_USAGE% full" fi log_message "INFO: Monitoring check completed successfully"
sudo chmod +x /usr/local/bin/nginx-monitor.sh

Common issues

SymptomCauseFix
502 Bad Gateway errorBackend containers not runningpodman ps to check containers, podman start container_name to restart
SSL certificate errorsLet's Encrypt validation failedCheck DNS points to server, run sudo certbot certificates
Load balancing not workingAll requests go to same backendCheck upstream configuration, verify least_conn directive
High response timesNo connection poolingAdd keepalive 32 to upstream block and proxy_http_version 1.1
Cache not workingCache directory permissionssudo chown www-data:www-data /var/cache/nginx/proxy
Health checks failingContainers restarting frequentlyCheck container logs with podman logs container_name
Rate limiting too aggressiveLegitimate users blockedIncrease burst size in limit_req zone=general burst=10
Log files growing too largeLogrotate not workingTest with sudo logrotate -f /etc/logrotate.d/nginx-app

Next steps

Running this in production?

Want this handled for you? Setting up Nginx reverse proxy once is straightforward. Keeping it patched, monitored, backed up and tuned across environments is the harder part. See how we run infrastructure like this for European SaaS and e-commerce teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed cloud infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.