Skip to content
Galley beta

Galley — self-hosted preview environments v1 beta

A URL for every pull request.
Your stack. Your servers.

Open a PR, get a real environment — your services, your Postgres, your Redis. Push a commit, it rebuilds. Close the PR, it's gone.

v1 is in public beta — APIs, schemas, and behavior may shift before 1.0 stabilises. Run it on a host where occasional breakage is fine; production-critical previews aren't quite the right fit yet.

Why this exists

Shared staging is a queue.

How it is today
pr 1 pr 2 pr 3 pr 4 pr 5 pr 6 staging

Six PRs share one staging. The reviewer is looking at whichever branch deployed last, and the database state is whatever the last person left it in.

With Galley
pr 1 pr-1.preview pr 2 pr-2.preview pr 3 pr-3.preview pr 4 pr-4.preview pr 5 pr-5.preview pr 6 pr-6.preview

Each PR gets its own subdomain, its own containers, its own database. Webhook closes, environment goes away.

How it's different

Three things the hosted previews can't do.

01 · Whole stack

Not just the frontend.

Postgres, Redis, queues, workers — anything you boot in dev comes up in the preview, on a private network, reachable by its galley.yml name.

02 · Self-hosted

Runs on your hardware.

One docker compose up on a box you own. Source, secrets, and snapshots stay on your network. No telemetry, no phone-home.

Self-hosting guide ↗

03 · Real config

It's your compose file.

galley.yml or your existing docker-compose.yml. Build with your Dockerfile or fall back to language autodetect — both unprivileged.

galley.yml reference ↗

The config

One galley.yml per repo.

Existing docker-compose.yml? That works too. TTL, domain, and access live on the project, not in the repo.

version: 1

services:
  web:
    kind: web
    build:
      path: ./web
    expose: 3000
    depends_on: [api]
    env:
      API_URL: http://api:3001

  api:
    kind: api
    build:
      path: ./api
    expose: 3001
    depends_on: [postgres, cache]
    env:
      DATABASE_URL: postgres://app:pw@postgres:5432/app
      REDIS_URL: redis://cache:6379

  postgres:
    kind: database
    image: postgres:16
    expose: 5432
    env:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: pw
      POSTGRES_DB: app

  cache:
    kind: cache
    image: redis:7
    expose: 6379
  1. L4 kind: web gets a public route + screenshots
  2. L7 build.path: Dockerfile if present, else language autodetect
  3. L9 depends_on orders boot; siblings come up healthy first
  4. L11 reach the api as host "api" — services use galley.yml names
  5. L13 kind: api gets a public route, no screenshots
  6. L22 database, cache, queue, worker, other — internal-only kinds
  7. L28 image-only services skip the build step

Install

Three lines to a working server.

One compose file pulls every control-plane service as a published image. Point a wildcard DNS record at the host and you have previews.

curl -fsSL https://galley.sh/install/docker-compose.yml -o docker-compose.yml
echo "GALLEY_MASTER_KEY=$(openssl rand -hex 32)" > .env
docker compose up -d
# On a separate host, after generating a token in
# Admin → Agents → New agent.
sudo docker create --name x galleysh/agent:v1
sudo docker cp x:/usr/local/bin/galley-agent /usr/local/bin/
sudo docker rm x
sudo systemctl enable --now galley-agent

Full walk-through with DNS, TLS, and the master key in the quick start docs ↗.