All tutorials
On this page
Step 1: install a FiveM server on Debian
Set up the FXServer artifact, create a dedicated system user, write a systemd unit and run the txAdmin setup through an SSH tunnel without ever opening port 40120.
This is the first part of the FiveM series. By the end an FXServer runs as its own system service, txAdmin is set up, and the server comes back on its own after the machine reboots.
One point runs through the whole guide: port 40120 is closed from the first minute. The initial setup therefore goes through an SSH tunnel instead of an open port. That is twenty seconds of extra effort and it removes the window in which a freshly installed management panel sits open on the internet. That window gets scanned, reliably.
Requirements
- Debian 11 or newer with root access
- a server key from the Cfx.re keymaster
- SSH access to the machine that allows port forwarding
apt update
apt install -y curl xz-utils git ca-certificatesStep 1.1: user and directories
FXServer does not run as root. The process loads third-party resources and executes their code, and it needs no system privileges for that.
adduser --system --group --home /opt/fivem --shell /usr/sbin/nologin fivem
mkdir -p /opt/fivem/server /opt/fivem/server-data
chown -R fivem:fivem /opt/fivemSeparating the two directories is not cosmetic, it is the precondition for step 3:
/opt/fivem/
├── server/ ← the artifact. run.sh + alpine/. Replaced on every update
└── server-data/ ← your resources/, server.cfg, cache/. Left aloneStep 1.2: install the FXServer artifact
The Linux builds live at runtime.fivem.net/artifacts/fivem/build_proot_linux/master/. Every entry is a folder made of build number and hash. Take one from the recommended line, not blindly the topmost one.
cd /opt/fivem/server
curl -fsSL -o fx.tar.xz \
"https://runtime.fivem.net/artifacts/fivem/build_proot_linux/master/<NUMBER>-<HASH>/fx.tar.xz"
tar -xf fx.tar.xz
rm fx.tar.xz
chmod +x run.sh
chown -R fivem:fivem /opt/fivem/serverAfterwards there are exactly two things in there: run.sh and alpine/. The
actual server binary sits inside at alpine/opt/cfx-server/FXServer.
Step 1.3: get the server data
sudo -u fivem git clone https://github.com/citizenfx/cfx-server-data.git \
/opt/fivem/server-dataYou do not need a server.cfg at this point. txAdmin's setup wizard writes one
in a moment, including the license key and the resource list.
Step 1.4: close the panel port
txAdmin listens on 0.0.0.0:40120, so on every network interface and
unencrypted. That has to go before the server first faces the internet.
The port is therefore closed in the firewall, not in the server configuration:
ufw allow 30120/tcp
ufw allow 30120/udp
ufw allow 22/tcp
ufw deny 40120/tcp
ufw enable
ufw status verboseGame port 30120 has to be open or nobody finds the server. The panel port is
explicitly denied. The reverse proxy from
step 2 and the SSH tunnel from step 1.7
still reach txAdmin, because both arrive over 127.0.0.1 and never pass the
firewall in the first place.
The panel port itself is set on run.sh, not in server.cfg:
+set txAdminPort 40120Step 1.5: systemd unit
The unit is available as a commented template:
curl -fsSL https://uploads.musiker15.de/fivem/fivem.service \
-o /etc/systemd/system/fivem.serviceOr write it by hand:
nano /etc/systemd/system/fivem.service[Unit]
Description=FiveM FXServer with txAdmin
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=fivem
Group=fivem
WorkingDirectory=/opt/fivem/server-data
ExecStart=/opt/fivem/server/run.sh \
+set serverProfile default \
+set txAdminPort 40120
Restart=always
RestartSec=15
TimeoutStartSec=120
TimeoutStopSec=30
KillMode=mixed
KillSignal=SIGINT
StandardOutput=journal
StandardError=journal
SyslogIdentifier=fivem
LimitNOFILE=65535
# Restrained hardening. Anything restricting ptrace or namespaces breaks the
# proot build, see above.
PrivateTmp=true
ProtectKernelTunables=true
ProtectControlGroups=true
[Install]
WantedBy=multi-user.targetsystemctl daemon-reload
systemctl enable --now fivem
systemctl status fivemFour lines in there are not a matter of taste:
KillMode=mixed instead of process. run.sh is only a wrapper. With
KillMode=process only the wrapper receives the signal and the child processes
keep running. systemd then considers the service stopped while the server is
still holding the game port, and the next start fails on a port in use.
KillSignal=SIGINT. FXServer treats SIGINT as an orderly shutdown. With
the default SIGTERM it falls over harder.
After=network-online.target plus Wants=. network.target alone only
means the networking subsystem was started, not that an address is configured.
The server then tries to reach Cfx during boot, gets no route, and keeps running
without an entry in the server list.
No ProtectHome= if the server data lives under /home. In this example
everything is under /opt, so the line is not needed at all. If you run the
server from /home/fivem/, do not set ProtectHome=true or the service can no
longer see its own directory.
Step 1.6: verify the port is really closed
Locally txAdmin listens on every interface, as expected. That is fine:
ss -tlnp | grep 40120What matters is the check from outside, from a different machine. On the server itself you only measure the loopback route and always get an answer:
# On another machine, not on the server
curl -m 5 http://<server-ip>:40120/ ; echo "exit: $?"Expect a timeout, so exit code 28. If the panel answers there, the ufw rule
from step 1.4 is not taking effect. Most common cause: Docker or another service
has put its own iptables rules ahead of ufw.
ufw status verbose
iptables -L INPUT -n --line-numbers | head -20Counter-check for the game port, which has to be open:
ss -ulnp | grep 30120Step 1.7: initial setup through an SSH tunnel
On its first start txAdmin writes a PIN to the log. It is only valid for a few minutes.
journalctl -u fivem -n 50 | grep -i pinSince the port is not exposed, you forward it to your own machine over SSH. The following command runs on your PC, not on the server:
ssh -N -L 40120:127.0.0.1:40120 root@<server-ip>While that window stays open, the panel is reachable at
http://127.0.0.1:40120/ in your browser. Enter the PIN there and create the
master account. The wizard then walks through license key, server data directory
(/opt/fivem/server-data) and the first server.cfg.
Step 1.8: is it running?
systemctl status fivem
journalctl -u fivem -n 40
# Is the game port answering
ss -ulnp | grep 30120After a reboot the service has to come back on its own, which is what the
enable is for. Trying it once is worth it before players depend on it:
reboot
# after it comes up
systemctl is-active fivemCommon problems
| Message or symptom | Cause | Fix |
|---|---|---|
| Service will not start, no useful error in the log | systemd hardening blocks proot | drop SystemCallFilter, RestrictNamespaces and NoNewPrivileges |
Permission denied on start | run.sh not executable or wrong owner | chmod +x run.sh, chown -R fivem:fivem |
| Port 30120 already in use on start | KillMode=process, children survive | switch to KillMode=mixed |
| Service starts but cannot find its directory | ProtectHome=true with a path under /home | remove the line or move the server to /opt |
Panel reachable from outside on :40120 | ufw rule not taking effect, often Docker iptables | ufw status verbose, iptables -L INPUT -n |
| Game server suddenly unreachable for everyone | interface convar set, it binds FXServer too | remove the convar, close the port in the firewall |
Browser cannot reach 127.0.0.1:40120 | tunnel not open, or started on the server instead of locally | run the command from step 1.7 on your own PC |
| PIN in the log has expired | the PIN is only valid for a few minutes | systemctl restart fivem, read the log immediately |
| Server does not appear in the server list | license key missing, or no network at boot | check sv_licenseKey and network-online.target in the unit |
All at once
If you would rather not walk through steps 1.1 to 1.5 by hand, use the installer. It creates the unit, the update script and the cron entry in one run and asks for every path up front. It is described in step 3.
On to step 2
The SSH tunnel gets old quickly, and it does not work for more than one person. The next part puts Apache in front so the panel is reachable on its own subdomain over HTTPS, with a working live console: