Nginx and You

LonghornPHP 2026

Chris Lemon

Overview

  1. Who? What? Why?
  2. nginx Primer
  3. "Real World" Examples

Who?

Chris Lemon

Chris Lemon
  • Team Lead @ Vehikl
  • Meetup Organizer (GPUG)
  • Avid Record Collector
  • DevOps Dabbler

What?

nginx

Why?

nginx is cool!

Why?

really, really cool!

Why?

The Developer/DevOps Divide

Why?

Is PHP the solution?

Why?

PHP is slow
(in comparison)

Why?

Infinitely Configurable

Why?

Nginx and You

Nginx Primer

  • Web Server, Reverse Proxy, Load Balancer
  • Middle Man Between User and App
  • Takes Request and Directs it to Correct Destination
  • Uses RegEx To Match URI to file or route
  • Capable of handling 10,000s requests a second
  • Modules provide additional directives and functionality
  • Everything handled from configuration files

proxy_pass vs fastcgi_pass

proxy_pass


location /api/ {
    proxy_pass http://backend:3000;
}
  

fastcgi_pass


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

nginx.conf - The Basics

  • Scope blocks {}
  • $variables
  • if ($something){} statements
  • include
  • Directives

nginx.conf - Example


user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}
http {
    include mime.types;
    sendfile on;
    keepalive_timeout 65;

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

        location = /health {
            return 200 "OK";
        }

        location ^~ /uploads/ {
            root /var/www/storage;
            expires 30d;
        }

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

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

        location ~ \.php$ {
            include fastcgi_params;
            fastcgi_pass unix:/run/php/php8.5-fpm.sock;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        }

        location ~ /\.(?!well-known).* {
          deny all;
        }
    }
}
  

nginx.conf - Example

Main Context


user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}
  

nginx.conf - Example

HTTP Block


http {
    include mime.types;
    sendfile on;
    keepalive_timeout 65;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}
  

nginx.conf - Example

Server Block


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

nginx.conf - Example

Location Blocks


location = /health {
    return 200 "OK";
}

location ^~ /uploads/ {
    root /var/www/storage;
    expires 30d;
}

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

nginx.conf - Example

Location Blocks (cont.)


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

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.5-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

location ~ /\.(?!well-known).* {
    deny all;
}
  

Config Tips and Tricks

  • nginx -t is your friend
  • Break the config into multiple small files
  • Order matters
  • Make small, incremental changes
  • Consider version control

"Real World" Example

VinylVault.com

"Real World" Example

VinylVault.com

  • E-Commerce site specializing in selling records
  • React SPA Front End + Laravel Backend App
  • Aging server located on premise
  • Performance issues + bugs
  • Starting to hit growing pains
  • V2 in progress
Redirects

Redirects - Simple


server {
    listen 80;
    server_name vinylvault.com;

    location = /about-us {
        return 301 /about;
    }
}
  

Redirects - Simple Demo

GET https://vinylvault.com/about-us
HTTP/2 301 
location: https://vinylvault.com/about

Redirects - Rewrite


server {
    listen 80;
    server_name vinylvault.com;

    rewrite ^/album/(\d+)$ /albums/$1 permanent;
}
  

Redirects - Rewrite Demo

GET https://vinylvault.com/album/1
HTTP/2 301 
location: https://vinylvault.com/albums/1

Redirects - Map


map $request_uri $new_url {
    /get-in-touch       /contact;
    /what-we-sell       /services;
    /blog/vinyl-is-back /blog/vinyl-never-left;
    /flash-sale         /promotions;
}

server {
    listen 80;
    server_name vinylvault.com;

    location / {
        if ($new_url) {
            return 301 $new_url;
        }
    }
}
  

Redirects - Map Demo

GET https://vinylvault.com/flash-sale
HTTP/2 301 
location: https://vinylvault.com/promotions
Caching

Caching - Static Files


 location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
    expires 365d;
    add_header Cache-Control "public";
}
  

