Skip to main content

Server Block Setup

Learning Focus

Leave this lesson with a working understanding of server block setup that you can apply immediately in production.

End-to-end workflow for adding a new site to Nginx, entirely from the terminal.


Quick New Site Script

new-site.sh
#!/bin/bash
# Add a new Nginx site — usage: sudo bash new-site.sh example.com
DOMAIN="${1:?Usage: $0 <domain>}"
WEB_ROOT="/var/www/$DOMAIN/public"
CONF="/etc/nginx/sites-available/$DOMAIN.conf"

# 1. Create web root
mkdir -p "$WEB_ROOT"
chown -R www-data:www-data "/var/www/$DOMAIN"
chmod -R 755 "/var/www/$DOMAIN"
echo "<h1>$DOMAIN on Nginx</h1>" > "$WEB_ROOT/index.html"

# 2. Write server block
cat > "$CONF" <<NGINX
server {
listen 80;
server_name $DOMAIN www.$DOMAIN;
return 301 https://\$host\$request_uri;
}

server {
listen 443 ssl http2;
server_name $DOMAIN www.$DOMAIN;

ssl_certificate /etc/ssl/certs/$DOMAIN.crt;
ssl_certificate_key /etc/ssl/private/$DOMAIN.key;
ssl_protocols TLSv1.2 TLSv1.3;

root $WEB_ROOT;
index index.php index.html;

access_log /var/log/nginx/$DOMAIN.access.log;
error_log /var/log/nginx/$DOMAIN.error.log warn;

add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;

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

location ~ \\.php\$ {
try_files \$uri =404;
include fastcgi.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}

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

location ~ /\\. { deny all; }
location ~* \\.(env|log|sh)\$ { deny all; }
}
NGINX

# 3. Enable and test
ln -sf "$CONF" "/etc/nginx/sites-enabled/$DOMAIN.conf"
nginx -t && nginx -s reload

echo "[✓] Site $DOMAIN created and enabled"
echo " Web root: $WEB_ROOT"
echo " Config: $CONF"
echo " Test: curl -H 'Host: $DOMAIN' http://localhost/"

Step-by-Step Manual Workflow

Step 1 — Web Root

DOMAIN="example.com"
WEB_ROOT="/var/www/$DOMAIN/public"

sudo mkdir -p "$WEB_ROOT"
sudo chown -R www-data:www-data "/var/www/$DOMAIN"
sudo chmod -R 755 "/var/www/$DOMAIN"

# Test file
echo "<h1>$DOMAIN</h1>" | sudo tee "$WEB_ROOT/index.html"

Step 2 — Write Config

sudo vim /etc/nginx/sites-available/example.com.conf

Paste your server block (see Config Syntax Reference).

Step 3 — Enable Site

sudo ln -s /etc/nginx/sites-available/example.com.conf \
/etc/nginx/sites-enabled/example.com.conf

Step 4 — Test and Reload

# Test syntax
sudo nginx -t

# Reload (zero downtime)
sudo nginx -s reload

Step 5 — Verify

# Test response
curl -H "Host: example.com" http://localhost/

# Check error log for any issues
sudo tail -20 /var/log/nginx/example.com.error.log

# Check access log
sudo tail -5 /var/log/nginx/example.com.access.log

WordPress-Specific Config

/etc/nginx/sites-available/wordpress.conf
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}

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

ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;

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

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

add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;

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

# Block PHP in uploads
location ~* /(?:uploads|files)/.*\.php$ { deny all; }

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

# Static assets
location ~* \.(css|js|png|jpg|jpeg|gif|ico|webp|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}

# Block sensitive
location ~* \.(env|log|ini|bak|sql|sh)$ { deny all; }
location ~ /\. { deny all; }

# Limit wp-login.php
location = /wp-login.php {
limit_req zone=login burst=5 nodelay;
include fastcgi.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
}

Reverse Proxy Site Config

/etc/nginx/sites-available/nodeapp.conf
upstream nodeapp {
server 127.0.0.1:3000;
keepalive 32;
}

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

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

ssl_certificate /etc/ssl/certs/app.example.com.crt;
ssl_certificate_key /etc/ssl/private/app.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;

access_log /var/log/nginx/app.example.com.access.log;
error_log /var/log/nginx/app.example.com.error.log warn;

location / {
proxy_pass http://nodeapp;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 10s;
proxy_read_timeout 60s;
}
}

Common Setup Mistakes

MistakeSymptomFix
Forgot try_files $uri =404 before fastcgi_passSecurity riskAdd it — prevents arbitrary file execution
Root set in location block onlyPath issues on some locationsSet root in server block, inherit to locations
Forgot to symlink to sites-enabledSite not loadedln -s sites-available/site sites-enabled/site
Didn't run nginx -t before reloadConfig error crashes on restartAlways test first
Wrong PHP socket path502 Bad Gatewayls /run/php/ and use exact filename
add_header in nested locationHeaders lost at server levelSet all headers in server block
No return 301 for HTTPMixed content warningsAdd HTTP → HTTPS redirect server block