Releases are published at https://xtq.ro/web/download/. Each version has its own folder with these files:
| File | What it is |
|---|---|
xtq-web-<version>-linux-amd64.tar.gz |
Static Linux binary for x86_64 |
xtq-web-<version>-linux-arm64.tar.gz |
Static Linux binary for ARM64 |
xtq-web-<version>-windows-amd64.zip |
Windows binary for x86_64 |
SHA256SUMS |
SHA-256 sums of the packages and the installer |
https://xtq.ro/web/download/latest.txt holds the newest version number.
A Linux package unpacks into a folder named xtq-web-<version>-linux-<arch> with xtq-web, xtq-web.service, xtq-web.conf.example, README.txt and LICENSE. The Windows zip has xtq-web.exe, xtq-web.conf.example, README.txt and LICENSE.
The installer
curl -fsSL https://xtq.ro/web/install.sh | sh
Options go after sh -s --:
| Option | Effect |
|---|---|
--docker |
Install as a Docker container instead of a systemd service |
--version <x.y.z> |
Install that version instead of the latest |
-h, --help |
Print the usage and exit |
curl -fsSL https://xtq.ro/web/install.sh | sh -s -- --version 1.0.1
curl -fsSL https://xtq.ro/web/install.sh | sh -s -- --docker
The installer runs only on Linux, on x86_64 or ARM64. It needs curl or wget, and root or sudo.
What it does in both modes
It reads the latest version from latest.txt, unless you passed --version. It downloads the package for your architecture and SHA256SUMS, and refuses to go on if the sum does not match (checksum mismatch, download refused).
It creates /etc/xtq-web. If /etc/xtq-web/xtq-web.conf does not exist, it copies the example config there. If it exists, it is left alone.
If /etc/xtq-web/env does not exist, it creates it with mode 600 and a single comment line. This file holds settings such as XTQW_ACME=1 and XTQW_ADMIN_TOKEN, one NAME=value per line, and both modes read it. An existing file is left alone.
It runs the new binary's xtq-web check against /etc/xtq-web/xtq-web.conf, on the host in native mode and inside the new container image in Docker mode. If the check fails, the installer stops before it touches the running service or container.
Native mode (the default)
It installs the binary to /usr/local/bin/xtq-web with mode 755.
If systemd is running, it also:
- creates the system user
xtq-web(no home directory, no login shell) if it doesn't exist yet, - installs
xtq-web.serviceto/etc/systemd/system/xtq-web.service, unless a different file is already there (see below), - runs
systemctl daemon-reload, enables the service and restarts it.
If /etc/systemd/system/xtq-web.service exists and you changed it, the installer keeps it and writes the new default next to it as /etc/systemd/system/xtq-web.service.new, with the message kept your /etc/systemd/system/xtq-web.service, the new default is in xtq-web.service.new. Compare the two with diff, and if you want the new one, move it in place and run systemctl daemon-reload. A unit exactly as an earlier release shipped it is recognised and simply replaced.
Without systemd it installs only the binary and prints the command to start it:
XTQW_CONFIG=/etc/xtq-web/xtq-web.conf xtq-web
Docker mode
With --docker, the installer installs Docker with Docker's official script from get.docker.com if docker is missing. It then builds a local image tagged xtq/web:<version> and xtq/web:latest. The image is built FROM scratch and holds only the binary and a CA certificate bundle. It sets XTQW_CONFIG=/etc/xtq-web/xtq-web.conf and XTQW_ACME_DIR=/var/lib/xtq-web/acme.
It runs xtq-web check inside the new image, with the same folders mounted as the container gets, so the paths in your config are checked as the server will see them. If that fails, it stops and leaves the running container alone.
It then stops any container named xtq-web with docker stop -t 20, which gives it time to finish its open requests, removes it, and starts a new one like this:
docker run -d --name xtq-web \
--restart unless-stopped \
--network host \
--ulimit nofile=1048576:1048576 \
--read-only \
--cap-drop ALL --cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges \
--log-opt max-size=10m --log-opt max-file=3 \
--stop-timeout 20 \
--env-file /etc/xtq-web/env \
-v /etc/xtq-web:/etc/xtq-web:ro \
-v /srv:/srv:ro \
-v /var/www:/var/www:ro \
-v /run/xtq:/run/xtq \
-v xtq-web-acme:/var/lib/xtq-web/acme \
xtq/web:<version>
/srv, /var/www and /run/xtq are mounted only when they exist on the host when the installer runs. /srv and /var/www are read-only. /run/xtq is writable, since connecting to the xTQ PHP socket there needs it.
Host networking means no NAT between clients and xTQ Web, so it sees the real client address. If the container is not up a second later, the installer prints its logs and fails.
Things to know about the Docker setup:
- Inside the container you see
/etc/xtq-weband whichever of/srv,/var/wwwand/run/xtqwere mounted, at the same paths as on the host. Files you serve from anywhere else, Unix sockets outside/run/xtq, and certificate files or IP list files outside/etc/xtq-webmust be mounted too, at the same paths your config uses. - Variables in
/etc/xtq-web/envare read when the container is created. After changing the file, run the installer again with--docker, or recreate the container yourself.docker restartkeeps the old values. - Certificates from Let's Encrypt live in the Docker volume
xtq-web-acme. - Reload with
docker kill -s HUP xtq-web.
A container with your own flags, here with another site folder mounted:
sudo docker stop -t 20 xtq-web
sudo docker rm xtq-web
sudo docker run -d --name xtq-web \
--restart unless-stopped \
--network host \
--ulimit nofile=1048576:1048576 \
--read-only \
--cap-drop ALL --cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges \
--log-opt max-size=10m --log-opt max-file=3 \
--stop-timeout 20 \
--env-file /etc/xtq-web/env \
-v /etc/xtq-web:/etc/xtq-web:ro \
-v /opt/sites:/opt/sites:ro \
-v xtq-web-acme:/var/lib/xtq-web/acme \
xtq/web:latest
/etc/xtq-web/env holds one NAME=value per line, for example XTQW_ACME=1. Lines starting with # are ignored.
Manual install from the tarball
Pick the version and architecture, then download and verify:
V=1.0.1
A=amd64
curl -fsSLO https://xtq.ro/web/download/$V/xtq-web-$V-linux-$A.tar.gz
curl -fsSLO https://xtq.ro/web/download/$V/SHA256SUMS
grep " xtq-web-$V-linux-$A.tar.gz\$" SHA256SUMS | sha256sum -c -
tar -xzf xtq-web-$V-linux-$A.tar.gz
cd xtq-web-$V-linux-$A
Install the binary and a config:
sudo install -m 755 xtq-web /usr/local/bin/xtq-web
sudo mkdir -p /etc/xtq-web
sudo cp -n xtq-web.conf.example /etc/xtq-web/xtq-web.conf
xtq-web check /etc/xtq-web/xtq-web.conf
Then the user and the service:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin xtq-web
sudo install -m 644 xtq-web.service /etc/systemd/system/xtq-web.service
sudo systemctl daemon-reload
sudo systemctl enable --now xtq-web
Windows
Unzip xtq-web-<version>-windows-amd64.zip, copy xtq-web.conf.example to a config file of your own and start the binary with XTQW_CONFIG pointing at it.
In Command Prompt:
set XTQW_CONFIG=C:\xtq-web\xtq-web.conf
xtq-web.exe
In PowerShell:
$env:XTQW_CONFIG = "C:\xtq-web\xtq-web.conf"
.\xtq-web.exe
Ctrl-C stops it. The Windows build proxies and serves files like the Linux one. The differences:
- There is no
SIGHUPon Windows, so a config change needs a restart. phpsites andunix:backends need Linux or macOS. On Windows the config loads, but requests to them fail.XTQW_ACME_DIRdefaults to/var/lib/xtq-web/acme. Set it to a real Windows folder if you use automatic HTTPS.- The zip has no service wrapper. Use a service manager of your choice to run it in the background.
xtq-web.exe check C:\xtq-web\xtq-web.conf validates a config the same way as on Linux.
Upgrading
Run the installer again. It downloads the new version, checks your existing config with the new binary, replaces /usr/local/bin/xtq-web, installs the new unit file unless you have a different one (see Native mode), and restarts the service. Your /etc/xtq-web/xtq-web.conf, /etc/xtq-web/env and everything else in /etc/xtq-web is kept.
The restart lets requests in flight finish first, for up to XTQW_DRAIN_SECS seconds (default 10), see Stop. New connections are refused for the moment between the old process and the new one. Open WebSockets are cut, and so is a download that takes longer than the drain time. Clients reconnect.
The simplest way to keep your own service settings is a drop-in, made with systemctl edit xtq-web. Drop-ins live in /etc/systemd/system/xtq-web.service.d/, are never touched by the installer, and the unit file itself stays as shipped.
In Docker mode, running the installer again with --docker builds the new image, stops the old container with docker stop -t 20 so its requests can finish, and recreates it with the installer's own docker run flags. If you run the container with your own flags, rebuild and recreate it yourself.
To install a specific version, or go back to one:
curl -fsSL https://xtq.ro/web/install.sh | sh -s -- --version 1.0.1
Uninstalling
Native install:
sudo systemctl disable --now xtq-web
sudo rm /etc/systemd/system/xtq-web.service
sudo systemctl daemon-reload
sudo rm /usr/local/bin/xtq-web
sudo userdel xtq-web
That keeps your config and certificates. To remove them too:
sudo rm -r /etc/xtq-web
sudo rm -r /var/lib/xtq-web
Docker install:
sudo docker rm -f xtq-web
sudo docker image rm xtq/web:latest xtq/web:1.0.1
sudo docker volume rm xtq-web-acme
The systemd unit
This is the xtq-web.service from the package, line by line.
| Setting | What it does |
|---|---|
Documentation=https://xtq.ro/web/docs/ |
Where systemctl status points for help |
After=network-online.target, Wants=network-online.target |
Start once the network is up |
Type=simple |
xtq-web runs in the foreground |
User=xtq-web, Group=xtq-web |
Runs as the unprivileged xtq-web user |
Environment=XTQW_CONFIG=/etc/xtq-web/xtq-web.conf |
The config file |
Environment=XTQW_ACME_DIR=/var/lib/xtq-web/acme |
Where Let's Encrypt certificates are stored |
EnvironmentFile=-/etc/xtq-web/env |
Optional file with more variables, such as XTQW_ADMIN_TOKEN. The - means a missing file is fine |
ExecStartPre=/usr/local/bin/xtq-web check ... |
Refuses to start on a config with a mistake in it |
ExecStart=/usr/local/bin/xtq-web |
The server |
ExecReload=... check ... then ExecReload=/bin/kill -HUP $MAINPID |
systemctl reload checks the config first and sends SIGHUP only if it passes |
Restart=always, RestartSec=1 |
Restarted one second after it exits for any reason |
TimeoutStopSec=20 |
On stop or restart, systemd waits up to 20 seconds for the process to exit before it kills it. That leaves room for open requests to finish within the default XTQW_DRAIN_SECS of 10 |
LimitNOFILE=1048576 |
Room for many open connections |
StateDirectory=xtq-web |
Creates /var/lib/xtq-web, owned by the service user and writable |
AmbientCapabilities=CAP_NET_BIND_SERVICE, CapabilityBoundingSet=CAP_NET_BIND_SERVICE |
May bind ports below 1024, and nothing else that needs root |
NoNewPrivileges=true |
Can never gain privileges |
ProtectSystem=strict |
The whole filesystem is read-only for the service, except /var/lib/xtq-web |
ProtectHome=true |
/home, /root and /run/user are invisible |
PrivateTmp=true |
The service gets its own empty /tmp |
PrivateDevices=true, ProtectKernelTunables=true, ProtectKernelModules=true, ProtectControlGroups=true, RestrictSUIDSGID=true, LockPersonality=true |
Further sandboxing |
What the sandbox means in practice:
- Files you serve must be readable by the
xtq-webuser and must not live under/homeor/root. - A backend socket in
/tmpis not visible to the service, because ofPrivateTmp. Put sockets under/runinstead. - The service can't write anywhere except
/var/lib/xtq-web. xTQ Web only needs to write there for certificates.
Secrets such as XTQW_ADMIN_TOKEN go in /etc/xtq-web/env, readable only by root. systemd reads it before dropping privileges. The installer creates the file. After a manual install, create it first with sudo install -m 600 /dev/null /etc/xtq-web/env. Then add a line and restart:
echo "XTQW_ADMIN_TOKEN=$(openssl rand -hex 32)" | sudo tee -a /etc/xtq-web/env >/dev/null
sudo systemctl restart xtq-web
Environment variables are read when the process starts, so a change to them needs systemctl restart, not reload.
