# How Brume works

> Every part of Brume, why it's there, and what an attacker gets if they break it. Click any piece of the diagram, or follow a tour.

The design of alpha 0.1. Shown in the recommended modes: Cloudflare Tunnel for public traffic, Tailscale for administration. The other modes are chosen on the host, and are less safe. This is https://brume.dev/architecture in Markdown, for agents and terminals. The page itself has an interactive diagram.

## The ideas behind it

- The public path and the admin path never meet.
- The engine decides. The agent acts.
- Only the agent holds Docker, and it listens on nothing.
- One system user and one socket per service.
- Brume can't delete what it sent to S3.
- Signed updates, never backwards.

## Every component

### Internet (visitors and GitHub)

Your users and GitHub's webhooks come from here. Nothing on your server waits for them: they reach your apps through Cloudflare.

How it works:

- Visitors open your apps on your domain, over HTTPS.
- GitHub calls Brume's webhook when you push to a configured branch.
- DNS points to Cloudflare, never to your server's IP.

Security rules:

- **No inbound port.** The firewall denies all inbound traffic, SSH included. Scanning your server's IP finds nothing to attack.
- **A leaked IP doesn't matter.** Nothing listens on it.

Talks to: Cloudflare Tunnel. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/networking.mdx

### Cloudflare Tunnel (no inbound port)

It brings public traffic in without opening a port. cloudflared, on your server, dials out to Cloudflare, and requests come back through that connection.

How it works:

- Cloudflare holds the public certificates, HSTS and the redirect to HTTPS: no Let's Encrypt, no port 80.
- cloudflared sends everything to Traefik, which answers 404 for unknown domains.
- A wildcard on your domain points to the tunnel, so a new service needs no DNS change.
- The trade-off: Cloudflare decrypts your public traffic. Direct ingress on ports 80 and 443 is the alternative, chosen on the host.

Security rules:

- **Tunnel config managed on the server.** A route added in the Cloudflare dashboard must not divert your traffic. Whether a local config really blocks dashboard routes is still being checked.
- **Unknown connectors raise an alert.** A stolen tunnel secret lets someone run a second connector. A read-only token lets the engine spot it and alert you.
- **API token forgotten after install.** Only the tunnel secret and a read-only token stay on the server, and the token can't change anything.

If it's compromised: Reading and modifying public traffic. cloudflared reaches only Traefik and the webhook.

Talks to: Internet, Traefik. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/networking.mdx

### Traefik (routes to your apps)

It routes every public request to the right app. It handles all public traffic, so it runs with as few rights as possible.

How it works:

- Listens only on its fixed IP in the ingress network.
- Reaches each exposed app through a network shared with that app alone.
- The agent writes its config to a folder Traefik mounts read-only.

Security rules:

- **Never the Docker socket.** A router holding the socket is root on the host. Its config comes from files instead.
- **On no environment network.** So it never reaches your databases.
- **Forwarded headers from cloudflared only.** Otherwise a client could show your apps a spoofed IP.
- **Webhook path: GitHub IPs only.** It cuts the noise. The webhook's signature check stays the real barrier.
- **No port, no root, read-only.** Every public request goes through it.

If it's compromised: Reading and modifying public traffic, and reaching the exposed services. Not the databases.

Talks to: Cloudflare Tunnel, Your apps, Caddy, brume webhook. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/networking.mdx

### Your apps (containers)

Your code, in containers. Each environment is its own network, cut off from the others.

How it works:

- Deployed by image digest, so a moved tag never changes production.
- Without a volume, a new version starts next to the old one, and traffic switches once its healthcheck passes. With a volume, the service stops, then restarts.
- Secrets arrive as environment variables when the container starts.

Security rules:

- **No outbound internet by default.** You turn egress on per service, so a compromised app can't call out unless you allowed it.
- **No route to your tailnet or private networks.** nftables rules stop a compromised container from using your server to reach your other machines.
- **Root in a container is not root on the host.** userns-remap maps it to an unprivileged user, so an escape doesn't give root directly.
- **Dangerous options off, unless root allows them.** --privileged, host mounts or the Docker socket need an edit to policy.toml, never a click in the panel.

If it's compromised: Its environment, nothing else.

Talks to: Traefik. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/runtime.mdx

### Caddy (static sites)

It serves your static sites. Recent versions stay on disk, so a rollback is instant.

How it works:

- Each domain serves a link to the folder of its revision. A deploy or a rollback switches the link at once.
- TLS ends at Cloudflare, so Caddy's automatic HTTPS is off.
- Its config is JSON generated by Brume, mounted read-only.

Security rules:

- **Read-only files, no root.** A compromised Caddy can't rewrite your sites on disk.
- **Hidden files are refused.** Builds often leave .env or .git in the output folder.
- **Archives unpacked without privileges.** Paths with ../ and symlinks are rejected, so Caddy can't serve files outside the site.
- **Admin API locked in its container.** The policy forbids any other container from sharing its network.

If it's compromised: Serving modified content until it restarts.

Talks to: Traefik. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/runtime.mdx

### brume webhook (GitHub pushes)

It receives GitHub's push events. It's the only Brume component reachable from the internet, so it's a separate, tiny process.

How it works:

- Checks the HMAC signature before it parses anything.
- Turns a push into a single call to the engine: enqueue_build(repo_id, ref).
- The repository comes from Brume's config, never from the payload.

Security rules:

- **Separate from the API.** Inside the API, a flaw in its parsing would give every admin action.
- **Signature first, in constant time.** It parses no unsigned data from the internet.
- **Duplicate deliveries are ignored.** GitHub signs no timestamp, so this stops replays.
- **One operation, no outbound network.** Even a hijacked webhook can only queue a build of a repository you configured.

If it's compromised: Queuing a build of an already configured repository.

Talks to: Traefik, brume engine. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/builds-and-deployments.mdx

### Your tailnet (you, the CLI, AI agents)

The way in for administration. The panel, the CLI and SSH live on your private Tailscale network, not on the internet.

How it works:

- tailscale serve exposes the panel over HTTPS, on a *.ts.net name.
- It adds your Tailscale identity to each request. Brume checks it against an allowlist set at install.
- Your databases can be opened to your laptop over TCP, one by one, never through a public port.

Security rules:

- **Being on the tailnet is not enough.** A compromised device or a shared tailnet would get in, so the panel also asks for a passkey.
- **Your policy must keep it off your devices.** A tailnet's default policy allows everything, so yours must forbid it: then a compromised server can't reach your laptop.
- **Never Tailscale Funnel.** Funnel would publish the panel on the internet.

If it's compromised: Your Tailscale identity alone gives a root shell through Tailscale SSH, an accepted risk in 0.1. The panel still asks for your passkey.

Talks to: brume api. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/admin-panel.mdx

### brume api (the panel)

It serves the panel and its API, to your browser and to the CLI. Every request goes on to the engine, which decides.

How it works:

- Listens only on 127.0.0.1. tailscale serve brings it to your tailnet.
- Forwards each request, with its session or its CLI signature, to the engine over a unix socket.
- Streams logs to the panel, once the engine has checked your session.

Security rules:

- **Two factors: Tailscale identity and passkey.** Being on your tailnet alone isn't enough.
- **The engine checks every session.** A compromised API can do nothing without a session you opened.
- **CLI requests are signed on your device.** The API can neither forge nor change them, only drop them.
- **Strict CSP, same-origin only.** The panel shows hostile content, such as log lines and commit messages. It must never run in your browser.
- **Localhost only, no outbound network.** Code running in it can neither download tools nor send data out.

If it's compromised: Nothing without a session opened by you. During a session, the panel's actions, without the dangerous options.

Talks to: Your tailnet, brume engine. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/admin-panel.mdx

### S3 bucket (state and backups)

Your server is disposable. Brume's state and your database dumps live in your bucket, so a new server comes back from it.

How it works:

- The engine streams its event log there, with the secrets encrypted.
- The agent sends database dumps, encrypted with your age key, after test-restoring each one.
- Any S3-compatible storage. Another provider than your VPS is advised.

Security rules:

- **Brume can't delete anything.** Its credentials can't delete, change lifecycle rules or suspend versioning. A compromised server can't erase what already left.
- **Encrypted for a key the server lacks.** The server can encrypt them, never read them.
- **Versioning or Object Lock required.** An overwritten object keeps its original.
- **Bucket changed on the host only.** A stolen session can't send your next backups somewhere else.

If it's compromised: Whoever has the S3 credential reads the event log, which shows how you are set up. Secrets and backups stay encrypted. They could also write fake events that a restore would replay, an accepted risk in 0.1. Whoever holds the storage provider's account can delete the bucket itself.

Talks to: brume engine, brume agent. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/secrets-and-backups.mdx

### brume engine (state and secrets)