Caching - FastCGI


fastcgi_cache_path /var/cache/nginx/fastcgi
                   levels=1:2
                   keys_zone=my_cache:10m
                   max_size=1g
                   inactive=60m
                   use_temp_path=off;
  

Caching - FastCGI


location ~ \.php$ {
       # ...
        fastcgi_cache my_cache;

        fastcgi_cache_key "$scheme$request_method$host$request_uri";

        fastcgi_cache_valid 200 10m;
        fastcgi_cache_valid 404 1m;

        fastcgi_cache_background_update on

        add_header X-Cache-Status $upstream_cache_status;
    }
  

Caching - Downtime Handling


fastcgi_cache_use_stale error
                        timeout
                        http_500
                        http_502
                        http_503
                        http_504
                        updating;
  

Caching - No Cache + Bypass


fastcgi_no_cache $cookie_nocache $arg_nocache;
fastcgi_cache_bypass $http_pragma $arg_bypasscache;
  

Caching - Other Config Options

Minimum Uses


fastcgi_cache_min_uses 1;
      

Cache Methods


fastcgi_cache_methods GET HEAD;
      

Background Updating


fastcgi_cache_background_update on;
    

Caching Demo — Cache MISS

GET https://vinylvault.com/albums/1
HTTP/2 200 
X-Cache-Status: MISS
{"id":1,"title":"The Dark Side of the Moon","artist":"Pink Floyd","price":"$34.99","generated_at":"2026-10-16 13:57:31"}

Caching Demo — Cache HIT

GET https://vinylvault.com/albums/1
HTTP/2 200 
X-Cache-Status: HIT
{"id":1,"title":"The Dark Side of the Moon","artist":"Pink Floyd","price":"$34.99","generated_at":"2026-10-16 13:57:31"}

Caching Demo — Bypass

GET https://vinylvault.com/albums/1?bypasscache=1
HTTP/2 200 
x-cache-status: BYPASS
{"id":1,"title":"The Dark Side of the Moon","artist":"Pink Floyd","price":"$34.99","generated_at":"2026-10-16 14:07:08"}

Caching Caveats

  • Don't cache dynamic data
  • Be strategic
  • Know the limitations of your hosting environment
Access Control

Access Control - IP Address


location /admin {
  deny  192.168.1.1;
  allow 192.168.1.0/24;
  allow 10.1.1.0/16;
  allow 2001:0db8::/32;
  deny  all;
}
  

Access Control - IP Demo

GET https://vinylvault.com/admin
HTTP 403

Access Control - Http Basic Auth


location /staff {
    auth_basic           "VinylVault Staff Only";
    auth_basic_user_file conf/htpasswd;
}
  
Browser basic auth prompt

Access Control - Auth Request


location /members/ {
    auth_request /auth;
}

location = /auth {
    proxy_pass https://auth-server.com
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Original-URI $request_uri;
}
  

Access Control - Auth Request Demo

GET https://vinylvault.com/members
HTTP 401

Access Control - Auth Request Demo

GET https://vinylvault.com/members
X-Auth-Token: ThisIsASecretToken
{
  "access": "granted",
  "message": "You are authenticated"
}
Rate Limiting

Rate Limiting


limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

    ...

    server {

        ...

        location /search/ {
           limit_req_status 429;
           limit_req zone=one burst=5;
        }
}
  

Rate Limiting Demo

GET https://vinylvault.com/search?q=taylor+swift × 8 at once
Request 4: 200
Request 7: 429
Request 5: 429
Request 3: 429
Request 1: 429
Request 8: 429
Request 6: 429
Request 2: 200
Geolocation

Geolocation

  • Some assembly required
  • Install http_geoip_module
  • v1 (.dat) vs v2 (.mmdb)
  • MaxMind account
  • Set up regular updates

Geolocation - Configuration


