Docs

How Girder works, how to install it on your own server, and how to run it day to day, by hand or through an agent.

Overview

Girder is a self-hosted PaaS: one static Go binary that turns a Linux server into a platform you deploy to with git push. Everything runs on your hardware. There is no hosted control plane and no per-app metering.

What happens when you push

  • You push to Girder's built-in git server over SSH. Girder is always the deploy remote (build + ship). Hosted forges (GitHub, GitLab, Forgejo) are optional for code hosting and review — see GitHub, GitLab, Forgejo.
  • A pre-receive hook builds your app from its Dockerfile (or Nixpacks) and streams the build log back to your terminal. A failed build rejects the push, so broken code never reaches production.
  • The new container rolls out behind Traefik with a health check and zero downtime. If the health check fails, Girder rolls back to the previous image on its own.
  • Postgres, Redis, S3, domains, TLS, cron jobs, and backups are platform primitives, one command each.

Multi-node works the same way: provision more servers from the CLI or dashboard, assign apps, or set a multi-node replica set. Same code path as single-node.

Requirements

  • A Linux server with a public IPv4. Any provider works; a Hetzner cx23 or bigger is the reference setup.
  • A domain whose DNS is managed by Cloudflare (free plan is fine), plus a Cloudflare API token with Zone:DNS:Edit on that domain. This is how Girder does automatic DNS and Let's Encrypt certificates.
  • A Tailscale account (free) and an auth key from the admin console. Tailscale is not optional: it is how you reach git, the dashboard, and every admin surface. Only ports 80 and 443 are ever public.
  • Optional: a Hetzner API token. It enables the cloud firewall (80 and 443 only) and lets you provision extra nodes straight from the CLI.

Install

Download the release linked in your purchase email and copy it to your server (or build from source). Then run the installer as root:

# on the server, as root
$ ./girder init \
    --domain apps.example.com \
    --email you.example.com \
    --cf-token <cloudflare-token> \
    --cf-zone <zone-id> \
    --tailscale-key <tskey-...> \
    --hcloud-token <optional>

Missing flags are prompted for. The installer:

  • installs Docker and Tailscale and joins your tailnet,
  • locks the firewall to 80 and 443 only (ufw, plus the Hetzner cloud firewall with a token),
  • installs and starts the Girder daemon as a systemd service,
  • writes the config and points the wildcard *.apps.example.com at the server.

Later updates are one command on the server: girder update downloads the latest release, verifies it against the published checksums, swaps the binary, and restarts.

License

Release builds check for a license at daemon start. Install the file attached to your purchase email:

$ girder license install /root/license.json
$ girder license status        # customer, edition, update window

Your license is perpetual. When the included update year lapses, the software keeps running exactly as it is; only new updates stop until you renew.

Licenses carry an edition: standard (default; omitted on older files) or platform (multi-user features when they ship). The same binary runs both. Agencies hosting clients on Standard today: see the agency playbook.

Everyday use

Your first app

$ girder apps create myapp
$ cd myapp
$ git remote add girder ssh://git@your-server:2222/myapp.git
$ git push girder main
# → https://myapp.apps.example.com (real certificate, zero downtime)

The machine that runs git push must be on your Tailscale network (laptop, self-hosted runner, or a CI job that joins the tailnet). Port 2222 is not public on purpose.

The daily commands

  • Services: girder services add myapp postgres (or redis, s3) provisions the service and injects credentials as env vars on the next deploy. Multi-process apps share one database with --from.
  • Domains: girder domains add myapp app.example.com maps a custom host with automatic DNS and TLS. A download kind serves the app's bucket publicly.
  • Env vars: set per app, encrypted at rest. You can list names, never values. Changing env or service URLs requires a redeploy (push again, or rollback to the current sha) so containers pick up the new values. A plain restart does not re-inject env.
  • App config: port, health path, memoryMB / pidsLimit / cpus (docker resource caps; empty clears), deploy branch, buildArgs, deployHook (runs after health, before traffic flip), previews / previewsDB, rateLimit, multi-node nodes.
  • Staging and previews: durable staging env, seed/scrub, idle sleep, promote, and PR preview hosts. See Staging and previews.
  • Rollback: girder apps rollback myapp <sha> returns to a previous image (and refreshes env from the secret store).
  • Cron: girder jobs add myapp nightly --schedule "*/15 * * * *" --cmd "..." runs inside the app's container.
  • Backups: nightly by default, girder backup now on demand. Prove restores with girder backup verify; a restore always lands in a scratch database first, never over live data. The dashboard shows the last backup window (how long dumps and restic took per database) so you can see capacity pressure early.
  • More nodes: girder nodes create node-2 provisions a server, girder apps assign myapp node-2 moves the app, DNS included. Replica sets: apps config myapp nodes node-1,node-2.
  • Build node: girder config set buildNode node-2 runs Docker image builds on that machine instead of the control node (empty value = control). Builds push to the internal registry; control still orchestrates deploys and scans.
  • Deploy queue: one deploy runs at a time. Pushing the same app branch again drops older queued jobs for that branch so one app cannot fill the queue. The dashboard overview shows the queue; apps may show queued (2/3).
  • Dashboard: http://your-server:8080 on the tailnet. Get the token with girder api-token. Config covers secrets, PgBouncer, registry move, build node, Trivy enforce, and more.
  • Sanity check: girder doctor verifies the whole installation.
  • Support bundle: girder support-bundle (or dashboard Config) writes a redacted diagnostics archive for support. No secrets inside. Send it with a short description of the problem.

