Docker Labels
Define declarative tunnels using Docker container labels.
In Docker mode, rstream run --docker watches running containers and derives tunnel configuration from container labels. The model is intentionally similar to other label-driven ingress tools: the application containers declare the desired exposure, and a single agent reconciles tunnels.
The watcher owns desired state rather than a one-shot start. Temporary Engine failures and resource conflicts are retried with bounded backoff. This lets a replacement agent recover a hostname or port as soon as the previous control channel releases it, without requiring another manual restart.
Only labels with the rstream.tunnel. prefix are considered. Labels are grouped by tunnel name using the <name>.<key> pattern, and each tunnel group must define a forward label.
When a tunnel omits explicit values, the parser defaults publish=true and protocol=http.
For a hands-on Docker Compose workflow that exposes a homelab Grafana dashboard from Docker labels, see Expose a Homelab Grafana Dashboard Without Port Forwarding using rstream.
Compose example
The example below runs the reconciler alongside two services. Each service declares a tunnel through labels.
services:
rstream:
image: rstream/rstream:latest
command: ["-v", "--log-format", "json", "run", "--docker", "--watch"]
restart: unless-stopped
user: "${RSTREAM_UID:-1000}:${RSTREAM_GID:-1000}"
group_add:
- "${DOCKER_GID:?set DOCKER_GID to the Docker socket group}"
environment:
RSTREAM_ENGINE: "<engine>"
RSTREAM_AUTHENTICATION_TOKEN: "<token>"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
api:
image: my-api:latest
labels:
rstream.tunnel.api.forward: "8080"
rstream.tunnel.api.publish: "true"
rstream.tunnel.api.protocol: "http"
rstream.tunnel.api.http.auth.token: "true"
admin:
image: my-admin:latest
labels:
rstream.tunnel.admin.forward: "9000"
rstream.tunnel.admin.publish: "true"
rstream.tunnel.admin.protocol: "http"
rstream.tunnel.admin.http.auth.rstream: "true"The reconciler needs permission to inspect containers and watch Docker events. On Linux, set RSTREAM_UID="$(id -u)", RSTREAM_GID="$(id -g)", and DOCKER_GID="$(stat -c '%g' /var/run/docker.sock)" before starting Compose. This keeps the process non-root while granting the supplementary group required by the socket. Access to /var/run/docker.sock should still be treated as privileged access to the Docker host. For hardened hosts, place a restricted Docker socket proxy in front of the daemon or run the reconciler under the host's normal Docker access controls.
For long-running homelab or server deployments, run the reconciler in the same Compose project as the services it exposes and give it a normal restart policy. If the host already has a working rstream CLI context, mount that context read-only instead of writing a bearer token into the Compose file:
services:
rstream:
image: rstream/rstream:latest
command: ["-v", "--log-format", "json", "run", "--docker", "--watch"]
restart: unless-stopped
user: "${RSTREAM_UID:-1000}:${RSTREAM_GID:-1000}"
group_add:
- "${DOCKER_GID:?set DOCKER_GID to the Docker socket group}"
environment:
RSTREAM_CONFIG: /home/rstream/.rstream/config.yaml
RSTREAM_CONTEXT: "${RSTREAM_CONTEXT:-}"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /home/app/.rstream:/home/rstream/.rstream:roAn empty RSTREAM_CONTEXT keeps the current context selected in the mounted
configuration. Set it only when this deployment must select another existing
context explicitly; context names are user-defined and the sample does not
assume names such as production or staging.
After starting or changing labels, verify the reconciled state from the CLI:
rstream tunnel list --filter 'labels.rstream.source=docker'Also exercise the replacement path at least once on a persistent deployment:
docker compose restart rstream
docker compose logs --since=2m rstream
rstream tunnel list --filter 'labels.rstream.source=docker,status=online'If an old lease is still closing, a verbose watcher may log a resource
conflict and its next retry. Readiness requires a later Tunnel created entry
for every declared tunnel.
The Docker watcher logs are intentionally quiet during steady state, so rstream tunnel list is the most direct way to confirm which tunnels exist and which stable domains they use.
Applications that enforce reverse-proxy headers may also need application-level configuration. For example, if an upstream rejects requests after seeing X-Forwarded-For, configure that application to trust the Docker bridge or reverse proxy address before exposing it through a tunnel.
Label schema
The top-level keys under each tunnel name include forward, publish, protocol, type, host, upstream-tls, trusted-ips, geoip, label.<key>, and nested keys under http.* and tls.*. Unknown keys are rejected.
The table below lists the most commonly used label keys.
| Label | Meaning |
|---|---|
rstream.tunnel.<name>.forward | Upstream target as port or host:port. |
rstream.tunnel.<name>.publish | Whether the tunnel is published. |
rstream.tunnel.<name>.protocol | Tunnel protocol (http, tls, tcp, dtls, quic). |
rstream.tunnel.<name>.port | Reserved public port for a published TCP tunnel. |
rstream.tunnel.<name>.host | Requested stable domain hostname. |
rstream.tunnel.<name>.upstream-tls | Enable TLS between engine and upstream. |
rstream.tunnel.<name>.label.<k> | User-defined label attached to the tunnel. |
rstream.tunnel.<name>.http.version | Upstream HTTP version (http/1.1, h2c, h3). |
rstream.tunnel.<name>.http.auth.token | Enable token authentication. |
rstream.tunnel.<name>.http.auth.rstream | Enable rstream Auth. |
rstream.tunnel.<name>.http.gate.challenge | Enable challenge mode. |
rstream.tunnel.<name>.tls.mode | terminated or passthrough. |
rstream.tunnel.<name>.tls.minVersion | tls1.2 or tls1.3. |
rstream.tunnel.<name>.tls.ciphers | Comma-separated TLS cipher names. |
rstream.tunnel.<name>.tls.mtls | Require mTLS for clients of the published tunnel. |
rstream.tunnel.<name>.allow-cross-region-routing | Allow ingress and tunnel owner to be in different regions. |
rstream.tunnel.<name>.datagram-guaranteed-delivery | Require reliable delivery for a datagram tunnel. |
rstream.tunnel.<name>.tls.alpns | Comma-separated custom ALPN values for TLS, DTLS, or QUIC tunnels. |
The host label maps to the tunnel hostname property and is validated as a stable domain inside the current project namespace.
For raw TCP, use protocol: "tcp" and omit port for an ephemeral address. A reserved address uses the port assigned by the Control plane:
labels:
rstream.tunnel.ssh.forward: "22"
rstream.tunnel.ssh.publish: "true"
rstream.tunnel.ssh.protocol: "tcp"
rstream.tunnel.ssh.port: "10042"TCP labels do not accept host, HTTP, TLS, or edge authentication options. The downstream connection is forwarded without rstream encryption; use a secure application protocol such as SSH.
Forward targets and validation
A bare port selects the container address on --docker-network when available,
otherwise the first discovered network address, ordered by network name. Explicit
host:port targets must name the container or a discovered network address.
Arbitrary external hosts are rejected; use apply YAML for those targets.
IPv6-only networks and explicit
[IPv6]:port targets follow the same network restrictions. Each
network prefers IPv4 when available and otherwise uses its global IPv6 address.
Tunnel validation is shared with forward and apply YAML. Upstream TLS uses
upstream-tls, equivalent to CLI --upstream-tls and YAML tunnel.upstreamTLS.
Explicit false remains distinct from an omitted value.