Deploying Grovedock
A single pandod server with SQLite is enough for a team on one machine. For more, run
stateless servers on Postgres or CockroachDB with objects in S3, GCS or Azure, and put a
regional cache and a node daemon between CI machines and the servers.
The whole topology with Docker Compose
The repository’s docker-compose.yml
starts Postgres, an S3-compatible store, a server, a regional cache and a node daemon. They
listen on the network, so they need a token: put one in .env next to the compose file first
(docker compose refuses to start without it):
echo "PANDO_TOKEN=$(openssl rand -hex 24)" > .env
docker compose up -d
export $(cat .env)
pando clone http://localhost:7420 work --objects http://localhost:7422
--objects sends file reads through the node daemon; commits and lands still go to the
server.
For free, on one machine
deploy/free-tier sets up a
production server on Oracle Cloud’s Always Free ARM machine: HTTPS by Caddy, a hardened systemd
service, and hourly incremental backups to a free Neon database and R2 bucket. It costs nothing
until usage outgrows the free tiers, and moves to Postgres and object storage with pandod copy
when it does.
Hosting many repositories
One pandod can host any number of repositories, each with its own storage, for a whole
company: organisations, members, teams, per-repository roles and access tokens.
# 1. The first administrator's token, written straight into the registry (shown once).
pandod token --platform sqlite:///var/lib/pando/platform.db --account kabir > ~/.pando-admin-token
# 2. The server. With a platform token in the registry, every call needs a token.
pandod server --listen 0.0.0.0:7420 \
--platform sqlite:///var/lib/pando/platform.db \
--repos-metadata 'sqlite:///var/lib/pando/repos/{repo}.db' \
--repos-objects 'file:///var/lib/pando/objects/{repo_id}' \
--platform-admins kabir
# 3. An organisation, a repository, people and agents.
export PANDO_TOKEN=$(cat ~/.pando-admin-token) PANDO_SERVER=https://pando.example.com
pando org create lumen
pando repo create lumen/fleet
pando org member lumen mira@lumen.dev write
pando team set lumen/platform --member mira@lumen.dev
pando repo grant lumen/fleet lumen/platform admin
pando token create perf-fleet --as agent/perf-05 --repo lumen/fleet --role write
--platformis the registry: which repositories exist, and who may use them.--repos-metadataand--repos-objectsare templates:{repo}becomesOWNER/NAMEand{repo_id}becomesOWNER--NAME. Each repository gets its own database and its own landing queues, so one busy repository never slows another, and a repository can be backed up, moved or deleted on its own.- Clients name a repository by adding
/OWNER/NAMEto the server address; the web console lists them. A repository call that names none is refused: the server’s own--metadatarepository is not reachable in this mode. To keep a single-repository server’s history, create the repository and copy it in withpandod copy. --multi-tenant(PANDO_MULTI_TENANT=1) is for a server that strangers share; see Multi-tenant servers.--max-orgs-per-account(default 3) caps how many organisations a person may create there.- Webhooks never call loopback, private networks or cloud metadata addresses.
--webhook-allow-privatelets them call loopback and private networks (self-hosted CI on an internal network); it can’t be combined with--multi-tenant.
The web console
Each release ships the console as pando-console-vVERSION.tar.gz. Unpack it and pass
--console-dir to pandod server to serve it from the server’s own address, or serve it from
your proxy; see Host it yourself.
Signing in from a browser
On a platform server, --login-listen ADDR serves sign-in pages (Google or any OpenID Connect
provider, and email links) that hand the console an ordinary platform token. Run it on a
loopback address and route /login/* to it from the reverse proxy that serves the console, on
the same host name as --public-url:
pando.example.com {
handle /login/* {
reverse_proxy 127.0.0.1:7430
}
# … the console and gRPC-Web routes
}
| Setting | Flag / environment |
|---|---|
| Public address | --public-url / PANDO_PUBLIC_URL (https; http only for localhost) |
| Provider | --oidc google=https://accounts.google.com (repeatable); PANDO_OIDC_GOOGLE_CLIENT_ID, PANDO_OIDC_GOOGLE_CLIENT_SECRET or …_FILE |
--smtp-url / PANDO_SMTP_URL, --mail-from / PANDO_MAIL_FROM; PANDO_SMTP_USERNAME, PANDO_SMTP_PASSWORD or …_FILE | |
| Bot challenge | --turnstile-site-key; PANDO_TURNSTILE_SECRET or …_FILE (unset: none) |
| Who may sign up | --signup waitlist|open|closed / PANDO_SIGNUP (default waitlist, managed with pando admin signup) |
The proxy must set X-Forwarded-For, which the per-address request limits use. With sign-in
on, the server always requires a token, even before any exists. Setup, the waitlist and
linking the first administrator are described in the platform notes.
Pre-land checks
With --land-hook, the server sends each proposed land to your URL before it moves the
branch: who is landing, the branch, the changeset and its paths. Any 2xx answer lets it
land; anything else, including a timeout, refuses it. Use it to require green CI or an
approved review. The payload is described in Architecture.
Running a hosted service
Taking a repository down
pandod repo --platform sqlite:///var/lib/pando/platform.db disable acme/leaky --reason "takedown request 2026-118"
pandod repo --platform sqlite:///var/lib/pando/platform.db enable acme/leaky
disable refuses every read and write of the repository, for everyone including
administrators, with “disabled by the operator: REASON”, and hides it from listings. Nothing is
deleted. It works while the server runs, which notices within 5 seconds. Both are recorded in
the audit log (disable_repository, enable_repository) when the command is given
--audit-log (pandod --audit-log FILE repo …).
Deleting a repository
pandod repo --platform sqlite:///var/lib/pando/platform.db delete acme/old \
--repos-metadata 'sqlite:///var/lib/pando/repos/{repo}.db' \
--repos-objects 'file:///var/lib/pando/objects/{repo_id}' --confirm acme/old
This deletes the registry rows (change requests, reviews, issues, webhooks and their
deliveries, grants, protections) and the repository’s SQLite database and file:// object
directory. Objects in a cloud bucket and Postgres metadata are named in the output for you to
remove. It cannot be undone: take a pandod backup first. Platform tokens limited to the
repository keep its name in their list; revoke them if the name will be reused.
Purging a leaked secret
History is content-addressed, so a committed secret can’t be edited out in place. Rotate the secret first (that is what actually protects you), then:
- Disable the repository (
pandod repo disable) so nobody fetches it meanwhile. - Rewrite the history without the secret:
pando export-gitto a git repository, remove the file or string withgit filter-repo, andpando import-gitinto a new repository. - Delete the old repository (
pandod repo delete; on Windows, stop the server first, because Windows won’t delete files a running server holds open); recreate it under the same name if you want to, and import the rewritten history there. - Ask everyone with a checkout to clone again; old checkouts still hold the secret.
Deleting an object while other changesets still reference it is not supported: there is no object garbage collection yet, so objects a deleted branch alone referenced stay in storage until the repository is deleted. Plan capacity accordingly.
Next steps
- Organisations, people and access: roles, teams, tokens, protected branches.
- Security and audit: single sign-on, access rules and the audit log.
- Backup and restore:
pandod copyandpandod backup, and how to restore a platform server. - Scaling and operations: caches, node daemons, compaction and monitoring.
- Configuration: every common setting in one table.