Postgres pooler (PgBouncer)

Optional. Enable from the dashboard Config page or girder config set pgbouncer true. Girder smoke-tests auth, then rewrites each app's DATABASE_URL to port 6432. Redeploy every app that uses Postgres so containers see the new URL. The same applies when you disable it (back to :5432). Transaction pooling is the default; apps that keep named prepared statements across transactions (some job runners) should stay on direct Postgres or use session pooling. Watch connection pressure on the Backup page (or GET /api/postgres/pressure): warn at about 70% of max_connections, critical at 90%.

Staging and previews

Girder has two kinds of non-production deploys. They look similar in the URL shape but serve different jobs.

  • Staging is one durable environment per app: stable host, configurable branch (default staging), optional own node, isolated database by default, promote to production.
  • Previews are ephemeral: every non-production branch (when previews is on) gets a short-lived host that goes away when the branch is deleted.

Where to find it in the dashboard

  1. Open the dashboard on your tailnet (default port 8080; token from girder api-token).
  2. Go to Apps and open an app.
  3. Scroll to the staging card for durable staging (enable, seed, sleep, promote, and related settings).
  4. In the same app detail, under general config, set previews and optional previewsDB for PR-style hosts.
  5. Custom hosts: app domains can target production or staging when you map a domain.

Staging: how it works

Enable staging from the staging card (or CLI). Girder does not deploy until you push the staging branch.

$ girder apps staging enable myapp
$ git push girder staging
# → https://staging--myapp.<appsDomain>

$ girder apps staging promote myapp     # production redeploys from staging image
$ girder apps staging seed myapp        # copy prod DB → staging DB (optional scrub)
$ girder apps staging sleep myapp       # stop container; wake starts it again
$ girder apps staging wake myapp
  • Host: staging--<app>.<appsDomain> (one DNS label under your apps wildcard). If the app has custom production domains, staging also attaches staging.<domain> (e.g. cometx.netstaging.cometx.net; www. is collapsed). Opt out with stagingAutoDomains false. Explicit staging domains (dashboard env = staging) are still supported.
  • Branch: default staging; change with stagingBranch on the card or girder apps config myapp stagingBranch develop.
  • Node: optional stagingNode / stagingNodes. Empty means the same placement as production.
  • Env: staging containers get GIRDER_ENV=staging and GIRDER_STAGING=1.
  • Database: isolated Postgres by default (role/db <app>_stg). Production data is untouched. Opt in to share prod with stagingSharePostgres (dangerous; not recommended).
  • Redis: if the app has Redis, staging gets its own container (platform-redis-<app>-stg) so queues and caches never touch production. Workers that borrow Redis from another app reuse that app’s staging Redis.
  • S3: if the app has object storage, staging gets bucket <app>-stg with its own keys. Production bucket is never written by staging deploys. Borrowers reuse the owner’s staging bucket.
  • Staging env: optional key/value overrides applied only to staging deploys (stagingEnv, e.g. FEATURE_X=1,LOG_LEVEL=debug).
  • Seed on enable: optional stagingSeedOnEnable runs seed-from-prod when you turn staging on (after creating the staging DB).
  • Stacks / workers: if a worker borrows Postgres with services add worker postgres --from web, staging for the worker uses the web staging database so the stack shares one staging dataset. Dashboard stacks can enable/disable staging for mates and promote entire stack.
  • Seed from production: overwrites the staging DB with a dump of live production, then runs optional scrub SQL (stagingScrubSQL), e.g. redacting emails. If the app has S3, also mirrors the production bucket into <app>-stg (staging objects are replaced; production is read-only). Does not write to production.
  • Idle sleep: set stagingIdleSleepHours (e.g. 24). After that many hours without activity (deploy, seed, or wake), Girder stops the staging container. Wake starts it again; push to the staging branch also redeploys. 0 or empty turns auto-sleep off.
  • Promote: redeploys production from the image currently running on staging. It does not copy the staging database to production. Use promote when the build is good; use seed when you want realistic data in staging.

