Self-hosting Xonotic with Docker for LAN games
I remember playing Xonotic over LAN at college. Hosting it again is a way to bring those matches back: a few computers on the same network, a map, and someone to shoot at.
Xonotic is a free, open-source arena shooter. This guide starts with a quick LAN game, then runs a dedicated server with Docker Compose so it keeps running when the host player leaves. We build our own container from the official release, keep configuration outside the image, and publish only the game port.
The quickest LAN match
Install the same current Xonotic release on everyone's computer. On one computer, open Multiplayer → Create, choose a map and game type, and start the match. Other players connect to that computer's LAN address.
This is a listen server: the host also plays. For a separate machine that hosts without a player, use the dedicated setup below. The default game port is UDP 26000, not TCP. Official server FAQ.
Use Ethernet where possible, and make sure the players are on a network that permits communication between devices. Guest Wi-Fi and access-point client isolation can prevent a LAN match even when everyone has internet access.
Prepare the Docker project
Use an x86-64 Linux host with Docker Engine and the Compose plugin. This example packages Xonotic 0.8.6, the release currently linked on the official download page. The roughly 1.2 GB archive includes the game assets. Allow several GB of disk space for downloading, extracting, and building the image. This recipe targets linux/amd64; it is not a native ARM image.
sudo install -d -o "$(id -u)" -g "$(id -g)" /srv/xonotic
cd /srv/xonotic
# The container runs as UID/GID 10001.
sudo install -d -o 10001 -g 10001 -m 0750 state state/data
mkdir -p backupsCreate .env with the actual IPv4 address assigned to your Docker host's LAN interface. Reserve that address in DHCP so it does not change:
XONOTIC_BIND_IP=192.168.1.50Create .dockerignore so runtime state and backups never enter the image build context:
state/
backups/
.env
.git/Dockerfile
Save this as /srv/xonotic/Dockerfile. The build downloads the official ZIP, verifies its pinned SHA-512 checksum, and copies the extracted release into a separate runtime stage. The checksum below is from the official 0.8.6 checksum file.
FROM debian:bookworm-slim AS download
ARG XONOTIC_VERSION=0.8.6
ARG XONOTIC_SHA512=cb39879e96f19abb2877588c2d50c5d3e64dd68153bec3dd1bebedf4d765e506afa419c28381d7005aed664cb1a042571c132b5b319e4308cab67745d996c2a6
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl unzip \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /tmp/download
RUN curl --fail --location --retry 3 \
"https://dl.xonotic.org/xonotic-${XONOTIC_VERSION}.zip" -o xonotic.zip \
&& echo "${XONOTIC_SHA512} xonotic.zip" | sha512sum --check --strict - \
&& unzip -q xonotic.zip -d /opt \
&& chmod +x /opt/Xonotic/xonotic-linux64-dedicated
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates libsdl2-2.0-0 libvorbisfile3 libcurl4 libpng16-16 libgmp10 \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --gid 10001 xonotic \
&& useradd --uid 10001 --gid 10001 --home-dir /state \
--no-create-home --shell /usr/sbin/nologin xonotic \
&& install -d -o 10001 -g 10001 /state/data
COPY --from=download /opt/Xonotic /opt/Xonotic
WORKDIR /opt/Xonotic
ENV HOME=/state
USER 10001:10001
EXPOSE 26000/udp
ENTRYPOINT ["./xonotic-linux64-dedicated"]
CMD ["-userdir", "/state", "-sessionid", "lan-server", "+serverconfig", "server.cfg"]The release and checksum must be updated together when upgrading. Pin the Debian base image to a reviewed digest too if you need reproducible base layers. The -userdir and +serverconfig arguments follow the upstream dedicated-server instructions and launcher.
docker-compose.yaml
Save beside the Dockerfile:
name: xonotic-lan
services:
xonotic:
build:
context: .
image: local/xonotic:0.8.6
platform: linux/amd64
restart: unless-stopped
init: true
stdin_open: true
tty: true
stop_grace_period: 30s
ports:
- "${XONOTIC_BIND_IP:?Set your host LAN IPv4 address in .env}:26000:26000/udp"
volumes:
- ./state:/state
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
labels:
- traefik.enable=false
- com.centurylinklabs.watchtower.enable=false
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"The configuration below lives at state/data/server.cfg on the host and /state/data/server.cfg in the container. The writable bind mount preserves settings and runtime state when the container is recreated. Docker manages restarts; no separate systemd game service is needed.
server.cfg: a small LAN arena
From /srv/xonotic, create state/data/server.cfg:
sudo nano state/data/server.cfg
sudo chown 10001:10001 state/data/server.cfg
sudo chmod 640 state/data/server.cfgPaste this starting configuration:
hostname "School LAN Revival"
sv_motd "Welcome back. Have fun, and give new players a chance."
sv_public 0
port 26000
maxplayers 12
// Start with deathmatch and bundled maps.
gametype dm
g_maplist "stormkeep warfare xoylent"
g_maplist_shuffle 1
timelimit_override 10
fraglimit_override 30
// Fill a quiet match with bots.
minplayers 4
skill 3
// Keep remote administration disabled for this setup.
rcon_password ""
sv_status_privacy 1
sv_weaponstats_file ""minplayers 4 fills the match toward four participants using bots; it is not four bots in addition to all humans. Tune skill to your group. The final setting disables the weapon-statistics submission configured in the example server file. For more settings, start with the official example configuration.
sv_public 0 keeps the server off the master-server listing while allowing direct queries. It is not a firewall or admission control. A reachable host can still accept connections. Restrict UDP 26000 to your LAN/VPN in the host firewall, and do not configure an internet port-forward for a LAN-only server. Engine definition of server visibility.
The Compose mapping binds UDP 26000 to the specified host IPv4 address instead of all interfaces. That limits the published destination, but is not a source allowlist: routed networks may still reach it. Keep the router's WAN port-forward disabled for LAN-only play and use Docker-aware firewall policy if restricting source subnets. Docker-published ports can bypass ordinary UFW input rules, so a plain ufw allow/deny recipe is not sufficient here. Docker port publishing and firewall behavior.
Build, start, and join
From /srv/xonotic:
docker compose config --quiet
docker compose build --pull xonotic
docker compose up -d xonotic
docker compose logs -f --tail=100 xonoticThe first build downloads the release and runtime packages; later builds can reuse cached layers. Wait until the server loads a map, then connect from another computer running the same Xonotic release. Open the game console with Shift+Escape and enter your Docker host's address:
connect 192.168.1.50:26000Use the host's LAN address, not a container IP or 127.0.0.1. LAN discovery may not cross a Docker bridge, so direct connection is the reliable starting point. Guest Wi-Fi client isolation can also block connections.
# Logs, resource use, and normal lifecycle operations:
docker compose logs --tail=100 xonotic
docker compose stats xonotic
docker compose restart xonotic
docker compose stop xonoticAfter editing the mounted server.cfg, restart between matches. After changing .env or Compose settings, use docker compose up -d to recreate the container with those settings. Restarting alone does not apply a changed port mapping.
For the interactive server console, run docker compose attach xonotic; status shows players. Detach with Ctrl+P, then Ctrl+Q. Avoid Ctrl+C, which can signal the server. Use docker compose stop for an intentional shutdown; RCON remains disabled.
Mix up the matches
Change gametype in server.cfg, then restart between matches:
| Value | Game type |
|---|---|
dm |
Deathmatch |
tdm |
Team deathmatch |
ctf |
Capture the flag |
ca |
Clan arena |
ft |
Freeze tag |
When switching modes, also set g_maplist "" to let the server find maps suitable for that mode instead of keeping the small deathmatch selection. g_instagib 1 is another optional twist; restore g_instagib 0 for normal weapons. These are built-in settings, so no mod installation is needed for this starting setup. Configuration reference.
Begin with the bundled maps. Custom maps add a distribution step: clients need the assets, and automatic downloads need configuration. Follow the release's Docs/mapdownload.txt before adding a custom rotation.
Friends outside the LAN
For a private group, a VPN that lets the players reach the server is a useful extension of the LAN setup. Set XONOTIC_BIND_IP to the host's VPN IPv4 address, recreate the container with docker compose up -d, and connect to that address. Ensure the VPN interface exists before starting the container. Keep sv_public 0 if you do not want master-list advertising.
If you deliberately host over the internet behind a home router, use the host's LAN IPv4 address for the binding, forward UDP 26000 to it, and configure Docker-aware firewall policy accordingly. Set sv_public 1 only if you also want public discovery. A private IPv4 address behind carrier-grade NAT may require a VPN or a different hosting arrangement.
Xonotic gameplay is not HTTP. The HTTP routers in the Traefik guide do not carry this traffic. Direct UDP routing is enough for this setup; there is no browser console or TLS certificate to configure.
Maintenance and troubleshooting
Back up state/, Dockerfile, docker-compose.yaml, and .env before changing releases. Run this from /srv/xonotic between matches:
(
set -e
mkdir -p backups
docker compose stop xonotic
trap 'docker compose up -d xonotic' EXIT
sudo tar -czf "backups/xonotic-$(date +%F-%H%M%S).tgz" \
state Dockerfile docker-compose.yaml .env .dockerignore
)The subshell restarts the container on exit, including if archiving fails; check the archive and command output before counting it as a successful backup. Copy backups off the host. To restore, stop the container, move the existing state aside, extract a trusted archive, and preserve UID/GID 10001 ownership. Rebuild the saved Dockerfile and start again.
For an upgrade, update the Dockerfile's release and checksum plus the Compose image tag, then run docker compose build --pull xonotic and docker compose up -d xonotic. Test first with a copy of the state, a distinct project name and host port, and a distinct local image tag. Keep clients and server on the same release. Avoid updating the game files inside a running container; those changes would disappear on recreation.
| Problem | Check |
|---|---|
| Nothing appears in the browser | Try direct connect; public listing is disabled and discovery may not cross the bridge |
| Connection times out | .env host address, UDP mapping, Docker-aware firewall, server logs, and Wi-Fi isolation |
| Cannot assign requested address | XONOTIC_BIND_IP must exist on the Docker host; check DHCP or VPN interface state |
| Address already in use | Another container or native server may already publish/listen on UDP 26000 |
Permission denied in /state |
Host state/ and its contents need to be writable by UID/GID 10001 |
| Defaults load instead of your settings | Use state/data/server.cfg, not state/server.cfg |
| Exec format error | This recipe uses the x86-64 release; ARM needs a suitable build or configured emulation |
| A new mode has no usable maps | Reset g_maplist and use bundled maps before adding custom ones |
| You keep getting beaten by bots | Lower skill, or use minplayers 0 for human players only |
References
- Official downloads and checksums
- Xonotic FAQ
- Dedicated-server README
- Example server configuration
- Command, alias, and cvar search
Related
Linked from
- 🌿 Tools
Practical guides for the software I reach for when something breaks the battletested, often…