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
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
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
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
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
| Symptom | Cause | Fix |
|---|---|---|
| 502 Bad Gateway error | Backend containers not running | podman ps to check containers, podman start container_name to restart |
| SSL certificate errors | Let's Encrypt validation failed | Check DNS points to server, run sudo certbot certificates |
| Load balancing not working | All requests go to same backend | Check upstream configuration, verify least_conn directive |
| High response times | No connection pooling | Add keepalive 32 to upstream block and proxy_http_version 1.1 |
| Cache not working | Cache directory permissions | sudo chown www-data:www-data /var/cache/nginx/proxy |
| Health checks failing | Containers restarting frequently | Check container logs with podman logs container_name |
| Rate limiting too aggressive | Legitimate users blocked | Increase burst size in limit_req zone=general burst=10 |
| Log files growing too large | Logrotate not working | Test with sudo logrotate -f /etc/logrotate.d/nginx-app |
Next steps
- Set up NGINX monitoring with Prometheus and Grafana for web server observability
- Configure NGINX rate limiting and advanced security rules for DDoS protection
- Secure Podman containers with SELinux and AppArmor mandatory access controls
- Configure Podman container orchestration with systemd for production deployments
- Implement NGINX WAF with ModSecurity for enhanced container security
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Default values
DOMAIN=""
EMAIL=""
# Usage function
usage() {
echo "Usage: $0 -d DOMAIN -e EMAIL"
echo " -d DOMAIN Domain name for SSL certificate (e.g., example.com)"
echo " -e EMAIL Email for Let's Encrypt certificate"
exit 1
}
# Parse command line arguments
while getopts "d:e:h" opt; do
case $opt in
d) DOMAIN="$OPTARG" ;;
e) EMAIL="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
if [[ -z "$DOMAIN" || -z "$EMAIL" ]]; then
usage
fi
# Error handling
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
systemctl stop nginx 2>/dev/null || true
podman stop app1 app2 app3 2>/dev/null || true
podman rm app1 app2 app3 2>/dev/null || true
}
trap cleanup ERR
# Check if running as root or with sudo
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root or with sudo${NC}"
exit 1
fi
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
NGINX_USER="www-data"
NGINX_SITES_DIR="/etc/nginx/sites-available"
NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
NGINX_USER="nginx"
NGINX_SITES_DIR="/etc/nginx/conf.d"
NGINX_ENABLED_DIR="/etc/nginx/conf.d"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
NGINX_USER="nginx"
NGINX_SITES_DIR="/etc/nginx/conf.d"
NGINX_ENABLED_DIR="/etc/nginx/conf.d"
;;
*)
echo -e "${RED}Unsupported distro: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect distribution${NC}"
exit 1
fi
echo -e "${GREEN}[1/8] Updating system packages...${NC}"
$PKG_UPDATE
echo -e "${GREEN}[2/8] Installing nginx, podman, and certbot...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL nginx podman certbot python3-certbot-nginx
elif [[ "$PKG_MGR" == "dnf" ]]; then
$PKG_INSTALL nginx podman certbot python3-certbot-nginx
# Enable EPEL if needed for certbot on RHEL-based systems
if ! command -v certbot &> /dev/null; then
$PKG_INSTALL epel-release
$PKG_INSTALL certbot python3-certbot-nginx
fi
else
$PKG_INSTALL nginx podman certbot python3-certbot-nginx
fi
echo -e "${GREEN}[3/8] Starting and enabling services...${NC}"
systemctl enable --now nginx
systemctl enable --now podman
echo -e "${GREEN}[4/8] Creating example application containers...${NC}"
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
# Wait for containers to start
sleep 5
echo -e "${GREEN}[5/8] Creating custom index pages for load balancing demo...${NC}"
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'
echo -e "${GREEN}[6/8] Configuring nginx with upstream servers...${NC}"
cat > /etc/nginx/nginx.conf << EOF
user $NGINX_USER;
worker_processes auto;
pid /run/nginx.pid;
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;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
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;
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 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 $NGINX_ENABLED_DIR/*.conf;
EOF
# Add sites-enabled include for Debian-based systems
if [[ "$PKG_MGR" == "apt" ]]; then
echo " include /etc/nginx/sites-enabled/*;" >> /etc/nginx/nginx.conf
fi
echo "}" >> /etc/nginx/nginx.conf
echo -e "${GREEN}[7/8] Creating virtual host configuration...${NC}"
VHOST_FILE="$NGINX_SITES_DIR/$DOMAIN.conf"
if [[ "$PKG_MGR" == "apt" ]]; then
VHOST_FILE="$NGINX_SITES_DIR/$DOMAIN"
fi
cat > "$VHOST_FILE" << EOF
server {
listen 80;
server_name $DOMAIN www.$DOMAIN;
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_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
EOF
# Enable site for Debian-based systems
if [[ "$PKG_MGR" == "apt" ]]; then
ln -sf "$VHOST_FILE" "/etc/nginx/sites-enabled/"
rm -f /etc/nginx/sites-enabled/default
fi
# Set correct permissions
chown root:root "$VHOST_FILE"
chmod 644 "$VHOST_FILE"
# Test nginx configuration
nginx -t
# Reload nginx
systemctl reload nginx
# Configure firewall
if command -v ufw &> /dev/null; then
ufw --force enable
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
elif command -v firewall-cmd &> /dev/null; then
systemctl enable --now firewalld
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
fi
echo -e "${GREEN}[8/8] Obtaining SSL certificate...${NC}"
certbot --nginx -d "$DOMAIN" -d "www.$DOMAIN" --non-interactive --agree-tos --email "$EMAIL"
# Setup auto-renewal
systemctl enable --now certbot.timer 2>/dev/null || {
echo "0 0,12 * * * root certbot renew --quiet" >> /etc/crontab
}
echo -e "${GREEN}Installation completed successfully!${NC}"
echo -e "${YELLOW}Verification:${NC}"
echo "1. Nginx status: $(systemctl is-active nginx)"
echo "2. Podman containers:"
podman ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
echo "3. Test load balancing:"
echo " curl http://$DOMAIN (will redirect to HTTPS)"
echo " curl https://$DOMAIN"
echo -e "${GREEN}Your nginx reverse proxy with SSL is now configured!${NC}"
Review the script before running. Execute with: bash install.sh