Run as a systemd service
Supervise the native tunnelctl binary as a systemd .service unit — no container.
If tunnelctl is installed natively (from a package — see Quickstart),
you can run a tunnel directly as a systemd service unit (.service), with no container
involved. As with Quadlet, the trick is to run tunnelctl up in
the foreground and let systemd supervise it — never -d. And because it runs on the
host, there's no networking to set up: localhost:8080 is simply the host's service.
User service vs. system service
Run it as a user service (systemctl --user) so it reuses the token from your own
tunnelctl login. A system service runs as root or a dedicated account, which must
have logged in itself — see the last section.
Paths below assume the binary is at /usr/local/bin/tunnelctl (where the packages install
it); check yours with command -v tunnelctl.
1. Log in once
tunnelctl login # or: tunnelctl login --no-browserThe token lands in ~/.config/tunnelctl and is refreshed automatically; the service reuses it.
2. A single-tunnel unit
Save as ~/.config/systemd/user/tunnelctl-myapp.service:
[Unit]
Description=tunnelctl tunnel — myapp
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/usr/local/bin/tunnelctl up myapp 8080
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.targetEnable and start:
systemctl --user daemon-reload
systemctl --user enable --now tunnelctl-myapp.service
loginctl enable-linger "$USER" # start on boot without an active login session(Unlike a generated Quadlet unit, a hand-written unit can be systemctl enabled directly.)
3. Many tunnels: a template unit
One file for any slug — a template unit tunnelctl@.service, instantiated as
tunnelctl@<slug>.service. %i is the instance name (the slug):
[Unit]
Description=tunnelctl tunnel — %i
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/usr/local/bin/tunnelctl up %i
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.targettunnelctl up %i reuses the target you saved the first time you ran
tunnelctl up <slug> <target>. Start one instance per slug:
tunnelctl up myapp 8080 # once, to save the target
systemctl --user enable --now tunnelctl@myapp.service
systemctl --user enable --now tunnelctl@api.servicePrefer to keep the target explicit (no reliance on saved state)? Use a per-instance
EnvironmentFile:
# in [Service]
EnvironmentFile=%h/.config/tunnelctl/units/%i.env
ExecStart=/usr/local/bin/tunnelctl up %i ${TARGET}# %h/.config/tunnelctl/units/myapp.env
TARGET=8080Operate
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.serviceSystem-wide service
To run without any user session, install to /etc/systemd/system/ and run as a dedicated
account that has logged in once:
[Service]
User=tunnelctl
ExecStart=/usr/local/bin/tunnelctl up myapp 8080
Restart=on-failure
RestartSec=5
# Optional hardening — keep the config/state dir readable/writable:
NoNewPrivileges=true
ProtectSystem=strictsudo -u tunnelctl tunnelctl login --no-browser # once
sudo systemctl enable --now tunnelctl-myapp.serviceNon-default environment
To point a unit at another environment, set the profile vars, e.g.
Environment=TUNNELCTL_API_URL=… TUNNELCTL_OIDC_ISSUER=…. See env.
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 unit (re)starts. If your identity provider expires the refresh token, log in again (step 1).