http {
    geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
        $geoip2_country_code country iso_code;
        $geoip2_country_name country names en;
    }

    geoip2 /usr/share/GeoIP/GeoLite2-City.mmdb {
        $geoip2_city         city names en;
        $geoip2_region       subdivisions 0 iso_code;
        $geoip2_timezone     location time_zone;
    }
}
  

Geolocation - Blocking


map $geoip2_country_code $blocked_country {
    default  0;
    RU       1;
    KP       1;
}

server {
    if ($blocked_country) {
        return 403 "We can't ship there.";
    }
}
  

Geolocation - Redirecting


map $geoip2_country_code $redirect_url {
    default  "";
    GB       "https://uk.vinylvault.com";
    FR       "https://fr.vinylvault.com";
}

server {
    if ($redirect_url) {
        return 302 $redirect_url;
    }
}
  

Geolocation - Usage in PHP

Add to headers


location / {
    fastcgi_param HTTP_X_COUNTRY_CODE $geoip2_country_code;
    fastcgi_param HTTP_X_CITY         $geoip2_city;
}
  

Geolocation Demo

GET https://vinylvault.com/shop
{
  "country_code": "US",
  "city": "Austin",
  "local_currency": "USD",
  "local_price": 25.89
}
Split Testing

Split Testing


http {
    split_clients "$remote_addr" $ab_variant {
        20%   "checkout_v2";
        25%   "checkout_v3";
        *     "control";
    }

    server{
      ...
      location / {
        ...
        fastcgi_param HTTP_X_AB_VARIANT $ab_variant;
      }
      add_header Set-Cookie "ab_variant=$ab_variant;" always;
    }
}
  

Split Testing Demo

GET https://vinylvault.com/checkout
{
  "nginx_demo": "split_clients — A/B variant assignment",
  "variant_from_server_var": "checkout_v2",
  "variant_from_cookie": "not set",
  "variant_description": "Simplified one-page checkout"
}

Split Testing Demo — Override via Cookie

GET https://vinylvault.com/checkout
Cookie: ab_variant=checkout_v3
{
  "nginx_demo": "split_clients — A/B variant assignment",
  "variant_from_server_var": "checkout_v2",
  "variant_from_cookie": "checkout_v3",
  "variant_description": "Express checkout with saved addresses"
}

Split Testing - Other Uses


http {
    geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
        $geoip2_country_code country iso_code;
    }

    split_clients "$remote_addr" $in_block_bucket {
        25%   1;
        *     0;
    }
    map "$geoip2_country_code:$in_block_bucket" $should_block {
        "CA:1"  1;
        default 0;
    }
    server {
        if ($should_block) {
            return 403 "Access denied.";
        }
    }
}
  
Image Processing

Image Processing

  • Some assembly required
  • Requires image_filter_module
  • Needs libgd installed
  • Supports JPEG, GIF, PNG, WebP

Image Processing - Filter Types


image_filter resize 300 200;

image_filter crop 300 200;

image_filter rotate 90;

image_filter size;
  

Image Processing - Usage


location ~* ^/images/(.+\.(jpg|jpeg|png|gif|webp))$ {
    image_filter        resize $arg_w $arg_h;
    image_filter_buffer 10m;

    expires             30d;
}
  

Image Processing - Usage


<img src="/images/abbey-road.jpg?w=300&h=300" />"
  

Image Processing Demo — Resize

GET https://vinylvault.com/images/abbey-road.jpg?w=300&h=300
HTTP/2 200 
content-type: image/jpeg
content-length: 23862

Image Processing Demo — Resize, cont.

GET https://vinylvault.com/images/abbey-road.jpg?w=50&h=50
HTTP/2 200 
content-type: image/jpeg
content-length: 1739
Request Mirroring

Request Mirroring


