Self-Host rstream Engine CE with Docker Compose

Deploy rstream Engine Community Edition with Docker Compose, configure TLS and JWT authentication, run an agent validation, publish a tunnel, verify routing, and automate certificate renewal.


This guide deploys rstream Engine Community Edition as a self-hosted edge runtime. The engine runs in your infrastructure, exposes a TLS listener, accepts outbound rstream agents, and serves published tunnel traffic from hostnames under a domain you control.

Community Edition is intentionally direct. It has no hosted Control plane, no dashboard project provisioning, no managed credential records, and no automatic certificate issuance inside the engine. You choose one engine hostname, provide TLS certificates for that hostname and its tunnel wildcard, sign agent JWTs with your engine secret, and operate the runtime like any other edge service.

The guide has two phases. The first phase is a reproducible local deployment that uses the same engine image, config shape, TLS reload path, JWT auth path, Engine API, and agent control channel used in production. The second phase replaces only the local conveniences: the self-signed CA becomes an ACME certificate, high local ports become 80 and 443, Docker DNS aliases become public DNS records, and the temporary validation agent becomes the agents running next to your workloads.

The validation agent is deliberately not part of the engine service. It represents a workload-side agent, which is where rstream agents normally run. Keeping that boundary explicit avoids turning a server deployment guide into an artificial all-in-one demo.

For reference details, see Self-Hosted, Deployment, Configuration, and Operations.

Contents

What You Will Deploy

The local stack contains the engine and one upstream application:

ServiceImagePurpose
enginerstream/rstream-engine-ce:latestRuns rstream-engine-ce with static TLS, JWT auth, and Prometheus metrics.
apphashicorp/http-echo:1.0Local upstream service used to prove published tunnel routing.

The validation agent is launched separately with docker run. That separation matters: the server deployment is the engine, while agents run near applications they expose. In production, the equivalent agent normally runs on an application host, in a device image, in a Kubernetes workload, or in another runtime environment that can reach the upstream service.

The local hostnames are:

HostnameUse
edge.localhost.rstream.testBase engine host. Agents and API clients connect here.
hello.t.edge.localhost.rstream.testPublished tunnel hostname.

In production, replace edge.localhost.rstream.test with your own engine host, such as edge.example.com, and create DNS records for edge.example.com and *.t.edge.example.com.

The local deployment uses high ports only so it can run on a workstation without taking over privileged ports. The engine still listens on 80, 443, and 9090 inside the container, which keeps the container configuration close to the production shape.

Prepare The Working Directory

Run the local validation from an empty directory:

mkdir -p rstream-engine-ce/tls
cd rstream-engine-ce
export COMPOSE_PROJECT_NAME="rstreamceguide"
export RSTREAM_ENGINE_HOST="edge.localhost.rstream.test"
export RSTREAM_TUNNEL_HOST="hello.t.${RSTREAM_ENGINE_HOST}"
export RSTREAM_HTTP_PORT="18080"
export RSTREAM_TLS_PORT="18443"
export RSTREAM_PROM_PORT="19090"
export RSTREAM_RUNTIME_UID="$(id -u)"
export RSTREAM_RUNTIME_GID="$(id -g)"

These variables define three separate concerns. RSTREAM_ENGINE_HOST is the public identity of the engine and must match certificate SANs. RSTREAM_TUNNEL_HOST is the stable domain requested by the validation agent. RSTREAM_RUNTIME_UID and RSTREAM_RUNTIME_GID make the bind-mounted private key readable by the engine process without making it world-readable.

The local ports avoid conflicts with services already bound to 80, 443, or 9090. Production deployments normally publish the engine on 80 and 443.

Create The Local TLS Certificate

The local certificate is signed by a temporary CA and is only for validation. It includes the same SAN shape as production: the base engine host and the tunnel wildcard used by published endpoints.

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out tls/ca.key
openssl req -x509 -new -key tls/ca.key -sha256 -days 3650 -subj "/CN=rstream local test CA" -out tls/ca.crt
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out tls/key.pem
openssl req -new -key tls/key.pem -subj "/CN=${RSTREAM_ENGINE_HOST}" -out tls/server.csr
cat > tls/server.ext <<EOF
subjectAltName=DNS:${RSTREAM_ENGINE_HOST},DNS:*.t.${RSTREAM_ENGINE_HOST}
keyUsage=digitalSignature
extendedKeyUsage=serverAuth
EOF
openssl x509 -req -in tls/server.csr -CA tls/ca.crt -CAkey tls/ca.key -CAcreateserial -out tls/cert.pem -days 397 -sha256 -extfile tls/server.ext
chmod 600 tls/ca.key tls/key.pem
chmod 644 tls/ca.crt tls/cert.pem

