Nginx Application Deployment Guide
This article provides a complete Nginx application deployment runbook, covering everything from installation to reverse proxy configuration, SSL certificates, SELinux policies, and log rotation. It applies to the common scenario of using Nginx as a front-end proxy forwarding to Node.js, Python (Gunicorn/uWSGI), or PHP-FPM backends.
Prerequisites
Section titled “Prerequisites”- Server has completed the New Server Baseline Checklist
- A domain name pointing to the server IP
- Backend application deployed and listening on a local port (e.g.,
127.0.0.1:3000)
Step 1: Install Nginx
Section titled “Step 1: Install Nginx”From the AppStream Repository
Section titled “From the AppStream Repository”dnf install -y nginxFrom the Official Nginx Repository (recommended for the latest stable version)
Section titled “From the Official Nginx Repository (recommended for the latest stable version)”cat > /etc/yum.repos.d/nginx.repo << 'EOF'[nginx-stable]name=nginx stable repobaseurl=https://nginx.org/packages/centos/$releasever/$basearch/gpgcheck=1enabled=1gpgkey=https://nginx.org/keys/nginx_signing.keymodule_hotfixes=trueEOF
dnf install -y nginxStart and enable the service:
systemctl enable --now nginx
# Verifysystemctl status nginxcurl -I http://localhostStep 2: Configure the Server Block
Section titled “Step 2: Configure the Server Block”Remove the default configuration and create an application-specific one:
# Back up the default configmv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.bakCreate the application config file /etc/nginx/conf.d/myapp.conf:
# Upstream backend definitionupstream myapp_backend { server 127.0.0.1:3000; keepalive 32;}
server { listen 80; server_name example.com www.example.com;
# Logging access_log /var/log/nginx/myapp_access.log; error_log /var/log/nginx/myapp_error.log;
# Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Static files (if applicable) location /static/ { alias /var/www/myapp/static/; expires 30d; add_header Cache-Control "public, immutable"; }
# Reverse proxy to backend location / { proxy_pass http://myapp_backend; proxy_http_version 1.1; 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_set_header Connection "";
# Timeouts proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; }
# Health check endpoint location /health { proxy_pass http://myapp_backend/health; access_log off; }}Backend-Specific Configurations
Section titled “Backend-Specific Configurations”PHP-FPM backend:
upstream php_backend { server unix:/run/php-fpm/www.sock;}
location ~ \.php$ { fastcgi_pass php_backend; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params;}Python (Gunicorn) backend:
upstream gunicorn_backend { server unix:/run/gunicorn/myapp.sock;}
location / { proxy_pass http://gunicorn_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;}Test and reload the configuration:
nginx -tsystemctl reload nginxStep 3: SSL Certificates (Let’s Encrypt + Certbot)
Section titled “Step 3: SSL Certificates (Let’s Encrypt + Certbot)”3.1 Install Certbot
Section titled “3.1 Install Certbot”dnf install -y epel-releasednf install -y certbot python3-certbot-nginx3.2 Obtain a Certificate
Section titled “3.2 Obtain a Certificate”certbot --nginx -d example.com -d www.example.comCertbot will automatically modify the Nginx configuration to enable SSL. Verify the resulting configuration:
nginx -t3.3 Automatic Renewal
Section titled “3.3 Automatic Renewal”Certbot automatically sets up a systemd timer for renewal:
# Check the renewal timersystemctl list-timers | grep certbot
# Test renewal (dry run)certbot renew --dry-run3.4 SSL Optimization
Section titled “3.4 SSL Optimization”Add global SSL optimization in /etc/nginx/conf.d/ssl-params.conf:
# SSL protocols and cipher suitesssl_protocols TLSv1.2 TLSv1.3;ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';ssl_prefer_server_ciphers on;
# SSL session cachessl_session_cache shared:SSL:10m;ssl_session_timeout 1d;ssl_session_tickets off;
# OCSP Staplingssl_stapling on;ssl_stapling_verify on;resolver 8.8.8.8 1.1.1.1 valid=300s;resolver_timeout 5s;
# HSTSadd_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;Step 4: SELinux Policy Configuration
Section titled “Step 4: SELinux Policy Configuration”By default, SELinux does not allow Nginx to initiate connections to network backends. You must configure the appropriate booleans:
# Allow Nginx to connect to network backendssetsebool -P httpd_can_network_connect 1
# If Nginx needs to serve content from user home directoriessetsebool -P httpd_enable_homedirs 1
# If Nginx needs to connect to a databasesetsebool -P httpd_can_network_connect_db 1If static files are in a non-standard path, set the SELinux context:
# Label web contentsemanage fcontext -a -t httpd_sys_content_t "/var/www/myapp(/.*)?"restorecon -Rv /var/www/myappIf Nginx listens on a non-standard port:
# Add port label (e.g., port 8080)semanage port -a -t http_port_t -p tcp 8080Troubleshoot SELinux denials:
# View denial recordsausearch -m AVC -ts recent
# Generate policy suggestionsaudit2why < /var/log/audit/audit.logStep 5: Firewall Configuration
Section titled “Step 5: Firewall Configuration”# Allow HTTP and HTTPSfirewall-cmd --permanent --add-service=httpfirewall-cmd --permanent --add-service=https
# Reloadfirewall-cmd --reload
# Verifyfirewall-cmd --list-servicesStep 6: Systemd Service for the Backend Application
Section titled “Step 6: Systemd Service for the Backend Application”Create a systemd service unit for the backend application. Node.js example:
cat > /etc/systemd/system/myapp.service << 'EOF'[Unit]Description=My ApplicationDocumentation=https://example.com/docsAfter=network-online.targetWants=network-online.target
[Service]Type=simpleUser=appuserGroup=appuserWorkingDirectory=/opt/myappExecStart=/usr/bin/node /opt/myapp/server.jsRestart=on-failureRestartSec=5StartLimitInterval=60StartLimitBurst=3
# Environment variablesEnvironment=NODE_ENV=productionEnvironment=PORT=3000EnvironmentFile=-/opt/myapp/.env
# Security hardeningNoNewPrivileges=trueProtectSystem=strictProtectHome=trueReadWritePaths=/opt/myapp/data /var/log/myappPrivateTmp=true
# Resource limitsLimitNOFILE=65536MemoryMax=512M
# LoggingStandardOutput=journalStandardError=journalSyslogIdentifier=myapp
[Install]WantedBy=multi-user.targetEOFGunicorn example:
cat > /etc/systemd/system/myapp.service << 'EOF'[Unit]Description=Gunicorn Application ServerAfter=network-online.target
[Service]Type=notifyUser=appuserGroup=appuserWorkingDirectory=/opt/myappExecStart=/opt/myapp/venv/bin/gunicorn \ --workers 4 \ --bind unix:/run/gunicorn/myapp.sock \ --access-logfile /var/log/myapp/access.log \ --error-logfile /var/log/myapp/error.log \ wsgi:appExecReload=/bin/kill -s HUP $MAINPIDRestart=on-failureRuntimeDirectory=gunicorn
[Install]WantedBy=multi-user.targetEOFStart the service:
# Create the application useruseradd -r -s /sbin/nologin appuser
# Set directory permissionschown -R appuser:appuser /opt/myapp
systemctl daemon-reloadsystemctl enable --now myapp
# Verifysystemctl status myappcurl http://127.0.0.1:3000/healthStep 7: Health Checks
Section titled “Step 7: Health Checks”7.1 Application-Level Health Check
Section titled “7.1 Application-Level Health Check”Ensure your backend application implements a /health endpoint that returns current service status. Minimal example (Node.js):
app.get('/health', (req, res) => { res.status(200).json({ status: 'ok', uptime: process.uptime(), timestamp: Date.now() });});7.2 External Monitoring Script
Section titled “7.2 External Monitoring Script”Create a simple health check script:
cat > /usr/local/bin/health-check.sh << 'SCRIPT'#!/bin/bashURL="http://127.0.0.1/health"HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$URL" --max-time 5)
if [ "$HTTP_CODE" != "200" ]; then echo "[$(date)] Health check failed: HTTP $HTTP_CODE" >> /var/log/health-check.log systemctl restart myapp systemctl reload nginxfiSCRIPT
chmod +x /usr/local/bin/health-check.shSchedule periodic checks:
cat > /etc/cron.d/health-check << 'EOF'*/5 * * * * root /usr/local/bin/health-check.shEOF7.3 End-to-End Verification
Section titled “7.3 End-to-End Verification”After deployment, perform a full end-to-end test:
# HTTP to HTTPS redirectcurl -I http://example.com
# HTTPS responsecurl -I https://example.com
# Backend health checkcurl https://example.com/health
# SSL certificate infoecho | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -datesStep 8: Log Rotation
Section titled “Step 8: Log Rotation”Nginx’s default log rotation is usually already configured. Add rules for custom application logs:
cat > /etc/logrotate.d/myapp << 'EOF'/var/log/myapp/*.log { daily missingok rotate 14 compress delaycompress notifempty create 0640 appuser appuser sharedscripts postrotate /bin/systemctl reload myapp > /dev/null 2>&1 || true endscript}
/var/log/nginx/myapp_*.log { daily missingok rotate 30 compress delaycompress notifempty create 0640 nginx adm sharedscripts postrotate /bin/kill -USR1 $(cat /run/nginx.pid 2>/dev/null) 2>/dev/null || true endscript}EOFTest the configuration:
logrotate -d /etc/logrotate.d/myappDeployment Checklist
Section titled “Deployment Checklist”| # | Check Item | Verification Command | Expected Result |
|---|---|---|---|
| 1 | Nginx running | systemctl status nginx | active |
| 2 | Config syntax | nginx -t | syntax is ok |
| 3 | SSL certificate | certbot certificates | Valid and not expired |
| 4 | SELinux boolean | getsebool httpd_can_network_connect | on |
| 5 | Firewall | firewall-cmd --list-services | http https |
| 6 | Backend service | systemctl status myapp | active |
| 7 | Health check | curl http://127.0.0.1/health | 200 OK |
| 8 | HTTPS access | curl -I https://example.com | 200 OK |
| 9 | Log rotation | logrotate -d /etc/logrotate.d/myapp | No errors |
Troubleshooting
Section titled “Troubleshooting”| Issue | Troubleshooting Steps |
|---|---|
| 502 Bad Gateway | Check if backend service is running; verify SELinux httpd_can_network_connect; review /var/log/nginx/myapp_error.log |
| 403 Forbidden | Check file permissions; verify SELinux context with ls -Z; run restorecon |
| SSL certificate expired | Run certbot renew; check systemd timer systemctl status certbot-renew.timer |
| Connection refused | Check firewall-cmd --list-all; confirm Nginx is listening on the correct port with ss -tlnp |