🌐

nginx

29 notes  •  Web Hosting

Enable Gzip Compression in Nginx

Enabling gzip compression reduces the size of transmitted responses, improving page load times for clients. Add or uncomment the following directives in the http {} block of /etc/nginx/nginx.conf.

Prerequisites

  • Nginx installed and running
  • Write access to /etc/nginx/nginx.conf

Steps

Open the main Nginx config:

sudo nano /etc/nginx/nginx.conf

Add or update the following inside the http {} block:

http {
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_min_length 256;
    gzip_types
        text/plain
        text/css
        application/json
        application/javascript
        text/xml
        application/xml
        application/xml+rss
        text/javascript
        image/svg+xml
        application/x-font-ttf
        font/opentype;
}

Test and reload:

sudo nginx -t
sudo systemctl reload nginx

Key Settings

  • gzip_comp_level — 1 (fastest) to 9 (smallest); 6 is a good balance
  • gzip_min_length — skip compression for responses smaller than this (bytes)
  • gzip_vary on — adds Vary: Accept-Encoding header for cache correctness

Notes

Verify compression is active with curl:

curl -H "Accept-Encoding: gzip" -I https://example.com/

Look for Content-Encoding: gzip in the response headers.

Nginx Default File Paths

This reference lists the default file and directory paths used by Nginx on Debian/Ubuntu and RHEL/CentOS systems.

Check Installed Version and Compile Options

nginx -V

Key Paths

# Main configuration directory
/etc/nginx/

# Primary configuration file
/etc/nginx/nginx.conf

# Virtual host (server block) definitions
/etc/nginx/sites-available/    # available configs
/etc/nginx/sites-enabled/      # symlinks to active configs

# Default document root (served files)
/usr/share/nginx/html/         # RHEL/CentOS default
/var/www/html/                 # Debian/Ubuntu default

# Default site config
/etc/nginx/sites-available/default

# Per-request config snippets (included by sites)
/etc/nginx/conf.d/

# Log files
/var/log/nginx/access.log
/var/log/nginx/error.log

# PID file
/var/run/nginx.pid

Notes

Enable a site by creating a symlink from sites-available to sites-enabled:

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

Install PHP 7.2 + phpMyAdmin + Nginx on Ubuntu 16.04

This guide walks through installing Nginx, PHP 7.2-FPM, MySQL, and phpMyAdmin on Ubuntu 16.04.

Prerequisites

  • Ubuntu 16.04 server with root or sudo access
  • SSH access to the server

Steps

1. Update packages

sudo apt update && sudo apt upgrade -y

2. Install Nginx (mainline)

echo "deb http://nginx.org/packages/mainline/ubuntu/ xenial nginx" | sudo tee -a /etc/apt/sources.list
echo "deb-src http://nginx.org/packages/mainline/ubuntu/ xenial nginx" | sudo tee -a /etc/apt/sources.list
wget -qO - https://nginx.org/keys/nginx_signing.key | sudo apt-key add -
sudo apt update
sudo apt install -y nginx
sudo systemctl start nginx
sudo systemctl enable nginx

3. Install MySQL

sudo apt install -y mysql-server
sudo mysql_secure_installation

4. Install PHP 7.2 and required extensions

sudo apt install -y software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install -y php7.2-fpm php7.2-mysql php7.2-curl php7.2-gd     php7.2-mbstring php7.2-xmlrpc php7.2-xml php7.2-zip
sudo systemctl restart php7.2-fpm
sudo systemctl enable php7.2-fpm

5. Install phpMyAdmin

sudo apt install -y phpmyadmin
# Create symlink into your web root
sudo ln -s /usr/share/phpmyadmin /var/www/html/phpmyadmin

6. Configure Nginx server block

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

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

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php7.2-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }
}
sudo nginx -t && sudo systemctl reload nginx

Notes

Check service statuses with:

sudo service nginx status
sudo service php7.2-fpm status
sudo service mysql status

Fix Nginx PHP-FPM "Connection Refused" Error

The error connect() failed (111: Connection refused) in Nginx logs means Nginx is trying to reach PHP-FPM on a TCP port, but PHP-FPM is configured to listen on a Unix socket (or vice versa).

Diagnose the Problem

Check the error log:

sudo tail -f /var/log/nginx/error.log

Example error:

connect() failed (111: Connection refused) while connecting to upstream,
upstream: "fastcgi://127.0.0.1:9000"

Steps

1. Check what PHP-FPM is actually listening on

sudo grep -i "^listen" /etc/php/7.2/fpm/pool.d/www.conf

Typical outputs:

# Unix socket (preferred):
listen = /run/php/php7.2-fpm.sock

# TCP port:
listen = 127.0.0.1:9000

2. Match the Nginx fastcgi_pass directive to the PHP-FPM listen value

For a Unix socket:

location ~ \.php$ {
    fastcgi_pass unix:/run/php/php7.2-fpm.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

For TCP port:

location ~ \.php$ {
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

3. Verify PHP-FPM is running, then reload Nginx

sudo systemctl status php7.2-fpm
sudo nginx -t && sudo systemctl reload nginx

Key Settings

  • listen in /etc/php/<ver>/fpm/pool.d/www.conf — defines the socket or address PHP-FPM binds to
  • fastcgi_pass in Nginx — must match the PHP-FPM listen value exactly

Notes

Unix sockets are faster than TCP for local communication. TCP is easier to use when Nginx and PHP-FPM run in separate containers.

LEMP Stack Installation: Nginx, MariaDB, PHP, phpMyAdmin on Ubuntu

Install a full LEMP stack (Linux, Nginx, MariaDB, PHP) with phpMyAdmin on Ubuntu 14.04 or later.

Prerequisites

  • Ubuntu server with sudo privileges
  • Ports 80 and 443 open in firewall

Steps

1. Install Nginx

sudo apt-get update
sudo apt-get install -y nginx
sudo service nginx start

Set worker_processes to match CPU count (lscpu to check):

sudo nano /etc/nginx/nginx.conf
# Set: worker_processes 4;
sudo service nginx restart

2. Install MariaDB

sudo apt-get install -y mariadb-server mariadb-client
sudo service mysql start
sudo mysql_secure_installation

3. Install PHP and PHP-FPM

sudo apt-get install -y php5 php5-fpm php5-mysql

Set cgi.fix_pathinfo=0 to prevent path traversal exploits:

sudo nano /etc/php5/fpm/php.ini
# Find and set: cgi.fix_pathinfo=0
sudo service php5-fpm restart

4. Configure Nginx to pass PHP to FPM

sudo nano /etc/nginx/sites-available/default
server {
    listen 80 default_server;
    root /usr/share/nginx/html;
    index index.php index.html index.htm;
    server_name localhost;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php5-fpm.sock;
        fastcgi_index index.php;
        include fastcgi.conf;
    }
}
sudo nginx -t && sudo service nginx restart

5. Install phpMyAdmin

sudo apt-get install -y phpmyadmin
sudo ln -s /usr/share/phpmyadmin /usr/share/nginx/html/phpmyadmin

Notes

Test that PHP is processing correctly by creating a test file:

echo '<?php phpinfo(); ?>' | sudo tee /usr/share/nginx/html/info.php

Remove it after testing: sudo rm /usr/share/nginx/html/info.php

Nginx Config: HTTP and HTTPS with PHP-FPM

A complete Nginx virtual host configuration serving both HTTP (port 80) and HTTPS (port 443) with PHP-FPM as the backend.

Prerequisites

  • SSL certificate and key files available on disk
  • PHP-FPM installed and running

Configuration

# /etc/nginx/sites-available/example.com

# HTTP server block — redirect all traffic to HTTPS
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name example.com www.example.com;

    # Redirect all HTTP to HTTPS
    return 301 https://$host$request_uri;
}

