Cruma
DashboardDownload

Run Cruma in a container

Ship your app and the Cruma agent in one container: the agent starts your app, and a registered tunnel gives it a public address. The tunnel credentials, the app's passwords and every other secret come from the container's environment or from mounted secret files. None of them is in the image, in git or in the config file.

🚧 In development

The agent release that adds cruma up, the CRUMA_TUNNEL_ID and CRUMA_TUNNEL_SECRET variables and ${env:…} / ${file:…} references is not published yet. The installer still installs an agent without them, so the image below does not start as described until that release ships.

What you need

  • A tunnel ID and secret from your account. A container never falls back to an anonymous tunnel.
  • A glibc-based image such as Debian, Ubuntu or Fedora on x86_64 or arm64. The Linux installer does not support musl images such as Alpine.
  • Your app, and a cruma.yaml next to it.

The project

cruma.yaml hosts the app as a process, serves it behind basic auth and holds no secret, only references. It sets no tunnel_id or tunnel_secret, so the agent reads CRUMA_TUNNEL_ID and CRUMA_TUNNEL_SECRET (see Credentials from the environment).

backends: []

processes:
  - id: web
    command: python3
    args: ["app.py"]
    working_directory: "$cfg_dir"
    env:
      DB_PASSWORD: "${env:APP_DB_PASSWORD}"

frontends:
  - hostname: app                  # app.<tunnel-id>.tun.cruma.io
    process_id: web
    middlewares:
      - type: basic_auth
        users:
          - ["admin", "${env:APP_BASIC_AUTH_PASSWORD}"]

listeners:
  - kind: cruma

The agent gives the process a free port in PORT; your app listens on it. See Configuration for processes, frontends and middlewares.

The Dockerfile

FROM debian:bookworm-slim

RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates curl python3 \
 && rm -rf /var/lib/apt/lists/*

RUN useradd --create-home app
USER app

RUN curl -fsSL https://files.cruma.io/files/tunnel-agent/install/linux-install.sh | bash
ENV PATH="/home/app/.local/bin:${PATH}"

WORKDIR /home/app/site
COPY --chown=app:app app.py cruma.yaml ./

CMD ["cruma", "--headless", "up"]
  • Replace python3 and app.py with your own runtime and app.
  • The installer needs no sudo and puts cruma in ~/.local/bin.
  • --headless is required: a container has no terminal, and without the flag the agent tries to open its terminal interface and stops.
  • cruma up finds cruma.yaml in the working directory and fails closed (see When something is missing).
  • Put no ENV, ARG or COPY of a secret in the Dockerfile: image layers and docker history show them to everyone who can pull the image.
docker build -t shop .

Pass the secrets at run time

With -e NAME and no value, Docker takes the value from your shell:

docker run -d --name shop \
  -e CRUMA_TUNNEL_ID \
  -e CRUMA_TUNNEL_SECRET \
  -e APP_DB_PASSWORD \
  -e APP_BASIC_AUTH_PASSWORD \
  shop

Or keep them in an env file outside git, one NAME=value per line without quotes:

docker run -d --name shop --env-file cruma.env shop

The app is then at https://app.<tunnel-id>.tun.cruma.io, behind the password.

Docker secrets and Compose

Secrets mounted as files appear under /run/secrets/<name>. Point the secret fields at the files with ${file:PATH}. The file's trailing whitespace is removed, so a newline at the end is fine. The tunnel ID is not secret and stays in the environment. Use this cruma.secrets.yaml instead of cruma.yaml:

backends: []

tunnel_secret: "${file:/run/secrets/cruma_tunnel_secret}"

processes:
  - id: web
    command: python3
    args: ["app.py"]
    working_directory: "$cfg_dir"
    env:
      DB_PASSWORD: "${file:/run/secrets/app_db_password}"

frontends:
  - hostname: app
    process_id: web
    middlewares:
      - type: basic_auth
        users:
          - ["admin", "${file:/run/secrets/app_basic_auth_password}"]

listeners:
  - kind: cruma

The Compose file mounts it over the image's cruma.yaml. Keep secrets/ out of git and out of the build context (.gitignore and .dockerignore).

services:
  app:
    build: .
    environment:
      - CRUMA_TUNNEL_ID
    volumes:
      - ./cruma.secrets.yaml:/home/app/site/cruma.yaml:ro
    secrets:
      - cruma_tunnel_secret
      - app_db_password
      - app_basic_auth_password

secrets:
  cruma_tunnel_secret:
    file: ./secrets/cruma_tunnel_secret
  app_db_password:
    file: ./secrets/app_db_password
  app_basic_auth_password:
    file: ./secrets/app_basic_auth_password
CRUMA_TUNNEL_ID=TUNNEL_ID docker compose up -d

Kubernetes

Store the credentials in a Secret named cruma-credentials, with the same keys the Kubernetes ingress controller uses, in your app's namespace. The app's own secrets go in a second one:

kubectl create secret generic cruma-credentials \
  --from-literal=CRUMA_TUNNEL_ID=TUNNEL_ID \
  --from-literal=CRUMA_TUNNEL_SECRET=SECRET_KEY
kubectl create secret generic app-secrets \
  --from-literal=APP_DB_PASSWORD=DB_PASSWORD \
  --from-literal=APP_BASIC_AUTH_PASSWORD=LOGIN_PASSWORD

envFrom turns each key into an environment variable, which is all the image's cruma.yaml needs:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop
spec:
  replicas: 1
  selector:
    matchLabels:
      app: shop
  template:
    metadata:
      labels:
        app: shop
    spec:
      containers:
        - name: shop
          image: registry.example.com/shop:1.0
          envFrom:
            - secretRef:
                name: cruma-credentials
            - secretRef:
                name: app-secrets

Keep replicas: 1. A tunnel accepts one agent unless it is on Pro, and a second pod is refused until the first one disconnects.

To mount the secrets as files instead, create one Secret with a key per file and put cruma.secrets.yaml in a ConfigMap:

kubectl create secret generic app-secret-files --from-file=secrets/
kubectl create configmap cruma-config --from-file=cruma.yaml=cruma.secrets.yaml

Then replace envFrom in the container and add the volumes:

          env:
            - name: CRUMA_TUNNEL_ID
              valueFrom:
                secretKeyRef:
                  name: cruma-credentials
                  key: CRUMA_TUNNEL_ID
          volumeMounts:
            - name: secrets
              mountPath: /run/secrets
              readOnly: true
            - name: config
              mountPath: /home/app/site/cruma.yaml
              subPath: cruma.yaml
              readOnly: true
      volumes:
        - name: secrets
          secret:
            secretName: app-secret-files
        - name: config
          configMap:
            name: cruma-config

Where secrets live

PlaceSecrets allowed
Container environment (-e, --env-file, Compose environment, Kubernetes env and envFrom)Yes, read with ${env:NAME}; the tunnel credentials need no reference
Mounted secret file (Compose secrets, a Kubernetes Secret volume)Yes, read with ${file:PATH}
cruma.yamlReferences only. cruma up refuses a literal secret
Image layers (ENV, ARG, COPY) and gitNo
  • The agent logs, shows and saves the reference, never the value. cruma config show, the desktop app and MCP tools show ${env:APP_DB_PASSWORD}, and the tunnel secret shows as ${env:CRUMA_TUNNEL_SECRET} or nothing.
  • A hosted process receives the container's environment except CRUMA_TUNNEL_ID, CRUMA_TUNNEL_SECRET and any variable the agent itself reads through ${env:NAME} in its own fields, such as the basic-auth password above. It receives its own env entries under the names you gave them. A process that needs a withheld variable names it in its env.
  • Anyone who can run docker inspect on the container, or read the Pod, can read its environment. Where that matters, mount files.
  • A tunnel's ID and secret are scoped to that one tunnel. A leaked container environment exposes that tunnel and nothing else in your account.

When something is missing

The agent stops with exit code 1 and serves nothing, so an orchestrator shows a failing container instead of a public tunnel that no one meant to open. The error names what is missing and never prints a value.

SituationError
No tunnel credentialsthis run needs a registered tunnel, but no credentials are set. Set CRUMA_TUNNEL_ID and CRUMA_TUNNEL_SECRET, or set tunnel_id and tunnel_secret in the config file, or run cruma config set-credentials``
Only one of the twotunnel credentials are incomplete: tunnel_secret is not set. Set both tunnel_id and tunnel_secret in the config, or both CRUMA_TUNNEL_ID and CRUMA_TUNNEL_SECRET
A referenced variable is not setsecret reference in config field `processes[web].env.DB_PASSWORD`: `${env:APP_DB_PASSWORD}`: environment variable is not set
A referenced file is missingsecret reference in config field `tunnel_secret`: `${file:/run/secrets/cruma_tunnel_secret}`: cannot read file: entity not found
A literal secret in cruma.yamlcruma up refused the project file because it contains literal secrets:, then each field path such as frontends[0].middlewares[0].users[0].password (password)

If Cruma rejects the tunnel ID or secret, the agent logs "Cruma rejected this tunnel ID or secret" and stops connecting; see Setting credentials.