Nginx

One nginx instance serves every site. Each site has its own server block in /etc/nginx/sites-available/<site>.conf, symlinked into sites-enabled/. tulixhost owns the file and rewrites it on create_vhost.sh; if you need per-site directives, put them in the per-site extension file (see below).

Where everything lives

pathcontents
/etc/nginx/nginx.confMain config. tulixhost patches server_tokens off, body size limits, and three rate-limit zones into the http { } block on first run (marked by a tulixhost-managed comment).
/etc/nginx/sites-available/<site>.confPer-site vhost. Managed by create_vhost.sh.
/etc/nginx/sites-enabled/<site>.confSymlink to the above.
/etc/nginx/snippets/tulixhost-security.confGlobal security headers + dotfile/backup-ext deny.
/etc/nginx/snippets/tulixhost-tls.confTLS protocol + cipher + stapling defaults.
/etc/nginx/conf.d/00-tulixhost-default.confCatch-all returning 444 to unknown hostnames — stops drive-by IP scanners.
/etc/nginx/dhparam.pem2048-bit DH params, generated once on first setup.sh.
/data/web/<site>/conf/nginx-extra.confYour per-site extension file. Included from the vhost. Survives re-renders.

Security snippet contents

tulixhost-security.conf is included by every per-site vhost. It applies:

add_header X-Frame-Options            "SAMEORIGIN"           always;
add_header X-Content-Type-Options     "nosniff"              always;
add_header Referrer-Policy            "strict-origin-when-cross-origin" always;
add_header X-XSS-Protection           "1; mode=block"        always;
add_header Permissions-Policy         "interest-cohort=()"   always;

location ~ /\.(?!well-known) { deny all; access_log off; log_not_found off; }
location ~* \.(bak|old|orig|swp|swo|inc|conf|ini|env|envtulix|sql|dist)$ { deny all; }

The two location blocks are critical: the first deny all dotfiles (except .well-known for ACME), and the second blocks accidental exposure of source/config files named with common backup or config extensions.

TLS snippet contents

tulixhost-tls.conf is included only by HTTPS server blocks:

ssl_protocols              TLSv1.2 TLSv1.3;
ssl_ciphers                ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
ssl_prefer_server_ciphers  off;
ssl_session_cache          shared:tulixSSL:10m;
ssl_session_tickets        off;
ssl_stapling               on;
ssl_stapling_verify        on;
add_header                 Strict-Transport-Security "max-age=63072000" always;

Mozilla intermediate cipher suite. HSTS is set to two years; if you ever want to back out of HTTPS, you need a transition window (set HSTS to 0 first, wait for clients to expire, then redirect).

Rate-limit zones

Three zones are defined globally in nginx.conf and available to every site:

zoneratetypical use
tulix_general30 req/sec per IPDefault for all routes.
tulix_login5 req/sec per IPWrap your login endpoint with this.
tulix_conn50 concurrent per IPLimits total open connections from one client.

The default vhost applies tulix_general with burst=60 and tulix_conn with limit 50. To rate-limit a specific endpoint harder, add to conf/nginx-extra.conf:

location = /login {
    limit_req zone=tulix_login burst=10 nodelay;
    try_files $uri /index.php?$query_string;
}

FPM proxy

The vhost routes PHP through the per-site FPM socket:

location ~ \.php$ {
    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/data/web/<site>/tmp/php-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO        $fastcgi_path_info;
    fastcgi_param HTTPS            on;     # or off in the HTTP-only vhost
    fastcgi_read_timeout 120;
}

try_files $uri =404 is the defense against the classic cgi.fix_pathinfo bug. We also set cgi.fix_pathinfo=0 in the global PHP config as defense in depth.

The per-site extension file

conf/nginx-extra.conf is included from inside the server { } block, so anything you put there acts on the site's vhost. Common uses:

Raise body size

client_max_body_size 512m;

Static-asset caching

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

Reverse-proxy a sub-path to a backend

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

Force a redirect

location = /old-page { return 301 /new-page; }

Reloading

sudo nginx -t                   # validate
sudo systemctl reload nginx     # if valid

Reload is graceful — existing in-flight requests finish on the old workers, new ones use the new config. If nginx -t fails, do not reload — fix the syntax first. create_vhost.sh validates before reloading; if it dies in the middle of a render, your nginx is still running on the previous good config.

HTTP/3 / QUIC

Not enabled by default. Ubuntu's stock nginx ships with QUIC support starting at 1.25.x. To enable for a site, add to its nginx-extra.conf:

listen 443 quic reuseport;
listen [::]:443 quic reuseport;
add_header Alt-Svc 'h3=":443"; ma=86400';

And open UDP/443 on the firewall: sudo ufw allow 443/udp.