server {
    listen 80;

    location / {
        mirror /mirror;
        mirror_request_body on;
        ...
    }

    location /mirror {
        internal;
        proxy_pass https://v2.vinylvault.com$request_uri;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
  

Request Mirroring Demo

GET https://v2.vinylvault.com/mirror-log
{
  "mirrored_requests": [
    {
      "method": "GET",
      "uri": "/mirror-log?nocache=1",
      "ip": "203.0.113.10",
      "at": "2026-10-16 13:57:37"
    },
    {
      "method": "GET",
      "uri": "/checkout?nocache=1",
      "ip": "203.0.113.10",
      "at": "2026-10-16 13:57:36"
    },
    {
      "method": "GET",
      "uri": "/checkout",
      "ip": "203.0.113.10",
      "at": "2026-10-16 13:57:36"
    },
    {
      "method": "GET",
      "uri": "/shop?nocache=1",
      "ip": "203.0.113.10",
      "at": "2026-10-16 13:57:35"
    },
    {
      "method": "GET",
      "uri": "/search?q=taylor+swift",
      "ip": "203.0.113.10",
      "at": "2026-10-16 13:57:35"
    },
    {
      "method": "GET",
      "uri": "/search?q=taylor+swift",
      "ip": "203.0.113.10",
      "at": "2026-10-16 13:57:34"
    },
    {
      "method": "GET",
      "uri": "/members?nocache=1",
      "ip": "203.0.113.10",
      "at": "2026-10-16 13:57:34"
    },
    {
      "method": "GET",
      "uri": "/members",
      "ip": "203.0.113.10",
      "at": "2026-10-16 13:57:33"
    },
    {
      "method": "GET",
      "uri": "/albums/1?nocache=1",
      "ip": "203.0.113.10",
      "at": "2026-10-16 13:57:33"
    },
    {
      "method": "GET",
      "uri": "/albums/1",
      "ip": "203.0.113.10",
      "at": "2026-10-16 13:57:33"
    },
    {
      "method": "GET",
      "uri": "/albums/1",
      "ip": "203.0.113.10",
      "at": "2026-10-16 13:57:31"
    },
    {
      "method": "GET",
      "uri": "/sitemap.xml",
      "ip": "203.0.113.10",
      "at": "2026-10-16 13:29:50"
    },
    {
      "method": "GET",
      "uri": "/sitemap.xml",
      "ip": "203.0.113.10",
      "at": "2026-10-16 11:26:35"
    },
    {
      "method": "GET",
      "uri": "/",
      "ip": "203.0.113.10",
      "at": "2026-10-16 11:21:10"
    },
    {
      "method": "GET",
      "uri": "/sitemap.xml",
      "ip": "203.0.113.10",
      "at": "2026-10-16 09:21:24"
    },
    {
      "method": "GET",
      "uri": "/",
      "ip": "203.0.113.10",
      "at": "2026-10-16 08:21:24"
    },
    {
      "method": "GET",
      "uri": "/sitemap.xml",
      "ip": "203.0.113.10",
      "at": "2026-10-16 07:24:01"
    },
    {
      "method": "GET",
      "uri": "/",
      "ip": "203.0.113.10",
      "at": "2026-10-16 05:38:47"
    },
    {
      "method": "GET",
      "uri": "/sitemap.xml",
      "ip": "203.0.113.10",
      "at": "2026-10-16 05:29:21"
    },
    {
      "method": "GET",
      "uri": "/sitemap.xml",
      "ip": "203.0.113.10",
      "at": "2026-10-16 03:25:18"
    }
  ]
}

What We Did With nginx

  • Redirects (simple, rewrite, map)
  • Caching (static files, FastCGI, downtime handling)
  • Access Control (IP, basic auth, auth_request)
  • Rate Limiting
  • Geolocation
  • Split Testing
  • Image Processing
  • Request Mirroring

In Conclusion

  • The right tool for the right job
  • Infrastructure should work in service of our application
  • nginx is really cool!
Thank you!

Let's Connect

QR code for chrislemon.org
https://chrislemon.org
Feedback QR code
Leave Feedback