Clowld Space Docs

Self-host quickstart

Start Clowld Space with Docker Compose, log in, and connect your first computer.

Self-hosting Clowld Space has two parts:

PartWhat it runsWhere it lives
Clowld Space serviceWeb, API, and PostgreSQLOne machine with Docker installed
ComputerThe Clowld Space daemon and AI coding toolsThe computer developers actually work on

They can be the same machine or separate ones. Self-hosting replaces only the Clowld Space part.

This guide uses Docker Compose. For Kubernetes deployments, see the Self-hosting guide in the repository.

Before you start

The machine running the Clowld Space service needs:

  • Docker Engine or Docker Desktop, with docker compose working
  • Git, Make, curl, and OpenSSL
  • Ports 3000 and 8080 free on the machine

Confirm Docker and Compose are available first:

docker info
docker compose version

Clowld Space uses Compose v2, invoked as docker compose. The legacy docker-compose v1 is not supported.

The computer also needs at least one AI coding tool installed and logged in, such as Claude Code, Codex, or Cursor. The Clowld Space CLI is installed in step 5.

1. Start Clowld Space

On the machine that will run the service:

git clone --depth 1 https://github.com/sooodasama/clowld-space.git
cd clowld-space
make selfhost

On first run, make selfhost will:

  1. Create .env from .env.example
  2. Generate a random JWT_SECRET, PostgreSQL password, and CLOWLD_VCS_SECRET_KEY (the encryption key for self-hosted Git integrations)
  3. Pull the PostgreSQL, Clowld Space backend, and Clowld Space frontend images
  4. Create persistent volumes and start the three containers
  5. Wait until the backend starts answering health checks

Running make selfhost again reuses the existing .env and volumes; it does not regenerate secrets.

make selfhost pulls published images and does not build the code in your checkout. To test local source, use make selfhost-build.

Published images follow a release tag, while a plain git clone checks out main, which usually runs ahead. Clowld Space release tags use the clowld-vX.Y.Z form. If you build anything from this checkout — the CLI or images via make selfhost-build — choose one exact release tag first. Use that same tag for CLOWLD_SPACE_IMAGE_TAG in Compose, and for both images.backend.tag and images.frontend.tag in Helm, so the source, CLI, and running images stay on one release:

git fetch --tags --depth 1
git tag --list 'clowld-v*' --sort=-v:refname
# Choose the release you want from the list, then check it out:
git checkout clowld-vX.Y.Z

2. Confirm the services are ready

Check container status:

docker compose -f docker-compose.selfhost.yml ps

postgres should show healthy, and backend and frontend should be running. Then check the backend, the database, and migrations:

curl -fsS http://localhost:8080/readyz

The expected response is:

{"status":"ok","checks":{"db":"ok","migrations":"ok"}}

The backend container runs database migrations on every startup before serving; there is no manual migration command to run.

3. Choose how to access

Local access

Open http://localhost:3000 directly. You also won't need to pass any URLs when running clowld setup self-host later.

Remote access

Docker Compose binds 3000 and 8080 to 127.0.0.1 only. Do not switch this to 0.0.0.0 to expose the ports to the public internet; use a reverse proxy with HTTPS instead.

The example below uses two domains:

  • app.example.com: the Clowld Space web app
  • api.example.com: the API, health checks, and daemon connections

First set the public URLs in .env:

FRONTEND_ORIGIN=https://app.example.com
CLOWLD_APP_URL=https://app.example.com
CLOWLD_PUBLIC_URL=https://api.example.com

With this configuration all browser traffic stays on the app domain, so cookies never cross domains and COOKIE_DOMAIN is not needed. If the browser talks to the api domain directly instead, you must set it — see Environment variables.

Then set up DNS and proxy the local ports with Caddy:

app.example.com {
    # CLI setup GETs the exact /health path and needs a 200; route it directly
    # so this also works with older web releases that do not forward /health
    handle /health {
        reverse_proxy 127.0.0.1:8080
    }

    # Hand browser WebSockets straight to the backend
    @ws path /ws /ws/*
    handle @ws {
        reverse_proxy 127.0.0.1:8080 {
            flush_interval -1
        }
    }

    # Everything else goes to the frontend, which forwards API and login requests
    handle {
        reverse_proxy 127.0.0.1:3000
    }
}

api.example.com {
    reverse_proxy 127.0.0.1:8080 {
        flush_interval -1
    }
}

If you would rather serve everything on a single origin (one domain, or one host with one port — common on small servers), route the daemon-critical paths to the backend explicitly and let the frontend handle the rest:

clowld.example.com {
    # CLI reachability probe: `clowld setup` GETs the exact <server-url>/health
    # path and needs a 200. This direct route works across web releases.
    handle /health {
        reverse_proxy 127.0.0.1:8080
    }

    # Browser realtime WebSocket; preserve the Upgrade request at the backend.
    handle /ws {
        reverse_proxy 127.0.0.1:8080 {
            flush_interval -1
        }
    }

    # Daemon long-connection WebSocket: daemons dial
    # {server-url}/api/daemon/ws, not /ws. Without this route the daemon
    # silently falls back to polling after the handshake fails.
    handle /api/daemon/ws {
        reverse_proxy 127.0.0.1:8080 {
            flush_interval -1
        }
    }

    # Everything else goes to the frontend, which forwards API and login requests
    handle {
        reverse_proxy 127.0.0.1:3000
    }
}

For this single-origin variant, set the public URL variables explicitly:

FRONTEND_ORIGIN=https://clowld.example.com
CLOWLD_APP_URL=https://clowld.example.com
CLOWLD_PUBLIC_URL=https://clowld.example.com
# Optional explicit daemon API origin; otherwise it falls back as documented below.
CLOWLD_DAEMON_SERVER_URL=https://clowld.example.com

Then use https://clowld.example.com for both --server-url and --app-url. CLOWLD_PUBLIC_URL is for public webhook and callback URLs; it is not the daemon-only override. If those URLs are the same, keeping it set to the same origin is valid. The explicit CLOWLD_DAEMON_SERVER_URL line is optional, but makes the daemon route contract unambiguous.

Caddy obtains TLS certificates and forwards WebSockets. After editing .env, recreate the containers with up -d so the new configuration takes effect:

docker compose -f docker-compose.selfhost.yml up -d
curl -fsS https://api.example.com/readyz
curl -fsS https://app.example.com/api/config | grep -o '"daemon_server_url":"[^"]*"'

For a single-origin deployment, replace both hosts above with https://clowld.example.com. The last command prints daemon_server_url, the public URL daemons use for API and WebSocket connections: CLOWLD_DAEMON_SERVER_URL when set, then CLOWLD_PUBLIC_URL, otherwise the app URL (CLOWLD_APP_URL, falling back to FRONTEND_ORIGIN). If both CLOWLD_APP_URL and FRONTEND_ORIGIN are empty, the field is omitted entirely even when the daemon or public URL variable is set. If it prints a localhost address, set FRONTEND_ORIGIN and CLOWLD_APP_URL explicitly to the public URL and recreate the containers. Because /api/config is unauthenticated, never put credentials or query-string secrets in these URLs.

docker compose restart only restarts existing containers and does not re-read .env. After changing configuration, run docker compose -f docker-compose.selfhost.yml up -d to re-read .env.

4. Log in and create a workspace

Open http://localhost:3000 locally, or the https://app.example.com you just configured, and enter your email to request a verification code.

No email service is configured by default. After requesting a code, read it from the backend logs:

docker compose -f docker-compose.selfhost.yml logs backend \
  | grep "Verification code"

The logs contain a line like:

[DEV] Verification code for you@example.com: 123456

Enter the code and create your first workspace. Once Resend or SMTP is configured, codes are sent by email and members no longer need to read container logs — see Auth setup.

Self-hosted deployments default to APP_ENV=production, which disables fixed verification codes. Do not set CLOWLD_DEV_VERIFICATION_CODE on a public instance.

5. Connect a computer

Run the commands below on the computer that runs your AI coding tools, which is not necessarily the server running Docker.

Runs execute with the full permissions of the user running the daemon — they can read and write everything that user can. Run the daemon as a dedicated Unix user, in a container, or in a VM rather than under your personal account. See the security model.

Install the Clowld Space CLI first:

macOS / Linux

curl -fsSL https://updates.clowld.xyz/cli/install.sh | bash

Windows PowerShell

irm https://updates.clowld.xyz/cli/install.ps1 | iex

If the Clowld Space service runs on this same computer:

clowld setup self-host

If the Clowld Space service runs on another machine, pass the two URLs you configured earlier:

clowld setup self-host \
  --server-url https://api.example.com \
  --app-url https://app.example.com

The command first checks the exact <server-url>/health path and requires HTTP 200, then opens a browser to complete login. After login it stores local credentials and starts the daemon. A Server not reachable error means the probe did not receive 200 — for example, the proxy sends /health to an older web release that does not forward it, the proxy does not route it to the backend, or DNS, TLS, firewall, timeout, or a backend 5xx is failing. See Troubleshooting.

Confirm the connection:

clowld daemon status

The output should show:

  • Daemon: running
  • Agents listing the AI coding tools installed on this machine
  • Workspaces greater than 0

6. Complete the first run

Back in Clowld Space, once an online runtime appears in the runtime list, create an agent and assign it your first issue.

When the run shows as completed and the agent's reply appears in the timeline, the self-hosted service, the computer, and the AI coding tool are all connected. For detailed steps, see steps 3-5 of the quickstart.

Common admin commands

Run all of these from the clowld repository directory:

# Check status
docker compose -f docker-compose.selfhost.yml ps

# Tail backend logs
docker compose -f docker-compose.selfhost.yml logs -f backend

# Apply .env changes
docker compose -f docker-compose.selfhost.yml up -d

# Stop the services, keeping the volumes
docker compose -f docker-compose.selfhost.yml down

To upgrade to the latest published images:

git pull --ff-only
docker compose -f docker-compose.selfhost.yml pull
docker compose -f docker-compose.selfhost.yml up -d
curl -fsS http://localhost:8080/readyz

Two ways to upgrade a Docker Compose install. On an existing install they produce the same result — the selfhost target in the Makefile runs the same docker compose pull + up -d, and additionally creates .env when it is missing and waits on /health before printing a status summary. Pick whichever you prefer:

cd clowld-space
git pull
make selfhost
cd clowld-space
git pull
docker compose -f docker-compose.selfhost.yml pull
docker compose -f docker-compose.selfhost.yml up -d

What git pull actually does

git pull updates docker-compose.selfhost.yml itself — new environment variables, new services, changed healthchecks. It is not how you get a new Clowld Space version.

The version you end up running is resolved by docker compose pull, which asks GHCR what the tag points at right now. A checkout that is months out of date will still pull today's latest images; conversely, git pull alone changes nothing until you pull images and recreate the containers.

If you pinned CLOWLD_SPACE_IMAGE_TAG, neither command upgrades anything. Both images resolve as ${CLOWLD_SPACE_IMAGE_TAG:-latest} (services.backend.image and services.frontend.image in docker-compose.selfhost.yml), and .env.example ships CLOWLD_SPACE_IMAGE_TAG=latest. If your .env pins an exact release, pull just re-fetches that same tag and you stay on the old version — no error, no warning. Check before you upgrade:

grep CLOWLD_SPACE_IMAGE_TAG .env
# CLOWLD_SPACE_IMAGE_TAG=clowld-v1.0.5   ← pinned: edit to `latest` (or the release you want) first

Your .env is not overwritten

make selfhost only generates .env when the file is missing. Re-running it on an existing install leaves your JWT_SECRET, Postgres password, email settings, and FRONTEND_ORIGIN exactly as they were.

Back up Postgres first

Migrations are forward-only, so take a dump before upgrading a deployment you care about:

docker compose -f docker-compose.selfhost.yml exec -T postgres \
  pg_dump -U clowld clowld > clowld-space-backup.sql && gzip clowld-space-backup.sql

Do not pipe pg_dump straight into gzip. A shell reports the exit status of the last command in a pipeline, so pg_dump … | gzip > backup.sql.gz exits 0 even when the dump failed — leaving a perfectly valid 20-byte archive containing nothing. Redirecting to a file first makes pg_dump's own exit status the one that counts, and && only compresses a dump that actually succeeded.

Use your own POSTGRES_USER / POSTGRES_DB from .env if you changed them from the clowld defaults. Data lives in the named volume clowld_pgdata, which survives docker compose down — but not down -v.

Migrations run themselves

As in Step 1, the backend container runs ./migrate up on startup (docker/entrypoint.sh) before serving traffic. There is no separate upgrade command — bringing up the new image is the migration step. Watch it happen:

docker compose -f docker-compose.selfhost.yml logs -f backend

Migrations run automatically when the backend starts; migrations that backfill historical data (such as 103) complete the backfill automatically too. In rare cases the automatic backfill fails with refusing to drop legacy daily rollups — see Troubleshooting.

Verify with /readyz, not /health

/health is a liveness probe — it returns {"status":"ok"} as long as the process is up, including when migrations failed. /readyz (readyHandler, registered in server/cmd/server/router.go; /healthz is an alias) checks the database and the applied migration set, so it is the one that catches a bad upgrade:

curl -s localhost:8080/readyz
# {"status":"ok","checks":{"db":"ok","migrations":"ok"}}

Anything other than HTTP 200 with both checks ok means the new version did not finish migrating — check the backend logs before sending traffic at it.

Kubernetes

Helm has its own upgrade path: set images.backend.tag / images.frontend.tag to the release you want in your values file, then helm upgrade. Changing the tag changes the pod spec, so Kubernetes pulls the new image and rolls the Deployments — this is the reliable route.

kubectl -n clowld rollout restart is not an upgrade on its own. The chart ships pullPolicy: IfNotPresent (deploy/helm/clowld-space/values.yaml), so a node that already has that tag cached reuses the old image and the restart silently changes nothing — the same class of trap as a pinned CLOWLD_SPACE_IMAGE_TAG. If you want to track a floating tag that way, set images.backend.pullPolicy / images.frontend.pullPolicy to Always first. See the Self-hosting guide.

docker compose down keeps pgdata and backend_uploads. Adding -v deletes these volumes, including the database; do not run docker compose down -v unless you intend to wipe the instance.

Common issues

SymptomCheck first
/readyz does not return okRun docker compose -f docker-compose.selfhost.yml logs backend postgres.
No verification code arrivesRequest a code, then search the backend logs for Verification code.
setup self-host reports the server is unreachableFrom the computer, request https://api.example.com/health to confirm DNS, TLS, and the reverse proxy are all reachable. A 404 usually means /health was sent to an older web release that does not forward it; route the exact path to the backend (Troubleshooting).
The daemon lists no AgentsConfirm the AI coding tools are on PATH and logged in, then run clowld daemon restart.
Issues stay queuedRun clowld daemon status to confirm the daemon is running and connected to the workspace.

For more scenarios, see Troubleshooting.

Next steps