The control plane. It decides every command and owns the state and the secrets, but never touches Docker.

How it works:

- Keeps the state as an event log in SQLite, and ships it to S3 in the background.
- Checks every request: the session or the CLI signature, then the policy.
- Sends the agent the full desired state of its machine, never a diff.

Security rules:

- **No Docker socket.** A compromised engine doesn't give root on the host without a kernel or runc flaw.
- **No untrusted data.** No git clone, no archive, no webhook or log parsing: each parser would be a door to the secrets.
- **Unix sockets only, one per client.** File permissions decide who talks to it, and each socket has its own operations.
- **Master key in systemd credentials.** Only the engine gets it at startup. Other services can't read it, even through a bug that reads files.

If it's compromised: The apps, their secrets and their data, within the limits of policy.toml. Not root on the host without a kernel or runc flaw.

Talks to: brume api, brume webhook, S3 bucket, brume updater, brume telemetry, brume agent. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/architecture.mdx

### brume updater (signed updates)

It installs new versions of Brume. It runs only during an update, and accepts only signed binaries.

How it works:

- The engine reads brume.dev/latest.json, a manifest signed offline, and starts the updater when a newer version exists.
- The updater checks everything again: the signature, the version and the hash of the binary.
- Then it replaces the binary and restarts the services.

Security rules:

- **It doesn't trust the engine.** A compromised engine can't make it install an unsigned binary.
- **Never backwards.** An old, vulnerable version stays signed, so the updater refuses any version that is not newer.
- **Two public keys built in.** If the signing key is lost or stolen, a release signed with the backup key, kept offline, replaces it.
- **The binaries belong to root.** A compromised service, other than the agent, can't persist by replacing its own binary.

If it's compromised: Equivalent to root, but it runs only during an update and accepts only signed binaries.

Talks to: brume engine. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/host-and-installation.mdx

### brume telemetry (logs, in DuckDB)

It stores and searches the logs, apart from the rest, so a flood of logs can't slow the engine down.

How it works:

- Docker sends each container's logs to it, over a unix socket only root can write to.
- The panel reads logs through the engine, which checks your session first.
- Logs are kept for a limited time: apps sometimes write secrets in them.

Security rules:

- **No network at all.** Even a successful injection gets no network, no secrets and no Docker.
- **A hardened DuckDB.** Parameterized queries, no file access, no extensions, and settings locked once set.
- **Memory and CPU caps.** A flood of logs can't starve the server.

If it's compromised: Reading the logs and the telemetry. No network, no secrets, no Docker.

Talks to: brume engine, Docker. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/observability.mdx

### brume agent (the Docker socket)

The only part of Brume that drives Docker. It makes the engine's desired state real, and enforces its own policy.

How it works:

- A reconciliation loop: after a crash or a reboot, it puts everything back on its own.
- Builds each container's config from an allowlist, and copies nothing from a request.
- Runs the builds and the backups, and reports drift, such as a container started by hand.

Security rules:

- **It listens on nothing.** It connects to the engine. Nothing can connect to it.
- **It doesn't trust the engine.** It enforces policy.toml on every container, even against a compromised engine.
- **Minimal code, no untrusted data.** With the Docker socket, it is as powerful as root. The git clone and archive extraction run in unprivileged processes.

If it's compromised: Everything, on its machine. With the updater, it is the only Brume component in this position, hence its minimal code.

Talks to: brume engine, Docker, S3 bucket, BuildKit. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/architecture.mdx

### Docker (your containers)

The container runtime. Whoever holds its socket is root, so in Brume only the agent does.

How it works:

- Runs your apps, Traefik, Caddy, cloudflared and the builder.
- Sends each container's logs to brume telemetry, never through the engine.
- Version 28.5.2 or later, with the runc fixes of November 2025.

Security rules:

- **No container publishes a port.** Docker bypasses the firewall: a published port would be exposed despite it.
- **userns-remap is on.** Root in a container maps to an unprivileged user on the host.
- **Default seccomp and AppArmor, never off.** The one exception is the builder, an accepted risk.
- **nftables rules load before Docker.** Whatever rules Docker adds, a container can't reach your tailnet, private networks or the metadata endpoint.

Talks to: brume agent, brume telemetry. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/runtime.mdx

### BuildKit (builds your images)

It turns your repository into an image with Railpack, in its own container, so neither the engine nor the agent runs your build.

How it works:

- Clones with a GitHub token limited to that repository, valid for one hour.
- Build secrets go through --secret, never ARG or ENV, and are masked in the logs.
- Produces an image. The agent loads it and deploys it by digest.

