Running TeamSpeak

#infrastructure #servers #podman #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.

  1. Right-click the server name and select Edit Virtual Server
  2. Go to the Security tab
  3. Set Encryption to Globally Enabled
  4. 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