SSD Nodes Learn 🎉 VPS from $5.50/mo
Guides Matt ConnorBy Matt Connor

Self-host Rakazo: your own AI bot on a VPS

Run Rakazo on your own VPS: Node 22, pnpm, Postgres and Graphile Worker in Docker Compose, the sandbox provider choice, key handling, and honest sizing.

What self-hosting Rakazo actually runs

Self-hosting Rakazo means running five things on one Linux server: PostgreSQL, a Graphile Worker process, the API, the web app, and one sandbox container for every bot that is awake. Rakazo is an open-source alternative to Grok Bot, published by elie222 under the Apache 2.0 licence. Each bot gets its own thread, its own computer, its own memory and its own history, and it can spawn peers or short-lived subagents.

That last part is why this belongs on a VPS (virtual private server) and not on a desktop. A bot that holds memory and runs scheduled work has to be reachable while you sleep. A laptop that suspends drops the queue.

Rakazo is early beta as of August 2026, so treat this as a working setup rather than a finished appliance. The stack is TypeScript throughout: React 19 and Vite for the web app, Hono for the API, Postgres with Prisma, Better Auth for accounts, and Graphile Worker for background jobs. Graphile Worker stores its queue inside Postgres, so there is no Redis and no second data store to run. .env.example sets WAKEUP_DRIVER=graphile, which means a bot waking up is a Postgres-backed job. Stop Postgres and every scheduled bot action stops with it. If you would rather assemble an agent from parts instead of running someone else's product, building your own agent from components is the other route.

Why a 1 GB plan will not hold this

Count the processes. Postgres is one. The API is a Node process. The worker is a second. The web app is a third. The sandbox supervisor is a fourth. Then every running bot gets a container holding a graphical Linux desktop and a browser.

The project's own self-host doc gives one honest figure: a 2 vCPU and 4 GB machine is enough for the API, the worker and Postgres when E2B owns the bot desktops. That is the number for the control plane alone, with the heavy part hosted elsewhere. Set SANDBOX_PROVIDER=docker and those desktops move onto your VPS, so 4 GB becomes the floor rather than the target. Start at 8 GB if you plan to keep more than one bot awake, and measure the real number with docker stats while a bot works. The browser inside the sandbox is what moves the memory line, so a spec sheet will not tell you. For the general method of sizing a box for agent work, how much RAM and CPU an agent VPS actually needs goes through the measurement in detail.

One setting keeps this from getting worse. .env.example ships SANDBOX_IDLE_MS=600000 with the comment that it pauses E2B computers, or stops Docker ones, after that many idle milliseconds. Ten minutes idle and the computer goes away. The minimum accepted value is 30000. Without that, every bot you ever opened would hold memory forever.

Disk counts too. The sandbox image, the Node modules and the Postgres volume share one disk, so 40 GB is a sensible starting point.

Pin a version before you clone

Rakazo moves fast and main is not a release. As of 16 August 2026 the repository carries exactly one tag, v0.1.0-beta, published on 13 August 2026 and marked a prerelease.

git clone https://github.com/elie222/rakazo.git
cd rakazo
git checkout 53b119a68d9ef843d23aa3b7e3719b6be7b51fdb
git log -1 --format='%H %ci'

That commit is the one v0.1.0-beta points at. Pin the commit rather than the branch or the tag. A branch changes under you on the next git pull, and a tag is a movable label that a maintainer can repoint, so neither one identifies a tree you can return to. A commit identifier cannot move. Write yours down next to your other server facts, because when an upgrade breaks the cheap fix is git checkout <old commit> and a rebuild, and that only works if you know which commit was working.

Requirements: Node 22, pnpm 9, and Docker

node -v
pnpm -v
docker --version

package.json declares "engines": { "node": ">=22" } and "packageManager": "pnpm@9.15.0", so node -v must print v22 or higher. The Node package in the Ubuntu archive is usually older than that, so install from NodeSource or nvm. pnpm comes with Node through corepack:

corepack enable
corepack prepare pnpm@9.15.0 --activate

Docker Engine plus the compose plugin covers the rest, and your user has to be able to reach the daemon. If docker ps answers permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock, add your user to the docker group and open a new login shell. Know what that grants first: membership in docker is equivalent to root on the machine, because anyone in that group can start a container that mounts the host filesystem.

Configure .env, then start Postgres

cp .env.example .env
chmod 600 .env

Two values must change before anything faces a network. .env.example ships BETTER_AUTH_SECRET=replace-with-32-plus-character-secret and ENCRYPTION_KEY=replace-with-64-char-hex-or-passphrase. Rakazo rejects those placeholder values outside development, so a half-configured deploy fails loudly instead of running on a secret that is published in the repository.

openssl rand -base64 48
openssl rand -hex 32

Then bring up the database on its own and run the migrations.

docker compose --env-file .env -f infra/compose/docker-compose.yml up postgres -d
pnpm install
pnpm db:generate
pnpm db:migrate
pnpm sandbox:build

