Back to Learn

Setup Nginx + PHP + SSL with Certbot on Ubuntu | NOC.org

Overview

This guide walks through setting up a production-ready web server on Ubuntu with Nginx as the web server, PHP-FPM for PHP processing, and a free SSL certificate from Let's Encrypt via Certbot. The instructions apply to Ubuntu 22.04 and 24.04 LTS, though the general process is similar on other Debian-based distributions.

Prerequisites

  • A server running Ubuntu 22.04 or 24.04 LTS with root or sudo access
  • A registered domain name with DNS A records pointing to your server's IP address
  • Ports 80 and 443 open in your firewall

Start by updating the package index:

sudo apt update && sudo apt upgrade -y

Step 1: Install Nginx

sudo apt install nginx -y

Verify Nginx is running:

sudo systemctl status nginx

You should see "active (running)". If you visit your server's IP address in a browser, you will see the default Nginx welcome page.

Enable Nginx to start automatically on boot:

sudo systemctl enable nginx

Step 2: Install PHP-FPM

PHP-FPM (FastCGI Process Manager) is the recommended way to run PHP with Nginx. Install PHP and commonly needed extensions:

# Ubuntu 24.04 ships PHP 8.3
sudo apt install php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-zip -y

Check which PHP-FPM version was installed and verify it is running:

php -v
sudo systemctl status php8.3-fpm

If you are on Ubuntu 22.04, the default is PHP 8.1, so the service name would be php8.1-fpm.

Step 3: Configure the Nginx Server Block

Create a new server block configuration for your domain:

sudo nano /etc/nginx/sites-available/example.com

Add the following configuration:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    root /var/www/example.com/public;
    index index.php index.html;

    # Logging
    access_log /var/log/nginx/example.com.access.log;
    error_log /var/log/nginx/example.com.error.log;

    # Handle PHP files
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # Deny access to .htaccess files
    location ~ /\.ht {
        deny all;
    }

    # Static files caching
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
}

Create the web root directory and a test PHP file:

sudo mkdir -p /var/www/example.com/public
echo "<?php phpinfo();" | sudo tee /var/www/example.com/public/index.php
sudo chown -R www-data:www-data /var/www/example.com

Enable the site and test the configuration:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

If nginx -t reports "syntax is ok" and "test is successful", the configuration is valid.

Step 4: PHP-FPM Pool Configuration

The default PHP-FPM pool configuration works for most sites, but you should review and tune it for production. The pool config is located at /etc/php/8.3/fpm/pool.d/www.conf:

# Key settings to review:

# Process manager - 'dynamic' is recommended for most sites
pm = dynamic

# Maximum number of child processes
pm.max_children = 50

# Number of children created on startup
pm.start_servers = 5

# Minimum number of idle children
pm.min_spare_servers = 5

# Maximum number of idle children
pm.max_spare_servers = 35

# Maximum requests per child before respawning (prevents memory leaks)
pm.max_requests = 500

The pm.max_children value depends on your server's available memory. A rough formula: (Total RAM - RAM for OS and other services) / Average PHP process memory. You can check PHP process memory with:

ps --no-headers -o "rss,cmd" -C php-fpm8.3 | awk '{sum+=$1; n++} END {print sum/n/1024 " MB average per process"}'

After making changes, restart PHP-FPM:

sudo systemctl restart php8.3-fpm

Step 5: Install Certbot and Obtain an SSL Certificate

Certbot automates the process of obtaining and installing SSL certificates from Let's Encrypt:

sudo apt install certbot python3-certbot-nginx -y

Obtain a certificate for your domain. Certbot will automatically modify your Nginx configuration to enable SSL:

sudo certbot --nginx -d example.com -d www.example.com

Certbot will ask for your email address (for renewal notifications) and whether to redirect HTTP to HTTPS. Always choose to redirect — there is no reason to serve a site over plain HTTP in production.

After Certbot finishes, your Nginx configuration will be updated with SSL directives, and your site will be accessible over HTTPS.

Step 6: Auto-Renewal

Let's Encrypt certificates expire after 90 days. Certbot installs a systemd timer that automatically renews certificates before they expire:

# Check the timer status
sudo systemctl status certbot.timer

# Test renewal (dry run)
sudo certbot renew --dry-run

If the dry run succeeds, auto-renewal is working. Certbot checks for renewal twice daily and only renews certificates that are within 30 days of expiration.

If for some reason the systemd timer is not present, you can add a cron job as a fallback:

# Add to root's crontab
sudo crontab -e

# Renew at 3:30 AM daily
30 3 * * * certbot renew --quiet --post-hook "systemctl reload nginx"

Step 7: Testing SSL

Verify your SSL configuration is working correctly:

# Test with curl
curl -I https://example.com

# Test SSL handshake with openssl
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | openssl x509 -noout -subject -dates

This shows the certificate subject and validity dates. You can also use online tools like SSL Labs (ssllabs.com/ssltest) for a comprehensive SSL test and grade.

Step 8: Security Hardening

The default Certbot SSL configuration is good but can be improved. Edit your Nginx server block (or create a shared snippet) to add stronger TLS settings:

# /etc/nginx/snippets/ssl-hardening.conf

# Only allow TLS 1.2 and 1.3
ssl_protocols TLSv1.2 TLSv1.3;

# Strong cipher suite
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;

# HSTS - Enforce HTTPS for 1 year
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 1.1.1.1 valid=300s;
resolver_timeout 5s;

# SSL session caching
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

Include it in your server block:

server {
    listen 443 ssl http2;
    server_name example.com;

    include snippets/ssl-hardening.conf;

    # ... rest of configuration
}

For a complete guide to security headers beyond HSTS, see our articles on security headers and configuring security headers.

Complete Nginx Configuration Example

Here is what a fully configured server block looks like after all steps:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;
    root /var/www/example.com/public;
    index index.php index.html;

    # SSL (managed by Certbot)
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Security hardening
    include snippets/ssl-hardening.conf;

    # Logging
    access_log /var/log/nginx/example.com.access.log;
    error_log /var/log/nginx/example.com.error.log;

    # PHP-FPM
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~ /\.ht {
        deny all;
    }

    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
}

Test and reload:

sudo nginx -t && sudo systemctl reload nginx

Troubleshooting Common Issues

  • 502 Bad Gateway: PHP-FPM is not running or the socket path is wrong. Check systemctl status php8.3-fpm and verify the fastcgi_pass path matches the socket in /run/php/.
  • Permission denied: Ensure the web root is owned by www-data and PHP-FPM runs as www-data.
  • Certbot fails: Make sure port 80 is open, DNS points to your server, and no other process is using port 80 during the challenge.
  • Mixed content warnings: After enabling HTTPS, update all hardcoded http:// URLs in your application to https:// or use protocol-relative URLs.
  • Blank PHP pages: Check the PHP error log at /var/log/php8.3-fpm.log and enable display_errors = On temporarily in php.ini for debugging.

Summary

Setting up Nginx with PHP-FPM and SSL on Ubuntu is a standard procedure for deploying modern web applications. Install Nginx and PHP-FPM from the default repositories, create a server block for your domain, use Certbot to obtain a free Let's Encrypt SSL certificate, and harden the TLS configuration for production use. With auto-renewal configured, your certificates will stay valid without manual intervention. For additional security, add HSTS headers and the full suite of security headers to protect your site and its visitors.

Improve Your Websites Speed and Security

14 days free trial. No credit card required.