Docker & Compose
mkdir -p ~/nouride && cd ~/nouridedocker run -d --name nouride --restart unless-stopped \ -p 18254:18254 \ -e HOST=0.0.0.0 \ -v "$PWD/nouride:/app/.nouride" \ ghcr.io/nouverse-tech/nouride:latestNothing to configure first. A fresh install boots with no config.toml and no agents, and everything
after that happens in the browser.
docker compose logs -f # the first-run password is printed hereCompose
name: nouride
services: nouride: image: ghcr.io/nouverse-tech/nouride:latest container_name: nouride restart: unless-stopped ports: - "${PORT:-18254}:${PORT:-18254}" # Every credential reaches the container this way. Secret *names* are chosen in config.toml — # `api_key = "nougate"` reads $NOUGATE — so they cannot be enumerated under `environment`. env_file: - path: .env required: false environment: - HOST=${HOST:-0.0.0.0} - PORT=${PORT:-18254} - LOG_LEVEL=${LOG_LEVEL:-info} volumes: # The whole install as one mount: config, database, secrets, agent packs, workspace. - ./nouride:/app/.nouride:z healthcheck: test: ["CMD", "bun", "-e", "await fetch('http://localhost:18254/health').then(r=>process.exit(r.ok?0:1))"] interval: 30s timeout: 5s retries: 3 start_period: 10s logging: driver: "json-file" options: max-size: "10m" max-file: "3"Why env_file and not just environment. Compose reads .env either way, but only to substitute
${...} in the compose file itself — any key not also listed under environment would be silently
ignored by the daemon. And the list under environment is deliberately short, because credential
variable names are chosen in config.toml and cannot be enumerated here.
You can also keep credentials out of .env entirely and set them from the dashboard. They land in
.nouride/data/secrets.json, which is on the mounted volume.
One mount, one backup
.nouride/├── config.toml├── data/ SQLite, secrets.json, the control token — 0600├── agents/ persona packs└── workspace/ what the agents produceTo keep the agents’ output on a different disk from the daemon’s state, mount workspace separately
and point workspace_dir at it. The three paths are separate config keys for exactly that.
The registry
echo "$GHCR_TOKEN" | docker login ghcr.io -u <github-username> --password-stdindocker compose -f docker-compose.prod.yml up -dA classic PAT, or a fine-grained token with read:packages and nothing more. For Kubernetes the same
credential becomes an imagePullSecret:
kubectl create secret docker-registry ghcr \ --docker-server=ghcr.io --docker-username=<github-username> --docker-password="$GHCR_TOKEN"The image contains no credentials, so making the package public is a real option rather than a
compromise — and it is the difference between one docker login and a token to rotate on every
machine that pulls.
What is in the image, and what is not
openssh-client, util-linux (for script(1), which is what makes interactive sessions work) and
ripgrep. Nothing else — not git, not kubectl, not a cloud CLI.
The container runs as uid 1000, so the agent cannot install anything at runtime: apk add fails
with a permission error, and anything it did install would be gone on the next image build. Add what
your work needs at build time:
docker build --build-arg EXTRA_PACKAGES="git kubectl helm" -t nouride .For infrastructure work over SSH you usually need less than you expect — kubectl runs on the
machine at the far end, not in here.
Reaching other machines over SSH
The image carries openssh-client. Two things it cannot carry have to come from outside.
The key. Forward your ssh-agent socket rather than mounting a private key. A key on disk with no
passphrase is a secret this process can read and any subprocess it starts can copy; a socket is a
path whose other end decides, per request, whether to sign. SSH_AUTH_SOCK is on the exec
environment allowlist for exactly this.
services: nouride: environment: - SSH_AUTH_SOCK=/ssh-agent volumes: - ${SSH_AUTH_SOCK}:/ssh-agent # Linux # macOS (Docker Desktop / OrbStack) exposes the host agent at a fixed path instead: # - /run/host-services/ssh-auth.sock:/ssh-agent - ./ssh:/home/bun/.ssh # so known_hosts survives a recreateHost keys. Without a persisted known_hosts, every host is unknown on every container recreate —
and there is no terminal to answer the prompt, so the command hangs until its timeout rather than
failing. That is the confusing shape: it looks like the network, and it is a missing file. Either
mount the directory as above, or accept new hosts on first sight with
ssh -o StrictHostKeyChecking=accept-new.
Interactive sessions
They work in the container, and that is not a small thing. exec runs with stdin closed — the right
default for a one-shot command — but the session tool opens a persistent process the agent can type
into, so a bare ssh host, kubectl exec -it, vim and a sudo password prompt are all reachable.
The terminal comes from script(1) in util-linux, which is in the image: a child that checks
isatty sees a real pty. A native macOS install does not get this.
Restarting
docker compose restartdocker compose pull && docker compose up -d # upgradenouride restart inside a container reports docker and refuses: the restart policy lives outside
the container, so exiting is only a restart if you configured one.
WhatsApp in its own container
# Enable together with `bridge_transport = "ws"` in config.toml. wa-bridge: image: ghcr.io/nouverse-tech/nouride-wa-bridge:latest container_name: nouride-wa-bridge restart: unless-stopped environment: - BRIDGE_PORT=8099 volumes: - ./nouride/data/whatsapp:/app/session:z[gateways.whatsapp.main]bridge_transport = "ws"bridge_url = "ws://wa-bridge:8099"