The private key is stored with mode 0600 because the engine only needs read access at runtime. In production, use a publicly trusted certificate, keep the same SAN shape, and have the certificate automation write files with the same ownership model.

Create The Engine Secret And Agent Token

CE validates HS256 JWTs using one shared engine secret. The engine does not call the hosted Control plane, so token validity is a local cryptographic check: the JWT signature must verify with auth.jwt.token_jwt_secret, and the token should carry normal time bounds such as iat and exp.

The token below is short-lived and enough for the local validation agent. The metrics token is separate because Prometheus scraping is a different access surface.

export RSTREAM_ENGINE_JWT_SECRET="$(openssl rand -base64 32)"
export RSTREAM_METRICS_TOKEN="$(openssl rand -base64 32)"
export RSTREAM_AGENT_TOKEN="$(
  node <<'NODE'
const crypto = require("crypto");
const now = Math.floor(Date.now() / 1000);
const secret = process.env.RSTREAM_ENGINE_JWT_SECRET;
const b64 = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
const header = b64({ alg: "HS256", typ: "JWT" });
const payload = b64({ type: "pat", sub: "local-agent", iat: now, exp: now + 3600 });
const signature = crypto.createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url");
console.log(`${header}.${payload}.${signature}`);
NODE
)"

Use your secret manager for production tokens. The local config below stores the token inline because this is a disposable validation stack. For a production macOS agent, store the token in Keychain; for Linux services, inject it from the service manager, orchestrator, or secret manager that starts the agent.

Write The Engine Configuration

Create the engine configuration. This YAML describes stable runtime structure: which listeners exist, which certificate provider is active, which authentication backend is enabled, and whether Prometheus is exposed.

cat > config.yaml <<EOF
engine:
  host: ${RSTREAM_ENGINE_HOST}
  log_level: info
http:
  enabled: true
tls:
  enabled: true
certs:
  static:
    enabled: true
auth:
  jwt:
    enabled: true
metrics:
  prometheus:
    enabled: true
EOF

Put environment-specific values and secrets in .env. This split keeps reusable structure in YAML and host-specific material out of the config file. The engine reads the YAML first, then applies RSTREAM_ENGINE_ environment overrides.

cat > .env <<EOF
COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}
RSTREAM_HTTP_PORT=${RSTREAM_HTTP_PORT}
RSTREAM_TLS_PORT=${RSTREAM_TLS_PORT}
RSTREAM_PROM_PORT=${RSTREAM_PROM_PORT}
RSTREAM_ENGINE_AUTH__JWT__TOKEN_JWT_SECRET=${RSTREAM_ENGINE_JWT_SECRET}
RSTREAM_ENGINE_CERTS__STATIC__CERT_FILE=/etc/rstream/tls/cert.pem
RSTREAM_ENGINE_CERTS__STATIC__KEY_FILE=/etc/rstream/tls/key.pem
RSTREAM_ENGINE_HTTP__LISTEN_ADDR=[::]:80
RSTREAM_ENGINE_TLS__LISTEN_ADDR=[::]:443
RSTREAM_ENGINE_METRICS__PROMETHEUS__LISTEN_ADDR=0.0.0.0:9090
RSTREAM_ENGINE_METRICS__PROMETHEUS__BEARER_TOKEN=${RSTREAM_METRICS_TOKEN}
RSTREAM_RUNTIME_UID=${RSTREAM_RUNTIME_UID}
RSTREAM_RUNTIME_GID=${RSTREAM_RUNTIME_GID}
EOF

Create the agent context used only for the validation step. It points to the base engine host, not to a project endpoint, because CE does not have projects.

cat > client.yaml <<EOF
version: 1
defaults:
  context:
    name: local-ce
contexts:
  - name: local-ce
    engine: ${RSTREAM_ENGINE_HOST}:443
    auth:
      token:
        storage:
          kind: inline
          value: ${RSTREAM_AGENT_TOKEN}
EOF

Now write the Compose file for the engine and sample upstream service. The engine gets a Docker network alias for edge.localhost.rstream.test; this replaces public DNS during the local validation. The user field makes the container run with the same UID/GID that owns the mounted key.pem.