# HTTPS server block
server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;

    server_name example.com www.example.com;
    root /var/www/example.com/public;
    index index.php index.html;

    # SSL certificates
    ssl_certificate     /etc/ssl/certs/example.com.crt;
    ssl_certificate_key /etc/ssl/private/example.com.key;

    # Modern TLS only
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";

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

    # PHP-FPM handler
    location ~ \.php$ {
        try_files $uri =404;
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    }

    # Deny hidden files
    location ~ /\.ht {
        deny all;
    }

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

Steps

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

Key Settings

  • ssl_certificate — path to concatenated cert + intermediates (ssl-bundle.crt)
  • fastcgi_pass — must match the PHP-FPM pool listen directive
  • try_files — enables clean URLs for PHP frameworks

Fix Magento 2 + Varnish 5 Error 503 Backend Fetch Failed

Magento 2.x returns a Varnish 503 "Backend Fetch Failed" error because the Varnish health check URL does not match the Nginx location block for PHP entry points.

Symptoms

  • HTTP 503 errors served by Varnish
  • Varnish logs show: Backend fetch failed
  • Stack: Magento 2.x + Varnish 4.x or 5.x + Nginx + PHP 7.x

Fix 1: Update Varnish health check URL

Edit /etc/varnish/default.vcl and find the health check probe. Change:

.url = "/pub/health_check.php";

To:

.url = "/health_check.php";

Fix 2: Add health_check to Nginx PHP location block

In your Magento Nginx config (often nginx.conf.sample inside the Magento root), find the PHP entry point location and add health_check:

# Before:
location ~ (index|get|static|report|404|503)\.php$ {

# After:
location ~ (index|get|static|report|404|503|health_check)\.php$ {

Steps

# Edit Varnish VCL
sudo nano /etc/varnish/default.vcl

# Edit Magento Nginx config
sudo nano /var/www/magento/nginx.conf.sample

# Reload both services
sudo systemctl reload varnish
sudo nginx -t && sudo systemctl reload nginx

Notes

This fix applies to Varnish 4.x and 5.x with Nginx. The bug is that Magento's generated VCL uses a /pub/ prefix path that Nginx does not expose as a PHP endpoint by default.

Nginx: Redirect HTTP to HTTPS

Configure Nginx to permanently redirect all HTTP traffic to HTTPS using a return 301 directive.

Configuration

# /etc/nginx/sites-available/example.com

# Block 1: Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    # Permanent redirect — preserves the full URI path
    return 301 https://www.example.com$request_uri;
}

# Block 2: HTTPS server
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    root /var/www/example.com;
    index index.php index.htm index.html;

    # SSL certificates
    ssl on;
    ssl_certificate     /etc/ssl/certs/example.com.crt;
    ssl_certificate_key /etc/ssl/private/example.com.key;

    # Strong TLS configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
    add_header X-Content-Type-Options "nosniff";

    # PHP-FPM
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/var/run/php-fpm/www.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

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

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

Steps

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

Key Settings

  • return 301 — permanent redirect; use 302 only for temporary redirects
  • $request_uri — preserves path and query string in the redirect
  • http2 — enable HTTP/2 on the HTTPS listener for performance

Install a Commercial SSL Certificate on Nginx

Install a commercially purchased SSL certificate (e.g., from Comodo/Sectigo, DigiCert, or similar CA) on an Nginx server.

Prerequisites

  • Certificate file (domain.crt) and CA bundle (domain.ca-bundle) from your CA
  • Private key (domain.key) generated when you created the CSR
  • Nginx installed with write access to config files

Steps

1. Bundle the certificate with the CA chain

cat domain.crt domain.ca-bundle > ssl-bundle.crt

2. Store cert files in Nginx ssl directory

sudo mkdir -p /etc/nginx/ssl/example.com/
sudo mv ssl-bundle.crt /etc/nginx/ssl/example.com/
sudo mv example.com.key /etc/nginx/ssl/example.com/
sudo chmod 600 /etc/nginx/ssl/example.com/*.key

3. Configure the Nginx server block

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    root /var/www/example.com;
    index index.php index.html;

    # Certificate bundle (cert + intermediates)
    ssl_certificate     /etc/nginx/ssl/example.com/ssl-bundle.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com/example.com.key;

    # TLS settings
    ssl_session_timeout 1d;
    ssl_session_cache   shared:SSL:50m;
    ssl_session_tickets off;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    # HSTS (6 months)
    add_header Strict-Transport-Security "max-age=15768000";

    # OCSP Stapling (optional but recommended)
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/nginx/ssl/example.com/ssl-bundle.crt;
    resolver 8.8.8.8 8.8.4.4 valid=300s;

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

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

4. Test and reload

sudo nginx -t -c /etc/nginx/nginx.conf
sudo systemctl reload nginx

Notes

  • The order in the bundle must be: site cert first, then intermediate(s), then root
  • For wildcard or multi-domain certs, reference the same cert files in each relevant server block
  • Verify installation with an SSL checker such as SSL Labs: https://www.ssllabs.com/ssltest/

Nginx Rewrite Rules: Redirect Subfolder to Root

Use Nginx location blocks with return directives to redirect requests from a subfolder path to the site root or another URL.

Example: Redirect /subfolder/ to /

server {
    listen 80;
    server_name example.com;

    # Redirect /ubb/ and everything under it to root
    location ^~ /ubb/ {
        return 302 /;
    }

    # Permanent redirect example
    location ^~ /old-section/ {
        return 301 /new-section/;
    }

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

Key Directives

  • ^~ — prefix match; stops searching for regex locations once matched
  • return 301 — permanent redirect (browser caches the redirect)
  • return 302 — temporary redirect (browser does not cache)

Rewrite with regex capture

# Redirect /blog/123 to /articles/123
location ~ ^/blog/(.*)$ {
    return 301 /articles/$1;
}

Notes

Prefer return over rewrite for simple redirects — it is faster and clearer. Use rewrite only when you need to modify the URI internally without sending a redirect to the client.

sudo nginx -t && sudo systemctl reload nginx

Nginx Config with PHP 5.6-FPM and Let's Encrypt SSL

A complete Nginx server block for a PHP 5.6-FPM site with Let's Encrypt SSL, including the HTTP-to-HTTPS redirect managed by Certbot.

Configuration

# /etc/nginx/sites-available/example.com

# HTTPS server block
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com;

    root  /var/www/html;
    index index.php index.html index.htm;

    # Let's Encrypt certificates (managed by Certbot)
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    # Route all requests through index.php (framework front-controller)
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP 5.6-FPM handler
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass    unix:/var/run/php/php5.6-fpm.sock;
        fastcgi_index   index.php;
        fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include         fastcgi_params;
        fastcgi_read_timeout 300;
    }

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

# HTTP redirect block (managed by Certbot)
server {
    listen 80;
    listen [::]:80;
    server_name example.com;

    # Certbot adds this redirect automatically
    if ($host = example.com) {
        return 301 https://$host$request_uri;
    }

    return 404;
}

Notes

  • fastcgi_read_timeout 300 — increase for long-running PHP processes (e.g., imports)
  • PHP 5.6 is end-of-life; upgrade to PHP 7.4+ or 8.x for security support
  • Run certbot --nginx -d example.com to obtain and auto-configure Let's Encrypt certs
sudo nginx -t && sudo systemctl reload nginx

Nginx Config for a Symfony/Framework App with HTTPS Redirect

A Nginx server block for a PHP framework application (e.g., Symfony, Laravel) using a web/ document root subdirectory, with Let's Encrypt SSL and HTTP-to-HTTPS redirect.

Configuration

# /etc/nginx/sites-available/example.com

# HTTPS server block
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com;

    # Symfony/framework web root is typically /web or /public
    root  /var/www/html/web;
    index index.php index.html index.htm;

    # Let's Encrypt certificates (managed by Certbot)
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    # Front-controller routing (Symfony app.php or index.php)
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP-FPM handler
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass    unix:/var/run/php/php5.6-fpm.sock;
        fastcgi_index   index.php;
        fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include         fastcgi_params;
        fastcgi_read_timeout 300;
    }

    # Deny hidden files (.env, .htaccess, etc.)
    location ~ /\. {
        deny all;
    }
}

# HTTP redirect block
server {
    listen 80;
    server_name example.com;

    if ($host = example.com) {
        return 301 https://$host$request_uri;
    }

    return 404;
}

Key Settings

  • root /var/www/html/web — adjust to /public for Laravel or modern Symfony
  • try_files — sends all non-file requests to index.php for framework routing
  • fastcgi_read_timeout 300 — extend for CLI-style requests or heavy tasks
sudo nginx -t && sudo systemctl reload nginx

Nginx Production Vhost with SSL and Access Logs

A production-ready Nginx virtual host configuration with SSL termination, access/error logs, and modular includes for PHP and WordPress.

Configuration

# /etc/nginx/sites-available/example.com

server {
    # HTTP — redirect to HTTPS
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

server {
    # HTTPS
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com www.example.com;

    root  /var/www/example.com/htdocs;
    index index.php index.html index.htm;

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

    # SSL — commercial cert bundle (cat site.crt + ca-bundle > ssl-bundle.crt)
    ssl_certificate     /etc/nginx/ssl/example_com.bundle;
    ssl_certificate_key /etc/nginx/ssl/example_com.key;
    ssl_protocols       TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    # Modular includes (adjust paths for your setup)
    include /etc/nginx/conf.d/php.conf;          # PHP-FPM handler
    include /etc/nginx/conf.d/wordpress.conf;    # WordPress-specific rules
    include /etc/nginx/conf.d/security.conf;     # Deny hidden files, etc.

    # Or include site-specific overrides:
    # include /var/www/example.com/conf/nginx/*.conf;

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

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/run/php/php7.4-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\. {
        deny all;
    }

    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|svg)$ {
        expires 30d;
        access_log off;
    }
}

Notes

To create the certificate bundle from a commercial CA:

cat domain.crt domain.ca-bundle > /etc/nginx/ssl/example_com.bundle
sudo nginx -t && sudo systemctl reload nginx

Fix: nginx [emerg] could not build the server_names_hash

Nginx fails to start or reload with the error [emerg] could not build the server_names_hash, you should increase server_names_hash_bucket_size when a server_name value is longer than the current hash bucket size allows.

Steps

1. Open the main Nginx config

sudo nano /etc/nginx/nginx.conf

2. Find or add the directive in the http{} block

http {
    server_names_hash_bucket_size 64;   # default; increase if error persists
    ...
}

3. If the error persists, double the value and retry

# Increase in powers of 2: 64 → 128 → 256 → 512
server_names_hash_bucket_size 128;

4. Test and reload after each change

sudo nginx -t
sudo systemctl reload nginx

Key Settings

  • server_names_hash_bucket_size — must be a power of 2; sets the hash table bucket size for server names
  • server_names_hash_max_size — increase this too (e.g., 512) if you host a large number of virtual hosts

Notes

Long subdomains (e.g., very-long-subdomain.example.com) or many virtual hosts are the most common triggers. The error message always reads the same regardless of the current value, so keep doubling until it clears.

Password-Protect /wp-admin in Nginx

Add HTTP Basic Authentication to the WordPress admin area (/wp-admin/) in Nginx to require a username and password before the login page is shown.

Prerequisites

  • apache2-utils installed (provides htpasswd)
  • Nginx with PHP-FPM configured for WordPress

Steps

1. Create the htpasswd file

sudo mkdir -p /etc/nginx/htpasswd
sudo htpasswd -c /etc/nginx/htpasswd/wpadmin adminuser
# Enter and confirm a strong password when prompted

2. Add the location block to your server config

For HTTP connections:

location ^~ /wp-admin/ {
    auth_basic           "Restricted";
    auth_basic_user_file /etc/nginx/htpasswd/wpadmin;

    # Deny direct access to auth files
    location ~* \.(htaccess|htpasswd) {
        deny all;
    }

    # Pass PHP to FPM
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        include             fastcgi_params;
        fastcgi_param       SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass        unix:/run/php/php7.4-fpm.sock;
        fastcgi_read_timeout 60s;
    }
}

For HTTPS connections (add HTTPS on fastcgi param):

location ^~ /wp-admin/ {
    auth_basic           "Restricted";
    auth_basic_user_file /etc/nginx/htpasswd/wpadmin;

    location ~* \.(htaccess|htpasswd) {
        deny all;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        include             fastcgi_params;
        fastcgi_param       SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param       HTTPS on;
        fastcgi_pass        unix:/run/php/php7.4-fpm.sock;
        fastcgi_read_timeout 60s;
    }
}

3. Test and reload

sudo nginx -t && sudo systemctl reload nginx

Notes

  • wp-login.php also benefits from protection — add a similar block for it
  • To add more users: sudo htpasswd /etc/nginx/htpasswd/wpadmin anotheruser (omit -c to append)

Nginx Config with PHP-FPM over TCP Port

Configure Nginx to communicate with PHP-FPM using a TCP port upstream block instead of a Unix socket. This pattern is common when Nginx and PHP-FPM run on separate hosts or in containers.

Configuration

# /etc/nginx/sites-available/example.com

# --- HTTP ---

# Define PHP-FPM upstream (TCP port)
upstream php-handler-http {
    server 127.0.0.1:9000;
    # Uncomment to use socket instead:
    # server unix:/var/run/php5-fpm.sock;
}

server {
    listen 80 default_server;
    server_name example.com;

    root /var/www/html;
    index index.php;

    client_max_body_size 2G;
    fastcgi_buffers 64 4K;

    access_log /var/log/nginx/site_http_access.log combined;
    error_log  /var/log/nginx/site_http_error.log;

    location = /favicon.ico { log_not_found off; access_log off; }
    location = /robots.txt  { allow all; log_not_found off; access_log off; }

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

    # Deny hidden auth files
    location ~* \.(htaccess|htpasswd) { deny all; }

    # PHP handler
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        include         fastcgi_params;
        fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass    php-handler-http;
        fastcgi_read_timeout 60s;
    }

    # Long-lived cache for static assets
    location ~* \.(jpg|jpeg|gif|bmp|ico|png|css|js|swf)$ {
        expires 30d;
        access_log off;
    }
}

# --- HTTPS ---

upstream php-handler-https {
    server 127.0.0.1:9000;
}

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

    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    root /var/www/html;
    index index.php;

    client_max_body_size 2G;
    fastcgi_buffers 64 4K;

    access_log /var/log/nginx/site_https_access.log combined;
    error_log  /var/log/nginx/site_https_error.log;

    location = /favicon.ico { log_not_found off; access_log off; }
    location = /robots.txt  { allow all; log_not_found off; access_log off; }

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

    location ~* \.(htaccess|htpasswd) { deny all; }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        include         fastcgi_params;
        fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param   HTTPS on;
        fastcgi_pass    php-handler-https;
        fastcgi_read_timeout 60s;
    }

    location ~* \.(jpg|jpeg|gif|bmp|ico|png|css|js|swf)$ {
        expires 30d;
        access_log off;
    }
}

Steps

sudo nginx -t && sudo systemctl reload nginx

Key Settings

  • upstream block — defines the backend; use TCP (127.0.0.1:9000) or socket
  • fastcgi_param HTTPS on — required in HTTPS blocks so PHP knows the connection is secure
  • client_max_body_size — controls max upload size; match with upload_max_filesize in php.ini

Install Magento 2 on CentOS 7 with Nginx, PHP, and MySQL 8

Step-by-step setup of Magento 2 on CentOS 7 with Nginx, PHP 7.2 (Remi), MySQL 8, and Let's Encrypt SSL.

Prerequisites

  • CentOS 7 server with root access
  • Domain name pointed at the server IP
  • Magento marketplace credentials (public/private key)

Steps

1. Install Nginx

rpm -ivh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm
yum install -y nginx
systemctl start nginx && systemctl enable nginx

2. Set up Let's Encrypt

yum install -y certbot
openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
mkdir -p /var/lib/letsencrypt/.well-known
chgrp nginx /var/lib/letsencrypt
chmod g+s /var/lib/letsencrypt
mkdir -p /etc/nginx/snippets

Create ACME challenge snippet at /etc/nginx/snippets/letsencrypt.conf:

location ^~ /.well-known/acme-challenge/ {
    allow all;
    root /var/lib/letsencrypt/;
    default_type "text/plain";
    try_files $uri =404;
}

3. Install PHP 7.2 via Remi

yum install -y epel-release
rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-7.rpm
yum --enablerepo=remi-php72 install -y php php-xml php-soap php-xmlrpc     php-mbstring php-json php-gd php-mcrypt php-mysql php-fpm php-pdo     php-opcache php-devel php-iconv php-intl php-bcmath php-zip
yum install -y git zip

Tune PHP settings:

sed -i "s/memory_limit = .*/memory_limit = 756M/" /etc/php.ini
sed -i "s/upload_max_filesize = .*/upload_max_filesize = 256M/" /etc/php.ini
sed -i "s/max_execution_time = .*/max_execution_time = 18000/" /etc/php.ini
sed -i "s/;date.timezone.*/date.timezone = UTC/" /etc/php.ini
sed -i "s/;opcache.save_comments.*/opcache.save_comments = 1/" /etc/php.d/10-opcache.ini

4. Create Magento database

mysql -u root -p
CREATE DATABASE magentodb;
CREATE USER 'magentouser'@'localhost' IDENTIFIED BY 'StrongPassword!';
GRANT ALL PRIVILEGES ON magentodb.* TO 'magentouser'@'localhost' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EXIT;

5. Install Composer and Magento

curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/bin/composer

# Create Magento system user
useradd -m -U -r -d /usr/share/nginx/html magento
usermod -aG nginx magento
chmod 750 /usr/share/nginx/html

# Install Magento (enter Marketplace keys when prompted)
sudo -u magento composer create-project     --repository-url=https://repo.magento.com/     magento/project-community-edition     /usr/share/nginx/html

6. Run Magento installer

php /usr/share/nginx/html/bin/magento setup:install     --base-url=https://your-domain.com/     --base-url-secure=https://your-domain.com/     --admin-firstname="Admin"     --admin-lastname="User"     --admin-email="admin@example.com"     --admin-user="admin"     --admin-password="SecureAdminPass1!"     --db-name="magentodb"     --db-host="localhost"     --db-user="magentouser"     --db-password="StrongPassword!"     --currency=USD     --timezone=America/Chicago     --use-rewrites=1

7. Configure PHP-FPM pool for Magento

Create /etc/php-fpm.d/magento.conf:

[magento]
user  = magento
group = nginx
listen.owner = magento
listen.group = nginx
listen = /run/php-fpm/magento.sock
pm = ondemand
pm.max_children = 50
pm.process_idle_timeout = 10s
pm.max_requests = 500
chdir = /
systemctl restart php-fpm

8. Nginx virtual host for Magento

upstream fastcgi_backend {
    server unix:/run/php-fpm/magento.sock;
}

# HTTP: redirect to HTTPS and serve ACME challenges
server {
    listen 80;
    server_name your-domain.com;
    include snippets/letsencrypt.conf;
    return 301 https://your-domain.com$request_uri;
}

# HTTPS
server {
    listen 443 ssl http2;
    server_name your-domain.com;

    ssl_certificate     /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
    include snippets/ssl.conf;

    set $MAGE_ROOT /usr/share/nginx/html;
    include /usr/share/nginx/html/nginx.conf.sample;
}
sudo nginx -t && sudo systemctl reload nginx

Notes

After installation, set correct file permissions:

cd /usr/share/nginx/html
find var generated vendor pub/static pub/media app/etc -type f -exec chmod g+w {} +
find var generated vendor pub/static pub/media app/etc -type d -exec chmod g+ws {} +
chown -R magento:nginx .
chmod u+x bin/magento

Fix: Nginx "no ssl_certificate defined" on SSL Port

The error no "ssl_certificate" is defined in server listening on SSL port while SSL handshaking occurs when a server block listens on port 443 without the ssl keyword or without a certificate, causing SNI to fail for all virtual hosts on that port.

Cause

A default or catch-all server block listens on port 443 without SSL configured. Nginx selects this block for all incoming TLS connections before SNI can route to the correct block.

# Problematic pattern — port 443 without ssl keyword
server {
    listen 443 default_server;   # missing "ssl" here
    server_name _;
    ...
}

Fix 1: Remove or correct the offending default server block

Check all enabled configs for bare 443 listeners:

grep -r "listen 443" /etc/nginx/sites-enabled/

Any block with listen 443 that lacks ssl and a certificate must be fixed or removed:

# Correct form
server {
    listen 443 ssl default_server;
    server_name _;
    ssl_certificate     /etc/nginx/ssl/default.crt;
    ssl_certificate_key /etc/nginx/ssl/default.key;
    return 444;   # drop connection for unmatched SNI
}

Fix 2: Ensure only one default_server per port

# Wrong — two server blocks both claim default_server on 443
server { listen 443 ssl default_server; server_name example.com; ... }
server { listen 443 ssl default_server; server_name www.example.com; ... }

# Correct — only one default_server
server { listen 443 ssl default_server; server_name example.com www.example.com; ... }

Fix 3: Verify file permissions on cert/key

# Nginx worker must be able to read these files
sudo chmod 640 /etc/letsencrypt/live/example.com/fullchain.pem
sudo chmod 640 /etc/letsencrypt/live/example.com/privkey.pem
sudo chown root:www-data /etc/letsencrypt/live/example.com/privkey.pem

Steps

sudo nginx -t
sudo systemctl reload nginx
# Watch error log to confirm fix
sudo tail -f /var/log/nginx/error.log

Notes

After any config change, always run nginx -t before reloading — it catches syntax errors but not all logical errors like missing ssl on a 443 listener.

Fix Nginx 502 Bad Gateway: PHP-FPM Socket Unavailable

A 502 Bad Gateway from Nginx with the error connect() to unix:/var/run/php/php7.x-fpm.sock failed (11: Resource temporarily unavailable) means PHP-FPM is running but its process pool is exhausted or the socket is not writable by Nginx.

Symptoms

connect() to unix:/var/run/php/php7.0-fpm.sock failed (11: Resource temporarily unavailable)
while connecting to upstream, client: 66.249.76.49, server: example.com

Steps

1. Check PHP-FPM status

sudo systemctl status php7.4-fpm
sudo journalctl -u php7.4-fpm --since "10 minutes ago"

2. Check pool configuration

sudo nano /etc/php/7.4/fpm/pool.d/www.conf

Verify these settings:

# Socket path must match fastcgi_pass in Nginx
listen = /run/php/php7.4-fpm.sock

# Socket permissions — Nginx user must be able to write
listen.owner = www-data
listen.group = www-data
listen.mode  = 0660

# Pool sizing — increase if under heavy load
pm = dynamic
pm.max_children      = 20
pm.start_servers     = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6

3. Verify socket directory is writable

ls -la /run/php/
# Owner of .sock file must match listen.owner above

4. Restart PHP-FPM and reload Nginx

sudo systemctl restart php7.4-fpm
sudo systemctl reload nginx

5. Confirm Nginx fastcgi_pass matches the socket path

location ~ \.php$ {
    fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    ...
}

Key Settings

  • pm.max_children — max concurrent PHP processes; size to available RAM (~30MB per process typical)
  • listen.owner / listen.group — must match the user Nginx runs as
  • Error 11 (EAGAIN) means the socket backlog is full — increase pm.max_children or tune pm mode

Add CORS Headers in Nginx

Configure Nginx to add Access-Control-Allow-Origin and related CORS headers so browsers allow cross-origin requests to your server.

Simple CORS for fonts and static assets

# Allow cross-origin requests for web font files
location ~* \.(eot|ttf|woff|woff2|svg)$ {
    add_header Access-Control-Allow-Origin  *;
    add_header Access-Control-Allow-Headers X-Requested-With;
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
}

Full CORS with OPTIONS preflight handling

location / {
    # Handle preflight OPTIONS request
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin'  'https://app.example.com';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers'
            'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,Range';
        add_header 'Access-Control-Max-Age' 1728000;  # 20 days
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;
        return 204;
    }

    # Add CORS headers to actual responses
    if ($request_method = 'GET') {
        add_header 'Access-Control-Allow-Origin'  'https://app.example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    }

    if ($request_method = 'POST') {
        add_header 'Access-Control-Allow-Origin'  'https://app.example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    }

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

Steps

sudo nginx -t && sudo systemctl reload nginx

Key Settings

  • Access-Control-Allow-Origin — use a specific origin (not *) when credentials are involved
  • Access-Control-Allow-Credentials: true — required if sending cookies or Authorization headers cross-origin
  • Access-Control-Max-Age — how long browsers cache preflight results (seconds)

Notes

Using Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true is invalid and will be rejected by browsers. Use an explicit origin when credentials are required.

Nginx Config for WordPress Multisite

Configure Nginx to serve a WordPress Multisite (subdirectory or subdomain) installation with SSL, PHP-FPM, and correct rewrite rules for multisite routing.

Configuration

# /etc/nginx/sites-available/multisite.example.com

server {
    listen 443 ssl;
    server_name *.example.com example.com www.example.com;

    root  /home/example/public_html;
    index index.php index.html index.htm;

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

    # SSL certificates
    ssl on;
    ssl_certificate     /etc/pki/tls/certs/example.com.bundle;
    ssl_certificate_key /etc/pki/tls/private/example.com.key;
    ssl_protocols       TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_ciphers         EECDH+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA256:!RC4:!aNULL:!eNULL:!MD5:!3DES;
    ssl_prefer_server_ciphers on;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 60m;

    # Main location — WordPress front-controller
    location / {
        try_files $uri $uri/ /index.php?$args;
        add_header Strict-Transport-Security "max-age=31536000";
        add_header X-Content-Type-Options nosniff;
    }

    # Add trailing slash to wp-admin requests
    rewrite /wp-admin$ $scheme://$host$uri/ permanent;

    # Static asset caching
    location ~* \.(jpeg|jpg|png|gif|bmp|ico|svg|css|js)$ {
        expires max;
    }

    # Deny hidden files
    location ~ /\. {
        access_log off;
        log_not_found off;
        deny all;
    }

    # Multisite: serve uploaded files via ms-files.php
    rewrite /files/$ /index.php last;
    if ($uri !~ wp-content/plugins) {
        rewrite /files/(.+)$ /wp-includes/ms-files.php?file=$1 last;
    }

    # Multisite: rewrite wp-* paths and PHP files for subsites
    if (!-e $request_filename) {
        rewrite ^/[_0-9a-zA-Z-]+(/wp-.*) $1 last;
        rewrite ^/[_0-9a-zA-Z-]+.*(/wp-admin/.*\.php)$ $1 last;
        rewrite ^/[_0-9a-zA-Z-]+(/.*\.php)$ $1 last;
    }

    # PHP-FPM handler
    location ~ [^/]\.php(/|$) {
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        if (!-f $document_root$fastcgi_script_name) {
            return 404;
        }
        fastcgi_pass  unix:/run/php/php7.4-fpm.sock;
        fastcgi_index index.php;
        include       fastcgi_params;
    }

    # Deny .htaccess and .htpasswd
    location ~* "/\.(htaccess|htpasswd)$" {
        deny all;
        return 404;
    }

    # Let's Encrypt ACME challenge
    location /.well-known/acme-challenge {
        default_type "text/plain";
        alias /var/lib/letsencrypt/.well-known/acme-challenge;
    }
}

# HTTP redirect
server {
    listen 80;
    server_name *.example.com example.com www.example.com;
    return 301 https://$host$request_uri;
}

Steps

sudo nginx -t && sudo systemctl reload nginx

Notes

For subdomain multisite, add *.example.com to your SSL certificate (wildcard cert) and ensure the DNS wildcard record points to your server. For subdirectory multisite, the rewrite rules above handle routing to subsites.

Automatic SSL for Multi-Tenant Apps with OpenResty and lua-resty-auto-ssl

Use OpenResty with the lua-resty-auto-ssl module to automatically obtain and renew Let's Encrypt certificates for arbitrary custom domains without manual intervention or cron jobs.

Prerequisites

  • Amazon Linux or compatible server (EC2 or VPS)
  • Ports 80 and 443 open
  • Domains pointed at the server IP

Steps

1. Install OpenResty

sudo yum-config-manager --add-repo https://openresty.org/package/amazon/openresty.repo
sudo yum install -y openresty openresty-resty

2. Install LuaRocks

wget http://luarocks.org/releases/luarocks-2.0.13.tar.gz
tar -xzvf luarocks-2.0.13.tar.gz
cd luarocks-2.0.13/
./configure --prefix=/usr/local/openresty/luajit     --with-lua=/usr/local/openresty/luajit/     --lua-suffix=jit     --with-lua-include=/usr/local/openresty/luajit/include/luajit-2.1
make && sudo make install

3. Install lua-resty-auto-ssl

sudo yum install -y gcc
sudo groupadd www
sudo usermod -a -G www ec2-user

sudo /usr/local/openresty/luajit/bin/luarocks install lua-resty-auto-ssl
sudo mkdir /etc/resty-auto-ssl
sudo chown -R root:www /etc/resty-auto-ssl/
sudo chmod -R 775 /etc/resty-auto-ssl

4. Generate a self-signed fallback certificate

sudo openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509     -subj '/CN=sni-support-required-for-valid-ssl'     -keyout /etc/ssl/resty-auto-ssl-fallback.key     -out /etc/ssl/resty-auto-ssl-fallback.crt

5. Configure OpenResty nginx.conf

sudo mv /usr/local/openresty/nginx/conf/nginx.conf         /usr/local/openresty/nginx/conf/nginx.conf.bak
sudo nano /usr/local/openresty/nginx/conf/nginx.conf
user ec2-user www;

events {
    worker_connections 1024;
}

http {
    lua_shared_dict auto_ssl          1m;
    lua_shared_dict auto_ssl_settings 64k;
    resolver 8.8.8.8 ipv6=off;

    init_by_lua_block {
        auto_ssl = (require "resty.auto-ssl").new()
        -- Allow all domains; restrict here if needed:
        auto_ssl:set("allow_domain", function(domain)
            return true
        end)
        auto_ssl:init()
    }

    init_worker_by_lua_block {
        auto_ssl:init_worker()
    }

    # HTTPS — certificates issued on first request per domain
    server {
        listen 443 ssl;
        ssl_certificate_by_lua_block { auto_ssl:ssl_certificate() }

        # Fallback cert (required for Nginx to start)
        ssl_certificate     /etc/ssl/resty-auto-ssl-fallback.crt;
        ssl_certificate_key /etc/ssl/resty-auto-ssl-fallback.key;

        location / {
            proxy_pass http://localhost:3000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }

    # HTTP — ACME challenge endpoint
    server {
        listen 80;
        location /.well-known/acme-challenge/ {
            content_by_lua_block { auto_ssl:challenge_server() }
        }
    }

    # Internal hook server for auto-ssl
    server {
        listen 127.0.0.1:8999;
        client_body_buffer_size 128k;
        client_max_body_size    128k;
        location / {
            content_by_lua_block { auto_ssl:hook_server() }
        }
    }
}

6. Start OpenResty

sudo service openresty start

Notes

  • Point any custom domain at the server IP; the cert is issued automatically on the first HTTPS request
  • Debug with: tail -F /usr/local/openresty/nginx/logs/error.log
  • For production, restrict allow_domain to validate domains against your database before issuing certs

Per-Domain SSL Automation with OpenResty and lua-resty-auto-ssl

Generate and renew Let's Encrypt SSL certificates on demand for arbitrary custom domains using OpenResty and the lua-resty-auto-ssl library. This approach eliminates cron jobs and manual cert management for multi-tenant platforms.

How It Works

When a user points their custom domain to your server, the first HTTPS request triggers an ACME HTTP-01 challenge. OpenResty fetches and stores the certificate automatically, then serves it for all subsequent requests. Renewals happen in the background.

Prerequisites

  • OpenResty installed (see the OpenResty installation guide)
  • LuaRocks package manager installed
  • lua-resty-auto-ssl installed via LuaRocks

Steps

1. Install dependencies (Ubuntu)

sudo apt-get install -y unzip gcc
# Install LuaRocks pointed at OpenResty's LuaJIT
wget http://luarocks.org/releases/luarocks-2.0.13.tar.gz
tar -xzvf luarocks-2.0.13.tar.gz && cd luarocks-2.0.13/
./configure --prefix=/usr/local/openresty/luajit     --with-lua=/usr/local/openresty/luajit/     --lua-suffix=jit     --with-lua-include=/usr/local/openresty/luajit/include/luajit-2.1
make && sudo make install

# Install lua-resty-auto-ssl
sudo luarocks install lua-resty-auto-ssl
sudo mkdir /etc/resty-auto-ssl
sudo chown $USER /etc/resty-auto-ssl

2. nginx.conf for auto-SSL multi-tenant platform

# /usr/local/openresty/nginx/conf/nginx.conf

user www-data www;

events { worker_connections 1024; }

http {
    lua_shared_dict auto_ssl          1m;
    lua_shared_dict auto_ssl_settings 64k;
    resolver 8.8.8.8 ipv6=off;

    init_by_lua_block {
        auto_ssl = (require "resty.auto-ssl").new()
        auto_ssl:set("allow_domain", function(domain)
            -- Validate against your own domain whitelist/database here
            return true
        end)
        auto_ssl:init()
    }

    init_worker_by_lua_block {
        auto_ssl:init_worker()
    }

    # HTTPS server — handles all custom domains
    server {
        listen 443 ssl http2;

        # Dynamically issue and serve per-domain certs via Lua
        ssl_certificate_by_lua_block { auto_ssl:ssl_certificate() }

        # Static fallback cert for your own primary domain
        ssl_certificate     /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

        location / {
            proxy_pass http://localhost:3000;
            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;
        }
    }

    # HTTP — ACME challenge handler
    server {
        listen 80;
        location /.well-known/acme-challenge/ {
            content_by_lua_block { auto_ssl:challenge_server() }
        }
        # Redirect all other HTTP to HTTPS
        location / {
            return 301 https://$host$request_uri;
        }
    }

    # Internal hook server
    server {
        listen 127.0.0.1:8999;
        client_body_buffer_size 128k;
        client_max_body_size    128k;
        location / {
            content_by_lua_block { auto_ssl:hook_server() }
        }
    }
}

3. Start and verify

sudo /usr/local/openresty/bin/openresty -t
sudo service openresty restart
tail -F /usr/local/openresty/nginx/logs/error.log

Notes

  • Certificates are stored in /etc/resty-auto-ssl/ and renewed automatically before expiry
  • Rate limits: Let's Encrypt allows 50 certs per registered domain per week — implement domain validation in allow_domain for production
  • Enable HTTP/2 during OpenResty compile: ./configure -j2 --with-pcre-jit --with-http_v2_module

Install LEMP Stack on CentOS 7 (Nginx, MariaDB, PHP)

Install a complete LEMP stack (Linux, Nginx, MariaDB, PHP) on CentOS 7, suitable for hosting dynamic websites and web applications.

Prerequisites

  • CentOS 7 server with a non-root sudo user
  • Ports 80 and 443 open in firewalld

Steps

1. Install Nginx

sudo yum install -y epel-release
sudo yum install -y nginx
sudo systemctl start nginx
sudo systemctl enable nginx

Open firewall ports:

sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload

2. Install MariaDB

sudo yum install -y mariadb-server mariadb
sudo systemctl start mariadb
sudo systemctl enable mariadb
sudo mysql_secure_installation

3. Install PHP 7.4

sudo yum install -y epel-release
sudo yum install -y https://rpms.remirepo.net/enterprise/remi-release-7.rpm
sudo yum-config-manager --enable remi-php74
sudo yum install -y php php-fpm php-mysqlnd php-mbstring php-xml php-gd php-curl php-zip php-json
sudo systemctl start php-fpm
sudo systemctl enable php-fpm

4. Configure PHP-FPM to use Unix socket

sudo nano /etc/php-fpm.d/www.conf
# Change listen from port to socket
listen = /run/php-fpm/www.sock
listen.owner = nginx
listen.group = nginx
listen.mode  = 0660

# Run as nginx user
user  = nginx
group = nginx
sudo systemctl restart php-fpm

5. Configure Nginx server block

sudo nano /etc/nginx/conf.d/example.com.conf
server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/example.com;
    index index.php index.html;

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

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/run/php-fpm/www.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\. {
        deny all;
    }
}
sudo mkdir -p /var/www/example.com
sudo chown -R nginx:nginx /var/www/example.com
sudo nginx -t && sudo systemctl reload nginx

6. Test PHP processing

echo '<?php phpinfo(); ?>' | sudo tee /var/www/example.com/info.php

Visit http://your-server-ip/info.php to confirm PHP is working, then remove the file:

sudo rm /var/www/example.com/info.php

Notes

  • MariaDB is a drop-in replacement for MySQL — no application code changes required when switching
  • PHP 7.4 is available via Remi repository; replace remi-php74 with remi-php80 or remi-php81 for newer versions

Host Multiple Sites with Nginx Load Balancing

Configure Nginx to host multiple domains on a single server and optionally distribute traffic across backend servers using upstream load balancing.

Part 1: Multiple Domains on One Server

Create per-domain config files

sudo nano /etc/nginx/sites-available/site1.example.com
server {
    listen 80;
    server_name site1.example.com;
    root /var/www/site1.example.com;
    index index.php index.html;

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

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php7.4-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}
sudo mkdir -p /var/www/site1.example.com
sudo ln -s /etc/nginx/sites-available/site1.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Add Let's Encrypt SSL per domain

sudo certbot --nginx -d site1.example.com
sudo certbot --nginx -d site2.example.com

Automate cert renewal

# Add to crontab (crontab -e)
0 3 * * * certbot renew --quiet --post-hook "systemctl reload nginx"

Part 2: Nginx Load Balancing

Load balancing methods

# Round-robin (default) — alternates requests across backends
upstream myapp {
    server 144.217.92.149;
    server 144.217.92.150;
}

# Least-connected — sends to backend with fewest active connections
upstream myapp {
    least_conn;
    server 144.217.92.149;
    server 144.217.92.150;
}

# IP hash — sticky sessions; same client always hits same backend
upstream myapp {
    ip_hash;
    server 144.217.92.149;
    server 144.217.92.150;
}

Load balancer server block (SSL terminates here)

# /etc/nginx/sites-available/lbtest.example.com

upstream myapp {
    server 144.217.92.149;
    server 144.217.92.150;
}

server {
    listen 80;
    server_name lbtest.example.com;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate     /etc/letsencrypt/live/lbtest.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/lbtest.example.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;

    location / {
        proxy_pass         http://myapp;
        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;
    }
}

Backend server config (no SSL needed)

server {
    listen 80;
    server_name lbtest.example.com;
    root /var/www/lbtest.example.com;

    # Restrict direct access — only accept traffic from load balancer
    allow 167.114.145.178;   # load balancer IP
    deny all;

    location / {
        try_files $uri $uri/ =404;
    }
}
sudo nginx -t && sudo systemctl reload nginx

Notes

SSL is terminated at the load balancer. Backend servers communicate over plain HTTP on your private network, which is acceptable and avoids double-encryption overhead. Use proxy_set_header X-Forwarded-Proto $scheme so backend apps know the original request was HTTPS.

Nginx Full CORS Config with OPTIONS Preflight

A complete Nginx configuration handling CORS headers for both static content and PHP endpoints, including OPTIONS preflight responses required by browsers for cross-origin requests with custom headers or credentials.

Configuration

# /etc/nginx/sites-available/api.example.com

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

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name api.example.com;

    root  /var/www/html;
    index index.php index.html;

    ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;

    # Static content — CORS with preflight
    location / {
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin'      'https://app.example.com';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Methods'     'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers'
                'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
            add_header 'Access-Control-Max-Age'  1728000;
            add_header 'Content-Type'            'text/plain; charset=UTF-8';
            add_header 'Content-Length'          0;
            return 204;
        }
        if ($request_method = 'POST') {
            add_header 'Access-Control-Allow-Origin'      'https://app.example.com';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Methods'     'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers'
                'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
        }
        if ($request_method = 'GET') {
            add_header 'Access-Control-Allow-Origin'      'https://app.example.com';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Methods'     'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers'
                'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
        }
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP endpoints — CORS with preflight (open origin for API endpoints)
    location ~ \.php$ {
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin'      '*';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Methods'     'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers'
                'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
            add_header 'Access-Control-Max-Age'  1728000;
            add_header 'Content-Type'            'text/plain; charset=UTF-8';
            add_header 'Content-Length'          0;
            return 204;
        }
        if ($request_method = 'POST') {
            add_header 'Access-Control-Allow-Origin'  '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers'
                'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
        }
        if ($request_method = 'GET') {
            add_header 'Access-Control-Allow-Origin'  '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers'
                'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
        }
        try_files $uri =404;
        include         /etc/nginx/fastcgi.conf;
        fastcgi_pass    unix:/run/php/php7.4-fpm.sock;
        fastcgi_index   index.php;
    }
}

Steps

sudo nginx -t && sudo systemctl reload nginx

Key Settings

  • Access-Control-Max-Age 1728000 — browsers cache the preflight for 20 days, reducing OPTIONS round-trips
  • return 204 — No Content response for OPTIONS; correct and efficient for preflight
  • Do not mix Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — browsers will reject it

Nginx Config for WordPress with Let's Encrypt SSL

A standard Nginx virtual host configuration for a WordPress site with PHP-FPM and Let's Encrypt SSL managed by Certbot.

Configuration

# /etc/nginx/sites-available/example.com

# HTTPS server block
server {
    listen 443 ssl;
    listen [::]:443 ssl ipv6only=on;
    server_name example.com www.example.com;

    root  /var/www/html;
    index index.php;

    # Let's Encrypt SSL (managed by Certbot)
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    # WordPress front-controller (enables clean URLs / permalinks)
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # PHP-FPM handler
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
    }

    # Block access to WordPress sensitive files
    location ~ /\.(ht|git|svn) {
        deny all;
    }

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

    # Block xmlrpc.php if not needed
    location = /xmlrpc.php {
        deny all;
    }

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

# HTTP redirect block (managed by Certbot)
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name example.com www.example.com;

    if ($host = www.example.com) {
        return 301 https://$host$request_uri;
    }
    if ($host = example.com) {
        return 301 https://$host$request_uri;
    }
    return 404;
}

Steps

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

# Obtain SSL cert with Certbot (if not already done)
sudo certbot --nginx -d example.com -d www.example.com

Notes

  • Match fastcgi_pass socket version to your installed PHP-FPM version (e.g., php8.1-fpm.sock)
  • Set WordPress WP_HOME and WP_SITEURL to the HTTPS URL in wp-config.php after enabling SSL
  • For uploads larger than 2MB, increase client_max_body_size and match upload_max_filesize in php.ini

Nginx Load Balancing with SSL Termination

Configure an Nginx upstream block to load balance traffic across multiple backend servers, with SSL terminating at the Nginx layer.

Configuration

# /etc/nginx/sites-available/example.com

# Define backend pool (round-robin by default)
upstream mywebapp {
    server 10.130.227.11;
    server 10.130.227.22;
    # Optional: mark a server as backup
    # server 10.130.227.33 backup;
    # Optional: weight-based distribution
    # server 10.130.227.11 weight=3;
    # server 10.130.227.22 weight=1;
}

server {
    listen 80;
    listen 443 ssl;
    server_name example.com www.example.com;

    # SSL configuration
    ssl_certificate         /etc/nginx/ssl/example.com/server.crt;
    ssl_certificate_key     /etc/nginx/ssl/example.com/server.key;
    ssl_trusted_certificate /etc/nginx/ssl/example.com/ca-certs.pem;

    ssl_session_cache   shared:SSL:20m;
    ssl_session_timeout 10m;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:!aNULL:!MD5:!DSS;
    ssl_prefer_server_ciphers on;

    add_header Strict-Transport-Security "max-age=31536000";

    # Proxy to upstream pool
    location / {
        proxy_pass         http://mywebapp;
        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;

        # Connection timeouts
        proxy_connect_timeout 5s;
        proxy_read_timeout    60s;
        proxy_send_timeout    60s;
    }
}

Steps

sudo nginx -t && sudo systemctl reload nginx

Key Settings

  • upstream — round-robin by default; add least_conn; or ip_hash; directive inside the block to change algorithm
  • proxy_set_header X-Forwarded-Proto — tells backend apps whether the original request was HTTP or HTTPS
  • proxy_set_header X-Real-IP — passes the real client IP to backend apps (otherwise they see the load balancer IP)
  • weight — distribute more traffic to higher-capacity backends

Notes

Health checks require Nginx Plus. For open-source Nginx, use a passive health check by adding max_fails and fail_timeout to each server:

upstream mywebapp {
    server 10.130.227.11 max_fails=3 fail_timeout=30s;
    server 10.130.227.22 max_fails=3 fail_timeout=30s;
}

Nginx Performance Tuning: worker_processes, Keepalive, Buffers, Cache, and Gzip

Key Nginx configuration changes that significantly improve throughput, response times, and SSL performance for production web applications.

Prerequisites

  • Nginx installed and serving traffic
  • Write access to /etc/nginx/nginx.conf and your site config

1. Worker Processes and Connections

# /etc/nginx/nginx.conf

# Set to number of CPU cores (or "auto")
worker_processes auto;

events {
    # Max simultaneous connections per worker
    worker_connections 1024;
    use epoll;           # Linux: most efficient event model
    multi_accept on;     # Accept all new connections at once
}

2. Keepalive Connections

http {
    # Keep connections open to reduce TCP handshake overhead
    keepalive_timeout  65;
    keepalive_requests 100;
}

3. Buffer Tuning

http {
    # Client request buffers
    client_body_buffer_size    128k;
    client_max_body_size       10m;
    client_header_buffer_size  1k;
    large_client_header_buffers 4 4k;

    # Proxy/FastCGI buffers (tune for your PHP response sizes)
    fastcgi_buffers      16 16k;
    fastcgi_buffer_size  32k;
}

4. Gzip Compression

http {
    gzip on;
    gzip_disable    "msie6";
    gzip_vary       on;
    gzip_proxied    any;
    gzip_comp_level 6;
    gzip_buffers    32 16k;
    gzip_http_version 1.1;
    gzip_min_length 250;
    gzip_types
        text/plain text/css application/json
        application/javascript text/xml application/xml
        application/xml+rss text/javascript
        image/svg+xml image/x-icon
        application/x-font-ttf font/opentype;
}

5. FastCGI Cache (microcaching for PHP)

http {
    # Define cache zone (store in /tmp/nginx_cache, 10MB key zone, 60min inactive)
    fastcgi_cache_path /tmp/nginx_cache
        levels=1:2
        keys_zone=FCGI:10m
        inactive=60m
        max_size=1g;
    fastcgi_cache_key "$scheme$request_method$host$request_uri";
}

# In your server block:
server {
    location ~ \.php$ {
        fastcgi_cache       FCGI;
        fastcgi_cache_valid 200 60m;
        fastcgi_cache_use_stale error timeout updating;
        add_header X-Cache $upstream_cache_status;
        ...
    }
}

6. SSL Session Cache

server {
    ssl_session_cache   shared:SSL:10m;   # ~40k sessions
    ssl_session_timeout 10m;
    ssl_session_tickets off;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
}

7. Security Headers

http {
    add_header X-Frame-Options           "SAMEORIGIN" always;
    add_header X-XSS-Protection          "1; mode=block" always;
    add_header X-Content-Type-Options    "nosniff" always;
    add_header Referrer-Policy           "no-referrer-when-downgrade" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}

Steps

sudo nginx -t
sudo systemctl reload nginx

Notes

  • Use nginx -V to check compiled modules — Brotli compression requires the ngx_brotli module compiled in
  • Microcaching (cache for 1-10 seconds) can dramatically reduce PHP-FPM load on high-traffic sites even for dynamic pages
  • Monitor cache hit rate with add_header X-Cache $upstream_cache_status and check response headers