Lesson 30 of 30 advanced 6 min read

Before this:Multi-container apps with ComposeServices & systemdSecrets & configuration management

Deploying GopherTrunk end to end

Key takeaways This capstone deploys GopherTrunk for real, two ways: as a docker-compose stack, and as a systemd service running the binary. Both pull together everything in the module — a versioned artifact, a config file, USB device access, persistent volumes, a health check, and updates with rollback.

Time to put it all together. This lesson walks GopherTrunk from a fresh repo to a running scanner on a server — the whole module, applied.

What we’re deploying

GopherTrunk is a long-running daemon that reads a USB SDR dongle, decodes trunked radio, and serves a web API and gRPC interface. A real deployment therefore needs: the binary or image, a config file, access to the USB device, somewhere to persist recordings and the call database, and process management so it survives crashes and reboots. We’ll do it with containers first, then the systemd alternative.

Path A — docker-compose

1. Get the repo and build the image. The compose file builds locally from the repo Dockerfile:

git clone https://github.com/mattcheramie/gophertrunk
cd gophertrunk
docker compose build          # builds the image from the Dockerfile

2. Provide config and find your dongle. Copy the example config and edit it, then find the USB device to pass through:

cp config.example.yaml config.yaml   # then edit device serials, talkgroups, etc.
lsusb                                # e.g. "Bus 003 Device 002: ID 0bda:2838"

Set the matching devices: path in docker-compose.yml (e.g. /dev/bus/usb/003/002), and make sure group_add matches the group your host’s udev rules grant (commonly plugdev, GID 46 on Debian).

3. Start the stack.

docker compose up -d          # start in the background
docker compose logs -f        # watch it come up

The compose file already sets restart: unless-stopped (survives crashes/reboots), maps the config read-only, persists recordings/ and calls.db in volumes, binds the API to 127.0.0.1 only, and drops all Linux capabilities except DAC_OVERRIDE for the USB device.

4. Verify health.

curl http://127.0.0.1:8080/api/v1/health    # -> {"status":"ok"}

That’s the same health endpoint Docker polls automatically every 30 seconds.

Path B — systemd (the binary)

Prefer running the plain binary as a service? GopherTrunk ships a hardened unit:

# build the binary (or download a release artifact)
go build -o bin/gophertrunk ./cmd/gophertrunk

# install the unit, binary, and config
sudo install -m 0644 docs/gophertrunk.service /etc/systemd/system/gophertrunk.service
sudo install -m 0755 bin/gophertrunk /usr/local/bin/gophertrunk
sudo install -d -m 0755 /etc/gophertrunk
sudo install -m 0640 config.example.yaml /etc/gophertrunk/config.yaml
# edit /etc/gophertrunk/config.yaml

sudo systemctl daemon-reload
sudo systemctl enable --now gophertrunk      # start now and on every boot

The unit handles the rest: Restart=on-failure for crash resilience, DeviceAllow + SupplementaryGroups=plugdev for USB access, and a stack of hardening directives (DynamicUser, ProtectSystem=strict, NoNewPrivileges) that sandbox the service. Tail its logs with:

journalctl -u gophertrunk -f

Handling the API token (a secret)

If you enable the API’s auth, keep the token out of config.yaml. Put it in a locked-down file and reference it — the secrets pattern GopherTrunk supports:

# in the systemd unit:
EnvironmentFile=-/etc/gophertrunk/env
# and in config.yaml: api.auth.token_file: /etc/gophertrunk/token

Updating and rolling back

To move to a new version:

# containers:
docker compose pull && docker compose up -d
# systemd: install the new binary, then
sudo systemctl restart gophertrunk

Watch the health check and logs after the update. If the new version misbehaves, roll back by redeploying the previous image tag or reinstalling the previous binary — the old artifact still exists.

You’ve deployed it

SDR dongle USB server GopherTrunk (container/systemd) :8080 web API
The finished deployment: a dongle feeds GopherTrunk on a server, managed by Docker or systemd, serving its API.

That’s a complete, production-shaped deployment — build, configure, run, secure, monitor, update — using every idea in this module on a real application.

Quick check: what two ways does this lesson deploy GopherTrunk?

Recap

  • GopherTrunk deploys as a docker-compose stack (build, config, USB device, volumes, health check) or a systemd service running the binary.
  • Both grant least-privilege USB access and provide crash/reboot resilience.
  • Keep the API token as a secret in a separate file, not in config.yaml.
  • Update by pulling/reinstalling the new version and watching health; roll back to the previous artifact if needed.

That’s the module — from source to a running, monitored, updatable service. Keep the glossary handy as a reference.

Frequently asked questions

Do I need Docker to run GopherTrunk in production?

No — you can run it either way. Docker plus docker-compose is the quickest path and keeps everything contained, but GopherTrunk also ships a systemd unit so you can run the plain binary as a managed service. This lesson shows both, since which you choose is a matter of preference and environment.

Why does deploying GopherTrunk involve USB device access?

GopherTrunk reads I/Q samples from a physical SDR dongle over USB, so the running service — whether a container or a systemd unit — needs permission to open that USB device. Both GopherTrunk’s compose file and its systemd unit include the device pass-through and group settings that grant exactly that access and nothing more.