Skip to content
Galley

Docs / Use

galley.yml reference

Every field, every default, every constraint.

galley.yml lives at the root of your repo. It tells Galley what to build and run for each preview. If you already have a docker-compose.yml, Galley parses that — galley.yml only needs to exist when you want fields compose doesn’t have (kind, expose, etc.) or when your compose has features Galley deliberately ignores (volumes, host networking).

Top-level shape

version: 1
services:
  <service-name>:
    # ...
FieldRequiredNotes
versionYesCurrently 1. Reserved for future schema bumps.
servicesYesMap of service name → service spec. At least one service.

Service names match ^[a-z][a-z0-9-]{0,39}$ — lowercase, may contain digits and hyphens, starts with a letter, max 40 chars. The name shows up in DNS aliases, container names, and Traefik labels, so the alphabet is locked down.

Service spec

services:
  api:
    kind: api
    build:
      path: ./api
      command: "go build -o /out/api ./cmd/api"
      start: "/out/api"
    image: ghcr.io/yourco/api:latest
    expose: 3001
    env:
      DATABASE_URL: postgres://app:pw@postgres:5432/app
      REDIS_URL: redis://cache:6379/0
      PUBLIC_URL: https://${GALLEY_PREVIEW_HOST_web}
    depends_on: [postgres, cache]
    restart: unless-stopped
    ephemeral: false
    health:
      path: /healthz
      status: 200
      timeout: 30s
    resources:
      cpus: "1"
      memory: 512m

Every field is optional except: each service needs either image or build. Set both and build wins (the built image gets the tag Galley generates).

kind

ValueBehavior
webPublic route on a subdomain (or the bare domain when there’s exactly one web). Screenshots fired on healthy.
apiPublic route on a subdomain. No screenshots.
workerBackground process. No inbound network, no public route.
databaseInternal-only. Default for image-only services with names like postgres, mysql, mariadb, mongo.
cacheInternal-only. Default for redis, valkey, memcached.
queueInternal-only. Default for nats, rabbitmq, kafka, redpanda.
otherNo defaults applied. Use when none of the above fit.

Omit kind and Galley infers from the image name (database/cache/queue) or defaults to web. When inference happens, the dashboard shows a warning on the deployment so you can pin the kind explicitly.

image vs build

  • Image-only. image: postgres:16 — Galley pulls and runs. No build step, no Dockerfile expected.
  • Build with Dockerfile. build.path: ./api and a Dockerfile at that path — built in an unprivileged sandbox, no daemon socket access, the worktree mounted read-only.
  • Build without Dockerfile. Same build.path with no Dockerfile — language autodetect inspects the source, picks a base image, and synthesizes a build. Works for Node, Go, Python, Ruby, Rust, JVM, .NET, PHP, Elixir, and a long tail of less-common stacks.

build.command and build.start are optional overrides for the autodetect path. They’re ignored when a Dockerfile is present (your Dockerfile is authoritative).

build.path must be a relative path inside the repo. Absolute paths and .. traversal are rejected.

expose

Single port the service listens on. Galley uses this for two things:

  • Web/api services: the port the public route forwards to.
  • Internal services: peer access on the env network (e.g., postgres:5432).

Only one port per service. If you really need multiple, run a thin reverse proxy in the container.

env

Map of environment variables rendered into the container at start. Values support interpolation:

  • ${VAR} — looks up project-level env vars and built-ins. Empty if unset.
  • ${VAR:-default} — uses default when VAR is unset or empty.
  • ${GALLEY_PREVIEW_HOST_<service>} — expands to <service>’s preview hostname for this PR. Use it to thread the right URL into a frontend’s PUBLIC_URL, an API’s CORS allowlist, etc.
  • $$ — a literal dollar sign.

Built-ins available in any value:

VariableValue
GALLEY_PROJECT_IDThe project’s ULID.
GALLEY_DEPLOYMENT_IDThe deployment’s ULID.
GALLEY_COMMIT_SHAThe commit being deployed.
GALLEY_PREVIEW_HOST_<service>The deploy-time hostname of <service>.

Project-level env vars are managed in the dashboard at Settings → Environment vars. Marking one secret: true encrypts it under the master key and redacts it from the UI after creation.

depends_on

List of service names that must be healthy before this service starts. Strict topological order — different from compose’s “started” semantics. If a dependency fails health checks, the dependent never boots and the deployment fails with a clear error.

restart

ValueBehavior
unless-stopped(default) Restart on crash, don’t restart on manual stop.
on-failureRestart only on non-zero exit.
neverOne-shot. Useful for migration runners.

ephemeral

true means Galley resets the service’s volumes on every deploy. Without it, named volumes persist across deploys within the same env’s lifetime. Containers and networks are always reset on deploy regardless.

health

Optional health probe. If absent, Galley uses the image’s HEALTHCHECK directive (or, lacking that, treats “container running” as healthy).

health:
  path: /healthz
  status: 200
  timeout: 30s
FieldDefaultNotes
pathHTTP path to probe. Probe runs against localhost:<expose>.
status200Acceptable status code. Only one value, no ranges.
timeout60sHow long Galley waits before declaring the service unhealthy and failing the deploy.

resources

Per-service CPU + memory caps, in the same format docker run --cpus and --memory accept:

resources:
  cpus: "1.5"   # 1.5 cores
  memory: 1g    # 1 GB

Default caps come from the agent’s GALLEY_DEFAULT_CPUS / GALLEY_DEFAULT_MEMORY env vars. Per-service caps override them.

Naming and reachability

Each service joins the env’s private network with a DNS alias matching its galley.yml name. So in the example above:

  • The api container reaches Postgres at postgres:5432.
  • The web (frontend) container reaches the API at api:3001.
  • Nothing outside the env can talk to postgres, cache, or worker — only web and api (HTTP-routable kinds) get a public route.

This is identical to how docker-compose.yml resolves service names. Existing apps keep working unchanged.

Worked example

A typical fullstack repo with a frontend, an API, Postgres, and a Redis cache:

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/0
    health:
      path: /healthz
      status: 200

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

  cache:
    kind: cache
    image: redis:7-alpine
    expose: 6379

Deployed:

  • web.pr-12-myrepo.preview.yourco.dev — the frontend, screenshotted on every deploy.
  • api.pr-12-myrepo.preview.yourco.dev — the API, no screenshots.
  • postgres and cache — internal-only, reachable from api by name.

Reading order with docker-compose.yml

Both files in the repo: galley.yml wins. The compose file is parsed only when galley.yml is absent. Compose features Galley ignores (and warns about) include:

  • volumes: — Galley doesn’t run host bind-mounts in previews. Use ephemeral if you need state to survive within a preview’s lifetime.
  • ports: — every routable service gets a public route on a subdomain; mapping host ports doesn’t make sense across many previews on one host.
  • network_mode, pid, ipc — host modes break preview isolation.
  • cap_add, privileged: true — agents refuse privileged containers.

The deployment timeline shows each warning so you can audit what got ignored.