cat > compose.yaml <<EOF
services:
  engine:
    image: rstream/rstream-engine-ce:latest
    command: ["--config", "/etc/rstream/config.yaml"]
    user: "\${RSTREAM_RUNTIME_UID}:\${RSTREAM_RUNTIME_GID}"
    env_file: [".env"]
    ports:
      - "127.0.0.1:\${RSTREAM_HTTP_PORT}:80"
      - "127.0.0.1:\${RSTREAM_TLS_PORT}:443"
      - "127.0.0.1:\${RSTREAM_PROM_PORT}:9090"
    volumes:
      - "./config.yaml:/etc/rstream/config.yaml:ro"
      - "./tls:/etc/rstream/tls:ro"
    networks:
      edge:
        aliases:
          - "${RSTREAM_ENGINE_HOST}"
  app:
    image: hashicorp/http-echo:1.0
    command: ["-listen=:8080", "-text=hello from rstream CE"]
    networks: [edge]
networks:
  edge: {}
EOF

SSL_CERT_FILE is not configured in the engine. It is only passed to the validation agent so that the agent trusts the temporary local CA. Production agents should trust the certificate through the operating system trust store instead.

Start The Engine

Start the engine and the upstream app:

docker compose up -d engine app
docker compose ps
docker compose logs --tail=100 engine

The engine should report that the TLS listener has started. At this point only the server runtime is up; no tunnel exists until an agent connects. If the engine exits immediately, inspect the certificate paths, the JWT secret, and the Prometheus bearer token.

Run An Agent Validation

Launch a validation agent in the same Docker network as the sample app. This container represents a workload-side agent: it can reach app:8080, it authenticates to the engine, and it asks the engine to publish hello.t.edge.localhost.rstream.test.

docker rm -f rstream-ce-agent >/dev/null 2>&1 || true
docker run --rm -d \
  --name rstream-ce-agent \
  --network "${COMPOSE_PROJECT_NAME}_edge" \
  -e RSTREAM_CONFIG=/etc/rstream/client.yaml \
  -e SSL_CERT_FILE=/etc/rstream/tls/ca.crt \
  -v "$PWD/client.yaml:/etc/rstream/client.yaml:ro" \
  -v "$PWD/tls/ca.crt:/etc/rstream/tls/ca.crt:ro" \
  rstream/rstream:latest \
  forward app:8080 --http --publish --name hello --host "$RSTREAM_TUNNEL_HOST" --output text

Wait for the tunnel to appear in the agent logs:

for i in $(seq 1 30); do
  docker logs rstream-ce-agent 2>&1 | grep -q "$RSTREAM_TUNNEL_HOST" && break
  sleep 1
done
docker logs rstream-ce-agent

The output should contain https://hello.t.edge.localhost.rstream.test. That proves the agent authenticated, opened the control channel, registered a published tunnel, and received the stable hostname it requested.

Verify The Engine And Tunnel

The following checks cover each boundary in the deployment. Metrics proves the operational endpoint is protected. OpenAPI proves the base engine hostname terminates TLS correctly. /api/tunnels proves the Engine API accepts the JWT. The final curl proves published traffic reaches the upstream service through the agent.

Check the metrics endpoint with its bearer token:

curl -fsS \
  -H "Authorization: Bearer ${RSTREAM_METRICS_TOKEN}" \
  "http://127.0.0.1:${RSTREAM_PROM_PORT}/metrics" >/dev/null

Check the Engine API over TLS:

curl -fsS \
  --resolve "${RSTREAM_ENGINE_HOST}:${RSTREAM_TLS_PORT}:127.0.0.1" \
  --cacert tls/ca.crt \
  "https://${RSTREAM_ENGINE_HOST}:${RSTREAM_TLS_PORT}/api/openapi.json" \
  | jq -r '.info.title + " " + .info.version'

List live tunnels through the base engine host:

curl -fsS \
  --resolve "${RSTREAM_ENGINE_HOST}:${RSTREAM_TLS_PORT}:127.0.0.1" \
  --cacert tls/ca.crt \
  -H "Authorization: Bearer ${RSTREAM_AGENT_TOKEN}" \
  "https://${RSTREAM_ENGINE_HOST}:${RSTREAM_TLS_PORT}/api/tunnels" \
  | jq

Call the published tunnel hostname:

curl -fsS \
  --resolve "${RSTREAM_TUNNEL_HOST}:${RSTREAM_TLS_PORT}:127.0.0.1" \
  --cacert tls/ca.crt \
  "https://${RSTREAM_TUNNEL_HOST}:${RSTREAM_TLS_PORT}/"