CLI flags: girder apps staging enable --stack myapp also enables other apps in the same stack. girder apps staging promote-stack myapp promotes every staging-enabled stack mate (partial success is reported if one app fails).

Previews: how they work

Turn on previews for the app. Pushes to any branch that is not the deploy branch and not the staging branch build a side instance:

  • Host: <branch-slug>--<app>.<appsDomain>.
  • Lifetime: deleting the git branch removes the preview container.
  • Database: by default previews share the app’s normal env (often production credentials). Set previewsDB to true for an isolated Postgres per branch, dropped when the branch is deleted. Only apps that own Postgres get a preview DB (borrowers keep using shared URLs).
  • Env: GIRDER_PREVIEW=1 and GIRDER_PREVIEW_BRANCH=…; with isolated DB also GIRDER_PREVIEW_DB=1.

Staging and previews can both be enabled. Push order of preference: staging branch → production deploy branch → other branches as previews (when on). Staging and preview databases are not included in nightly backup coverage (they are disposable).

Quick comparison

  • Staging: long-lived, one per app, promote image to prod, seed/scrub, optional sleep.
  • Preview: short-lived per branch, no promote path, optional ephemeral DB, gone with the branch.
  • Neither replaces the other. Use staging for a shared QA lane; use previews for PR noise.

Scale and ops

Girder is built for one operator (or a small team) with many apps, not a multi-tenant public cloud. These knobs keep the control plane light as you grow.

  • buildNode: girder config set buildNode <node> (or Config in the dashboard). Docker builds run on that node; images go to the internal registry. Empty string returns builds to the control node. Prefer a machine with spare CPU, not a reserved backup node.
  • registryNode / dataNode / defaultNode: move the image registry, shared Postgres/Redis/S3, and default app placement off the control node so control stays the brain (git, API, queue).
  • Deploy queue: up to two deploys at once (different apps; one job per app). A newer push for the same app and branch drops older queued jobs. config set pipelineWorkers N (1–4, restart daemon). Overview shows the queue.
  • Backup window: after each backup, the dashboard shows how long dumps and restic took, including per-database dump size and time. Use that to see when the nightly window is growing.
  • Updates: when a newer release is published, the overview can show Update now (downloads the binary, verifies checksums, restarts the control plane). You can still run girder update on the node over SSH.

Support bundle

When something is wrong and you need help (or you are helping a customer), collect a redacted diagnostics archive. It is safe to email: no API tokens, no app env values, no secrets.key, no age-encrypted env files, no git objects.

$ girder support-bundle -data /opt/girder/data
# → girder-support-YYYYMMDD-HHMMSS.tar.gz
$ # or: dashboard → Config → download support bundle
  • Includes: doctor output, version, public config, nodes, app config JSON, domains, backup last-run log and metrics, audit log tail, docker ps, disk use, postgres connection pressure, last probe timing, recent platform journal when available.
  • Send with: what you expected, what happened, and roughly when it started.
  • CLI equivalent of the dashboard download: same contents as GET /api/support/bundle (auth required on the API).

GitHub, GitLab, and Forgejo

Girder does not replace your forge. It is the place that builds and runs the code. You can use GitHub, GitLab, Forgejo, or none of them. What never changes: a deploy is a git push to Girder's SSH remote on the tailnet.

Do you need Actions or runners?

No, not for deploying. The simplest setup is two git remotes and a push from your laptop (on Tailscale). CI is only useful when you want automated gates (tests, lint) or deploy-on-merge without a human at a keyboard.

  • No CI: dual remotes. Push to the forge for backup/PR review; push to Girder to go live.
  • With CI: a job that can reach the tailnet runs tests, then git push to Girder. That needs a self-hosted runner on the tailnet, or a hosted runner that joins Tailscale for the job.

Option A — Dual remotes (no runners)

Best default for solo and small teams. Hosted forge for issues and PRs; Girder for production.

$ girder apps create myapp
$ cd my-repo
$ git remote add origin  git@github.com:you/myapp.git      # or gitlab / forgejo
$ git remote add girder  ssh://git@<tailscale-ip>:2222/myapp.git
$ git push -u origin main
$ git push girder main
# only the second push deploys

Day to day:

$ git push origin main    # save / open PR on the forge
$ git push girder main    # deploy
# or both:
$ git push origin main && git push girder main

Authorize your laptop's SSH public key on Girder (dashboard → git keys). The forge never needs that key.

Option B — Deploy from GitHub Actions / GitLab CI / Forgejo Actions

Use this when you want “merge to main → tests → deploy” without pushing from a laptop. The runner must reach Girder on the tailnet.

