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, deployed sequentially.
“Healthy” follows whatever HEALTHCHECK the container reports — either the one Galley renders from a health: block, or the image’s own HEALTHCHECK directive. Without either, “ready” means the container is Running, which races first-listen on long-startup services like Postgres. Declare health: on dependencies you really need to wait for.
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 rendered as a Docker HEALTHCHECK on the container. The probe uses wget --spider, which is built into busybox / alpine / debian-slim base images. Distroless or scratch images don’t ship wget — declare a HEALTHCHECK directly in your Dockerfile in that case.
health:
path: /healthz
status: 200 # informational; the probe checks for any 2xx
timeout: 30s # start-period grace before failures count
| Field | Default | Notes |
|---|---|---|
path | — | HTTP path to probe. Probe runs against 127.0.0.1:<expose>. |
status | 200 | Documented for clarity; wget --spider exits non-zero on any HTTP error. |
timeout | 30s | Start-period grace window. Probe failures during this window don’t count against retries. |
Probes run every 3 seconds with a 2-second per-probe timeout and 5 retries before “unhealthy”. The agent waits up to 60 seconds total for the container to reach healthy before failing the deploy.
The probe expects a 2xx — wget --spider exits non-zero on 4xx/5xx. Pick a path you know returns 200 when the service is up; many apps add a tiny /healthz route specifically for this.
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:
pr-12-myrepo.preview.yourco.dev— the frontend (single web service claims the bare env URL), screenshotted on every deploy.api-pr-12-myrepo.preview.yourco.dev— the API. Non-bare services hyphenate so a single*.preview.yourco.devwildcard covers them; 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 at a single-level wildcard hostname (<svc>-<env>or the bare<env>if it’s the only web); 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.