pnpm sandbox:build builds the bot computer image, defined in package.json as docker build -t rakazo/computer:local infra/sandboxes/computer. It is a graphical image, so the first build pulls a lot and takes time. Confirm it landed with docker image ls rakazo/computer, which should print one row.

The compose file publishes Postgres as 127.0.0.1:5433:5432, which is loopback only. Leave it that way. The development credentials are rakazo:rakazo, they are in the repository, and a Postgres port reachable from the internet with a published password is found by scanners within hours. The production compose file reads POSTGRES_PASSWORD instead, so set that to a random string when you get there.

The first run

pnpm dev

That starts four things: the API on port 3100, the Graphile Worker, the Vite web app on 5173, and the sandbox supervisor on 7091. The app is at http://127.0.0.1:5173, and you should get a sign-in page.

On a VPS you are not sitting at that machine, and you should not publish 5173 to reach it. Forward the ports over SSH (secure shell) from your own machine instead.

ssh -L 5173:127.0.0.1:5173 -L 3100:127.0.0.1:3100 you@your-server

Be careful with the difference between the two ways of running this. pnpm dev runs Vite on the host, bound locally. The compose file's web service publishes 5173:5173, on every interface. Bring up the full development compose stack on a public VPS and the app is exposed, so use the production file and its reverse proxy for anything you leave running.

Which sandbox provider is safe on a server?

This is the one setting to get right. SANDBOX_PROVIDER in .env takes four values.

  • docker is the default. Each bot gets its own container on your machine, built from the image pnpm sandbox:build produced. It is the fastest self-hosted setup.
  • e2b runs the bot computers on E2B and needs E2B_API_KEY. The project recommends it for public or multi-user deployments, because it keeps bot computers separate from the host running your API and your database.
  • desktop runs the bot's commands directly on the API and worker host. The repository's instruction is blunt: do not use it on a public or shared server.
  • fake is an in-process emulator for tests. It is not a runtime.

Take that desktop warning literally. In desktop mode there is no isolation boundary at all, so the bot runs shell commands as the user running the API process, with that user's home directory, that user's SSH keys, that user's cloud credentials and that user's .env. Text in a web page the bot reads becomes a command on your server. Desktop mode on a server is how a bot ends up holding your credentials. Use it on a machine you sit at, or not at all.

docker is a real boundary and an imperfect one. One bot cannot read another bot's files, because each has its own container. The supervisor that creates those containers, though, mounts /var/run/docker.sock, and control of the host Docker socket is control of the host. So keep the supervisor private. .env.example documents SANDBOX_SUPERVISOR_TOKEN as an optional separate service credential that defaults to BETTER_AUTH_SECRET when empty, which means leaving that secret at its placeholder protects the container-creating service with a string anyone can read on GitHub. Set both values. For the strongest separation available here, use e2b, or give Rakazo a machine that holds nothing else. That is the same reasoning behind running coding agents in a disposable VM: the cheapest way to survive an agent doing something wrong is for its machine to be worth nothing.

Where do model API keys go?

Rakazo has no managed model billing. You bring the key. .env.example sets PI_DEFAULT_PROVIDER=openrouter, so OPENROUTER_API_KEY is the usual place, and provider keys work through the same setting.

Keep the key in .env and out of any file you commit. Both compose commands in the repository pass --env-file .env, so values reach the containers without ever being written into YAML that git tracks. You can also leave OPENROUTER_API_KEY empty and paste a key in the app during onboarding, which is one more reason ENCRYPTION_KEY needs a real random value rather than the shipped placeholder.

Set a spending limit on the key at the provider before a bot ever uses it. A bot that loops is a bot that spends, and a per-key limit is the only stop that does not depend on you watching. Give this key its own name so you can revoke it alone.

Going from dev mode to something you can leave running

The repository ships a production compose file that runs Postgres, the API, the worker, the web app, and Caddy for TLS (transport layer security) certificates it obtains automatically. It expects E2B for the bot computers.

sudo DEPLOY_USER=deploy bash infra/compose/harden-host.sh
docker compose --env-file .env -f infra/compose/docker-compose.prod.yml up -d --build

harden-host.sh disables SSH password login, sets UFW (uncomplicated firewall) rules for SSH, HTTP and HTTPS, turns on fail2ban and applies AppArmor profiles. Read it before you run it, because it changes how you log in. Keep a second SSH session open while it runs.

The production .env needs more than the development one. The self-host doc lists this minimum.

NODE_ENV=production
RAKAZO_HOST=app.example.com
BETTER_AUTH_URL=https://app.example.com
WEB_ORIGIN=https://app.example.com
API_URL=https://app.example.com
POSTGRES_PASSWORD=<random>
BETTER_AUTH_SECRET=<random>
ENCRYPTION_KEY=<random>
E2B_API_KEY=<your key>
OPENROUTER_API_KEY=<your key>
SANDBOX_PROVIDER=e2b
AGENT_RUNTIME=pi
DATA_DIR=/data

Point an A record at the server before that first up. Caddy requests a certificate for the name in RAKAZO_HOST, and the request fails if the name does not resolve to this box or if port 80 is closed to the outside.

