Homelab / server deployment¶
Run Paseka on a separate machine (homelab NAS, mini-PC, always-on VPS on a private network) as a long-lived apiary: colony repo on disk, NATS with JetStream nearby, Queen Console reachable from your laptop over VPN/Tailscale.
This is not a multi-tenant cloud control plane. Queen Console still has no authentication — treat the host as a trusted workstation you SSH into or tunnel to.
Container files: docker/dev/ (README).
What you get¶
| Piece | Role on the server |
|---|---|
| Colony git repo | Source of truth (bind-mounted into the container) |
~/.config/paseka/<slug>/ |
Machine-local secrets, adapter config, session state |
| NATS + JetStream | Event bus (reuse an existing server or run the root docker-compose.yml) |
Cursor Agent CLI + CURSOR_API_KEY |
Default adapter for bees |
paseka binary |
Prebuilt in the image; Go toolchain included to rebuild |
| Queen Console | Default container command — web UI + API on port 8787 |
flowchart LR
laptop[Laptop browser / SSH]
console[paseka console :8787]
reactor[paseka run]
nats[NATS JetStream]
repo[Colony repo volume]
home[PASEKA_HOME volume]
laptop -->|VPN / Tailscale| console
console --> repo
console --> home
console --> nats
reactor --> nats
reactor --> repo
reactor --> home
Prerequisites¶
- Docker Engine + Compose plugin on the server
- A colony repository already initialized (
paseka init), or plan to init inside the container - NATS with JetStream reachable from the container (host port
4222, LAN IP, or another container network) - A Cursor API key for headless
agentruns (cursor.com/settings)
Optional on the same host: start the repo-root NATS stack:
Quick start (container apiary)¶
On the server, from a clone of this repository:
Edit .env:
| Variable | Meaning |
|---|---|
PASEKA_REPO |
Absolute host path to the colony git repo |
PASEKA_HOME |
Absolute host path mounted as /home/dev/.config/paseka |
CURSOR_API_KEY |
Cursor Agent key |
PASEKA_NATS_URL |
NATS URL (overrides nats.url in home config.yaml) |
PASEKA_CONSOLE_PORT |
Host port published to Queen Console (default 8787) |
DEV_UID / DEV_GID |
Match the user that owns the bind mounts |
PASEKA_BUILD_* |
Build stamp for the image — see Name the build |
Default process is:
Open http://<server>:8787 only on a trusted network (localhost, Tailscale IP, or an SSH tunnel). Do not expose Queen Console to the public internet without an auth front door you trust.
Name the build¶
paseka version inside the image answers which build it is, and Queen Console
shows the same stamp in its side menu and on the System route. Left alone it says
dev and nothing more, because .dockerignore keeps .git out of the build
context: the toolchain has no checkout to read a commit from, and a version it
invented would be worse than one it admits it does not have.
Fill the stamp from your checkout to fix that:
cd docker/dev
cat >> .env <<EOF
PASEKA_BUILD_COMMIT=$(git -C ../.. rev-parse HEAD)
PASEKA_BUILD_DATE=$(git -C ../.. show -s --format=%cI HEAD)
PASEKA_BUILD_TREE_STATE=$(git -C ../.. diff --quiet && echo clean || echo dirty)
EOF
docker compose up --build
docker compose run --rm --entrypoint paseka-dev paseka version
PASEKA_BUILD_VERSION takes a release tag when you are deploying one. Empty
values are left off the link rather than written as empty strings, so an
unstamped image still reports dev instead of a blank version.
Environment override for NATS¶
Non-empty PASEKA_NATS_URL wins over nats.url in ~/.config/paseka/<slug>/config.yaml. See CLI — NATS dependency.
Examples:
# NATS on the Docker host (compose adds host.docker.internal)
PASEKA_NATS_URL=nats://host.docker.internal:4222
# NATS elsewhere on the LAN
PASEKA_NATS_URL=nats://192.168.1.10:4222
colony_root must match the container path¶
Home config stores an absolute colony_root. Inside the container the repo is always:
Do not mount a laptop home config whose colony_root points at /Users/... or /home/you/Projects/... — resolve will fail.
Recommended pattern for a server:
- Use a dedicated
PASEKA_HOMEon the server (for example/var/lib/paseka/config), not a copy of your laptop~/.config/pasekawithout editing paths. - First boot: open a shell and init (or fix
colony_root):
docker compose run --rm --entrypoint bash paseka-dev
paseka init # writes colony_root: "/home/dev/workspace"
- Keep
PASEKA_NATS_URLin compose for the live NATS address so you do not hard-code hostnames into the yaml for every environment.
Typical day-to-day¶
Queen Console only¶
Use Reviews, agents, and trace views from the browser. Console itself does not require NATS; bus-backed panels need a reachable JetStream.
The header Host plaque and System tab show the OS view of the paseka console process: inside this container that is the container PID namespace (bees, node, java, and similar children), not the rest of the NAS. CPU/RAM figures come from /proc/stat and /proc/meminfo, which may still describe the host when cgroups do not hide them — do not treat RAM as this container’s limit. System Info is not a cluster control plane and does not call Docker APIs.
The Git plaque and tab operate this colony clone (Fetch / Push / ff-only Pull). Git mutations do not need NATS. See Git on the apiary.
Reactor on the same machine¶
Run the choreography loop in a second process (second compose service, docker compose run, or systemd on the host):
Both processes must share the same PASEKA_REPO, PASEKA_HOME, and PASEKA_NATS_URL.
AFK / HITL from the container¶
docker compose run --rm --entrypoint bash paseka-dev
paseka bee run scout --body "survey health endpoints"
paseka doctor
Interactive bee chat / Ghostty attach assume a usable TTY on that machine — fine over SSH, awkward through Console alone. For remote HITL, prefer Telegram gateway or SSH into the apiary.
Volumes (summary)¶
| Host (compose env) | Container | Contents |
|---|---|---|
PASEKA_REPO |
/home/dev/workspace |
Colony git tree, .paseka/ |
PASEKA_HOME |
/home/dev/.config/paseka |
Per-slug config.yaml, adapters, state.json |
CURSOR_HOME |
/home/dev/.cursor |
Cursor Agent / IDE state |
CURSOR_CONFIG |
/home/dev/.config/cursor |
e.g. auth.json |
CURSOR_API_KEY is enough for headless auth; mounting Cursor config dirs is optional convenience when you already logged in on that host.
Git on the apiary¶
Inbound fast-forward from Gitea/GitHub stays an operator webhook sidecar (git pull --ff-only into the colony clone). Console Fetch only updates remote-tracking refs. Console Pull is a backup when that sidecar missed a hook — not a second pull daemon inside Paseka.
Outbound depends on colony defaults.delivery:
| Policy | What leaves the apiary | Merger |
|---|---|---|
local_merge (default) |
Console/CLI approve merges locally; Queen Console Git → Push publishes default | Paseka, then you push main |
pull_request |
Final-gate approve pushes the worktree head and upserts a PR via home forge.command |
The forge (Gitea/GitHub merge button). Then pull default |
The Git tab never pushes worktree branches. Bind-mount the same HOME (and PATH) tea / gh / git credential helpers need. Paseka does not store Gitea/GitHub tokens. See pull-request delivery.
Do not auto-push default on review Approve. After a forge merge, a sidecar pull updates the clone; Git-tab Push of default is not required.
Security notes¶
- Queen Console has no login. Bind to Tailscale/VPN, SSH port-forward (
ssh -L 8787:127.0.0.1:8787 server), or a reverse proxy with auth — not a public:8787. - Keep
CURSOR_API_KEYand telegram tokens in.env/ secret store; never bake them into the image. - Match
DEV_UID/DEV_GIDto the owner of bind mounts so worktrees and run artifacts are not root-owned on the host.
Rebuild paseka inside the container¶
The image already contains a binary at /usr/local/bin/paseka, built with the
Queen Console Next bundle. To pick up local Go changes from the mounted repo:
docker compose run --rm --entrypoint bash paseka-dev -lc \
'go build -o /home/dev/workspace/paseka ./cmd/paseka && ./paseka console --help'
That binary carries the legacy console only: the mounted repo has no built
preview bundle, since it is a gitignored artifact rather than a committed file.
To rebuild with /next/ too, build the frontend on the host first — the
container has no Node toolchain — then compile Go in the container:
# host, in the colony repo (Node + pnpm required)
pnpm --dir web install --frozen-lockfile
pnpm --dir web build
# then in the container
docker compose run --rm --entrypoint bash paseka-dev -lc \
'go build -o /home/dev/workspace/paseka ./cmd/paseka'
Or rebuild the image (docker compose build) so /usr/local/bin/paseka is
refreshed — its web-builder stage always builds the bundle first, so this is
the simpler path.
Related¶
- Colony layout —
.paseka/vs machine-local home - CLI —
paseka console,paseka run,PASEKA_NATS_URL - Pull-request delivery — push head, pull default
- Interactive sessions — HITL on the apiary
- Telegram gateway — remote human gateway without exposing Console
- Root NATS compose:
docker-compose.yml - Container README:
docker/dev/README.md