Skip to content
Search RunEntLinux

Searches general documentation on RunEntLinux. Each distribution has its own documentation search.

    Browse this documentation

    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.

    • 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)
    Terminal window
    dnf install -y nginx
    Section titled “From the Official Nginx Repository (recommended for the latest stable version)”
    Terminal window
    cat > /etc/yum.repos.d/nginx.repo << 'EOF'
    [nginx-stable]
    name=nginx stable repo
    baseurl=https://nginx.org/packages/centos/$releasever/$basearch/
    gpgcheck=1
    enabled=1
    gpgkey=https://nginx.org/keys/nginx_signing.key
    module_hotfixes=true
    EOF
    dnf install -y nginx

    Start and enable the service:

    Terminal window
    systemctl enable --now nginx
    # Verify
    systemctl status nginx
    curl -I http://localhost

    Remove the default configuration and create an application-specific one:

    Terminal window
    # Back up the default config
    mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.bak

    Create the application config file /etc/nginx/conf.d/myapp.conf:

    # Upstream backend definition
    upstream 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;
    }
    }

    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:

    Terminal window
    nginx -t
    systemctl reload nginx

    Step 3: SSL Certificates (Let’s Encrypt + Certbot)

    Section titled “Step 3: SSL Certificates (Let’s Encrypt + Certbot)”
    Terminal window
    dnf install -y epel-release
    dnf install -y certbot python3-certbot-nginx
    Terminal window
    certbot --nginx -d example.com -d www.example.com

    Certbot will automatically modify the Nginx configuration to enable SSL. Verify the resulting configuration:

    Terminal window
    nginx -t

    Certbot automatically sets up a systemd timer for renewal:

    Terminal window
    # Check the renewal timer
    systemctl list-timers | grep certbot
    # Test renewal (dry run)
    certbot renew --dry-run

    Add global SSL optimization in /etc/nginx/conf.d/ssl-params.conf:

    # SSL protocols and cipher suites
    ssl_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 cache
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 1.1.1.1 valid=300s;
    resolver_timeout 5s;
    # HSTS
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

    By default, SELinux does not allow Nginx to initiate connections to network backends. You must configure the appropriate booleans:

    Terminal window
    # Allow Nginx to connect to network backends
    setsebool -P httpd_can_network_connect 1
    # If Nginx needs to serve content from user home directories
    setsebool -P httpd_enable_homedirs 1
    # If Nginx needs to connect to a database
    setsebool -P httpd_can_network_connect_db 1

    If static files are in a non-standard path, set the SELinux context:

    Terminal window
    # Label web content
    semanage fcontext -a -t httpd_sys_content_t "/var/www/myapp(/.*)?"
    restorecon -Rv /var/www/myapp

    If Nginx listens on a non-standard port:

    Terminal window
    # Add port label (e.g., port 8080)
    semanage port -a -t http_port_t -p tcp 8080

    Troubleshoot SELinux denials:

    Terminal window
    # View denial records
    ausearch -m AVC -ts recent
    # Generate policy suggestions
    audit2why < /var/log/audit/audit.log
    Terminal window
    # Allow HTTP and HTTPS
    firewall-cmd --permanent --add-service=http
    firewall-cmd --permanent --add-service=https
    # Reload
    firewall-cmd --reload
    # Verify
    firewall-cmd --list-services

    Step 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:

    Terminal window
    cat > /etc/systemd/system/myapp.service << 'EOF'
    [Unit]
    Description=My Application
    Documentation=https://example.com/docs
    After=network-online.target
    Wants=network-online.target
    [Service]
    Type=simple
    User=appuser
    Group=appuser
    WorkingDirectory=/opt/myapp
    ExecStart=/usr/bin/node /opt/myapp/server.js
    Restart=on-failure
    RestartSec=5
    StartLimitInterval=60
    StartLimitBurst=3
    # Environment variables
    Environment=NODE_ENV=production
    Environment=PORT=3000
    EnvironmentFile=-/opt/myapp/.env
    # Security hardening
    NoNewPrivileges=true
    ProtectSystem=strict
    ProtectHome=true
    ReadWritePaths=/opt/myapp/data /var/log/myapp
    PrivateTmp=true
    # Resource limits
    LimitNOFILE=65536
    MemoryMax=512M
    # Logging
    StandardOutput=journal
    StandardError=journal
    SyslogIdentifier=myapp
    [Install]
    WantedBy=multi-user.target
    EOF

    Gunicorn example:

    Terminal window
    cat > /etc/systemd/system/myapp.service << 'EOF'
    [Unit]
    Description=Gunicorn Application Server
    After=network-online.target
    [Service]
    Type=notify
    User=appuser
    Group=appuser
    WorkingDirectory=/opt/myapp
    ExecStart=/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:app
    ExecReload=/bin/kill -s HUP $MAINPID
    Restart=on-failure
    RuntimeDirectory=gunicorn
    [Install]
    WantedBy=multi-user.target
    EOF

    Start the service:

    Terminal window
    # Create the application user
    useradd -r -s /sbin/nologin appuser
    # Set directory permissions
    chown -R appuser:appuser /opt/myapp
    systemctl daemon-reload
    systemctl enable --now myapp
    # Verify
    systemctl status myapp
    curl http://127.0.0.1:3000/health

    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()
    });
    });

    Create a simple health check script:

    cat > /usr/local/bin/health-check.sh << 'SCRIPT'
    #!/bin/bash
    URL="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 nginx
    fi
    SCRIPT
    chmod +x /usr/local/bin/health-check.sh

    Schedule periodic checks:

    Terminal window
    cat > /etc/cron.d/health-check << 'EOF'
    */5 * * * * root /usr/local/bin/health-check.sh
    EOF

    After deployment, perform a full end-to-end test:

    Terminal window
    # HTTP to HTTPS redirect
    curl -I http://example.com
    # HTTPS response
    curl -I https://example.com
    # Backend health check
    curl https://example.com/health
    # SSL certificate info
    echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates

    Nginx’s default log rotation is usually already configured. Add rules for custom application logs:

    Terminal window
    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
    }
    EOF

    Test the configuration:

    Terminal window
    logrotate -d /etc/logrotate.d/myapp
    #Check ItemVerification CommandExpected Result
    1Nginx runningsystemctl status nginxactive
    2Config syntaxnginx -tsyntax is ok
    3SSL certificatecertbot certificatesValid and not expired
    4SELinux booleangetsebool httpd_can_network_connecton
    5Firewallfirewall-cmd --list-serviceshttp https
    6Backend servicesystemctl status myappactive
    7Health checkcurl http://127.0.0.1/health200 OK
    8HTTPS accesscurl -I https://example.com200 OK
    9Log rotationlogrotate -d /etc/logrotate.d/myappNo errors
    IssueTroubleshooting Steps
    502 Bad GatewayCheck if backend service is running; verify SELinux httpd_can_network_connect; review /var/log/nginx/myapp_error.log
    403 ForbiddenCheck file permissions; verify SELinux context with ls -Z; run restorecon
    SSL certificate expiredRun certbot renew; check systemd timer systemctl status certbot-renew.timer
    Connection refusedCheck firewall-cmd --list-all; confirm Nginx is listening on the correct port with ss -tlnp