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
| path | contents |
|---|---|
/etc/nginx/nginx.conf | Main 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>.conf | Per-site vhost. Managed by create_vhost.sh. |
/etc/nginx/sites-enabled/<site>.conf | Symlink to the above. |
/etc/nginx/snippets/tulixhost-security.conf | Global security headers + dotfile/backup-ext deny. |
/etc/nginx/snippets/tulixhost-tls.conf | TLS protocol + cipher + stapling defaults. |
/etc/nginx/conf.d/00-tulixhost-default.conf | Catch-all returning 444 to unknown hostnames — stops drive-by IP scanners. |
/etc/nginx/dhparam.pem | 2048-bit DH params, generated once on first setup.sh. |
/data/web/<site>/conf/nginx-extra.conf | Your 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:
| zone | rate | typical use |
|---|---|---|
tulix_general | 30 req/sec per IP | Default for all routes. |
tulix_login | 5 req/sec per IP | Wrap your login endpoint with this. |
tulix_conn | 50 concurrent per IP | Limits 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.