Skip to content

Getting started

Self-host Cloudzilla

Cloudzilla is a single static binary backed by PostgreSQL. There is no git executable to install, no message queue, and no separate search service — this guide takes you from a fresh host to a running forge.

Introduction

Everything Cloudzilla needs at runtime is the binary itself and a PostgreSQL database. Git transport (smart HTTP and SSH) is implemented in pure Go via go-git, full-text search runs on Postgres tsvector, and the UI is server-rendered HTML over the wire. That means a deployment is two moving parts you already know how to operate.

In a hurry? Skip to Installation — Docker, a CZ_DATABASE_DSN, and one migrate command get you a forge on port 8080.

What you'll need

  • A Linux host (amd64 or arm64), or Docker on any platform.
  • PostgreSQL 14+ — managed or self-run; it's the only runtime dependency. The bundled Compose stack runs PostgreSQL 18.
  • Two open ports: one for HTTP (default 8080) and one for SSH Git access (default 2222).

Installation

Pick whichever fits your stack. Each ships the same server binary plus a small bundled admin CLI (cloudzilla-cli) that runs database migrations — Docker is the fastest path to a running instance.

Docker

Pull the alpha image and point it at a database. The schema isn't applied automatically — run cloudzilla-cli migrate once (it's bundled in the image), and the database only needs to exist and be reachable.

docker
$ docker run -d --name cloudzilla \
    -p 8080:8080 -p 2222:2222 \
    -v cloudzilla_data:/data \
    -e CZ_DATABASE_DSN=postgres://cz:secret@db:5432/cloudzilla?sslmode=disable \
    -e CZ_SERVER_BASE_URL=https://git.example.com \
    -e CZ_AUTH_JWT_SECRET=replace-with-a-long-random-string \
    ghcr.io/mkappworks-dev/cloudzilla-app:v0.6.0

# run migrations once — the CLI is bundled in the same image
$ docker exec -it cloudzilla /app/cloudzilla-cli migrate
# then open http://localhost:8080 — first visit redirects to /setup

Docker Compose

If you want Postgres alongside it, this is the whole stack. After it's up, apply migrations once with docker compose exec cloudzilla /app/cloudzilla-cli migrate:

docker-compose.yml
services:
  cloudzilla:
    image: ghcr.io/mkappworks-dev/cloudzilla-app:v0.6.0
    ports: ["8080:8080", "2222:2222"]
    volumes: ["cloudzilla_data:/data"]
    environment:
      CZ_DATABASE_DSN: postgres://cz:secret@db:5432/cloudzilla?sslmode=disable
      CZ_SERVER_BASE_URL: https://git.example.com
      CZ_AUTH_JWT_SECRET: replace-with-a-long-random-string
      CZ_AUTH_COOKIE_SECURE: "true"
      CZ_SECURITY_SECRET_KEY: replace-with-at-least-32-random-bytes
    depends_on: [db]
  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_USER: cz
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: cloudzilla
    volumes: ["czdata:/var/lib/postgresql"]
volumes:
  czdata:
  cloudzilla_data:

From source

No release artifact yet — build it with make build, which embeds templates and compiled CSS into two static binaries (the server and the admin CLI). No Node.js or runtime to install.

build & run
$ git clone https://github.com/mkappworks-dev/cloudzilla-app.git
$ cd cloudzilla-app
$ make build   # produces dist/cloudzilla (server) and dist/cloudzilla-cli (admin)

$ export CZ_DATABASE_DSN=postgres://localhost/cloudzilla?sslmode=disable
$ export CZ_AUTH_JWT_SECRET=replace-with-a-long-random-string
$ ./dist/cloudzilla-cli migrate   # apply migrations once
$ ./dist/cloudzilla               # start the server on :8080

First run

Once you've run cloudzilla-cli migrate against an empty database, the server waits for you to create the first account.

  1. Run migrations. Apply the schema once with cloudzilla-cli migrate (see Installation). It's safe to re-run — applied migrations are skipped.
  2. Open the instance. Visit http://localhost:8080. The first request redirects to /setup.
  3. Create the superadmin. The first account created at /setup becomes the instance owner — full admin rights, including user and org management.
  4. Add an SSH key. Under Settings → SSH keys, paste your public key so you can push over SSH. See Git access.
  5. Create a repository and push your first commit. That's a working forge.

To invite more users when open registration is off, the superadmin manages invites and the allow_registration setting from Admin → Settings.

Configuration

Cloudzilla reads a config.yaml file, environment variables, or both — env vars (all prefixed CZ_) override the file, so containerized deployments can skip the file entirely. Dots in a config key map to underscores: auth.jwt_secret → CZ_AUTH_JWT_SECRET. The essentials:

VariableDescriptionDefault
CZ_DATABASE_DSNPostgreSQL connection string. required—
CZ_SERVER_BASE_URLPublic URL of the instance. Used in clone URLs, webhooks, and OAuth callbacks.http://localhost:8080
CZ_SERVER_PORTPort the web server binds to.8080
CZ_GIT_SSH_PORTPort the built-in Git SSH server binds to.2222
CZ_GIT_REPOS_ROOTFilesystem path for bare Git repository storage../git-repos
CZ_AUTH_JWT_SECRETSigning secret for JWT session cookies. set in proddev default
CZ_AUTH_COOKIE_SECURESet the Secure flag on cookies — enable when serving over HTTPS.false
CZ_SECURITY_SECRET_KEYEncrypts credentials the server must read back later, such as pull-mirror tokens. At least 32 bytes. set in prod—
CZ_SERVER_TRUSTED_PROXIESReverse-proxy IPs / CIDRs whose X-Forwarded-For is believed. Needed for per-IP rate limits behind a proxy.—
CZ_STORAGE_BACKENDWhere uploads such as avatars live: local or s3 (set the CZ_STORAGE_S3_* variables for any S3-compatible bucket).local
CZ_RATE_LIMIT_ENABLEDRate-limit pages, API, Git-over-HTTP, archives, and search per user and per IP. Budgets are tunable via CZ_RATE_LIMIT_*.true
CZ_METRICS_LISTEN_ADDRAddress of a separate listener serving Prometheus /metrics. Off when empty.—

Always set CZ_AUTH_JWT_SECRET and CZ_SECURITY_SECRET_KEY to long, fixed random strings in production. The built-in JWT default is a known dev placeholder — leaving it in place lets anyone forge session tokens.

Email & OAuth (optional)

Both are off until configured. Set CZ_SMTP_HOST (plus CZ_SMTP_PORT, CZ_SMTP_USERNAME, CZ_SMTP_PASSWORD, CZ_SMTP_FROM, CZ_SMTP_TLS) to enable email notifications, email-verified sign-up, and password reset by email. For Google sign-in, set CZ_OAUTH_GOOGLE_CLIENT_ID, CZ_OAUTH_GOOGLE_CLIENT_SECRET, and CZ_OAUTH_GOOGLE_REDIRECT_URL (must match the URI registered in the Google console).

Git access

Cloudzilla speaks both smart HTTP and SSH out of the box, both implemented in pure Go. There is no system git binary involved on the server side.

Over SSH

Add your public key under Settings → SSH keys, then clone using the instance's SSH port (default 2222):

clone over ssh
$ git clone ssh://git@git.example.com:2222/acme/forge.git
# 2222 is the default SSH git port — see CZ_GIT_SSH_PORT

The server's SSH host key is generated on first boot (CZ_GIT_SSH_HOST_KEY, persisted in the data volume). Keep it stable across restarts — replacing it makes clients report REMOTE HOST IDENTIFICATION HAS CHANGED.

Over HTTPS

Use a personal access token as the password — generate one under Settings → Tokens. Tokens can be scoped read-only or read/write per repository.

clone over https
$ git clone https://git.example.com/acme/forge.git
# username: your handle · password: a personal access token

Reverse proxy & TLS

For production, terminate TLS at a reverse proxy and forward HTTP traffic to Cloudzilla. SSH stays a direct TCP passthrough on its own port. A minimal Caddy config:

Caddyfile
git.example.com {
    reverse_proxy localhost:8080
}

Set CZ_SERVER_BASE_URL to the public HTTPS URL so generated clone links, webhooks, and OAuth callbacks point at the proxy rather than the internal port. Behind TLS, also set CZ_AUTH_COOKIE_SECURE=true so session cookies carry the Secure flag.

Health & metrics

Two unauthenticated probe endpoints answer on the main HTTP port, ahead of all middleware — no log line, no cookie, no rate limit.

  • /healthz — liveness: 200 ok whenever the process serves HTTP. The Docker image's HEALTHCHECK uses it.
  • /readyz — readiness: 200 when the database, applied migrations, and repository storage all check out, otherwise 503. Use it for load-balancer and Kubernetes readiness probes.

For Prometheus, set CZ_METRICS_LISTEN_ADDR (for example 127.0.0.1:9090) to start a second listener that serves only GET /metrics.

The metrics port has no authentication. Bind it to loopback or a private network only Prometheus can reach — don't publish it and don't proxy it.

Backups

Because all state — repositories, issues, users, settings — lives in PostgreSQL and on the repository volume, a backup is two things: a database dump and the data directory.

  • Database: a regular pg_dump of the Cloudzilla database captures all metadata and search indexes.
  • Repositories: snapshot the configured CZ_GIT_REPOS_ROOT (under Docker, the /data volume that also holds the SSH host key and, with the default local storage backend, uploaded avatars).
nightly dump
$ pg_dump cloudzilla | gzip > cloudzilla-$(date +%F).sql.gz

Upgrading

Upgrades are a binary (or image tag) swap, followed by a migration run. The server doesn't migrate on its own, so run the bundled CLI after pulling the new version.

  1. Back up the database first — see Backups.
  2. Pull the new tag (e.g. bump v0.5.0 → v0.6.0).
  3. Run migrations with the new cloudzilla-cli migrate, then restart the container or service.
  4. Watch the logs — the instance reports it's serving once it's up.

Postgres 17 → 18 (since v0.5.0). The Compose stack now runs postgres:18-alpine, which can't open a volume created by 17. pg_dump -Fc first, remove only the Postgres volume (not down -v), start the new stack, and pg_restore.

healthz and readyz are now reserved owner names, and migration 082 refuses to run while two accounts have emails that differ only by case. Fix any such accounts, then run cloudzilla-cli migrate again.

Cloudzilla is in alpha — read the changelog before upgrading. Breaking changes are called out there per release.