# Migrate from Compose

Turn a docker-compose.yml into a parity.yaml, key by key.

Most `docker-compose.yml` files map to `parity.yaml` key by key. The big change: databases, caches, object storage and mail catchers stop being containers you run. They become `managed` entries, and your services get their URLs as environment variables.

## A worked example [#a-worked-example]

A typical Compose file:

```yaml
# docker-compose.yml
services:
  web:
    build: .
    ports: ["3000:3000"]
    environment:
      DATABASE_URL: postgres://app:secret@postgres:5432/app
      REDIS_URL: redis://redis:6379
      SMTP_HOST: mailhog
      SMTP_PORT: "1025"
      S3_ENDPOINT: http://minio:9000
      S3_BUCKET: uploads
      SECRET_KEY: change-me
    depends_on:
      postgres: { condition: service_healthy }
      redis: { condition: service_started }
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
  worker:
    build: .
    command: npm run worker
    environment:
      DATABASE_URL: postgres://app:secret@postgres:5432/app
      REDIS_URL: redis://redis:6379
  migrate:
    build: .
    command: npm run migrate
    depends_on: [postgres]
  postgres:
    image: postgres:16
    environment: { POSTGRES_USER: app, POSTGRES_PASSWORD: secret, POSTGRES_DB: app }
    volumes: [pgdata:/var/lib/postgresql/data]
  redis:
    image: redis:7
  mailhog:
    image: mailhog/mailhog
  minio:
    image: minio/minio
    command: server /data
volumes:
  pgdata:
```

The same app on Parity:

```yaml
# parity.yaml
version: 1
project: shop

managed:
  postgres: { type: postgres, version: "16" }
  redis:    { type: redis }
  mail:     { type: email }
  storage:  { type: s3, buckets: [uploads] }

env_groups:
  common:
    SECRET_KEY: { generate: secret }

services:
  web:
    build: .
    ports: { http: 3000 }
    health: { http: /health }
    expose: true
    env_from: [common]
    depends_on: [postgres, redis, mail, storage]
    env:
      S3_ENDPOINT: ${storage.public_endpoint}
      S3_BUCKET:   ${storage.bucket.uploads}
  worker:
    build: { from: web }
    command: npm run worker
    env_from: [common]
    depends_on: [postgres, redis]

jobs:
  migrate: { service: web, command: npm run migrate, on: deploy }
```

What changed:

- **Postgres, Redis, MailHog and MinIO are gone as containers.** Each is a `managed` entry. `depends_on: [postgres]` gives the service `DATABASE_URL` and the `PG*` variables, `redis` gives `REDIS_URL`, and `mail` gives `SMTP_URL`, `SMTP_HOST` and `SMTP_PORT`. No password is written anywhere: each sandbox generates its own.
- **Keeping the names keeps the hosts.** A managed entry's name is its host, so naming it `postgres` keeps `postgres:5432` working for code that hardcodes it.
- **`migrate` is a job, not a service.** `on: deploy` runs it before services take traffic, and on every update.
- **`SECRET_KEY` is generated** once per sandbox, and shared by every service in the `common` group.
- **`worker` reuses `web`'s build** with `build: { from: web }`, so the image is built once.
- **The health check is an HTTP path**, not a command.
- **No `volumes`.** Managed data lives as long as the sandbox.

Run `parity check` to see every variable each service gets, then `parity up`.

## Field by field [#field-by-field]

