Each example is a complete config unless it says otherwise. Check it with xtq-web check <file> and load it with systemctl reload xtq-web.
A single reverse proxy
One app on port 3000, for one domain and its www:
bind 0.0.0.0:80
site example.com www.example.com {
proxy 127.0.0.1:3000
}
The app gets the client's Host, plus X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host.
The same with HTTPS
Certificates from Let's Encrypt, with plain HTTP redirected to HTTPS. Set XTQW_ACME=1 in the service environment.
bind 0.0.0.0:80
https 0.0.0.0:443
site example.com www.example.com {
bind 0.0.0.0:80
redirect https://{host}{uri} 301
}
site example.com www.example.com {
bind 0.0.0.0:443
proxy 127.0.0.1:3000
}
API and single-page app on one domain
The API on port 3000 under /api, the built frontend from disk, every other path loading the app:
bind 0.0.0.0:80
site app.example.com {
route /api/* {
proxy 127.0.0.1:3000
}
route /assets/* {
header Cache-Control "public, max-age=31536000, immutable"
files /var/www/app/dist
}
files /var/www/app/dist {
not_found /index.html
}
}
This assumes the API's own routes start with /api, as in app.get('/api/users'). If the API serves /users at its root instead, add strip:
route /api/* {
strip
proxy 127.0.0.1:3000
}
With strip, /api/users reaches the API as /users. Without it, as /api/users. Pick the one that matches your API, see strip.
Deep links like /settings get index.html with status 404, which browsers render normally. A missing file under /assets/ gets a plain 404, so a broken asset link is not answered with HTML.
Several sites on one server
bind 0.0.0.0:80
https 0.0.0.0:443
site example.com www.example.com {
proxy 127.0.0.1:3000
}
site api.example.com {
proxy 127.0.0.1:4000
}
site docs.example.com {
files /var/www/docs
}
# Any other subdomain.
site *.example.com {
respond 404 "no such app"
}
# Hostnames that are not ours: close the connection.
site * {
abort
}
With XTQW_ACME=1, certificates are issued for example.com, www.example.com, api.example.com and docs.example.com. The wildcard and * sites get the fallback certificate.
www to the apex
bind 0.0.0.0:80
https 0.0.0.0:443
site www.example.com {
redirect https://example.com{uri} 301
}
site example.com {
proxy 127.0.0.1:3000
}
Certificates are still issued for www.example.com, because challenges are answered before the redirect.
Load balancing three instances
bind 0.0.0.0:80
site app.example.com {
proxy 127.0.0.1:3001 127.0.0.1:3002 127.0.0.1:3003 {
health /api/health
health_interval 5s
health_timeout 2s
fail_duration 30s
}
}
Requests go to the instance with the fewest requests in flight. Each instance is checked every 5 seconds with a GET /api/health, and needs a 2xx within 2 seconds to stay in rotation. An instance that refuses a connection is also taken out for 30 seconds right away, and the request goes to another one. When you restart instances one at a time, new requests go to the others. Requests that were in flight on the restarted instance fail with 502.
For strict rotation instead, add lb round_robin. For a primary with standbys, lb first.
A WebSocket app
bind 0.0.0.0:80
https 0.0.0.0:443
site chat.example.com {
proxy 127.0.0.1:4000 {
health /health
timeout 30s
}
}
WebSockets need no extra config. timeout limits how long the backend may take to accept the handshake. Open connections have no time limit.
xTQ Web has no sticky sessions. If your WebSocket library falls back to long polling and runs on several instances, it usually needs every request of one client on the same instance. Run one instance, or configure the client to use WebSockets only.
If the same app also serves server-sent events or long polls, each open one holds a slot of the pool's in-flight cap. Raise it for many concurrent listeners:
proxy 127.0.0.1:4000 {
health /health
limit 4096 3s
}
A static site with SPA fallback
bind 0.0.0.0:80
site example.com {
route /assets/* {
header Cache-Control "public, max-age=31536000, immutable"
files /var/www/example
}
files /var/www/example {
not_found /index.html
}
}
For a plain static site with its own error page, use not_found /404.html instead.
Docs under a path, from their own folder
The files live in /var/www/docs, but should appear under /docs/:
site example.com {
route /docs {
redirect /docs/ 301
}
route /docs/* {
strip
files /var/www/docs
}
proxy 127.0.0.1:3000
}
/docs/guide/intro.html is read from /var/www/docs/guide/intro.html. A request for the folder /docs/guide redirects to /docs/guide/.
An admin area for some addresses only
The addresses live in a file, one per line, hostnames allowed:
# /etc/xtq-web/admins.list
203.0.113.7
198.51.100.0/24
office.example.com
bind 0.0.0.0:80
site admin.example.com {
header X-Robots-Tag "noindex, nofollow"
route * {
not ip @/etc/xtq-web/admins.list
rewrite /blocked.html
files /var/www/blocked {
status 403
}
}
proxy 127.0.0.1:3000
}
Visitors not in the list get /var/www/blocked/blocked.html with status 403. The file is read again every minute, so edits apply without a reload.
The same for one path of a larger site:
site example.com {
route /admin/* {
ip 203.0.113.7 @/etc/xtq-web/admins.list
proxy 127.0.0.1:3000
}
route /admin/* {
respond 403 "forbidden"
}
proxy 127.0.0.1:3000
}
The first /admin/* route matches only for listed addresses. Everyone else falls through to the second.
If a proxy or CDN sits in front, add it to trust, or every visitor has the proxy's address.
Login check by another service
A small service on port 9000 decides who may see /admin/ and /account/:
bind 0.0.0.0:80
site app.example.com {
route /admin/* {
auth 127.0.0.1:9000 /check/admin
proxy 127.0.0.1:3000
}
route /account/* {
auth 127.0.0.1:9000 /check/user
proxy 127.0.0.1:3000
}
proxy 127.0.0.1:3000
}
The service gets a GET /check/admin or GET /check/user with the visitor's cookies and headers. It answers 204 to let the request through, 302 with a Location to send the visitor to a login page, 401 or 403 to refuse, or 444 to close the connection. It does not see the original path, so use a different check path per area, as here.
Each proxy line is its own pool, so this config has three pools to the same app, each with its own in-flight cap.
A PHP app with xTQ PHP
xTQ PHP listening on /run/xtq/php.sock, the app in /var/www/blog:
bind 0.0.0.0:80
https 0.0.0.0:443
site blog.example.com {
php /run/xtq/php.sock /var/www/blog
}
Static files are served from the docroot directly, anonymous pages come from the page cache, and the rest goes to PHP through the waiting room. Set XTQW_SECRET so browser check passes survive restarts:
[Service]
Environment=XTQW_ACME=1
Environment=XTQW_SECRET=change-me-to-a-long-random-string
PHP and a Node API side by side:
site example.com {
route /api/* {
proxy 127.0.0.1:3000
}
php /run/xtq/php.sock /var/www/example
}
A maintenance page
Everyone gets a 503 while you work, and the health check keeps answering:
bind 0.0.0.0:80
site example.com www.example.com {
route /health {
respond 200 "ok"
}
route * {
header Retry-After 600
respond 503 "Down for maintenance. Back in a few minutes."
}
}
Let your own address through to the real app:
site example.com www.example.com {
route * {
ip 203.0.113.7
proxy 127.0.0.1:3000
}
route * {
header Retry-After 600
respond 503 "Down for maintenance. Back in a few minutes."
}
}
With an HTML page instead of text:
site example.com www.example.com {
rewrite /maintenance.html
files /var/www/maintenance {
status 503
}
}
Switch between the normal config and the maintenance one with systemctl reload xtq-web. Nobody is disconnected.
Behind Cloudflare
Cloudflare proxies every request, and xTQ Web should see the real visitor:
trust 127.0.0.1 ::1 cloudflare
bind 0.0.0.0:80
https 0.0.0.0:443
site example.com {
proxy 127.0.0.1:3000
}
With Cloudflare's SSL mode set to "Full", the self-signed fallback certificate is enough. For "Full (strict)", create an Origin CA certificate in Cloudflare and point XTQW_CERT and XTQW_KEY at it. Your app finds the visitor's address in X-Forwarded-For, followed by the Cloudflare hop, and ip rules check the visitor, not Cloudflare.
