The signed-in product is a long-running API, a Graphile Worker, Postgres, and a computer provider (Docker supervisor, E2B, Daytona, or Box). It is not a static site. The marketing site in apps/www can be hosted separately.
Same as the README quick start: .env from .env.example, Postgres via Compose, pnpm sandbox:build, pnpm dev, then http://127.0.0.1:5173. Electron: pnpm --filter @rakazo/desktop dev while that stack is up, choosing Existing instance with that address. The desktop app's This computer option instead installs and runs the published images itself with Docker Compose (see Published images), which clashes with pnpm dev on port 5173.
Pull Postgres and ghcr.io/elie222/rakazo/app into any empty folder. No clone or image build.
Requires Docker Engine, the Compose plugin, curl, and OpenSSL.
mkdir -p rakazo && cd rakazo &&
curl -fsSLO https://raw.githubusercontent.com/elie222/rakazo/main/infra/compose/install-images.sh &&
bash install-images.shThe installer downloads docker-compose.images.yml and .env.images.example, creates .env with
random secrets, then pulls and starts the images. It preserves an existing .env when rerun. To
customize the public URL, image tag, or optional providers before startup, run
bash install-images.sh --prepare-only, edit .env, then run bash install-images.sh.
Flags may be combined in either order: --prepare-only, --local.
Stage B of the installer (Compose YAML and .env.images.example) downloads from
DOWNLOAD_BASE. Override it with a generic HTTPS mirror of infra/compose — do not rely on
vendor-specific CDN defaults:
export RAKAZO_DOWNLOAD_BASE=https://example.com/mirror/rakazo/infra/compose
bash install-images.shTrailing slashes on RAKAZO_DOWNLOAD_BASE are trimmed; non-HTTPS bases are rejected. Downloads
use finite curl retries (--retry 3 --retry-delay 2 --retry-all-errors when supported).
To reuse files already present in the working directory (skip curl when the target exists), set
RAKAZO_DOWNLOAD_SKIP_EXISTING=1 and/or pass --local:
# after placing docker-compose.images.yml and .env.images.example locally
bash install-images.sh --local --prepare-only
# or
RAKAZO_DOWNLOAD_SKIP_EXISTING=1 bash install-images.sh --prepare-onlyIf skip mode is on and a required file is missing, the installer still downloads it (or fails with the URL in the error).
Stage A (fetching install-images.sh itself) is separate. When raw GitHub is unreachable, point the
bootstrap curl at your mirror of the installer script, for example:
export RAKAZO_INSTALLER_URL=https://example.com/mirror/rakazo/infra/compose/install-images.sh
mkdir -p rakazo && cd rakazo &&
curl -fsSLO "${RAKAZO_INSTALLER_URL}" &&
bash install-images.shStage C (docker compose pull) uses RAKAZO_IMAGE, RAKAZO_IMAGE_TAG,
RAKAZO_COMPUTER_IMAGE, and RAKAZO_COMPUTER_IMAGE_TAG (defaults
ghcr.io/elie222/rakazo/{app,computer}). When GHCR is unreachable, override those
four in .env to a registry you control — keep app and computer on the same
mirror. Do not rely on vendor-specific CDN defaults:
RAKAZO_IMAGE=registry.example.com/mirror/elie222/rakazo/app
RAKAZO_IMAGE_TAG=edge
RAKAZO_COMPUTER_IMAGE=registry.example.com/mirror/elie222/rakazo/computer
RAKAZO_COMPUTER_IMAGE_TAG=edgeAfter --prepare-only, edit .env then rerun bash install-images.sh (or
docker compose --env-file .env -f docker-compose.images.yml pull). Arm64 tag
pairing is unchanged — set both tags to the same published multi-arch release
(see Published images and tags).
postgres:16 and busybox:1 still pull from Docker Hub. Stage C does not cover
them; configure the Docker daemon registry-mirrors or vendor those images.
SANDBOX_PROVIDER defaults to docker. The images Compose file runs a sandbox supervisor
(from the app image, on the internal network only) and pulls ghcr.io/elie222/rakazo/computer.
Signup and local Docker computers work without an E2B account. Optional remote providers: set
SANDBOX_PROVIDER to e2b, daytona, or box and add the matching API key. Compose requires
SANDBOX_SUPERVISOR_TOKEN for the Docker path; leave it empty and compose up fails closed.
Optional: set OPENROUTER_API_KEY or connect a model in the UI after signup.
The example defaults to edge (main builds). Every publish is multi-arch (amd64 + arm64), so
arm64 hosts need no special tag. Do not assume latest is present until a stable release exists.
Open http://127.0.0.1:5173. The first registered user becomes the
deployment owner. Put TLS in front of :5173 for a public host and set the three public origins to
that HTTPS URL.
Images Compose binds web to loopback (127.0.0.1:5173). Terminate TLS on the host and proxy
there. Vite preview same-origin-proxies /api and /rpc, so do not expose :3100. Set
BETTER_AUTH_URL, WEB_ORIGIN, and API_URL to that same HTTPS origin, and set
RAKAZO_HOST to its hostname (for example, app.example.com).
app.example.com {
reverse_proxy 127.0.0.1:5173
}Open Agent computer on a bot, or send a message that uses the desktop, to see
the local Docker computer. For in-stack Caddy plus remote E2B computers, use the
production Compose path and infra/compose/Caddyfile.prod
instead of this host proxy.
- Copy
.env.exampleto.envand setBETTER_AUTH_SECRET,ENCRYPTION_KEY, andSCREEN_PROXY_SECRETto independent long random strings (32+ characters; 64 hex forENCRYPTION_KEY). Docker sandboxes also need a dedicatedSANDBOX_SUPERVISOR_TOKEN. Keep existingENCRYPTION_KEYvalues so stored credentials stay decryptable. - Set
OPENROUTER_API_KEY(andCOMPOSIO_API_KEYif you want Plugins). - Build the computer image:
pnpm sandbox:build(Compose also builds it via thecomputerservice). docker compose --env-file .env -f infra/compose/docker-compose.yml up --build- Open the web origin (
http://127.0.0.1:5173by default). The first registered user becomes the deployment owner.
On Windows, if an older clone with core.autocrlf=true leaves the computer pane hung on boot (bash\r in sandbox logs): from a clean worktree, set git config core.autocrlf false, run git add --renormalize . && git checkout -- ., then rebuild with pnpm sandbox:build.
Compose runs Postgres, the sandbox supervisor (Docker socket), API, worker, and a Vite preview of the web app. Bot computers are sibling containers (rakazo/computer:local) on separate per-bot networks; only the supervisor and screen proxy join each one. The API process does not get an unrestricted Docker socket; the supervisor owns the lifecycle.
Postgres is published on loopback only (127.0.0.1:5433 on the host). Do not expose that port on a public VPS. Change POSTGRES_PASSWORD and keep Postgres on an internal network when you deploy remotely.
The Docker supervisor is not published as its own image and is not exposed on the host. It runs from
the app image, stays on the internal Compose network, and holds the Docker socket because access to
it is equivalent to control of the Docker host. Docker sandboxes require SANDBOX_SUPERVISOR_TOKEN
(API, worker, supervisor). SCREEN_PROXY_SECRET signs browser-screen capabilities (API and web
proxy). Keep both distinct from BETTER_AUTH_SECRET.
New credentials use versioned AES-GCM with per-record salt and row-bound AAD. Legacy ciphertext stays readable.
On a VPS, put TLS in front of :5173 (or serve the web build behind your proxy) and set:
BETTER_AUTH_URL=https://app.example.com
WEB_ORIGIN=https://app.example.com
API_URL=https://app.example.comCookies and CORS follow those origins. SIGNUPS_ENABLED / SIGNUP_ALLOWLIST seed the initial deployment settings. After initialization, the deployment owner's Settings values are the effective signup policy.
Password changes for signed-in users require no email configuration. Forgotten-password recovery appears on sign-in only when a transactional email provider is available. Rakazo uses a provider-neutral contract and ships an SMTP adapter, so Amazon SES, Resend, and self-hosted SMTP servers use the same configuration:
SMTP_URL=smtps://smtp-user:replace-with-password@smtp.example.com:465
EMAIL_FROM=Rakazo <no-reply@example.com>For Resend, use smtp.resend.com, username resend, and an API key as the password. For Amazon
SES, use the regional SMTP endpoint and SES SMTP credentials; these are different from ordinary AWS
access keys. Verify the sender/domain with the provider before testing delivery. Keep credentials in
.env, never in tracked files. smtps:// uses implicit TLS; smtp:// is also supported but requires
STARTTLS. Rakazo rejects configuration that disables TLS or certificate verification.
Local source development can use the offline email emulator instead. It captures email without contacting a provider:
EMAIL_EMULATOR=trueThe emulator is forcibly disabled when NODE_ENV=production and requires the API to bind to a
loopback host. In NODE_ENV=development, captured messages are available from
http://127.0.0.1:3100/api/dev/emails with cache disabled; the API console logs only delivery
metadata, never reset tokens. The inbox route is not registered in test, staging, or production.
Optional:
SIGNUPS_ENABLED=true
SIGNUP_ALLOWLIST=you@example.com,@company.com
SANDBOX_PROVIDER=docker # or none, e2b, daytona, box. Keep fake only for pnpm test.
AGENT_RUNTIME=pi # Keep scripted only for pnpm test.
WAKEUP_DRIVER=graphile
SANDBOX_IDLE_MS=600000 # pause the bot computer after 10 minutes idle
SANDBOX_COMMAND_TIMEOUT_MS=300000 # stop a shell command after 5 minutes
MAX_TOOL_CALLS_PER_TURN= # optional Pi turn tool-call fuse; unset/0 = unlimited
E2B_API_KEY= # when SANDBOX_PROVIDER=e2b
DAYTONA_API_KEY= # when SANDBOX_PROVIDER=daytona
BOX_API_KEY= # when SANDBOX_PROVIDER=boxTo use an operator-controlled OpenAI-compatible server such as Ollama, LM Studio, llama.cpp, or MLX, list its model IDs and an endpoint that both the API and worker processes can reach:
RAKAZO_LOCAL_MODELS=qwen3:4b,llama3.1:8b
RAKAZO_LOCAL_MODELS_URL=http://127.0.0.1:11434/v1
RAKAZO_LOCAL_CONTEXT_WINDOW=32768
RAKAZO_LOCAL_MAX_TOKENS=4096The loopback default is suitable when running Rakazo from a source checkout. In Docker Compose,
use the model server's Compose service name or another address reachable from the containers.
Only configure an endpoint you control: prompts, attachments, and tool results sent to that model
leave Rakazo through this URL. Leave RAKAZO_LOCAL_MODELS blank to disable the provider.
Each user can also connect their own OpenAI-compatible endpoint from Connect a model /
Settings → Models on web and mobile. Choose OpenAI-compatible, enter the server base URL
(for example http://127.0.0.1:8000/v1 for Rapid-MLX, Ollama, LM Studio, llama.cpp, or vLLM),
the exact model id from that server, and an optional API key. By default Rakazo only allows
loopback, RFC1918, and host.docker.internal targets. To permit public hostnames, set
RAKAZO_OPENAI_COMPAT_ALLOW_PUBLIC=1 in the deployment environment. Public hostnames must resolve
only to public addresses; redirects and DNS answers that reach private or link-local networks are
rejected.
Do not commit .env. Never put COMPOSIO_API_KEY, OpenRouter keys, or provider tokens in git, logs, or chat.
The Electron desktop app is a client of the same API. Docker and E2B still apply. On first launch, Electron asks the deployment owner whether bots should keep using Docker or run on this Mac as you. SANDBOX_PROVIDER=desktop is a separate, explicit provider that always runs commands on the service host.
- Published images (
docker-compose.images.yml) default toSANDBOX_PROVIDER=dockerwith a local supervisor and publishedghcr.io/elie222/rakazo/computerimage. No E2B account required. Optional: sete2b,daytona, orboxplus the matching API key for remote computers. - Docker is the quick-start default for published images and for a source checkout / full local Compose stack. Workspace bots share a persistent Team Computer by default; Private computers are optional. Keep the supervisor private, as the included Compose files do.
- E2B runs bot computers away from the Rakazo host and is a good choice for public or multi-user
production deployments. Rakazo checkpoints the portable workspace and browser-profile directory to
DATA_DIR; the E2B disk is a runtime cache, not the durable source of truth. - Daytona provides the same remote-computer contract through Daytona sandboxes. Configure
DAYTONA_API_KEYand optionallyDAYTONA_API_URL/DAYTONA_TARGET. - Box by ASCII provides a managed Linux desktop through
BOX_API_KEYand optionallyBOX_API_URL. Rakazo always creates or resumes boxes withnoEnv: true, keeps the portable workspace under/home/user/rakazo-home, and refreshes a two-hour TTL. A Box currently exposes one shared desktop, so concurrent Team bots can still use shell and files but only one can use graphical tools at a time. - Desktop provider / This Mac runs commands on the API/worker host. Docker stays the default. The Electron app asks once; if you choose This Mac, bots can use working directories under your home folder. Do not enable it on a public or shared service. macOS does not show its own permission dialog for this.
- Fake is only an emulator for verification.
- None boots the product without a computer host (fallback when Docker/supervisor is not configured, or when a remote provider is selected without its API key).
./scripts/backup.shThis dumps Postgres (pg_dump) and archives data/ into backups/<stamp>/.
infra/compose/docker-compose.prod.yml runs the hosted product with Postgres, the API, worker, web app,
and automatic HTTPS through Caddy. It uses E2B for bot computers, so the VM never exposes a Docker
supervisor or browser containers. The root-equivalent updater sidecar is an explicit opt-in profile.
Before deploying to a new Ubuntu host, create and verify a key-only deploy account, then apply the
idempotent host-hardening baseline. It disables SSH passwords and root login, rate-limits SSH, allows
only SSH/HTTP/HTTPS through UFW, enables fail2ban, unattended security updates, AppArmor, audit rules,
and conservative kernel/network protections. Keep the provider console open until a fresh SSH login
succeeds after the script reloads SSH.
sudo DEPLOY_USER=deploy bash infra/compose/harden-host.shThe production host also uses infra/compose/docker-daemon.json to enable live restore, bounded local
container logs, default no-new-privileges, and the kernel NAT path instead of Docker's userland proxy.
- Point an
A/AAAArecord such asapp.example.comat the VM and allow inbound TCP 80/443 and UDP 443. If you use Cloudflare, enable the proxy with Full (strict) TLS and copyCaddyfile.cloudflare.exampleto an operator-controlled path outside the public checkout. SetCADDYFILE_PATHto that absolute path. The example drops application requests that do not come from Cloudflare's published IP ranges; reconcile those ranges whenever Cloudflare publishes a change. A Cloudflare Tunnel can replace the public web listeners. - Clone the repository on the VM and create a root
.envwith production-only values. At minimum setPOSTGRES_PASSWORD,BETTER_AUTH_SECRET,ENCRYPTION_KEY,SCREEN_PROXY_SECRET,OPENROUTER_API_KEY, the API key for your selected sandbox provider,RAKAZO_HOST, and the three public origins. SetRAKAZO_DEPLOY_DIRwhen the checkout is not at the supported Linux default,/srv/rakazo. Use URL-safe random values for database credentials. If you enable theupdaterprofile, also set a dedicatedRAKAZO_UPDATER_TOKEN(at least 32 characters) that differs fromBETTER_AUTH_SECRET,SANDBOX_SUPERVISOR_TOKEN, andSCREEN_PROXY_SECRET. - Keep registration allowlisted while the service is private:
NODE_ENV=production
RAKAZO_HOST=app.example.com
# Optional operator-owned override, for example the Cloudflare allowlist file:
# CADDYFILE_PATH=/etc/rakazo/Caddyfile.prod
BETTER_AUTH_URL=https://app.example.com
WEB_ORIGIN=https://app.example.com
API_URL=https://app.example.com
SIGNUPS_ENABLED=true
SIGNUP_ALLOWLIST=owner@example.com,reviewer@example.com
# e2b, daytona, or box
SANDBOX_PROVIDER=e2b
AGENT_RUNTIME=pi
WAKEUP_DRIVER=graphile
DATA_DIR=/data
# Absolute path of this checkout as the Docker daemon sees it. /srv/rakazo is the Linux default;
# set this explicitly for every other layout. See "The deploy directory must be one path" below.
RAKAZO_DEPLOY_DIR=/srv/rakazo
RAKAZO_IMAGE_TAG=local
# Optional: required only with `--profile updater`.
# RAKAZO_UPDATER_TOKEN=replace-with-32-plus-character-updater-token- Build the images from your checkout and start the stack, then verify its public health endpoint:
docker compose --env-file .env -f infra/compose/docker-compose.prod.yml \
build --build-arg GIT_SHA=$(git rev-parse HEAD)
docker compose --env-file .env -f infra/compose/docker-compose.prod.yml \
up -d --wait --pull never
curl --fail https://app.example.com/healthBuild, do not pull, for a first deployment. RAKAZO_IMAGE_TAG ships as local, a tag no
registry serves, so the commands above build api, worker, and web from the checkout you just
cloned. The opt-in command under Updater sidecar builds updater when needed.
Running docker compose … pull first — as earlier versions of this page told you to — fails outright
with error from registry: denied whenever the tag you are on has not been published, and there is
nothing to fall back to.
Passing GIT_SHA is what makes GET /health report a "revision"; a locally built image has no
other way to know its commit. Prebuilt images from the registry bake it in at publish time, so when
you switch to a release tag you should leave GIT_SHA unset — a value in .env would override what
the image already knows.
Once a release has been published you can switch this host to prebuilt images by setting
RAKAZO_IMAGE_TAG to that release tag and running pull followed by up -d --wait --pull never.
See Published images and tags for the tag contract.
The root .env is excluded from both Git and the Docker build context. The database, application data,
and Caddy certificates live in named Docker volumes.
The production Compose file pins Postgres and Caddy to multi-architecture manifest digests, and the published application/updater builds pin their base-image digests. Refresh those pins deliberately when taking upstream security updates; changing only the visible major tag does not change the content while a digest is present.
For the single-VM production layout, install infra/compose/backup-prod.sh as
/usr/local/sbin/rakazo-backup and enable the supplied rakazo-backup.timer. It creates a verified
Postgres custom-format dump plus an application-data archive under /var/backups/rakazo, with mode
0600 and seven-day rotation. These local snapshots help with operator mistakes but are not a
substitute for an encrypted off-host backup or provider snapshot.
./scripts/restore.sh backups/<stamp>A Compose deployment on a published release tag upgrades by moving that tag:
docker compose --env-file .env -f infra/compose/docker-compose.prod.yml pull api worker web
docker compose --env-file .env -f infra/compose/docker-compose.prod.yml \
up -d --wait --pull never api worker webA deployment on the default local tag has no registry to pull from, so it upgrades by rebuilding
the checkout instead:
git pull
GIT_SHA=$(git rev-parse HEAD) docker compose --env-file .env -f infra/compose/docker-compose.prod.yml \
up -d --wait --pull never --build api worker webup --wait does not report success until the new API is healthy and the worker and web containers
are running. The API's start command runs prisma migrate deploy before it serves, so migration
failure keeps health red. A failed CLI recreate does not auto-roll back; recover with the previous
RAKAZO_IMAGE_TAG (or rebuild local) and up -d --wait --pull never.
The updater sidecar has its own image and tag so an update never recreates the process performing
it. Move it deliberately by setting RAKAZO_UPDATER_IMAGE_TAG to the full sha-<commit> tag, then
running docker compose … pull updater && docker compose … up -d --wait --pull never updater.
Sidecar /apply and /rollback recover a failed recreate by redeploying the previously cached
image when possible; if that also fails, they report a possible mixed-version runtime.
Source checkouts (not Compose) still upgrade the old way: pull, rebuild with
GIT_SHA=$(git rev-parse HEAD), run pnpm --filter @rakazo/db migrate, then restart API and worker.
Product contracts stay compatible across cloud and self-hosted.
Before upgrading across migration 20260830200000_space_scope_names_and_user_credentials, check
that every existing model and voice credential still belongs to a member of its Space. This query
uses the pre-migration workspaceId column name and must return no rows:
SELECT 'model' AS credential_type, credential."id", credential."userId",
credential."workspaceId" AS "spaceId"
FROM "user_model_credentials" AS credential
LEFT JOIN "space_members" AS membership
ON membership."spaceId" = credential."workspaceId"
AND membership."userId" = credential."userId"
WHERE membership."id" IS NULL
UNION ALL
SELECT 'voice', credential."id", credential."userId", credential."workspaceId"
FROM "user_voice_credentials" AS credential
LEFT JOIN "space_members" AS membership
ON membership."spaceId" = credential."workspaceId"
AND membership."userId" = credential."userId"
WHERE membership."id" IS NULL;The migration renames columns used by the API and worker and is therefore a coordinated cutover, not an online rolling migration. Stop the old API and worker, apply the migration, and start the new versions together. Its lock waits are bounded so contention fails the migration instead of leaving application traffic queued indefinitely.
.github/workflows/publish-server-image.yml publishes to ghcr.io/<owner>/<repo>/…, derived from
${{ github.repository }} rather than hardcoded, so a fork's CI fills the fork's own namespace. For
this repository that is:
| Image | Contents |
|---|---|
ghcr.io/elie222/rakazo/app |
api, worker, web, and sandbox supervisor — one image, multiple commands |
ghcr.io/elie222/rakazo/computer |
Linux desktop used as each bot computer |
ghcr.io/elie222/rakazo/updater |
the updater sidecar, plus the Docker CLI |
infra/compose/docker-compose.images.yml is the no-checkout path for those app and computer tags
plus Postgres. The supervisor runs from the app image on the internal network only (not a separate
published supervisor image, and no host port). Production Compose (docker-compose.prod.yml) can
also pull the same app tags once RAKAZO_IMAGE_TAG is set to a published value.
If you deploy from your own fork, set RAKAZO_IMAGE and RAKAZO_UPDATER_IMAGE to your namespace —
your CI cannot publish into someone else's.
| Tag | Published on | Moves? |
|---|---|---|
local |
nothing — built locally by up --build |
rebuilt in place |
local-<full-commit> |
nothing — built on the server by a fork update | never |
vX.Y.Z, vX.Y |
release tags | conventionally no / on patch releases |
latest |
stable vX.Y.Z tags only (not prereleases) |
yes, to the newest stable release |
sha-<full-commit> |
every push and manual run | source-addressed; used by the updater sidecar |
edge |
pushes to main | yes, to the newest main build |
Every publish, including edge from main merges, is multi-arch (amd64 + arm64): each
architecture builds natively on its own runner and one manifest is assembled per image. Until a
stable vX.Y.Z has been published, GHCR may only have edge and sha-* tags; do not pin
latest unless that tag exists in the registry.
Building the images yourself does not need QEMU. docker compose up --build builds for the host's
own architecture, and a fork publishing multi-arch images should do what publish-server-image.yml
does: build each architecture on a native runner (GitHub Actions provides ubuntu-24.04-arm for
public repositories) and merge the digests into one manifest. QEMU emulation
(docker/setup-qemu-action, binfmt) still works if you have no native arm64 machine, but it is
many times slower, hours rather than minutes for the computer image.
The updater resolves the newest stable vX.Y.Z source tag but deploys its sha-<full-commit> image,
not latest or a moving minor tag. A registry tag is not an OCI digest and GHCR package writers can
replace it, so the trust boundary remains this repository's publishing credentials. The workflow
reduces that boundary by using SHA-pinned actions, read-only pull-request jobs, digest-pinned base
images, SBOM/provenance output, and a GitHub build attestation. Operators who require registry-level
content addressing can pin RAKAZO_IMAGE outside the automatic updater to a verified digest.
Rollback never contacts the registry: it redeploys the previous tag from the local Docker cache, so a later tag move cannot change rollback content. Do not prune the previous application image until the next update has been accepted. If it is missing, rollback fails closed instead of pulling new content under an old tag.
To populate the registry the first time, run the workflow manually (workflow_dispatch) or push a
v* tag. A manual run produces sha-<full-commit>; only a stable vX.Y.Z tag (no prerelease
suffix) produces latest, and any v* tag produces semver tags. The updater ignores prereleases
and refuses the official path until a stable vX.Y.Z exists.
Compose production deployments offer an opt-in updater profile on a private control network.
Normal deployments do not start it or require its credential. To enable it, set a dedicated
RAKAZO_UPDATER_TOKEN and explicitly start the profile:
docker compose --env-file .env -f infra/compose/docker-compose.prod.yml \
--profile updater up -d --build updaterIt exposes /health, /state, /plan, /apply, and /rollback at http://updater:7092 with
RAKAZO_UPDATER_TOKEN. Operator CLI upgrades above do not need it; the sidecar is for automated
apply/rollback over that private HTTP API.
The API cannot update itself — its image has no .git, and nothing inside the container would
restart it — so the work happens in a separate updater container that outlives the recreate:
- Official repository: resolves the newest stable release and its source commit with
git ls-remote --tags, pins the corresponding fullsha-<commit>image tag in.env, keeps the outgoing tag inRAKAZO_IMAGE_TAG_PREVIOUS, explicitly pulls the new image, then runsup -d --wait --pull never. No build runs on the server. - Fork (Advanced): a fork has no published images, so the sidecar fast-forwards the checkout in
RAKAZO_DEPLOY_DIRand runsup -d --build. This builds on the server and takes minutes rather than seconds. Point it only at a fork you control and have reviewed — the sidecar runs that Compose file through a root-equivalent Docker socket.
Updates and rollbacks run one at a time. A failed pull leaves running services alone; a failed recreate restores the previous environment
pin and attempts to redeploy the cached previous image. A failed fork build also restores the
pre-update branch and commit (including when checkout succeeded but merge did not) so a later
manual --build cannot deploy the rejected or unintended revision. Database migrations are not
reversed. The sidecar never recreates itself, never touches Postgres or Caddy, and never runs
migrations — that ordering belongs to the API start command.
Only https:// and ssh:// git remotes are accepted. Merges are fast-forward only. A dirty or
untracked source tree fails closed before anything runs (the application Dockerfile uses COPY . .).
RAKAZO_DEPLOY_DIR is bind-mounted into the updater at the same path it is read from
(${RAKAZO_DEPLOY_DIR}:${RAKAZO_DEPLOY_DIR}), and that is load-bearing rather than tidy. Production
Compose defaults both sides to /srv/rakazo; set the variable for any other layout. When the
updater runs docker compose -p <project> --file $RAKAZO_DEPLOY_DIR/infra/compose/docker-compose.prod.yml up -d,
the Compose CLI inside the container expands this file's relative bind mounts — ../../.env,
./Caddyfile.prod — against that path and hands the results to the daemon. The daemon has to be
able to resolve the same strings, or it silently creates empty directories where your .env and
Caddyfile should be. Compose makes the effective -p value available for interpolation but does
not automatically put it in a container's environment, so the production file explicitly assigns
COMPOSE_PROJECT_NAME to the updater. A standalone sidecar can instead set
RAKAZO_COMPOSE_PROJECT_NAME; the final fallback is rakazo-prod. Without that propagation, a
stack started with -p something-else would be left alone while a second project with a new empty
Postgres volume came up beside it.
The value therefore has to be the path the daemon sees, which is not always the path your shell sees:
- Linux. The daemon shares the host filesystem, so the checkout path is the answer:
/srv/rakazois the default and supported production layout. SetRAKAZO_DEPLOY_DIRexplicitly when the checkout is elsewhere. - Docker Desktop (Windows/macOS). The daemon runs in a VM that mounts your drive somewhere else.
On Windows,
C:appears at/run/desktop/mnt/host/c, so a checkout atC:\Users\you\rakazoisRAKAZO_DEPLOY_DIR=/run/desktop/mnt/host/c/Users/you/rakazo. Host Git may usecore.autocrlf=true; the updater ignores CR-only diffs so that does not block/apply. Verify the mount before deploying:
docker compose --env-file .env -f infra/compose/docker-compose.prod.yml \
--profile updater run --rm updater git -C "$RAKAZO_DEPLOY_DIR" log --oneline -1That must print your checkout's HEAD. The two tempting wrong answers both fail: a native Windows
path is rejected by the daemon (mount denied: … too many colons, because the drive letter's
colon collides with the bind-mount separator), and /mnt/c/... fails silently — the container
starts, the mount is an empty directory, and the updater simply reports no checkout.
The updater holds the Docker socket, which is root-equivalent on the host. It is scoped as narrowly as that allows:
- No
ports, so nothing is published on the host. - Only on the dedicated
controlnetwork shared with the API. Caddy is not attached, so the reverse proxy has no route to the updater. - Every route except
/healthrequires the shared bearer token, compared in constant time. - The process environment carries only updater settings (
RAKAZO_UPDATER_TOKEN, deploy path, image name, project name). Application secrets stay in the bind-mounted.envthat Compose reads for interpolation; they are not loaded into this container. - The Docker CLI lives only in the updater image. The api, worker, and web containers keep
cap_drop: ALLand no socket.
Enabling the updater profile requires RAKAZO_UPDATER_TOKEN to be a dedicated random value (at
least 32 characters in production). It must differ from BETTER_AUTH_SECRET,
SANDBOX_SUPERVISOR_TOKEN, and SCREEN_PROXY_SECRET. Leave the profile disabled if you would
rather not grant the capability.
The product cannot be “pushed live” as a Vercel serverless app. Graphile Worker, Postgres LISTEN, Pi runs, and Docker computers need durable processes and a sandbox host.
To run a hosted product (same codebase):
- Push
main(this checkout may be ahead of GitHub). - Provision managed Postgres 16 and run
pnpm db:migrate. - Run API and worker as always-on Node 22 services (Fly machines, a VM, ECS, k8s). Not lambda-style request handlers.
- Persist and back up
DATA_DIR(bot homes, browser profiles, artifacts). Today the concrete store is a local filesystem (LocalAgentHomeStore), so attach a Rakazo-owned durable volume shared by API and worker processes. The storage contract is separate from the computer-provider contract, but an object-storage implementation is not wired yet. - Choose computers:
SANDBOX_PROVIDER=e2b,daytona, orboxwith the matching provider key for a public or multi-user production service. Each Team or Private Computer reconnects to its sandbox id (providerRef), while workspace state is checkpointed outside the provider at run completion, explicit stop, and idle suspension. If that sandbox is gone—or the deployment changes providers—the replacement is hydrated from Rakazo's copy. Idle computers pause afterSANDBOX_IDLE_MS(default 10 minutes) and resume on the next message or Take control. Docker remains the local and trusted single-machine default. - A Hetzner CX22 (2 vCPU / 4 GB) is enough for API + worker + Postgres when E2B owns the desktops. 2 GB works for a quiet box; 8 GB is only needed if you also run Docker computers on that same machine.
- Set public HTTPS
WEB_ORIGIN/BETTER_AUTH_URL/API_URL, secrets, and an OpenRouter (or other Pi) deployment key if you want to skip per-user model keys. - Put the web app behind the same origin as
/apiand/rpc(Vite preview proxy, or a reverse proxy). Docker noVNC connections use short-lived signed/novnc/*capabilities; do not replace that route with an unrestricted port proxy. - Deploy
apps/wwwto your public website and pointapp.example.com(or similar) at the product origin. - Turn on
SIGNUP_ALLOWLISTuntil you want open registration. There is no Rakazo-managed model billing in version 1 — users bring keys.
Expo / desktop installers are clients of that origin (EXPO_PUBLIC_API_URL, RAKAZO_WEB_URL). They are not a Cloud control plane.
The iOS and Android app can also point at a self-hosted origin at runtime. On the sign-in screen, tap Use a custom server and enter the same HTTPS origin as WEB_ORIGIN (for example https://app.example.com). Store builds still default to EXPO_PUBLIC_API_URL; the in-app setting is an override for people running their own API. Changing the server signs the device out of any previous session.