Preferred: a self-hosted runner (or Forgejo/GitLab runner) installed on a machine already on your Tailscale network — a small always-on VPS, a home lab box, or the control node itself if you accept that trade-off. No public git port required.

Alternative: a hosted runner plus the official Tailscale action (GitHub) or equivalent, so the job joins the tailnet, pushes, then leaves. You still store a Girder deploy key as a secret.

Minimal GitHub Actions sketch (same idea on GitLab .gitlab-ci.yml or Forgejo Actions):

# .github/workflows/deploy.yml
name: deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: self-hosted   # on your tailnet
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: tests
        run: npm test      # or go test, etc.
      - name: push to Girder
        env:
          GIRDER_KEY: ${{ secrets.GIRDER_DEPLOY_KEY }}
          GIRDER_HOST: ${{ vars.GIRDER_TAILSCALE_IP }}
        run: |
          install -m 600 /dev/null ~/.ssh/id_ed25519
          printf '%s\n' "$GIRDER_KEY" > ~/.ssh/id_ed25519
          ssh-keyscan -p 2222 "$GIRDER_HOST" >> ~/.ssh/known_hosts
          git remote add girder "ssh://git@${GIRDER_HOST}:2222/myapp.git" || true
          git push girder HEAD:main

Create a deploy-only SSH key pair, add the public key on Girder (scoped to that app if you use ACLs), and store the private key as GIRDER_DEPLOY_KEY. Do not reuse your personal laptop key in CI.

Hosted runners without Tailscale cannot deploy. Girder's git port is intentionally not on the public internet. Either put the runner on the tailnet or join Tailscale for the duration of the job.

Option C — Mirror / sync (advanced)

Some teams push only to the forge and mirror into Girder with a forge “push mirror”, a webhook, or a scheduled sync. That still ends as a push (or force-push) into Girder's repo. Prefer Options A or B unless you already operate mirrors: debugging a failed build is clearer when the deploy push is explicit.

What CI should not do

  • Build Docker images in Actions and docker push into Girder's registry as the main path — Girder already builds on push and streams the log. CI is for tests and the git push, not a second build system (unless you know you need it).
  • Open port 2222 to the world so GitHub-hosted runners can connect without Tailscale.
  • Expect git push origin alone to deploy. Origin is the forge; Girder only deploys when it receives the push.

Security model

Only TCP 80 and 443 face the internet. Port 443 serves your apps; port 80 stays open to redirect to HTTPS and to keep plain-HTTP fetches like robots.txt and external health checks working. Everything else lives on your tailnet:

  • Git over SSH (port 2222): public-key only, with per-key app ACLs. Reachable on the tailnet only.
  • Dashboard and API (port 8080): tailnet-only and token-authenticated.
  • Postgres, Redis, S3: reachable only from app containers and the tailnet. Optional PgBouncer on 6432 for pooling.

Your laptop joins the same tailnet, which is how you push code and open the dashboard without exposing a single admin port. Install Tailscale on any machine that should deploy or administer the platform (including CI runners that push to Girder).

On top of that: per-app Docker networks, an encrypted secret store, an optional per-app rate limit, a read-only Docker API for the ingress layer, security headers (HSTS, nosniff, referrer policy) on every app by default, and a hash-chained audit log of every mutating call.

MCP and agents

Girder ships an MCP server, girder-mcp, that exposes the entire control plane as tools. An agent connected to it can create apps, set env vars, provision databases, map domains, read logs, roll back deploys, run backups, and manage nodes.

Connect your agent

Add the server to your MCP client (Claude Desktop, Cursor, or any other):

{
  "mcpServers": {
    "girder": {
      "command": "girder-mcp",
      "env": {
        "PLATFORM_API_URL": "http://<control-tailscale-ip>:8080",
        "PLATFORM_API_TOKEN": "<output of: girder api-token>"
      }
    }
  }
}

The URL is the control node's tailnet address, so the machine running the agent must be on the tailnet. The token never leaves your machines.

What the agent can do

  • Apps and deploys: list, create, logs, rollback, restart, env vars
  • Services: provision and share postgres, redis, s3
  • Domains and DNS: map hosts, toggle proxying, upsert raw records
  • Backups: run now, verify, restore to scratch, promote
  • Nodes: create, scale, patch, power, reclaim disk
  • Observability: health probes, uptime, the audit log

Agent skills

Two ready-made agent skills ship with Girder. One teaches an agent to install a new instance from a bare server, the other teaches it to deploy and operate apps. Install them into your agent and the whole lifecycle, from bare metal to rollback, becomes something you can delegate.

Try it: "Deploy this repo as myapp with a postgres database" or "the last build failed, tell me why" are both one prompt once the MCP server is connected.