Also set SIGNUP_ALLOWLIST=you@example.com. SIGNUPS_ENABLED=true is the default, so an instance on a public name accepts registrations from whoever finds it, and every new account gets a computer. Allowlist first. Relax it later if you want to.

Treat docs/self-host.md in the repository as the authority for production settings, because it changes with the code and this guide does not. Since Compose is doing the work, the ordinary rules apply, and the Docker Compose basics for a VPS cover why --env-file and named volumes matter more once a stack is something you leave alone for months.

Backups

Postgres and the data/ directory are the whole instance.

./scripts/backup.sh
./scripts/restore.sh backups/BACKUP_TIMESTAMP

backup.sh dumps Postgres and archives data/. For a machine you depend on, install infra/compose/backup-prod.sh as /usr/local/sbin/rakazo-backup with the timer the repository supplies, so rotation happens without you. A backup sitting on the same disk as the database is not a backup, so copy it off the box. Then restore it once, on a spare server, before you need it.

Why it fails, and what you will see

pnpm db:migrate cannot reach the database. The migration reports that it cannot reach the database server at 127.0.0.1:5433. Either the Postgres container is not up, or it is up and not ready yet. Run docker compose --env-file .env -f infra/compose/docker-compose.yml ps and look for the postgres service reporting healthy, because the compose file gives it a health check that runs every three seconds. A container that restarts in a loop usually means the pgdata volume was created with different credentials. docker compose ... down -v clears it, and it deletes the data with it.

The port is already taken. Bringing up Postgres fails with bind: address already in use when something else holds 5433, most often an earlier Rakazo stack you forgot to stop. sudo ss -lntp | grep 5433 names the process.

A bot never gets a computer. With SANDBOX_PROVIDER=docker and no rakazo/computer:local image, there is nothing to start. docker image ls rakazo/computer answers that in one line, and pnpm sandbox:build fixes it. If the supervisor cannot reach the Docker socket it cannot create containers either, and the message names the path: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock.

A long command dies partway. .env.example sets SANDBOX_COMMAND_TIMEOUT_MS=300000, so a single command inside a bot's computer is cut off after five minutes. Raise it for slow builds instead of assuming the sandbox crashed.

pnpm install breaks in confusing ways. Check node -v before anything else. The workspace declares >=22, and an older Node fails in dependency code rather than with a message about versions.

Sign-in works locally but not through the domain. BETTER_AUTH_URL, WEB_ORIGIN and API_URL all have to hold the same public origin as the address bar, scheme included. A stale http://127.0.0.1:5173 left in one of them is the usual cause of a session that never sticks.

Updating a pinned checkout

The upgrade path in the self-host doc is short: pull the new source, run the database migration, restart the API and the worker.

./scripts/backup.sh
git fetch --all
git checkout NEW_COMMIT_SHA
pnpm install
pnpm --filter @rakazo/db migrate
docker compose --env-file .env -f infra/compose/docker-compose.prod.yml up -d --build

Back up first. Migrations move forward, and a beta gives you no reverse path you can count on. Read the commits between your pinned SHA and the new one before taking them, because a project this young renames environment variables without announcing it, and a missing variable shows up as a service that starts and then exits. If you are still deciding whether Rakazo is the right thing to run at all, the roundup of self-hosted AI agents covers what else sits in this category and what each one costs to keep alive.

FAQ

Can I run Rakazo on a 1 GB VPS?

No. Postgres, the API, the worker, the sandbox supervisor and the web app all run at the same time, and with SANDBOX_PROVIDER=docker every awake bot adds a container holding a graphical desktop and a browser. The project's own doc calls 2 vCPU and 4 GB enough for the API, worker and Postgres only when E2B hosts the bot desktops. Treat 4 GB as the floor for the control plane, and go higher when the desktops run on your machine.

Is the desktop sandbox provider safe on a server?

No. desktop runs the bot's commands directly on the API and worker host, as the user running that process, with that user's files and credentials in reach. The repository says not to use it on a public or shared server. Use docker for a container per bot, or e2b when more than one person signs in.

Which version of Rakazo should I install?

As of 16 August 2026 there is one tag, v0.1.0-beta, published 13 August 2026 and marked a prerelease. Check out the commit it points at, 53b119a68d9ef843d23aa3b7e3719b6be7b51fdb, rather than tracking main. A branch moves under you and a tag can be repointed, so neither identifies a tree you can return to. Record the commit, because rolling back is only possible when you know which one worked.

Where do I put my OpenRouter API key?

In .env as OPENROUTER_API_KEY, and never in a compose file you commit. Both compose commands in the repository pass --env-file .env, so the value reaches the containers without being written into tracked YAML. You can also leave it empty and paste the key in the app during onboarding. Set a spending limit on the key at the provider, because a bot in a loop keeps calling the model until something stops it.

Do I need a domain name and TLS?

For anything past a first test, yes. The production compose file runs Caddy and obtains certificates automatically, and RAKAZO_HOST, BETTER_AUTH_URL, WEB_ORIGIN and API_URL must all carry the same public HTTPS origin. For a first look you can skip the domain: run pnpm dev and forward port 5173 over SSH instead of publishing it.