add doctor, scheduled backups and fetch; release 0.0.12
release / deb (push) Successful in 17s

- doctor [<name>] [--fix]: checks the system (user, programs, folders,
  zips, units and leftover copies in /etc, sandbox access to moved
  folders, journal, polkit, daemon, orphaned boot links and schedules)
  and each instance (ownership, server zip, config paths, mods, ports,
  failed starts/backups), with a fix for each. --fix applies the safe
  ones and re-checks them.
- backup enable|disable|now|list: a systemd timer per instance
  (terraria-backup@.timer, Persistent=), saving the world first, into
  /var/lib/terraria/backups/<name>/ (group-readable), pruning only
  scheduled backups. rename carries the schedule, delete removes it.
  upgrade's backups move there too.
- fetch vanilla|tmodloader [<version>] [--list] [--upgrade <name>]:
  downloads from terraria.org / tModLoader's GitHub releases, checks
  GitHub's sha256 and the zip, names it with its version.
- terractld may write /etc/systemd/system, for backup enable and
  doctor --fix over --host.
- Units, tmpfiles, packaging, completion, docs and changelog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-02 22:26:56 +00:00
1 parent 8a509f1398
commit fc2c08d47b
14 files changed
+1118 -56

No files matched your search

+15 -2
View File
@@ -1,10 +1,23 @@
# Changelog # Changelog
## 0.0.12 - unreleased ## 0.0.12 - 2026-10-02
- New `fetch` command to download server zips into `dist/`, named with their version:
- `terractl fetch tmodloader [<version>]` downloads from tModLoader's GitHub releases (default: the latest stable) and checks it against the SHA-256 checksum GitHub publishes. `terractl fetch vanilla [<version>]` downloads from terraria.org (default: its newest; older versions by number, `1449` or `1.4.4.9`).
- `--list` shows the versions available (tModLoader: stable and preview), and which are already downloaded. `--upgrade <name>` switches an instance to the zip with `upgrade` right after.
- Every zip is checked (complete, and the right kind) before it's put in `dist/`; zips already there aren't downloaded again. If a site doesn't have the version or can't be reached, it says so and where to download it by hand.
- Members of the `terraria` group can use it without `sudo` (except `--upgrade`). Needs `curl`.
- Scheduled backups, with a systemd timer per instance (`terraria-backup@<name>.timer`):
- `sudo terractl backup enable <name> [--every <when>] [--keep <number>]` (default: daily, keeping the newest 14). `--every` takes `hourly`, `daily`, `weekly`, a time such as `04:30`, or any systemd calendar expression. Missed backups are made after the machine starts again.
- `backup now <name>` makes one immediately, `backup disable <name>` stops the schedule (existing backups are kept), and `backup list [<name>]` shows schedules, the last and next run, and the backups.
- Each backup saves the world first if the server is running (as `export` does) and goes in `/var/lib/terraria/backups/<name>/` (`TERRARIA_BACKUPS` in `/etc/default/terractl`), readable by the `terraria` group. Only scheduled backups are pruned.
- `rename` takes the schedule along, `delete` turns it off, and purging the package removes the schedules (not the backups).
- New `doctor` command: `terractl doctor [<name>] [--fix]` checks the system (user, programs including ICU and curl, folders, zips, the systemd units and old copies in `/etc` that replace them, sandbox access to moved folders, journal access, polkit, the daemon, leftovers of deleted instances) and each instance (file ownership and modes, its server zip, paths in `serverconfig.txt`, missing mods, shared ports, failed starts and backups). Each problem comes with the command that fixes it. `--fix` applies the safe fixes: `fix-perms`, folder and zip permissions, journal access, the `ReadWritePaths=` drop-in for moved folders, and boot links or backup schedules of instances that are gone. It exits with status 1 if a problem remains.
- The daemon (`terractld.service`) may now write to `/etc/systemd/system`, so `backup enable` and `doctor --fix` also work through `--host`.
- Warnings about moved folders now also suggest `terractl doctor --fix`.
- New `upgrade` command to change an instance's server version, up or down: - New `upgrade` command to change an instance's server version, up or down:
- `terractl upgrade <name>` lists the zips in `dist/` for its type, marking the installed, pinned and newest ones. The `terraria` group can use it without `sudo`. - `terractl upgrade <name>` lists the zips in `dist/` for its type, marking the installed, pinned and newest ones. The `terraria` group can use it without `sudo`.
- `sudo terractl upgrade <name> <zip>|latest` checks the zip first (right type, readable by `terraria`, contains the server, passes `unzip -t`), stops the server (saving the world), backs up its worlds and configs to `instances/.backups/`, pins `ZIP=`, unpacks the zip right away and starts the server again if it was running. If unpacking fails, the instance is left as it was. `--no-backup` skips the backup. - `sudo terractl upgrade <name> <zip>|latest` checks the zip first (right type, readable by `terraria`, contains the server, passes `unzip -t`), stops the server (saving the world), backs up its worlds and configs to `/var/lib/terraria/backups/<name>/`, pins `ZIP=`, unpacks the zip right away and starts the server again if it was running. If unpacking fails, the instance is left as it was. `--no-backup` skips the backup.
- Going to an older version needs `--downgrade`, because worlds saved by a newer version can't be opened by an older one. - Going to an older version needs `--downgrade`, because worlds saved by a newer version can't be opened by an older one.
- Bash completion offers the instance's zips. Works with `--host`. - Bash completion offers the instance's zips. Works with `--host`.
- `reinstall` now always unpacks the **same** version again. Before, an instance with a blank `ZIP=` picked the newest zip in `dist/`, so `reinstall` could silently upgrade it. It now pins the zip the instance runs first, and refuses if that zip is gone from `dist/`. It also keeps the record of the installed version (`server/.installed`) until the new unpack is done. - `reinstall` now always unpacks the **same** version again. Before, an instance with a blank `ZIP=` picked the newest zip in `dist/`, so `reinstall` could silently upgrade it. It now pins the zip the instance runs first, and refuses if that zip is gone from `dist/`. It also keeps the record of the installed version (`server/.installed`) until the new unpack is done.
+8 -1
View File
@@ -64,7 +64,10 @@ install:
install -m 755 lib/terractl-remote $(DESTDIR)$(PKGLIBDIR)/terractl-remote install -m 755 lib/terractl-remote $(DESTDIR)$(PKGLIBDIR)/terractl-remote
$(SUBST) systemd/terraria@.service > $(DESTDIR)$(UNITDIR)/terraria@.service $(SUBST) systemd/terraria@.service > $(DESTDIR)$(UNITDIR)/terraria@.service
$(SUBST) systemd/terractld.service > $(DESTDIR)$(UNITDIR)/terractld.service $(SUBST) systemd/terractld.service > $(DESTDIR)$(UNITDIR)/terractld.service
chmod 644 $(DESTDIR)$(UNITDIR)/terraria@.service $(DESTDIR)$(UNITDIR)/terractld.service $(SUBST) systemd/terraria-backup@.service > $(DESTDIR)$(UNITDIR)/terraria-backup@.service
$(SUBST) systemd/terraria-backup@.timer > $(DESTDIR)$(UNITDIR)/terraria-backup@.timer
chmod 644 $(DESTDIR)$(UNITDIR)/terraria@.service $(DESTDIR)$(UNITDIR)/terractld.service \
$(DESTDIR)$(UNITDIR)/terraria-backup@.service $(DESTDIR)$(UNITDIR)/terraria-backup@.timer
install -m 644 systemd/terraria@.socket $(DESTDIR)$(UNITDIR)/ install -m 644 systemd/terraria@.socket $(DESTDIR)$(UNITDIR)/
install -m 644 config/sysusers.d/terractl.conf $(DESTDIR)$(SYSUSERSDIR)/ install -m 644 config/sysusers.d/terractl.conf $(DESTDIR)$(SYSUSERSDIR)/
install -m 644 config/tmpfiles.d/terractl.conf $(DESTDIR)$(TMPFILESDIR)/ install -m 644 config/tmpfiles.d/terractl.conf $(DESTDIR)$(TMPFILESDIR)/
@@ -97,10 +100,14 @@ install:
uninstall: uninstall:
@if [ -z "$(DESTDIR)" ] && [ -d /run/systemd/system ]; then \ @if [ -z "$(DESTDIR)" ] && [ -d /run/systemd/system ]; then \
systemctl disable --now terractld.service 2>/dev/null || true; \ systemctl disable --now terractld.service 2>/dev/null || true; \
for t in $$(systemctl list-unit-files --plain --no-legend 'terraria-backup@*.timer' | awk '{print $$1}'); do \
systemctl disable --now "$$t" 2>/dev/null || true; \
done; \
fi fi
rm -f $(DESTDIR)$(BINDIR)/terractl \ rm -f $(DESTDIR)$(BINDIR)/terractl \
$(DESTDIR)$(UNITDIR)/terraria@.service $(DESTDIR)$(UNITDIR)/terraria@.socket \ $(DESTDIR)$(UNITDIR)/terraria@.service $(DESTDIR)$(UNITDIR)/terraria@.socket \
$(DESTDIR)$(UNITDIR)/terractld.service \ $(DESTDIR)$(UNITDIR)/terractld.service \
$(DESTDIR)$(UNITDIR)/terraria-backup@.service $(DESTDIR)$(UNITDIR)/terraria-backup@.timer \
$(DESTDIR)$(SYSUSERSDIR)/terractl.conf $(DESTDIR)$(TMPFILESDIR)/terractl.conf \ $(DESTDIR)$(SYSUSERSDIR)/terractl.conf $(DESTDIR)$(TMPFILESDIR)/terractl.conf \
$(DESTDIR)$(COMPLETIONDIR)/terractl $(DESTDIR)$(POLKITDIR)/50-terractl.rules $(DESTDIR)$(COMPLETIONDIR)/terractl $(DESTDIR)$(POLKITDIR)/50-terractl.rules
rm -rf $(DESTDIR)$(DOCDIR) $(DESTDIR)$(DATADIR) $(DESTDIR)$(PKGLIBDIR) rm -rf $(DESTDIR)$(DOCDIR) $(DESTDIR)$(DATADIR) $(DESTDIR)$(PKGLIBDIR)
+3 -1
View File
@@ -13,12 +13,14 @@ sudo terractl attach calamity
- **Many servers, one host.** Each instance is `terraria@<name>.service`, with its own directory, port, world and mods. - **Many servers, one host.** Each instance is `terraria@<name>.service`, with its own directory, port, world and mods.
- **Vanilla and tModLoader side by side**, chosen per instance. - **Vanilla and tModLoader side by side**, chosen per instance.
- **No manual installs.** Drop the official server zips into `/var/lib/terraria/dist`; they're unpacked on demand, and each instance can be pinned to a specific version. `terractl upgrade` checks a new zip, backs up the worlds and switches version in one step. - **No manual installs.** `terractl fetch` downloads official Terraria and tModLoader server zips (or drop them into `/var/lib/terraria/dist` yourself); they're unpacked on demand, and each instance can be pinned to a specific version. `terractl upgrade` checks a new zip, backs up the worlds and switches version in one step.
- **Safe shutdowns.** Stopping a server saves the world first. - **Safe shutdowns.** Stopping a server saves the world first.
- **A live console.** `terractl attach` shows the server's output and lets you type commands into it; `terractl watch` just shows the output. - **A live console.** `terractl attach` shows the server's output and lets you type commands into it; `terractl watch` just shows the output.
- **Mod management.** `terractl mods` installs, enables and removes mods, with correct file ownership. - **Mod management.** `terractl mods` installs, enables and removes mods, with correct file ownership.
- **Tab completion** for commands, instance names, mods and console commands. - **Tab completion** for commands, instance names, mods and console commands.
- **Export and import.** Pack an instance into one `.tar.gz` for backups or moving to another machine, and import it anywhere, with paths, ports and ownership adjusted. - **Export and import.** Pack an instance into one `.tar.gz` for backups or moving to another machine, and import it anywhere, with paths, ports and ownership adjusted.
- **Scheduled backups.** `terractl backup enable <name>` backs an instance up daily (or on any schedule) with a systemd timer, saving the world first and keeping the newest 14.
- **Self-check.** `terractl doctor` checks the whole setup, explains what's wrong with the command to fix it, and `--fix` fixes what it safely can.
- **Manage other hosts.** Run any command on another host with `terractl --host nas ...`, and copy or move instances between hosts with `terractl copy` and `terractl move`, like LXD/Incus remotes. Hosts opt in by running the `terractld` daemon; access is per computer, over pinned TLS. - **Manage other hosts.** Run any command on another host with `terractl --host nas ...`, and copy or move instances between hosts with `terractl copy` and `terractl move`, like LXD/Incus remotes. Hosts opt in by running the `terractld` daemon; access is per computer, over pinned TLS.
- **Hardened.** Servers run as an unprivileged `terraria` user and can only write to their own data. - **Hardened.** Servers run as an unprivileged `terraria` user and can only write to their own data.
- **Access for moderators.** Members of the `terraria` group can start, stop and restart servers, use the console and read the servers' own logs, without `sudo` and without seeing the rest of the system's logs. - **Access for moderators.** Members of the `terraria` group can start, stop and restart servers, use the console and read the servers' own logs, without `sudo` and without seeing the rest of the system's logs.
+898 -38
View File
File diff suppressed because it is too large. Load diff
+37 -2
View File
@@ -52,6 +52,11 @@ _terractl_instances() {
fi fi
} }
# Instance names here, or on remote <host> if one is given
_terractl_any_instances() {
if [[ -n $1 ]]; then _terractl_remote_instances "$1"; else _terractl_instances; fi
}
# Remotes this user added with 'terractl remote add' # Remotes this user added with 'terractl remote add'
_terractl_remotes() { _terractl_remotes() {
local f=${TERRACTL_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/terractl}/remotes.json local f=${TERRACTL_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/terractl}/remotes.json
@@ -125,7 +130,7 @@ _terractl() {
cword=$COMP_CWORD cword=$COMP_CWORD
fi fi
local commands="new list info start stop restart enable disable attach watch cmd mods upgrade reinstall fix-perms delete rename export import" local commands="new list info start stop restart enable disable attach watch cmd mods upgrade reinstall fix-perms fetch backup doctor delete rename export import"
local console="save say playing kick ban motd password time dawn noon dusk midnight settle seed version maxplayers port clear help" local console="save say playing kick ban motd password time dawn noon dusk midnight settle seed version maxplayers port clear help"
# terractl --host <remote> <command> ...: the same as on this host, except # terractl --host <remote> <command> ...: the same as on this host, except
@@ -149,7 +154,7 @@ _terractl() {
return return
fi fi
local command=${words[1]} local command=${words[1]} prev=${words[cword-1]}
case $command in case $command in
list|journal-access|help|-h|--help|version|-V|--version) list|journal-access|help|-h|--help|version|-V|--version)
return ;; return ;;
@@ -176,6 +181,36 @@ _terractl() {
_terractl_reply "enable disable status token revoke" _terractl_reply "enable disable status token revoke"
fi fi
return ;; return ;;
doctor)
# doctor [<name>] [--fix]
if [[ $cur == -* ]]; then _terractl_reply "--fix"; else _terractl_reply "$(_terractl_any_instances "$host")"; fi
return ;;
fetch)
# fetch vanilla|tmodloader [<version>|latest] [--list] [--upgrade <name>]
if (( cword == 2 )); then
_terractl_reply "vanilla tmodloader"
elif [[ $prev == --upgrade ]]; then
_terractl_reply "$(_terractl_any_instances "$host")"
elif [[ $cur == -* ]]; then
_terractl_reply "--list --upgrade"
else
_terractl_reply "latest"
fi
return ;;
backup)
# backup enable|disable|now|list <name> [--every <when>] [--keep <number>]
if (( cword == 2 )); then
_terractl_reply "enable disable now list"
elif (( cword == 3 )); then
_terractl_reply "$(_terractl_any_instances "$host")"
elif [[ ${words[2]} == enable ]]; then
case $prev in
--every) _terractl_reply "daily weekly hourly monthly" ;;
--keep) ;;
*) _terractl_reply "--every --keep" ;;
esac
fi
return ;;
esac esac
if (( cword == 2 )); then if (( cword == 2 )); then
+3
View File
@@ -13,6 +13,9 @@
# Where server zips are read from. Outside /home, no drop-in is needed. # Where server zips are read from. Outside /home, no drop-in is needed.
#TERRARIA_DIST=/var/lib/terraria/dist #TERRARIA_DIST=/var/lib/terraria/dist
# Where 'terractl backup' (and 'upgrade', before switching) puts backups
#TERRARIA_BACKUPS=/var/lib/terraria/backups
# Seconds to wait for a server to save and exit on stop (keep below 90) # Seconds to wait for a server to save and exit on stop (keep below 90)
#TERRARIA_STOP_TIMEOUT=80 #TERRARIA_STOP_TIMEOUT=80
+3
View File
@@ -3,6 +3,9 @@ d /var/lib/terraria 0750 terraria terraria -
d /var/lib/terraria/instances 0750 terraria terraria - d /var/lib/terraria/instances 0750 terraria terraria -
# setgid + group-writable: members of the 'terraria' group can upload zips # setgid + group-writable: members of the 'terraria' group can upload zips
d /var/lib/terraria/dist 2775 root terraria - d /var/lib/terraria/dist 2775 root terraria -
# Backups ('terractl backup', and 'upgrade' before switching): readable by the
# group, like the instances themselves
d /var/lib/terraria/backups 2750 root terraria -
# The servers' journal (LogNamespace=terraria): readable by the terraria group, # The servers' journal (LogNamespace=terraria): readable by the terraria group,
# including files journald creates later (default ACL). 'A+' only acts on paths # including files journald creates later (default ACL). 'A+' only acts on paths
+109 -11
View File
@@ -52,7 +52,8 @@ Everything is installed by the `terraria-server-manager` package:
| `/usr/bin/terractl` | Helper script. The service calls it to set up, run and stop servers; you use it for everyday management. | | `/usr/bin/terractl` | Helper script. The service calls it to set up, run and stop servers; you use it for everyday management. |
| `/usr/lib/terraria-server-manager/terractl-remote` | The part of `terractl` that manages other hosts (`--host`, `remote`, `daemon`, `copy`, `move`), in Python. | | `/usr/lib/terraria-server-manager/terractl-remote` | The part of `terractl` that manages other hosts (`--host`, `remote`, `daemon`, `copy`, `move`), in Python. |
| `/usr/lib/systemd/system/terractld.service` | The daemon that lets other hosts manage this one. Off until you turn it on (see [Managing other hosts](#managing-other-hosts)). | | `/usr/lib/systemd/system/terractld.service` | The daemon that lets other hosts manage this one. Off until you turn it on (see [Managing other hosts](#managing-other-hosts)). |
| `/etc/default/terractl` | Optional overrides (locations, stop timeout, the daemon's address). | | `/usr/lib/systemd/system/terraria-backup@.timer`, `terraria-backup@.service` | Scheduled backups of an instance. Off until you turn them on with `terractl backup enable` (see [Backups](#backups)). |
| `/etc/default/terractl` | Optional overrides (locations of instances, zips and backups, stop timeout, the daemon's address). |
| `/etc/systemd/journald@terraria.conf` | Size and retention of the servers' own journal (see [Logs](#logs)). | | `/etc/systemd/journald@terraria.conf` | Size and retention of the servers' own journal (see [Logs](#logs)). |
| `/usr/lib/sysusers.d/terractl.conf` | Defines the `terraria` system user. | | `/usr/lib/sysusers.d/terractl.conf` | Defines the `terraria` system user. |
| `/usr/lib/tmpfiles.d/terractl.conf` | Creates `/var/lib/terraria` and its subdirectories. | | `/usr/lib/tmpfiles.d/terractl.conf` | Creates `/var/lib/terraria` and its subdirectories. |
@@ -76,6 +77,7 @@ On stop, `terractl service-stop` sends `exit` through the pipe so the world is s
├── dist/ Server zips (you add these; never modified) ├── dist/ Server zips (you add these; never modified)
│ ├── terraria-server-1458.zip │ ├── terraria-server-1458.zip
│ └── tModLoader-v2026.07.3.0.zip │ └── tModLoader-v2026.07.3.0.zip
├── backups/<name>/ Backups from 'terractl backup' and 'upgrade' (group-readable)
└── instances/ └── instances/
├── <name>/ One directory per instance ├── <name>/ One directory per instance
│ ├── instance.conf Which server type and zip to use │ ├── instance.conf Which server type and zip to use
@@ -87,7 +89,6 @@ On stop, `terractl service-stop` sends `exit` through the pipe so the world is s
│ ├── Mods/ tModLoader mods + enabled.json │ ├── Mods/ tModLoader mods + enabled.json
│ ├── ModConfigs/ tModLoader mod settings │ ├── ModConfigs/ tModLoader mod settings
│ └── banlist.txt │ └── banlist.txt
├── .backups/ Backups made by 'terractl upgrade'
└── .deleted/ Backups made by 'terractl delete' └── .deleted/ Backups made by 'terractl delete'
/run/terraria/<name>.stdin Console pipe (exists only while running) /run/terraria/<name>.stdin Console pipe (exists only while running)
@@ -326,6 +327,28 @@ The existing `terraria` user keeps `/opt/terraria` as its home directory. That's
## Server zips ## Server zips
Servers are unpacked from the official zips in `/var/lib/terraria/dist`. The easiest way to get them there is `fetch`, which downloads them and names them with their version:
```bash
terractl fetch tmodloader # the latest stable tModLoader
terractl fetch tmodloader 2026.07.3.0 # a specific version, e.g. the one your players have
terractl fetch vanilla # the latest Terraria server
terractl fetch vanilla 1449 # a specific version (1449 or 1.4.4.9)
terractl fetch tmodloader --list # recent tModLoader versions, stable and preview
```
tModLoader comes from its GitHub releases, and its download is checked against the checksum GitHub publishes. Vanilla comes from terraria.org, which only lists its newest version, but older ones can be fetched by number. Every zip is checked (complete, and the right kind) before it's put in `dist/`. Zips already there aren't downloaded again.
`--upgrade <name>` also switches an instance to the zip it just fetched, with `upgrade` (see [Upgrading](#upgrading-or-downgrading-an-instance)):
```bash
sudo terractl fetch tmodloader --upgrade calamity
```
`fetch` needs `curl` and internet access. Members of the `terraria` group can use it without `sudo` (except `--upgrade`). If a site changes its download address and `fetch` stops working, it says so; download the zip by hand as below.
### Adding zips by hand
Put the official downloads in `/var/lib/terraria/dist`: Put the official downloads in `/var/lib/terraria/dist`:
| Type | File name pattern | Where to get it | | Type | File name pattern | Where to get it |
@@ -634,7 +657,7 @@ Stable tModLoader and the latest vanilla Terraria can be based on different Terr
### Upgrading (or downgrading) an instance ### Upgrading (or downgrading) an instance
Put the new zip in `/var/lib/terraria/dist`, with the version in its name (see [Server zips](#server-zips)). To see which zips an instance can use and which one it runs: Get the new zip with `terractl fetch` (or put it in `/var/lib/terraria/dist` by hand, with the version in its name; see [Server zips](#server-zips)). To see which zips an instance can use and which one it runs:
``` ```
$ terractl upgrade calamity $ terractl upgrade calamity
@@ -656,7 +679,7 @@ sudo terractl upgrade calamity latest
1. **Checks the zip** before changing anything: it must be in `dist/`, match the instance's type, be readable by the `terraria` user, contain the server, and pass `unzip -t`. A damaged or half-downloaded zip is refused here, not on the next start. 1. **Checks the zip** before changing anything: it must be in `dist/`, match the instance's type, be readable by the `terraria` user, contain the server, and pass `unzip -t`. A damaged or half-downloaded zip is refused here, not on the next start.
2. **Stops the server** if it's running. Stopping saves the world. 2. **Stops the server** if it's running. Stopping saves the world.
3. **Backs up** the worlds and configs (not `server/`) to `/var/lib/terraria/instances/.backups/<name>-<date>-<old zip>.tar.gz`, readable only by root. Use `--no-backup` to skip this. 3. **Backs up** the worlds and configs (not `server/`) to `/var/lib/terraria/backups/<name>/<name>-<date>-<time>-before-<old zip>.tar.gz`. Use `--no-backup` to skip this. These backups are never deleted automatically, not even by scheduled backups' `--keep`.
4. **Pins `ZIP=`** in `instance.conf` to the new zip, so the version can't change by accident afterwards. 4. **Pins `ZIP=`** in `instance.conf` to the new zip, so the version can't change by accident afterwards.
5. **Unpacks the new zip** into `server/`. For tModLoader, the .NET runtime and logs are kept, as always. If this fails, the old pin is put back and the old `server/` is still in place, so the instance is exactly as it was. 5. **Unpacks the new zip** into `server/`. For tModLoader, the .NET runtime and logs are kept, as always. If this fails, the old pin is put back and the old `server/` is still in place, so the instance is exactly as it was.
6. **Starts the server again** if it was running. 6. **Starts the server again** if it was running.
@@ -680,9 +703,9 @@ You can still change version by hand: set `ZIP=` in `instance.conf` and restart.
To go back to the old version *and* the worlds as they were before the upgrade, replace the instance with the backup `upgrade` made. The backup is in the same format as `export`, so `import` restores it, pinned to the version it was taken with: To go back to the old version *and* the worlds as they were before the upgrade, replace the instance with the backup `upgrade` made. The backup is in the same format as `export`, so `import` restores it, pinned to the version it was taken with:
```bash ```bash
sudo ls /var/lib/terraria/instances/.backups/ terractl backup list calamity
sudo terractl delete calamity -y # keeps its own backup in .deleted/, just in case sudo terractl delete calamity -y # keeps its own backup in .deleted/, just in case
sudo terractl import calamity /var/lib/terraria/instances/.backups/calamity-20261002-213553-tModLoader-v2026.06.1.0.tar.gz sudo terractl import calamity /var/lib/terraria/backups/calamity/calamity-20261002-213553-before-tModLoader-v2026.06.1.0.tar.gz
sudo terractl enable calamity --now sudo terractl enable calamity --now
``` ```
@@ -892,19 +915,42 @@ On this host, copying *from* it works for members of the `terraria` group. Copyi
- **Revoking.** `sudo terractl daemon status` shows the trusted computers, and `sudo terractl daemon revoke laptop` takes one's access away immediately. - **Revoking.** `sudo terractl daemon status` shows the trusted computers, and `sudo terractl daemon revoke laptop` takes one's access away immediately.
- **Firewall.** The daemon doesn't need to be reachable from the internet. Limit port 7776 to your LAN or VPN, or set `TERRACTL_LISTEN` to a LAN or VPN address. - **Firewall.** The daemon doesn't need to be reachable from the internet. Limit port 7776 to your LAN or VPN, or set `TERRACTL_LISTEN` to a LAN or VPN address.
The daemon runs as root, because it creates and removes instances and starts services, but with systemd's protections it can't write to `/usr`, `/boot`, `/etc` or home folders. Its certificate, key and trust list are in `/var/lib/terractl/`. Files in transit are kept in `/var/lib/terractl/transfers/`, and removed once used, after 6 hours unused, or when the daemon restarts. The daemon runs as root, because it creates and removes instances and starts services, but with systemd's protections it can't write to `/usr`, `/boot`, `/etc` or home folders. The one exception is `/etc/systemd/system`, where `backup enable` and `doctor --fix` write their drop-ins. Its certificate, key and trust list are in `/var/lib/terractl/`. Files in transit are kept in `/var/lib/terractl/transfers/`, and removed once used, after 6 hours unused, or when the daemon restarts.
--- ---
## Backups ## Backups
Everything worth keeping is in `data/`, `instance.conf` and `serverconfig.txt`, which is exactly what `export` saves. A nightly backup kept for 14 days, as a root cron job (`sudo crontab -e`): Everything worth keeping is in `data/`, `instance.conf` and `serverconfig.txt`, which is exactly what a backup saves. `terractl backup` makes them on a schedule, and keeps a set number:
```cron ```bash
30 4 * * * /usr/bin/terractl export calamity /var/backups/terraria/calamity-$(date +\%F).tar.gz && find /var/backups/terraria -name 'calamity-*.tar.gz' -mtime +14 -delete sudo terractl backup enable calamity # daily, keeping the newest 14
sudo terractl backup enable calamity --every 04:30 --keep 30 # every day at 4:30, keep 30
sudo terractl backup enable calamity --every weekly --keep 0 # weekly, keep them all
terractl backup list # schedules and backups of all instances
sudo terractl backup now calamity # one now
sudo terractl backup disable calamity # stop (keeps existing backups)
``` ```
Create the folder first with `sudo mkdir -p /var/backups/terraria`. Separately, `upgrade` backs up an instance's worlds and configs before every version change (see [Undoing an upgrade](#undoing-an-upgrade)). In a crontab, `%` has a special meaning and must be written as `\%`, as above. Terraria also keeps a `.bak` copy of each world next to the world file, which is useful if the latest save is damaged. `--every` takes `hourly`, `daily`, `weekly`, `monthly`, a time such as `04:30`, or anything else systemd's `OnCalendar=` accepts (`'Sun 03:00'`, `'*-*-* 00/6:00'`; see `man systemd.time`). Running `backup enable` again changes the schedule.
How it works:
- **One systemd timer per instance**, `terraria-backup@<name>.timer`. A backup missed while the machine was off is made soon after it starts again. Backups start up to 10 minutes after the scheduled time, so several instances don't all start at once.
- **The world is saved first** if the server is running, as `export` does, so the backup has the current state. The server keeps running.
- **Backups go in `/var/lib/terraria/backups/<name>/`**, as `<name>-<date>-<time>.tar.gz`. Members of the `terraria` group can read them, as they can the instances themselves (`serverconfig.txt` can contain the server password). Change the folder with `TERRARIA_BACKUPS` in `/etc/default/terractl`.
- **Only scheduled backups are deleted** when there are more than `--keep`. The ones `upgrade` makes before changing version (`...-before-<zip>.tar.gz`) and ones you put there yourself are kept.
- **`rename` takes the schedule along**, and `delete` turns it off. Existing backups stay where they are in both cases.
To restore one, import it under a new name, or replace the instance with it:
```bash
sudo terractl import calamity-restored /var/lib/terraria/backups/calamity/calamity-20261002-040512.tar.gz
```
If a scheduled backup fails, `terractl backup list` and `terractl doctor` say so; the details are in `journalctl -u terraria-backup@<name>`.
Backups on the same disk don't protect against the disk failing. Copy `/var/lib/terraria/backups` elsewhere now and then, or put it on another disk (with `TERRARIA_BACKUPS`, or a mount). Terraria also keeps a `.bak` copy of each world next to the world file, which is useful if the latest save is damaged.
--- ---
@@ -926,6 +972,10 @@ The servers always run as the `terraria` user. Administration is simplest with `
| `upgrade <name> <zip>` | root only | Checked by the script. Stops and starts the server, and unpacks the zip as `terraria`. | | `upgrade <name> <zip>` | root only | Checked by the script. Stops and starts the server, and unpacks the zip as `terraria`. |
| `reinstall` | root (or `terraria`) | | | `reinstall` | root (or `terraria`) | |
| `fix-perms` | root only | Checked by the script. | | `fix-perms` | root only | Checked by the script. |
| `fetch` | root, or members of the `terraria` group | Writes to `dist/`, which the group may. `--upgrade` needs root. |
| `backup list` | root, or members of the `terraria` group | |
| `backup enable`, `disable`, `now` | root only | Checked by the script. |
| `doctor` | anyone | Without root, the checks that need it are skipped. `--fix` needs root. |
| `mods <name>` (list) | root, or members of the `terraria` group | | | `mods <name>` (list) | root, or members of the `terraria` group | |
| `mods add/enable/disable/remove` | root (or `terraria`) | Run as root, it sets ownership of the files it installs to `terraria`. | | `mods add/enable/disable/remove` | root (or `terraria`) | Run as root, it sets ownership of the files it installs to `terraria`. |
| `delete`, `rename` | root only | Checked by the script. | | `delete`, `rename` | root only | Checked by the script. |
@@ -1002,6 +1052,44 @@ While a server runs, whether systemd started it or you did, it holds a lock on t
## Troubleshooting ## Troubleshooting
### Start with `doctor`
`terractl doctor` checks the whole setup and explains anything that's wrong, with the command that fixes it:
```
$ sudo terractl doctor
terractl 0.0.12 on nas
System
ok user and group 'terraria'
ok programs (including ICU and curl for tModLoader)
ok folders /var/lib/terraria/instances and /var/lib/terraria/dist
ok server zips: 3 in /var/lib/terraria/dist
FAIL /etc/systemd/system/terraria@.service replaces the package's terraria@.service (left over from a manual setup?)
fix: rm /etc/systemd/system/terraria@.service && systemctl daemon-reload
ok the services can use /var/lib/terraria/instances and /var/lib/terraria/dist
ok the 'terraria' group can read the servers' journal
Instances
FAIL calamity: 3 files aren't owned by terraria, so the server can't change them (saving can fail)
fix: terractl fix-perms calamity
ok pvp (vanilla, port 7778, terraria-server-1458.zip)
2 problem(s), 0 warning(s).
terractl doctor --fix fixes 1 of them; the others need the steps shown.
```
It checks:
- **The system:** the `terraria` user and group; `unzip`, and ICU and `curl` if there are tModLoader instances; the instances, zips and backups folders; unreadable zips; the systemd units, including old copies in `/etc/systemd/system` that replace the package's; whether the services may use the folders set in `/etc/default/terractl` (see [Changing locations](#changing-locations)); the servers' journal; polkit; the daemon, if it's on; and servers or backups that are still scheduled for instances that no longer exist.
- **Each instance:** files the server can't change, or that others can read; its server zip (missing, wrong type, or a `ZIP=` changed by hand that hasn't been applied yet); paths in `serverconfig.txt` that point somewhere else (e.g. after copying it by hand); enabled mods that aren't installed; instances sharing a port; and failed starts or backups.
`--fix` fixes what's safe to fix by itself, and says what it did: file ownership and modes (`fix-perms`), folder and zip permissions, the servers' journal access, the drop-in for moved folders, and boot links or backup schedules for instances that are gone. Everything else, such as deleting unit files or opening up home folders, is left for you, with the steps shown. `terractl doctor <name>` checks only that instance (and the system).
Without `sudo`, `doctor` skips the checks that need root and says so. It exits with status 1 if it finds a problem, so it can be used in scripts and monitoring.
### Common problems
**tModLoader stops with `Aborted` right after "Launched Using Local Dotnet".** **tModLoader stops with `Aborted` right after "Launched Using Local Dotnet".**
The ICU library is missing, so .NET aborts at startup. Fix it with `sudo apt install libicu-dev`, then restart the instance. To confirm, check that `ldconfig -p | grep -c libicu` prints more than 0. The ICU library is missing, so .NET aborts at startup. Fix it with `sudo apt install libicu-dev`, then restart the instance. To confirm, check that `ldconfig -p | grep -c libicu` prints more than 0.
@@ -1073,6 +1161,7 @@ Its access was revoked with `daemon revoke`, or the host's `/var/lib/terractl` w
- common server console commands for `cmd` (`save`, `say`, `playing`, `kick`...); - common server console commands for `cmd` (`save`, `say`, `playing`, `kick`...);
- the options of `enable`, `disable`, `watch`, `upgrade`, `reinstall`, `delete`, `export`, `copy` and `move`; - the options of `enable`, `disable`, `watch`, `upgrade`, `reinstall`, `delete`, `export`, `copy` and `move`;
- server zips for `upgrade`, only those of the instance's type (on a remote, all of its zips); - server zips for `upgrade`, only those of the instance's type (on a remote, all of its zips);
- `vanilla`/`tmodloader` and the options for `fetch`, `backup` actions and schedules (`--every daily` etc.), and `--fix` for `doctor`;
- remote names after `--host`, and `<remote>:<name>` for `copy` and `move`. Instance names on a remote are asked from its daemon, so they can take a moment; - remote names after `--host`, and `<remote>:<name>` for `copy` and `move`. Instance names on a remote are asked from its daemon, so they can take a moment;
- `.tar.gz` files for `export` and `import`; - `.tar.gz` files for `export` and `import`;
- `mods` actions. For `mods add` it completes `.tmod` files and folders; for `mods enable` and `mods remove`, the installed mods; for `mods disable`, the enabled ones. - `mods` actions. For `mods add` it completes `.tmod` files and folders; for `mods enable` and `mods remove`, the installed mods; for `mods disable`, the enabled ones.
@@ -1101,6 +1190,15 @@ terractl upgrade <name> <zip>|latest [--downgrade] [--no-backup]
terractl reinstall <name> [--clean] Re-unpack the same version on next start terractl reinstall <name> [--clean] Re-unpack the same version on next start
(--clean also re-downloads .NET) (--clean also re-downloads .NET)
terractl fix-perms <name> Reset the owner and modes of all its files terractl fix-perms <name> Reset the owner and modes of all its files
terractl fetch vanilla|tmodloader [<version>] [--upgrade <name>]
Download a server zip (default: the latest)
terractl fetch vanilla|tmodloader --list Versions available to download
terractl backup enable <name> [--every <when>] [--keep <n>]
Back up on a schedule (default: daily, keep 14)
terractl backup disable <name> Stop scheduled backups (keeps existing ones)
terractl backup now <name> Back up now
terractl backup list [<name>] Schedules and backups
terractl doctor [<name>] [--fix] Check the setup; --fix fixes what it safely can
terractl mods <name> [list] Show installed mods terractl mods <name> [list] Show installed mods
terractl mods <name> add <folder|file.tmod>... Install and enable mods (and ModConfigs) terractl mods <name> add <folder|file.tmod>... Install and enable mods (and ModConfigs)
terractl mods <name> enable|disable <Mod>... Change which mods load terractl mods <name> enable|disable <Mod>... Change which mods load
+1 -1
View File
@@ -325,7 +325,7 @@ def host_addresses(listen_host, port):
# --------------------------------------------------------------- daemon ---- # --------------------------------------------------------------- daemon ----
EXEC_COMMANDS = {"list", "info", "new", "start", "stop", "restart", "enable", "disable", "cmd", "watch", EXEC_COMMANDS = {"list", "info", "new", "start", "stop", "restart", "enable", "disable", "cmd", "watch",
"mods", "reinstall", "upgrade", "fix-perms", "delete", "rename", "export", "import", "mods", "reinstall", "upgrade", "fix-perms", "doctor", "backup", "fetch", "delete", "rename", "export", "import",
"-V", "--version", "version", "-h", "--help", "help"} "-V", "--version", "version", "-h", "--help", "help"}
+3
View File
@@ -6,6 +6,9 @@ case "$1" in
# Forget which instances were enabled at boot # Forget which instances were enabled at boot
rm -f /etc/systemd/system/multi-user.target.wants/terraria@*.service rm -f /etc/systemd/system/multi-user.target.wants/terraria@*.service
rm -rf /etc/systemd/system/terraria@.service.d rm -rf /etc/systemd/system/terraria@.service.d
# Scheduled backups (the backups themselves stay in /var/lib/terraria/backups)
rm -f /etc/systemd/system/timers.target.wants/terraria-backup@*.timer
rm -rf /etc/systemd/system/terraria-backup@*.timer.d /etc/systemd/system/terraria-backup@*.service.d
# The daemon: whether it's on, its certificate and the computers it trusts # The daemon: whether it's on, its certificate and the computers it trusts
rm -f /etc/systemd/system/multi-user.target.wants/terractld.service rm -f /etc/systemd/system/multi-user.target.wants/terractld.service
rm -rf /etc/systemd/system/terractld.service.d /var/lib/terractl rm -rf /etc/systemd/system/terractld.service.d /var/lib/terractl
+1
View File
@@ -8,6 +8,7 @@ case "$1" in
remove|deconfigure) remove|deconfigure)
if [ -d /run/systemd/system ]; then if [ -d /run/systemd/system ]; then
systemctl stop terractld.service || true systemctl stop terractld.service || true
systemctl stop 'terraria-backup@*.timer' 'terraria-backup@*.service' || true
units=$(systemctl list-units --plain --no-legend --all 'terraria@*.service' | awk '{print $1}') units=$(systemctl list-units --plain --no-legend --all 'terraria@*.service' | awk '{print $1}')
if [ -n "$units" ]; then if [ -n "$units" ]; then
echo "Stopping Terraria instances: $units" echo "Stopping Terraria instances: $units"
+2
View File
@@ -19,6 +19,8 @@ StateDirectoryMode=0755
# computers, like sudo terractl does. Everything else is locked down. # computers, like sudo terractl does. Everything else is locked down.
NoNewPrivileges=true NoNewPrivileges=true
ProtectSystem=full ProtectSystem=full
# 'backup enable' and 'doctor --fix' write drop-ins for the Terraria units here
ReadWritePaths=/etc/systemd/system
ProtectHome=true ProtectHome=true
PrivateTmp=true PrivateTmp=true
PrivateDevices=true PrivateDevices=true
+22
View File
@@ -0,0 +1,22 @@
[Unit]
Description=Back up Terraria server (%i)
Documentation=file://@DOCDIR@/README.md
[Service]
Type=oneshot
# Saves the world first if the server is running, then archives the instance
# (without server/) into /var/lib/terraria/backups/%i/ and removes the oldest
# scheduled backups beyond the number to keep. Set up with:
# terractl backup enable %i [--every <when>] [--keep <number>]
ExecStart=@BINDIR@/terractl backup now %i
Nice=10
IOSchedulingClass=idle
# Runs as root, like 'sudo terractl backup now'
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
+13
View File
@@ -0,0 +1,13 @@
[Unit]
Description=Scheduled backups of Terraria server (%i)
Documentation=file://@DOCDIR@/README.md
[Timer]
# 'terractl backup enable %i --every <when>' sets the schedule in a drop-in
OnCalendar=daily
# Catch up on a backup missed while the machine was off
Persistent=true
RandomizedDelaySec=10min
[Install]
WantedBy=timers.target