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>:
# ...
| Field | Required | Notes |
|---|---|---|
version | Yes | Currently 1. Reserved for future schema bumps. |
services | Yes | Map 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
| Value | Behavior |
|---|---|
web | Public route on a subdomain (or the bare domain when there’s exactly one web). Screenshots fired on healthy. |
api | Public route on a subdomain. No screenshots. |
worker | Background process. No inbound network, no public route. |
database | Internal-only. Default for image-only services with names like postgres, mysql, mariadb, mongo. |
cache | Internal-only. Default for redis, valkey, memcached. |
queue | Internal-only. Default for nats, rabbitmq, kafka, redpanda. |
other | No 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: ./apiand aDockerfileat that path — built in an unprivileged sandbox, no daemon socket access, the worktree mounted read-only. - Build without Dockerfile. Same
build.pathwith 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}— usesdefaultwhenVARis 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’sPUBLIC_URL, an API’s CORS allowlist, etc.$$— a literal dollar sign.
Built-ins available in any value:
| Variable | Value |
|---|---|
GALLEY_PROJECT_ID | The project’s ULID. |
GALLEY_DEPLOYMENT_ID | The deployment’s ULID. |
GALLEY_COMMIT_SHA | The 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
| Value | Behavior |
|---|---|
unless-stopped | (default) Restart on crash, don’t restart on manual stop. |
on-failure | Restart only on non-zero exit. |
never | One-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
| Field | Default | Notes |
|---|---|---|
path | — | HTTP path to probe. Probe runs against localhost:<expose>. |
status | 200 | Acceptable status code. Only one value, no ranges. |
timeout | 60s | How 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
apicontainer reaches Postgres atpostgres:5432. - The
web(frontend) container reaches the API atapi:3001. - Nothing outside the env can talk to
postgres,cache, orworker— onlywebandapi(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.postgresandcache— internal-only, reachable fromapiby 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. Useephemeralif 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.