The response should be:

hello from rstream CE

In production, DNS should resolve the hostnames directly and the certificate should be publicly trusted, so --resolve, --cacert, and local high-port suffixes disappear.

Validate Certificate Reload

CE reloads the static certificate on later TLS handshakes when the certificate or key file changes. Capture the current fingerprint:

before="$(
  echo | openssl s_client \
    -connect "127.0.0.1:${RSTREAM_TLS_PORT}" \
    -servername "${RSTREAM_ENGINE_HOST}" \
    -showcerts 2>/dev/null \
    | openssl x509 -noout -fingerprint -sha256 \
    | cut -d= -f2
)"

Generate a replacement certificate with the same SANs and atomically replace the live files:

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out tls/key.new.pem
openssl req -new -key tls/key.new.pem -subj "/CN=${RSTREAM_ENGINE_HOST}" -out tls/server.new.csr
openssl x509 -req -in tls/server.new.csr -CA tls/ca.crt -CAkey tls/ca.key -CAcreateserial -out tls/cert.new.pem -days 397 -sha256 -extfile tls/server.ext
mv tls/key.new.pem tls/key.pem
mv tls/cert.new.pem tls/cert.pem
chmod 600 tls/key.pem

Confirm that a new TLS handshake sees a different certificate without restarting the engine:

after="$(
  echo | openssl s_client \
    -connect "127.0.0.1:${RSTREAM_TLS_PORT}" \
    -servername "${RSTREAM_ENGINE_HOST}" \
    -showcerts 2>/dev/null \
    | openssl x509 -noout -fingerprint -sha256 \
    | cut -d= -f2
)"
printf 'before=%s\nafter=%s\n' "$before" "$after"
test "$before" != "$after"

This check matters operationally because CE does not manage ACME itself. A certificate writer updates files on disk; the engine observes the replacement on a later TLS handshake and keeps serving traffic without a process restart.

Move The Same Shape To Production

Moving to production keeps the same engine contract and replaces the local-only pieces. Use a real domain such as edge.example.com, publish TCP 80 and 443, and create DNS records for the base host and tunnel wildcard:

edge.example.com       A/AAAA  <server-public-ip>
*.t.edge.example.com   CNAME   edge.example.com

Replace the local CA with an ACME certificate that covers:

edge.example.com
*.t.edge.example.com

Wildcard certificates require DNS validation with most ACME providers. The example below uses Certbot with Cloudflare DNS. The provider-specific ACME client can change; the contract with CE stays the same: keep tls/cert.pem and tls/key.pem current, write replacements atomically, and preserve ownership so the engine can read the private key.

Create the production directory and secrets. These commands assume a Linux host where Docker Compose manages the engine. Adapt the base directory to your own filesystem layout if your deployment standard uses another path.

sudo mkdir -p /opt/rstream-engine-ce/{tls,letsencrypt,secrets}
sudo chown -R "$USER:$(id -gn)" /opt/rstream-engine-ce
cd /opt/rstream-engine-ce
export RSTREAM_ENGINE_HOST="edge.example.com"
export ACME_EMAIL="ops@example.com"
export RSTREAM_ENGINE_JWT_SECRET="$(openssl rand -base64 32)"
export RSTREAM_METRICS_TOKEN="$(openssl rand -base64 32)"
export RSTREAM_RUNTIME_UID="$(id -u)"
export RSTREAM_RUNTIME_GID="$(id -g)"
cat > secrets/cloudflare.ini <<EOF
dns_cloudflare_api_token = <cloudflare-dns-edit-token>
EOF
chmod 600 secrets/cloudflare.ini

Write the production engine configuration. The YAML keeps only stable runtime structure; secrets and host-specific paths stay in .env.

cat > config.yaml <<EOF
engine:
  host: ${RSTREAM_ENGINE_HOST}
  log_level: info
http:
  enabled: true
tls:
  enabled: true
certs:
  static:
    enabled: true
auth:
  jwt:
    enabled: true
metrics:
  prometheus:
    enabled: true
