Skip to main content
A service is one container. Give it a public port and it gets its own URL; services in the same deployment reach each other by name over a private network. Service names match ^[a-z][a-z0-9-]{0,30}$ and must be unique within the spec.

build or image

Each service either builds from your repo or pulls a prebuilt image — exactly one.

build

FieldTypeRequiredNotes
contextstringyesPath within the repo (e.g. ., ./api).
dockerfilestringnoPath to the Dockerfile, relative to context. Defaults to Dockerfile.
argsmapnoBuild args.
services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
      args:
        NODE_ENV: development
        NPM_TOKEN: ${secret.NPM_TOKEN}
Builds are content-addressed: the image tag comes from the commit SHA plus a hash of the build inputs. If a previous deploy on the same commit produced the same inputs, the build is skipped and the cached image is reused.
Passing a secret into build.args is safe by itself, but if your Dockerfile does RUN echo $TOKEN the value gets baked into the image layer. Keep credentials out of build args unless your Dockerfile discards them inside the same RUN step.

image

services:
  redis:
    image: redis:7-alpine
For private registries, add the credentials once under Settings → Registries — see Private registries. Slipway wires them in automatically; no per-service field needed.

ports

Ports the container listens on. Omit ports and the service is a worker — internal-only, no URL, no readiness gate at cutover.
FieldTypeRequiredNotes
portintyesContainer port (1–65535).
publicboolnoIf true, gets a public URL at <env-name>-<prefix>-<id>.<apps-base>.
prefixstringnoThe <prefix> in the public hostname. Defaults to the service name.
domainstringnoA verified custom domain to serve under instead of the default.
ports:
  - { port: 3000, public: true }                 # served at <env>-web-<id>.<apps>
  - { port: 9000 }                               # internal-only
  - { port: 3001, public: true, domain: app.acme.com }
One public port per service. For path-based routing across services, define each separately and put a reverse proxy in your repo.

env

Environment variables, as a plain map:
env:
  LOG_LEVEL: info
  DATABASE_URL: ${secret.DATABASE_URL}
See Secrets & variables for the ${...} substitution syntax.

files

Project committed repo files into the container, read-only — the cluster-native counterpart of a Docker Compose bind mount, for config and seed files: a Postgres init.sql, an nginx.conf, a fixtures file. Each entry is <repo-path>:<container-path> — the source is a path in your repo relative to the spec file’s directory, the target an absolute path in the container.
services:
  db:
    image: postgres:16-alpine
    files:
      - init.sql:/docker-entrypoint-initdb.d/init.sql
slipway reads each file from your repo at the deploy commit and mounts it with subPath, so only that one file lands at the target — the rest of the directory (the image’s own contents) is untouched. A Compose ./init.sql:/docker-entrypoint-initdb.d/init.sql bind mount is translated to this automatically.
files are projected through a plaintext ConfigMap, so they’re for non-secret config and seed data only. Keep credentials in secrets (${secret.X}) and reference them from env — never from a committed, mounted file.
Constraints: small text files (each under ~1 MB), and the source must stay inside the repo (no absolute or .. paths). For data that must persist, or large/binary content, use a named volume instead.

depends_on

Sibling services that must be ready before this one starts — Compose’s service_healthy semantics. All services are created at once so images pull and volumes attach in parallel, but a service’s own process doesn’t start until every dependency passes its readiness probe. It waits instead of crash-looping.
services:
  api:
    build: { context: ./api }
    ports:
      - { port: 8080, public: true }
    depends_on:
      - db
  db:
    image: postgres:16
    ports:
      - { port: 5432 }
    volumes:
      - pgdata:/var/lib/postgresql/data
Dependencies must be services in the same spec. A service can’t depend on itself or form a cycle — both are rejected at parse time. A long dependency chain still adds up (each link waits for the one before it), and depends_on guarantees start ordering, not zero-downtime ordering (services run a single replica).

healthcheck

Three probe phases — set any subset:
  • startup — runs first and blocks the others until it passes. For slow-booting apps.
  • readiness — gates traffic and gates cutover: a deploy isn’t healthy and its URL doesn’t go live until every public service is ready.
  • liveness — runs continuously; failures restart the container.
healthcheck:
  startup:   { http: { path: /startup, port: 3000 }, periodSeconds: 5, failureThreshold: 30 }
  readiness: { http: { path: /ready,   port: 3000 }, periodSeconds: 5 }
  liveness:  { http: { path: /healthz, port: 3000 }, periodSeconds: 30 }
If a ported service has no healthcheck, slipway adds a default TCP readiness probe on its public port, so a URL only goes live once the app accepts connections. Declaring any healthcheck replaces that default.

Handlers

Each probe takes exactly one handler:
HandlerHealthy whenExample
httpany 2xx responsehttp: { path: /ready, port: 3000 }
tcpthe connection establishestcp: { port: 5432 }
execthe command exits 0exec: { command: [pg_isready] }
grpcthe gRPC health protocol reports servinggrpc: { port: 9000 }

Timing

All handlers accept the standard knobs — initialDelaySeconds, periodSeconds, timeoutSeconds, failureThreshold, and successThreshold (readiness/liveness only). Omit them to use sensible defaults. For most web apps a single readiness probe is enough. Add startup only when boot takes more than ~30 seconds; add liveness only when you’ve seen the app deadlock in a way readiness doesn’t catch. A worker with no ports needs no probe — it’s ready as soon as it’s running.

Resource limits

You don’t size containers in the spec. CPU, memory, and disk caps come from your plan. If a deploy asks for more than your plan allows, it fails with a clear error.