The vanilla Terraria dedicated server works, but TShock is what you actually want: REST API, granular permissions, a plugin system, and sane bans. It's the SourceMod of Terraria.

TL;DR:

sudo adduser --system --group --home /opt/terraria terraria
cd /opt/terraria
# grab the latest 5.x self-contained linux build from GitHub releases
curl -LO https://github.com/Pryaxis/TShock/releases/download/v5.2.0/TShock-5.2-for-Terraria-1.4.4.9-linux-x64-Release.zip
sudo -u terraria unzip TShock-*.zip -d tshock-server
cd tshock-server
sudo -u terraria ./TShock.Server -port 7777 -autocreate 3 -world /opt/terraria/worlds/main.wld

Details below.

Get TShock and pick the right build

TShock 5.x is .NET-based (it runs on the .NET runtime, not Mono anymore). The releases on the Pryaxis/TShock GitHub page ship in two flavors per platform:

  • Self-contained — bundles the .NET runtime. Bigger download, zero host dependencies. This is the one I use.
  • Framework-dependent — smaller, but you must install the matching .NET runtime yourself.

Grab the latest 5.x linux-x64 zip. Match the Terraria version in the filename to the client version your players run, or nobody connects.

sudo mkdir -p /opt/terraria
cd /opt/terraria
curl -LO https://github.com/Pryaxis/TShock/releases/download/v5.2.0/TShock-5.2-for-Terraria-1.4.4.9-linux-x64-Release.zip

The exception: if you want the framework-dependent build to save disk, install the runtime first:

sudo apt update
sudo apt install -y dotnet-runtime-8.0

Check the release notes for the exact .NET version — 5.2 is on .NET 8. Don't guess.

Run as a non-root user

Never run a game server as root. Create a dedicated system user, unzip under it, fix perms. If you haven't hardened the box yet, do my Ubuntu initial setup checklist first.

sudo adduser --system --group --home /opt/terraria terraria
sudo chown -R terraria:terraria /opt/terraria

cd /opt/terraria
sudo -u terraria unzip TShock-*.zip -d tshock-server
sudo chmod +x /opt/terraria/tshock-server/TShock.Server

The binary is TShock.Server. The first launch creates a tshock/ config dir, a ServerPlugins/ dir, and a logs/ dir next to it.

First run and config

Do one interactive run to generate the config and a world. -autocreate 3 makes a large world if none exists; sizes are 1 small, 2 medium, 3 large.

sudo -u terraria mkdir -p /opt/terraria/worlds
cd /opt/terraria/tshock-server
sudo -u terraria ./TShock.Server \
  -port 7777 \
  -autocreate 3 \
  -world /opt/terraria/worlds/main.wld

On first start TShock prints a setup code in the console and logs — use it in-game with /setup <code> to grant yourself superadmin, then /user group <yourname> superadmin and disable the setup code. Ctrl+C to stop once the world is generated.

Now edit tshock/config.json. The two things that actually matter:

{
  "Settings": {
    "ServerPassword": "change-me",
    "RestApiEnabled": true,
    "EnableTokenEndpointAuthentication": true,
    "ApplicationRestTokens": {
      "REPLACE_WITH_A_LONG_RANDOM_TOKEN": {
        "Username": "rest",
        "UserGroupName": "superadmin"
      }
    },
    "MaxSlots": 16,
    "ServerName": "diengdoh"
  }
}

Generate a real token, don't ship the placeholder:

openssl rand -hex 32

The REST API listens on 7878 by default. Smoke test it:

curl "http://127.0.0.1:7878/status"
curl "http://127.0.0.1:7878/v2/server/status?token=YOUR_TOKEN"

Keep 7878 bound to localhost or behind a reverse proxy — do not expose it raw. Only the game port 7777 goes public.

serverconfig.txt vs flags

You can pass everything as flags, or drop a serverconfig.txt and point at it with -config. For systemd I prefer flags — they're visible in the unit and there's no second source of truth. The flags I use:

Flag Purpose
-port 7777 listen port
-world <path> load this .wld
-autocreate 3 generate large world if missing
-maxplayers 16 slot cap
-secure enable anti-cheat checks

