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-fpmand verify thefastcgi_passpath matches the socket in/run/php/. - Permission denied: Ensure the web root is owned by
www-dataand PHP-FPM runs aswww-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 tohttps://or use protocol-relative URLs. - Blank PHP pages: Check the PHP error log at
/var/log/php8.3-fpm.logand enabledisplay_errors = Ontemporarily inphp.inifor 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.