Skip to content

WEB SERVER โ€” Web Server Configuration & Administration

Last reviewed: 2026-06-16

Purpose: Comprehensive knowledge base covering web server configuration, administration, and operational patterns for Nginx, Caddy, and Apache.


Table of Contents


1. Web Server Overview

Web servers accept HTTP(S) requests from clients and serve responses โ€” static files, proxied application traffic, or dynamically generated content. The three most common open-source web servers in modern DevOps environments are:

Server Language Configuration Style Key Strength
Nginx C Declarative directive blocks High performance, low resource usage
Caddy Go Simple Caddyfile (or JSON API) Automatic HTTPS, simplicity
Apache C Directive-based (.htaccess + main config) Rich module ecosystem, wide compatibility

2. Nginx

2.1 Installation

# Debian / Ubuntu
sudo apt update && sudo apt install nginx -y
sudo systemctl enable --now nginx

# RHEL / Rocky / Alma / Fedora
sudo dnf install nginx -y
sudo systemctl enable --now nginx

Verify with:

curl -I http://localhost

Expected: HTTP/1.1 200 OK

2.2 Server Blocks (sites-available Pattern)

Nginx uses the sites-available / sites-enabled convention to manage virtual hosts cleanly:

/etc/nginx/
โ”œโ”€โ”€ nginx.conf              # Main configuration
โ”œโ”€โ”€ sites-available/        # All server block configs (on disk)
โ””โ”€โ”€ sites-enabled/          # Symlinks to enabled configs (in use)

Creating a server block:

# /etc/nginx/sites-available/example.com
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    root /var/www/example.com;
    index index.html index.htm;

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

Enable it:

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

2.3 Reverse Proxy

Reverse proxying passes client requests to a backend application (e.g., Node.js, Python, Go).

server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_cache_bypass $http_upgrade;
    }
}

WebSocket support requires the Upgrade and Connection headers shown above.