Daemonize with systemd

Quick-and-dirty option first: screen or tmux. Fine for a quick test, bad for anything you care about — no auto-restart, dies with a bad SSH session.

tmux new -s terraria
# run the server, then Ctrl-b d to detach

For real, use systemd. I've got a longer writeup on unit files, but here's the one for TShock:

# /etc/systemd/system/terraria.service
[Unit]
Description=Terraria TShock Server
After=network.target

[Service]
Type=simple
User=terraria
Group=terraria
WorkingDirectory=/opt/terraria/tshock-server
ExecStart=/opt/terraria/tshock-server/TShock.Server \
  -port 7777 \
  -world /opt/terraria/worlds/main.wld \
  -autocreate 3 \
  -secure
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

WorkingDirectory matters — TShock resolves tshock/, ServerPlugins/, and logs/ relative to cwd. Get it wrong and it silently writes a fresh config somewhere else.

sudo systemctl daemon-reload
sudo systemctl enable --now terraria
sudo systemctl status terraria
sudo journalctl -u terraria -f

Graceful stop matters for save integrity — systemctl stop sends SIGTERM, which TShock handles by saving and exiting. Don't kill -9 a running world.

Firewall

Only the game port. With ufw (full ruleset notes in my ufw post):

sudo ufw allow 7777/tcp
sudo ufw reload

Leave 7878 (REST) off the public allowlist. If you need remote API access, tunnel it over SSH or front it with an authenticated reverse proxy — never ufw allow 7878.

Plugins

TShock plugins are .dll files. Drop them into ServerPlugins/ and restart.

sudo -u terraria cp SomePlugin.dll /opt/terraria/tshock-server/ServerPlugins/
sudo systemctl restart terraria

Match the plugin's targeted TShock major version — a 4.x plugin won't load on 5.x. Watch the boot log; failed plugins log a load error and the server keeps running without them, so check journalctl after dropping a new one in.

Back up the world files

The world lives in one .wld file. That file is your entire server. Back it up on a schedule — TShock also keeps .bak rollbacks, but those sit on the same disk.

# /opt/terraria/backup.sh
#!/usr/bin/env bash
set -euo pipefail
DEST=/opt/terraria/backups
mkdir -p "$DEST"
cp /opt/terraria/worlds/main.wld "$DEST/main-$(date +%F-%H%M).wld"
# keep last 48
ls -1t "$DEST"/main-*.wld | tail -n +49 | xargs -r rm

Cron it under the terraria user:

sudo -u terraria crontab -e
# every 30 min
*/30 * * * * /opt/terraria/backup.sh

Copying a live .wld is fine — Terraria writes atomically — but if you're paranoid, run the backup right after an in-game /save. Better yet, rsync the backups off-box.

Verify

After enabling everything, confirm the four things that break setups:

# 1. service up and stable
systemctl is-active terraria

# 2. game port listening
sudo ss -tlnp | grep 7777

# 3. REST answers locally
curl -s "http://127.0.0.1:7878/status"

# 4. firewall lets 7777 in, NOT 7878
sudo ufw status | grep -E '7777|7878'

Then connect from the actual Terraria client: Multiplayer → Join via IP, enter the box IP and 7777, password if set.

Gotchas

  • Terraria version mismatch. The single most common "can't connect." The TShock build targets one exact Terraria client version. If players updated and you didn't, they bounce. Pin both.
  • REST token exposed. A superadmin REST token is full server control over HTTP. Long random value, localhost-only binding, never in a public repo or screenshot.
  • Wrong WorkingDirectory. Without it in the unit, TShock generates a second config dir under / and ignores your edits. Always set it.
  • Running framework-dependent without the runtime. It exits immediately with a .NET-not-found error. Use the self-contained build or apt install dotnet-runtime-8.0.
  • -autocreate on an existing world. It only creates if the file is missing, so it's safe — but if your -world path is wrong, it'll happily generate a new empty world and you'll wonder where everyone's base went.

Same pattern as my Satisfactory server writeup: dedicated user, systemd, tight firewall, backups. Boring and bulletproof beats a screen session you forget about.


Related posts