EOF
cat > .env <<EOF
RSTREAM_ENGINE_AUTH__JWT__TOKEN_JWT_SECRET=${RSTREAM_ENGINE_JWT_SECRET}
RSTREAM_ENGINE_CERTS__STATIC__CERT_FILE=/etc/rstream/tls/cert.pem
RSTREAM_ENGINE_CERTS__STATIC__KEY_FILE=/etc/rstream/tls/key.pem
RSTREAM_ENGINE_HTTP__LISTEN_ADDR=[::]:80
RSTREAM_ENGINE_TLS__LISTEN_ADDR=[::]:443
RSTREAM_ENGINE_METRICS__PROMETHEUS__LISTEN_ADDR=127.0.0.1:9090
RSTREAM_ENGINE_METRICS__PROMETHEUS__BEARER_TOKEN=${RSTREAM_METRICS_TOKEN}
RSTREAM_RUNTIME_UID=${RSTREAM_RUNTIME_UID}
RSTREAM_RUNTIME_GID=${RSTREAM_RUNTIME_GID}
ACME_EMAIL=${ACME_EMAIL}
EOF

Write the production Compose file with the engine and certificate sidecar. The engine mounts TLS files read-only. Certbot owns ACME state and writes renewed material into the shared TLS directory through a deploy hook. After the first certificate is issued, the sidecar uses certbot renew, which is the normal long-running ACME maintenance path.

cat > compose.yaml <<EOF
services:
  engine:
    image: rstream/rstream-engine-ce:latest
    restart: unless-stopped
    command: ["--config", "/etc/rstream/config.yaml"]
    user: "\${RSTREAM_RUNTIME_UID}:\${RSTREAM_RUNTIME_GID}"
    env_file: [".env"]
    ports:
      - "80:80"
      - "443:443"
      - "127.0.0.1:9090:9090"
    volumes:
      - "./config.yaml:/etc/rstream/config.yaml:ro"
      - "./tls:/etc/rstream/tls:ro"
  certbot:
    image: certbot/dns-cloudflare:latest
    restart: unless-stopped
    environment:
      RSTREAM_RUNTIME_UID: "${RSTREAM_RUNTIME_UID}"
      RSTREAM_RUNTIME_GID: "${RSTREAM_RUNTIME_GID}"
    volumes:
      - "./letsencrypt:/etc/letsencrypt"
      - "./tls:/rstream-tls"
    secrets:
      - cloudflare_ini
    entrypoint: /bin/sh
    command:
      - -lc
      - |
        while :; do
          certbot renew --deploy-hook 'cp /etc/letsencrypt/live/rstream-engine/fullchain.pem /rstream-tls/cert.pem.new && cp /etc/letsencrypt/live/rstream-engine/privkey.pem /rstream-tls/key.pem.new && chown "$RSTREAM_RUNTIME_UID:$RSTREAM_RUNTIME_GID" /rstream-tls/cert.pem.new /rstream-tls/key.pem.new && chmod 0644 /rstream-tls/cert.pem.new && chmod 0600 /rstream-tls/key.pem.new && mv /rstream-tls/cert.pem.new /rstream-tls/cert.pem && mv /rstream-tls/key.pem.new /rstream-tls/key.pem'
          sleep 12h
        done
secrets:
  cloudflare_ini:
    file: ./secrets/cloudflare.ini
EOF

Bootstrap the first certificate before starting the engine. The engine cannot start without readable certificate files, so issuance happens once before docker compose up -d. After that, the Certbot sidecar takes over renewals.

docker compose run --rm --entrypoint certbot certbot certonly \
  --non-interactive \
  --agree-tos \
  --email "$ACME_EMAIL" \
  --dns-cloudflare \
  --dns-cloudflare-credentials /run/secrets/cloudflare_ini \
  --cert-name rstream-engine \
  -d "$RSTREAM_ENGINE_HOST" \
  -d "*.t.$RSTREAM_ENGINE_HOST"
sudo install -o "$RSTREAM_RUNTIME_UID" -g "$RSTREAM_RUNTIME_GID" -m 0644 letsencrypt/live/rstream-engine/fullchain.pem tls/cert.pem
sudo install -o "$RSTREAM_RUNTIME_UID" -g "$RSTREAM_RUNTIME_GID" -m 0600 letsencrypt/live/rstream-engine/privkey.pem tls/key.pem
docker compose up -d

Create the first production agent token from the same engine secret. In CE this is an operator responsibility: there is no project API that mints or stores agent tokens for you.

export RSTREAM_AGENT_TOKEN="$(
  node <<'NODE'
const crypto = require("crypto");
const now = Math.floor(Date.now() / 1000);
const secret = process.env.RSTREAM_ENGINE_JWT_SECRET;
const b64 = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
const header = b64({ alg: "HS256", typ: "JWT" });
const payload = b64({ type: "pat", sub: "prod-agent-01", iat: now, exp: now + 86400 });
const signature = crypto.createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url");
console.log(`${header}.${payload}.${signature}`);
NODE
)"

