> ## Documentation Index
> Fetch the complete documentation index at: https://docs.slipway.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Services

> Every field of services.<name> — build, image, ports, env, healthcheck, depends_on.

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`

| Field        | Type   | Required | Notes                                                                    |
| ------------ | ------ | -------- | ------------------------------------------------------------------------ |
| `context`    | string | yes      | Path within the repo (e.g. `.`, `./api`).                                |
| `dockerfile` | string | no       | Path to the Dockerfile, relative to `context`. Defaults to `Dockerfile`. |
| `args`       | map    | no       | Build args.                                                              |

```yaml theme={"system"}
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.

<Warning>
  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.
</Warning>

### `image`

```yaml theme={"system"}
services:
  redis:
    image: redis:7-alpine
```

For private registries, add the credentials once under **Settings → Registries** — see [Private registries](/configuration/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.

| Field    | Type   | Required | Notes                                                                                     |
| -------- | ------ | -------- | ----------------------------------------------------------------------------------------- |
| `port`   | int    | yes      | Container port (1–65535).                                                                 |
| `public` | bool   | no       | If true, gets a public URL at `<env-name>-<prefix>-<id>.<apps-base>`.                     |
| `prefix` | string | no       | The `<prefix>` in the public hostname. Defaults to the service name.                      |
| `domain` | string | no       | A [verified custom domain](/configuration/domains) to serve under instead of the default. |

```yaml theme={"system"}
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:

```yaml theme={"system"}
env:
  LOG_LEVEL: info
  DATABASE_URL: ${secret.DATABASE_URL}
```

See [Secrets & variables](/slipway-yaml/secrets-and-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.

```yaml theme={"system"}
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.

<Warning>
  `files` are projected through a plaintext ConfigMap, so they're for **non-secret** config and seed data only. Keep credentials in [secrets](/slipway-yaml/secrets-and-variables) (`${secret.X}`) and reference them from `env` — never from a committed, mounted file.
</Warning>

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](/slipway-yaml/volumes) 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.

```yaml theme={"system"}
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.

```yaml theme={"system"}
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:

| Handler | Healthy when                             | Example                              |
| ------- | ---------------------------------------- | ------------------------------------ |
| `http`  | any 2xx response                         | `http: { path: /ready, port: 3000 }` |
| `tcp`   | the connection establishes               | `tcp: { port: 5432 }`                |
| `exec`  | the command exits 0                      | `exec: { command: [pg_isready] }`    |
| `grpc`  | the gRPC health protocol reports serving | `grpc: { 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](/organization/plans). If a deploy asks for more than your plan allows, it fails with a clear error.
