Running TeamSpeak
Created:
TeamSpeak 3 is a voice communication server. This note deploys it as a rootless Podman container using Quadlets, following the patterns from Part 4.
Voice traffic uses UDP and cannot be reverse-proxied through Caddy. TeamSpeak handles encryption natively using AES-128, so Caddy is not involved at all.
All commands are run as $APP_USER unless otherwise noted.
Prerequisites
Complete Parts 1 through 4 of this series before continuing. You should have a working Podman installation with
systemd lingering enabled for $APP_USER.
Create Application Directory
mkdir -p ~/apps/teamspeak/data
First Run
The TeamSpeak server prints a one-time admin privilege key to stdout on its first start. Run the container interactively to capture this key.
podman run \
--name teamspeak-init \
--rm \
-e TS3SERVER_LICENSE=accept \
-v ~/apps/teamspeak/data:/var/ts3server:Z \
-p 9987:9987/udp \
docker.io/teamspeak:latest
Watch the output for a line containing the privilege key. It will look something like this.
------------------------------------------------------------------
I M P O R T A N T
------------------------------------------------------------------
Server Query Admin Account created
loginname= "serveradmin", password= "aBcDeFgH"
apikey= "BAAx..."
------------------------------------------------------------------
ServerAdmin privilege key created, please use it to gain
serveradmin rights for your virtualserver. please
also check the determine to make sure your server is
accessible.
token=AAAA1bbb2CCC3ddd4EEE5fff6GGG7hhh8III9jjj0
------------------------------------------------------------------
Save the privilege key somewhere safe. You will need it to claim server admin in the TeamSpeak client.
Stop the container with Ctrl+C.
Create Quadlet Files
Create the Quadlet directory if it doesn't exist.
mkdir -p ~/.config/containers/systemd
Network
Create ~/.config/containers/systemd/teamspeak.network.
[Network]
NetworkName=teamspeak
Container
Create ~/.config/containers/systemd/teamspeak.container.
[Container]
ContainerName=teamspeak
Image=docker.io/teamspeak:latest
Network=teamspeak.network
ReadOnly=true
Tmpfs=/tmp
Environment=TS3SERVER_LICENSE=accept
Volume=%h/apps/teamspeak/data:/var/ts3server:Z
PublishPort=9987:9987/udp
PodmanArgs=--security-opt no-new-privileges
PodmanArgs=--memory 512m
PodmanArgs=--cpus 1
[Service]
Restart=always
RestartSec=10
[Install]
WantedBy=default.target
| Directive | Purpose |
|---|---|
ContainerName=teamspeak |
Predictable container name |
Image |
The TeamSpeak 3 image from Docker Hub |
Network=teamspeak.network |
Reference the .network Quadlet. Podman creates the network first automatically |
ReadOnly=true |
Mount the container's root filesystem as read-only |
Tmpfs=/tmp |
Writable temporary directory since the root filesystem is read-only |
Environment |
Accept the TeamSpeak license agreement (required to start) |
Volume |
Mount persistent data. %h is expanded by systemd to the user's home directory. :Z applies SELinux relabeling for rootless Podman |
PublishPort=9987:9987/udp |
Publish the voice port. ServerQuery (10011/tcp) and file transfer (30033/tcp) remain internal |
--security-opt no-new-privileges |
Prevent processes from gaining privileges via setuid binaries |
--memory 512m |
Limit memory usage to 512 MB |
--cpus 1 |
Limit CPU usage to 1 core |
The TeamSpeak image's entrypoint runs as root, chowns the data directory, and drops to an unprivileged
teamspeak user via su-exec. This requires capabilities like CHOWN, DAC_OVERRIDE, SETUID, and SETGID
— all included in Podman's default capability set for rootless containers. Since rootless Podman already restricts
capabilities to a minimal set and the container runs within a user namespace, the defaults are appropriate here.
Configure Firewall
As the admin user, allow the TeamSpeak voice port through the firewall.
sudo ufw allow 9987/udp comment 'TeamSpeak Voice'
This is the only firewall rule needed. ServerQuery and file transfer ports are not exposed.
Start the Service
Reload the systemd daemon so Podman's generator picks up the Quadlet files.
systemctl --user daemon-reload
The WantedBy=default.target in the Quadlet file means the service is automatically enabled — it will start on
boot as long as the .container file exists. Start it.
systemctl --user start teamspeak.service
Verify the service is running.
systemctl --user status teamspeak.service
Check the logs if something doesn't look right.
journalctl --user -u teamspeak.service -f
Connect and Configure
Open the TeamSpeak 3 client and connect to your server's IP address. The default voice port (9987) is used automatically.
On first connect, the client will prompt you to enter a privilege key. Use the key you saved during the first run to claim server admin.
Once connected as server admin, enable voice encryption globally. In the client, navigate to the virtual server settings.
- Right-click the server name and select Edit Virtual Server
- Go to the Security tab
- Set Encryption to Globally Enabled
- Click OK
This enables TeamSpeak's built-in AES-128 encryption for all voice traffic.
Maintenance
Update the Image
Pull the latest image and restart the service.
podman pull docker.io/teamspeak:latest
systemctl --user restart teamspeak.service
View Logs
Container output is captured by journald.
journalctl --user -u teamspeak.service --since today
TeamSpeak also writes its own application logs to ~/apps/teamspeak/data/logs/. These files are owned by the
container's internal teamspeak user, which maps to a subordinate UID on the host in rootless Podman. Use
podman unshare to run commands inside the user namespace where the file ownership maps correctly.
podman unshare ls ~/apps/teamspeak/data/logs/
podman unshare cat ~/apps/teamspeak/data/logs/<filename>
Reset ServerAdmin Password
If the serveradmin password is lost, stop the service and run the container once with the password override environment variable.
systemctl --user stop teamspeak.service
podman run --rm \
-e TS3SERVER_LICENSE=accept \
-e TS3SERVER_SERVERADMIN_PASSWORD=newpassword \
-v ~/apps/teamspeak/data:/var/ts3server:Z \
docker.io/teamspeak:latest
Stop the container with Ctrl+C after it starts successfully, then restart the service.
systemctl --user start teamspeak.service
Backup
The persistent data lives in ~/apps/teamspeak/data/. The key contents are as follows.
| Path | Purpose |
|---|---|
ts3server.sqlitedb |
Main database — channels, permissions, users, groups |
files/ |
Files uploaded via the file browser |
logs/ |
Server log files |
Stop the service before taking a backup to ensure SQLite database consistency. Use podman unshare so the
copy can access directories owned by the container's mapped UID.
systemctl --user stop teamspeak.service
podman unshare cp -a ~/apps/teamspeak/data ~/apps/teamspeak/data-backup-$(date +%Y%m%d)
systemctl --user start teamspeak.service