CE token validation is deliberately local: the token is not created through a project API, is not persisted by the engine, and is not tied to a hosted project endpoint. Use short expirations, rotate through your secret-management process, and distribute tokens only to the agent runtime that needs them.

For production agent configuration, remove SSL_CERT_FILE and point agents to the public base engine host. On each workload host, save the context as the file selected by RSTREAM_CONFIG; this example uses /etc/rstream/agent.yaml owned by the operator account running the smoke test:

sudo install -d -o "$USER" -g "$(id -gn)" -m 0700 /etc/rstream
cat > /etc/rstream/agent.yaml <<EOF
version: 1
defaults:
  context:
    name: prod-ce
contexts:
  - name: prod-ce
    engine: ${RSTREAM_ENGINE_HOST}:443
    auth:
      token:
        storage:
          kind: inline
          value: ${RSTREAM_AGENT_TOKEN}
EOF
chmod 0600 /etc/rstream/agent.yaml

On macOS agents, store that token in Keychain instead of inline YAML. On Linux servers, keep the file owned by the service account or render it from the process manager, orchestrator secret, or secret manager used to start the agent. The server-side CE configuration does not change for either storage model because token verification remains the same HS256 check.

Validate The Production Deployment

Before routing real workloads through the engine, validate the public path and the private operational path separately. The first check proves the public certificate and base hostname. The second check proves the metrics endpoint is reachable only from the host or monitoring network. The third check proves an agent token can read live engine state.

curl -fsS "https://${RSTREAM_ENGINE_HOST}/api/openapi.json" \
  | jq -r '.info.title + " " + .info.version'
curl -fsS \
  -H "Authorization: Bearer ${RSTREAM_METRICS_TOKEN}" \
  "http://127.0.0.1:9090/metrics" >/dev/null
curl -fsS \
  -H "Authorization: Bearer ${RSTREAM_AGENT_TOKEN}" \
  "https://${RSTREAM_ENGINE_HOST}/api/tunnels" \
  | jq

Then run a real workload-side agent from a host that can reach the upstream service. This is the production equivalent of the local validation container:

export RSTREAM_CONFIG="/etc/rstream/agent.yaml"
rstream forward 127.0.0.1:8080 --http --publish --name prod-check --host "check.t.${RSTREAM_ENGINE_HOST}"

From another terminal or host, call the published hostname:

curl -fsS "https://check.t.${RSTREAM_ENGINE_HOST}/"

That final request proves the full path: public DNS, public TLS, engine routing, authenticated agent control channel, and upstream application reachability.

Operate The Deployment

Pin image tags in production. Treat an engine upgrade as an edge runtime change: pull the intended version, restart the engine service, and watch logs until listeners are up again.

docker compose pull engine
docker compose up -d engine
docker compose logs --tail=100 engine

Expect agents to reconnect when the engine restarts. Live tunnels are in memory, so agent retry behavior is part of the deployment contract.

Keep the JWT secret and issued tokens on a rotation schedule. CE currently validates one configured shared secret, so a secret rotation means issuing new agent tokens, updating the engine secret, restarting the engine, and restarting agents with the new tokens in the same maintenance window.

Stop the local validation stack when you are done:

docker rm -f rstream-ce-agent >/dev/null 2>&1 || true
docker compose down -v

Troubleshooting

If the engine exits with missing Prometheus bearer token for non-loopback listen address, either bind metrics to 127.0.0.1:9090 inside the container or set RSTREAM_ENGINE_METRICS__PROMETHEUS__BEARER_TOKEN.

If the validation agent cannot resolve edge.localhost.rstream.test in the local stack, confirm the engine service has the hostname listed under networks.edge.aliases and that the agent was started with --network "${COMPOSE_PROJECT_NAME}_edge".

If the agent reaches the engine but fails TLS verification, confirm SSL_CERT_FILE=/etc/rstream/tls/ca.crt is set for the local CA test. In production, use a certificate trusted by the operating system.

If the published tunnel hostname does not route, check that the tunnel is online in the agent logs, then call the tunnel host with the exact hostname reported by the agent. In production, verify *.t.<engine-host> DNS.

If a tunnel option is rejected as unavailable, confirm that it is part of the CE scope documented on Self-Hosted. Hosted and private-runtime controls are rejected by the public CE engine.