| Compose | Parity |
| --- | --- |
| `image` | `image` |
| `build: .` or `build: { context, dockerfile, args }` | The same, under `build`. `dockerfile` is inside `context`. |
| The same `build` on several services | `build: { from: <service> }` on all but one |
| `command` | `command`. A shell string runs under `sh -c`, a list runs as written. |
| `entrypoint` | Not a field. Change the image, or use a [`kubernetes.patch`](/manifest/advanced#kubernetes-the-escape-hatch). |
| `ports: ["3000:3000"]` | `ports: { http: 3000 }`, or nothing if the app reads `PORT` (set to 8080 by default). There are no host ports. |
| A port you open in the browser | `expose: true`, or `expose: [{ host: app, path: /api }]` |
| `environment` | `env`. Replace URLs to bundled databases with `depends_on` or `${db.url}`-style references. |
| Values shared by several services (`x-env: &env` plus `<<: *env`) | `env_groups` and `env_from`. Anchors still work too. |
| `env_file`, `.env` | Not read. Put non-secret values in `env` and credentials in the secret store (`{ secret: <ref> }`). |
| `depends_on` with `condition: service_healthy` | `depends_on: [<service>]`, which waits for its health check |
| `healthcheck` | `health: { http: /path }` or `{ tcp: <port> }` |
| `restart` | Not needed. Services always restart. |
| `user` | `user: "1000"`, numbers only. Root isn't allowed. |
| `deploy.resources` | `resources: { requests: ..., limits: ... }` |
| `deploy.replicas` | One per sandbox. `parity scale`, or `replicas` in [`environments`](/manifest/environments) for production. |
| `volumes` (named) | Not supported. Move the data to Postgres, Redis or S3. Scratch space: an `emptyDir` in a `kubernetes.patch`. |
| `volumes` (bind mounts of source) | Not needed. `parity up` uploads and builds your code. |
| `volumes` (bind mounts of config files, such as `./nginx.conf:/etc/nginx/nginx.conf`) | Copy the file into the image. For a third-party image, add a two-line Dockerfile (`FROM nginx:1.27` and `COPY nginx.conf /etc/nginx/nginx.conf`) and point the service's `build:` at it. The file is then versioned with the image and identical in every environment. |
| `develop.watch` | The same rules, for `sync`, `sync+restart` and `rebuild`, without `ignore`. `parity up` applies them, with no watcher. `develop.command` sets the dev command `parity up --dev` runs. See [Develop](/manifest/services#develop). |
| `networks`, `links`, `extra_hosts` | Not needed. Everything in a sandbox reaches everything else by name. |
| `profiles` | Not supported. List only what a sandbox should run. |
| `x-` extension fields | Work the same way. |
| One-off `docker compose run web <cmd>` | A job without `on`, run with `parity run <job>`, or `parity exec web -- <cmd>` |

## Bundled services, by image [#bundled-services-by-image]

| Compose image | Managed entry | Your app gets |
| --- | --- | --- |
| `postgres`, `pgvector/pgvector` | `{ type: postgres }` | `DATABASE_URL`, `PG*` |
| `redis`, `valkey`, `keydb` | `{ type: redis }` | `REDIS_URL` |
| `minio`, `localstack` (S3 only), `seaweedfs` | `{ type: s3, buckets: [...] }` | `AWS_ENDPOINT_URL_S3`, `AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` |
| `mailhog`, `mailpit`, `maildev` | `{ type: email }` | `SMTP_URL`, `SMTP_HOST`, `SMTP_PORT`; read mail with `parity inbox` |
| `clickhouse/clickhouse-server` | `{ type: clickhouse }` | `CLICKHOUSE_URL`, `CLICKHOUSE_USER`, `CLICKHOUSE_PASSWORD`, `CLICKHOUSE_DB` |
| `keycloak`, `dex` (for local OIDC) | `{ type: auth }` | `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` |
| `mysql`, `mariadb`, `mongo`, `rabbitmq`, `kafka`, `elasticsearch` | None | Run it as one of your own services with `image:`, keeping in mind it has no persistent volume, or move to a managed type. |

## Variable names that don't match [#variable-names-that-dont-match]

The standard variables follow common conventions. When your app reads other names, map them with references:

```yaml
env:
  DB_HOST:        ${postgres.host}
  DB_PASSWORD:    ${postgres.password}
  CACHE_URL:      "${redis.url}/1"
  MINIO_ENDPOINT: ${storage.endpoint}
  APP_URL:        ${web.public_url}
```

Use `public_endpoint` for storage URLs a browser follows (presigned uploads), and `endpoint` for server-only calls. `${web.public_url}` is the sandbox's public URL for `web`, for links in emails and OAuth callbacks.

## Common snags [#common-snags]

- **The container runs as root.** Sandboxes refuse root. Set `user: "<uid>"` to a numeric user that owns the app's files, or add `USER 1000` to the Dockerfile.
- **The app writes to its own directory at runtime** (caches, temp files). With a non-root user that directory may not be writable. Give it an `emptyDir` with a `kubernetes.patch`, or point the app at `/tmp`.
- **The app calls a third-party API.** List its hosts in [`externals`](/manifest/externals), or the call is blocked. New hosts work once the promoted manifest lists them.
- **A migration needs a superuser or an extension.** Sandboxes behave like Cloud SQL. Use an available extension, or create it from a `deploy` job before the migration.
- **The app needs a vendor key to boot** (Stripe, for example). Sandboxes never get staging or production keys: switch the feature off in sandboxes with a literal `env` value.
- **A port isn't 8080.** Set `ports: { http: <port> }`, or let the app read `PORT`.