2.4 Static File Serving & Caching Headers

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

    # Enable gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript image/svg+xml;
    gzip_min_length 256;

    # Cache static assets aggressively
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff2?|ttf|eot)$ {
        expires 365d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # HTML files โ€” short cache
    location ~* \.html?$ {
        expires 1h;
        add_header Cache-Control "public, must-revalidate";
    }

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

2.5 Rate Limiting

Nginx rate limiting uses the limit_req_zone and limit_req directives.

# Define a shared memory zone (zone name, size, rate)
# In http block (nginx.conf or conf.d/):

limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/s;

# Apply in a server or location block:

server {
    location /api/ {
        limit_req zone=api_limit burst=10 nodelay;
        proxy_pass http://backend;
    }

    location /login {
        limit_req zone=login_limit burst=3 nodelay;
        proxy_pass http://auth-backend;
    }
}
  • burst=10 โ€” allows up to 10 excess requests queued.
  • nodelay โ€” process burst immediately instead of throttling.

2.6 Load Balancing Upstreams

Define an upstream group and reference it in proxy_pass:

# In http block:
upstream app_backend {
    least_conn;                              # least connections strategy
    # available methods: round-robin (default), least_conn, ip_hash, random
    server 10.0.1.10:3000 weight=3;
    server 10.0.1.11:3000 weight=2;
    server 10.0.1.12:3000 backup;           # backup only
}

server {
    listen 80;
    server_name loadbalanced.example.com;

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

Health checks (Nginx Plus) or passive via max_fails and fail_timeout:

upstream app_backend {
    server 10.0.1.10:3000 max_fails=3 fail_timeout=30s;
    server 10.0.1.11:3000 max_fails=3 fail_timeout=30s;
}

2.7 SSL/TLS with Let's Encrypt

Use Certbot to obtain and renew certificates:

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run   # test auto-renewal

Certbot modifies your server block to add SSL directives automatically. The result looks like:

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

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

    # Modern SSL hardening
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # HSTS (HTTP Strict Transport Security)
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

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

# Redirect HTTP โ†’ HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

Auto-renewal is handled by a systemd timer (installed by Certbot):

sudo systemctl list-timers | grep certbot

3. Caddy

Caddy is written in Go and automates HTTPS by default via Let's Encrypt / ZeroSSL. Configuration is minimal.

3.1 Installation

# Official method (one binary)
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy

# Or direct download
curl -fsSL https://caddyserver.com/api/download -o /usr/local/bin/caddy
chmod +x /usr/local/bin/caddy

3.2 Caddyfile Configuration

Configuration lives in /etc/caddy/Caddyfile. The syntax is compact:

example.com {
    root * /var/www/example.com
    encode gzip
    file_server
}

Caddy automatically provisions and renews TLS certificates โ€” no Certbot needed.

3.3 Reverse Proxy

app.example.com {
    reverse_proxy 127.0.0.1:3000
}

With headers and health checks:

app.example.com {
    reverse_proxy 127.0.0.1:3000 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
        health_interval 10s
        health_timeout 2s
        health_path /health
    }
}

WebSocket is transparent in Caddy โ€” no special directives required.

3.4 Static File Serving & Caching

static.example.com {
    root * /var/www/static
    file_server {
        hide .git .env .ht*
    }
    encode gzip

    # Cache control via header
    header /static/* Cache-Control "public, immutable, max-age=31536000"
    header /assets/* Cache-Control "public, immutable, max-age=31536000"
    header /*.html Cache-Control "public, must-revalidate, max-age=3600"
}

3.5 Rate Limiting

Caddy v2 supports rate limiting via the rate_limit directive (available in the standard build):

api.example.com {
    rate_limit {
        zone dynamic {
            key {remote_host}
            events 30
            window 1m
        }
    }
    reverse_proxy 127.0.0.1:8080
}

3.6 Load Balancing

loadbalanced.example.com {
    reverse_proxy {
        to 10.0.1.10:3000 10.0.1.11:3000 10.0.1.12:3000
        lb_policy least_conn
        # Available: random, round_robin, least_conn, first, ip_hash, uri_hash
        health_uri /health
        health_interval 30s
    }
}

3.7 Automatic SSL/TLS with Let's Encrypt

Caddy automatically obtains and renews certificates. Configuration is zero โ€” just specify the domain name:

# /etc/caddy/Caddyfile
example.com {
    root * /var/www/example.com
    file_server
}

Caddy listens on both 80 and 443, serves an HTTP-01 challenge on port 80, and upgrades to HTTPS automatically. For staging (to avoid rate limits during testing):

CADDY_ACME_CA=https://acme-staging-v02.api.letsencrypt.org/directory caddy run

4. Apache

4.1 Installation

# Debian / Ubuntu
sudo apt update && sudo apt install apache2 -y
sudo systemctl enable --now apache2

# RHEL / Rocky / Alma / Fedora
sudo dnf install httpd -y
sudo systemctl enable --now httpd

4.2 VirtualHosts (sites-available Pattern)

Apache uses the same sites-available / sites-enabled convention:

/etc/apache2/
โ”œโ”€โ”€ apache2.conf
โ”œโ”€โ”€ sites-available/
โ””โ”€โ”€ sites-enabled/

VirtualHost example:

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/example.com

    <Directory /var/www/example.com>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/example.com_error.log
    CustomLog ${APACHE_LOG_DIR}/example.com_access.log combined
</VirtualHost>

Enable:

sudo a2ensite example.com
sudo apache2ctl configtest
sudo systemctl reload apache2

4.3 Reverse Proxy

Requires mod_proxy and mod_proxy_http:

sudo a2enmod proxy proxy_http proxy_balancer lbmethod_byrequests
sudo systemctl restart apache2
<VirtualHost *:80>
    ServerName app.example.com

    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/

    <Location />
        Require all granted
    </Location>
</VirtualHost>

With WebSocket:

<VirtualHost *:80>
    ServerName ws.example.com

    ProxyPass / ws://127.0.0.1:3000/
    ProxyPassReverse / ws://127.0.0.1:3000/
</VirtualHost>

4.4 SSL/TLS with Let's Encrypt

sudo apt install certbot python3-certbot-apache -y
sudo certbot --apache -d example.com -d www.example.com

This generates a configuration block like:

<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /var/www/example.com

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>

Redirect HTTP โ†’ HTTPS:

<VirtualHost *:80>
    ServerName example.com
    Redirect permanent / https://example.com/
</VirtualHost>

5. Comparison Table: Nginx vs Caddy vs Apache

Feature Nginx Caddy Apache
Configuration style Declarative block directives Caddyfile (simple) or JSON API XML-like <VirtualHost> blocks
Auto HTTPS No (requires Certbot) Yes โ€” built-in, automatic No (requires Certbot)
SSL/TLS management Manual or Certbot Fully automatic (Let's Encrypt / ZeroSSL) Manual or Certbot
Configuration reload nginx -s reload (hot) caddy reload (hot) apache2ctl graceful (hot)
Static file perf Excellent โ€” very low memory Excellent โ€” Go runtime, fast Good โ€” per-process overhead
Reverse proxy Native, highly configurable Native, simple syntax Via mod_proxy
Rate limiting Built-in (limit_req_zone) Built-in (rate_limit directive) Via mod_ratelimit or mod_qos
Load balancing Built-in upstream module Built-in reverse_proxy with policies Via mod_proxy_balancer
WebSocket support Requires explicit Upgrade headers Transparent โ€” no extra config Requires mod_proxy_wstunnel
Dynamic modules Loaded at compile or via dynamic mod Built-in modules, plugins in Go a2enmod / LoadModule runtime
.htaccess (per-dir) Not supported (discouraged) Not supported Native โ€” full support
Performance under load Event-driven, excellent Go goroutines, excellent Process/thread-based, higher memory
Learning curve Moderate Low Moderate to high
Best for High-traffic, microservices, reverse proxy Simple setup, automatic TLS, developer velocity Shared hosting, .htaccess-dependent apps

Quick Reference

When to use which server

Scenario Recommended
High-traffic static file serving Nginx or Caddy
Reverse proxy / API gateway Nginx
Zero-fuss auto HTTPS Caddy
Shared hosting with user-owned configs Apache
Microservices with many upstreams Nginx
Rapid development / prototyping Caddy
Legacy PHP app needing .htaccess overrides Apache

Essential commands

Action Nginx Caddy Apache
Test config nginx -t caddy validate apache2ctl configtest
Reload systemctl reload nginx systemctl reload caddy systemctl reload apache2
Restart systemctl restart nginx systemctl restart caddy systemctl restart apache2
View logs journalctl -u nginx -f journalctl -u caddy -f journalctl -u apache2 -f
Enable site ln -s ... sites-enabled/ Add to Caddyfile a2ensite <site>
Disable site rm sites-enabled/<site> Remove from Caddyfile a2dissite <site>
SSL renewal test certbot renew --dry-run Automatic (no action needed) certbot renew --dry-run

This article replaces the previous placeholder. For deep dives into Nginx-only or Caddy-only topics, see the dedicated CADDY module.