Security rules:

- **Rootless, no Docker socket.** A build can't drive the host's Docker.
- **One cache per environment.** A compromised staging build can't poison production's cache.
- **One build at a time, with limits.** A heavy build can't starve your apps or Brume.
- **Cut off from your tailnet.** Builds need npm or pip, not your machines.

If it's compromised: The current build, its secrets, the GitHub token and its environment's cache. Its sandbox is weaker than an app's, so with a kernel or runc flaw, a path to root: an accepted risk, since it builds your own code.

Talks to: brume agent. Docs: https://github.com/brumecloud/brume/blob/main/docs/content/docs/how-it-works/builds-and-deployments.mdx

## Tours

### A visitor opens your app

1. Someone opens app.example.com. The DNS points to Cloudflare, not to your server.
2. Cloudflare ends TLS and sends the request down the tunnel that cloudflared opened from your server.
3. Traefik matches the domain to a service. An unknown domain gets a 404.
4. The request reaches the app through a network it shares with Traefik alone.
5. A static site goes to Caddy instead, which serves its current revision from disk.

### You push to GitHub

1. You push to a configured branch. GitHub sends a signed webhook.
2. It comes in through the tunnel, like any public request.
3. Traefik lets only GitHub's IP ranges reach the webhook, with a rate limit.
4. The webhook checks the HMAC signature, then asks the engine to queue a build.
5. The engine checks that the ref is the configured branch.
6. It asks the agent for the build. The clone and the build run in the builder, never in the engine or the agent.
7. The agent starts BuildKit, which clones the repo with a one-hour token and builds the image with Railpack.
8. The agent loads the image into Docker, and the engine creates the revision and deploys it by digest.
9. Without a volume, the new version starts next to the old one, and traffic switches once its healthcheck passes. If it fails, the old version keeps running.

### You, or your AI agent, run a command

1. You click in the panel, or your AI agent calls the CLI. Both come in through your tailnet.
2. tailscale serve adds your identity. The API forwards the request with your session or the CLI's signature.
3. The engine checks the session or the signature, then the policy. Anything irreversible waits for your passkey.
4. The command becomes an event in the log, which leaves for your bucket.
5. The engine sends the agent the new desired state of the machine.
6. The agent checks it against its own policy, then drives Docker.
7. The agent reports what it observed, and the engine records the facts.

### Brume saves its state

1. Every change is an event in the engine's log, in SQLite.
2. The engine ships the log to your bucket in the background, secrets encrypted. An S3 outage blocks nothing.
3. The agent makes a consistent dump of each database, and test-restores it before it leaves.
4. The dump is encrypted for your age key, then uploaded. Brume can't delete it.
5. App logs go to brume telemetry, never to the bucket: logs can hold secrets.

### The server dies

1. Your server is gone. Your bucket still holds the log, the encrypted secrets and the dumps.
2. On a new VPS, brume restore takes the bucket credentials and your age key, then replays the log and checks its hash chain.
3. It creates a new tunnel and repoints your domain, so the old server gets no traffic if it comes back.
4. It rebuilds the images of the revisions in service from their commits.
5. It restores the last dump of each database, and shows you how old it is.
6. The agent starts everything. You sign in again: sessions don't survive a restore.

---

## More from brume.dev

Every page in Markdown. The whole site in one file: https://brume.dev/llms-full.txt. The index: https://brume.dev/llms.txt

- [Home](https://brume.dev/index.md)
- [Architecture](https://brume.dev/architecture/index.md)
- [Roadmap](https://brume.dev/roadmap/index.md)
- [Compare](https://brume.dev/vs/index.md)
- Brume vs [Coolify](https://brume.dev/vs/coolify/index.md), [Dokploy](https://brume.dev/vs/dokploy/index.md), [CapRover](https://brume.dev/vs/caprover/index.md), [Portainer](https://brume.dev/vs/portainer/index.md), [Kamal](https://brume.dev/vs/kamal/index.md), [Dokku](https://brume.dev/vs/dokku/index.md), [Easypanel](https://brume.dev/vs/easypanel/index.md), [Railway](https://brume.dev/vs/railway/index.md), [Render](https://brume.dev/vs/render/index.md), [Fly.io](https://brume.dev/vs/fly/index.md), [Vercel](https://brume.dev/vs/vercel/index.md), [Heroku](https://brume.dev/vs/heroku/index.md)
