tunnelctl
Concepts

Run with systemd (Quadlet)

Supervise a tunnel as a rootless systemd service using a Podman Quadlet unit.

For a tunnel that should come up at boot and restart on failure, run it as a Podman Quadlet unit. The container runs tunnelctl up in the foreground and systemd supervises it — no -d, no extra process manager. This builds on Running in a container. Prefer the native binary without a container? See Run as a systemd service.

Foreground, supervised by systemd

Let systemd be the single supervisor: the unit runs tunnelctl up <slug> <target> as the container's main process. Don't use the -d daemon flag here — if the foreground process detaches, systemd sees the service exit and the daemon child gets torn down.

1. Log in once (persistent config volume)

The unit runs unattended, so a token must already be on disk. Log in once into a named volume the unit will reuse:

podman run --rm -it \
  -v tunnelctl-config:/home/nonroot/.config/tunnelctl \
  oci.piblade.net/tunnelctl/tunnelctl-cli:latest \
  login --no-browser

2. The Quadlet unit

Rootless: save as ~/.config/containers/systemd/tunnelctl-myapp.container. The filename becomes the service name — tunnelctl-myapp.service.

[Unit]
Description=tunnelctl tunnel — myapp
Wants=network-online.target
After=network-online.target

[Container]
Image=oci.piblade.net/tunnelctl/tunnelctl-cli:0.8.4
# Entrypoint is `tunnelctl`, so Exec becomes: tunnelctl up myapp 8080
Exec=up myapp 8080
# Share the host network so localhost:8080 is the host's service
Network=host
# Reuse the login token + per-tunnel metadata
Volume=tunnelctl-config:/home/nonroot/.config/tunnelctl

[Service]
# tunnelctl rides out transient FRP drops itself; restart only on a hard exit
Restart=on-failure
RestartSec=5

[Install]
# Start at boot (with linger enabled)
WantedBy=default.target

3. Enable & start

# Reload the Quadlet generator and start now
systemctl --user daemon-reload
systemctl --user start tunnelctl-myapp.service

# Start on boot without an active login session
loginctl enable-linger "$USER"

The [Install] section is what makes the generated unit start at boot — don't run systemctl enable on it directly (it's a generated unit and that fails). For a system-wide service, drop the file in /etc/containers/systemd/ instead and use systemctl without --user.

Target a sibling container

To forward to a service in another container, drop Network=host and put both on a shared network, addressing the service by name:

[Container]
Image=oci.piblade.net/tunnelctl/tunnelctl-cli:0.8.4
Exec=up myapp web:8080
Network=appnet
Volume=tunnelctl-config:/home/nonroot/.config/tunnelctl

appnet is a podman network (podman network create appnet, or an appnet.network Quadlet unit referenced as Network=appnet.network), and the service container must be on it as web. See Running in a container for the networking details.

Operate

systemctl --user status tunnelctl-myapp.service
journalctl --user -u tunnelctl-myapp.service -f   # live logs
systemctl --user restart tunnelctl-myapp.service
systemctl --user stop tunnelctl-myapp.service

Token lifetime

A running tunnel stays up via the server-issued connection token, which doesn't expire. The stored OIDC token is only needed when the service (re)starts — to reserve the slug and mint the connection token. If your identity provider expires the refresh token, re-run step 1 to refresh the stored login.

On this page