# services

Generated from the code · as of CLI 0.6.0

One entry for each service in your compose project.

- **Type:** map of service name to entry
- **Required:** yes
- **Errors:** [`unbound-service`](https://docs.omniarc.eu/errors/unbound-service.md), [`orphan-entry`](https://docs.omniarc.eu/errors/orphan-entry.md), [`service-name-invalid`](https://docs.omniarc.eu/errors/service-name-invalid.md), [`removed-key`](https://docs.omniarc.eu/errors/removed-key.md), [`overlay-parse`](https://docs.omniarc.eu/errors/overlay-parse.md)

The keys are the compose service names, and compose and `arc.yaml` are matched by that name. Every service compose defines needs an entry, because OmniArc will not guess whether a service holds personal data. `omniarc sync` writes the missing entries for you, as guesses for you to review. A service that keeps data (a named volume, a writable bind inside the project, a database or cache image) gets `contains_pii: true` and `branch: false`, so branches start it empty; add `anonymize` rules, or set `contains_pii: false` for test data, to branch its data. A service that keeps nothing gets `contains_pii: false`.

An entry for a service compose does not define is refused too, which usually means a misspelling. A service name uses lowercase letters, digits, hyphens and underscores, starts with a letter, ends with a letter or digit, and is 40 characters or fewer, so compose's `affine_migration` is fine as it is. A service named `My_DB` has to be renamed in compose and in `arc.yaml`.

The file as a whole can be at most 64 KiB when you deploy. A real `arc.yaml` is a few KiB, so you meet this limit only if something has gone wrong.

```yaml
services:
  web:
    kind: web-service
    contains_pii: false
  db:
    kind: database
    contains_pii: true
    quiesce: { exec: [psql, -U, shop, -c, CHECKPOINT], timeout_secs: 30 }
    anonymize:
      engine: postgres
      database: shop
      user: shop
      rules:
        - { table: users, column: email, transform: fake-email }
        - { table: users, column: ssn, transform: redact }
  cache:
    contains_pii: true
    branch: false
  mailhog:
    deploy: false
```

## Keys that no longer exist

These per-service keys were removed. Each is refused with a message that says where the setting went.

| Old key | Use instead |
| --- | --- |
| `route` | the arc-level `edge` |
| `resources` | the arc-level `resources`. Per-container limits stay in compose |
| `scale` | dormancy is set per arc in the dashboard. `max` is compose's `deploy.replicas`. `quiesce` is the per-service `quiesce` |
| `dataset` | the arc-level `recordsize`. Volumes need no declaration, because the whole arc is branched |
| `ready_timeout_secs` | the arc-level `ready_timeout_secs` |
| `redact`, or `anonymize` written as a list | `anonymize` as a block with `engine`, `database`, `user` and `rules` |

## `services.*`

What OmniArc needs to know about one compose service.

- **Type:** map
- **Required:** yes
- **Errors:** [`pii-undeclared`](https://docs.omniarc.eu/errors/pii-undeclared.md), [`removed-key`](https://docs.omniarc.eu/errors/removed-key.md), [`deploy-false-with-config`](https://docs.omniarc.eu/errors/deploy-false-with-config.md), [`depends-on-undeployed`](https://docs.omniarc.eu/errors/depends-on-undeployed.md), [`volume-shared-across-branch`](https://docs.omniarc.eu/errors/volume-shared-across-branch.md), [`overlay-parse`](https://docs.omniarc.eu/errors/overlay-parse.md)

Only the keys on this page are accepted. Anything else is refused as an unknown key.

A service with `deploy: false` owes nothing else. Every other service must say whether it holds personal data with `contains_pii`.

## `services.*.kind`

What kind of service this is, which decides how its storage is tuned.

- **Type:** string, one of the kinds below
- **Required:** no
- **Errors:** [`overlay-parse`](https://docs.omniarc.eu/errors/overlay-parse.md)

| Kind | For |
| --- | --- |
| `database` | A relational database such as Postgres or MySQL |
| `document` | A document store such as MongoDB |
| `forge` | A Git forge |
| `cache` | A cache such as Redis |
| `object` | S3-style object storage |
| `web-service` | Backend code. Most compose services are this |
| `frontend` | Frontend code |

`database`, `document` and `forge` put the arc on a 16KiB block size unless you set `recordsize` yourself. With no `kind`, a service gets no class. If a service runs a database image, set `kind: database`: leaving it out keeps the 128KiB default, which makes a write-heavy Postgres noticeably slower.

## `services.*.deploy`

Whether OmniArc runs this service at all.

- **Type:** boolean
- **Required:** no
- **Default:** true
- **Errors:** [`deploy-false-with-config`](https://docs.omniarc.eu/errors/deploy-false-with-config.md), [`depends-on-undeployed`](https://docs.omniarc.eu/errors/depends-on-undeployed.md), [`edge-service-not-deployed`](https://docs.omniarc.eu/errors/edge-service-not-deployed.md), [`overlay-parse`](https://docs.omniarc.eu/errors/overlay-parse.md)

`deploy: false` means the service never runs on OmniArc. Use it for things that only make sense on a laptop, such as a mail catcher.

A deployed service that lists it under `depends_on` is refused, and so is an `edge` that points at it. A service with `deploy: false` cannot also set `contains_pii`, `anonymize` or `quiesce`, since none of them would ever apply.

## `services.*.branch`

Whether a branch of the arc gets a copy of this service's data.

- **Type:** boolean
- **Required:** no
- **Default:** true
- **Errors:** [`volume-shared-across-branch`](https://docs.omniarc.eu/errors/volume-shared-across-branch.md), [`overlay-parse`](https://docs.omniarc.eu/errors/overlay-parse.md)

With `branch: false` the service starts empty in every branch: its named volumes are emptied before it first starts there. Use it for a cache, or for data you never want in a preview.

A service that holds personal data and has `branch: false` needs no anonymize rules, because its data is never copied.

Two services that share a volume must agree on `branch`. Otherwise emptying the volume for one would empty it for the other too, and the file is refused. Give each its own volume.

## `services.*.contains_pii`

Whether this service holds personal data.

- **Type:** boolean
- **Required:** yes
- **Errors:** [`pii-undeclared`](https://docs.omniarc.eu/errors/pii-undeclared.md), [`pii-without-anonymize`](https://docs.omniarc.eu/errors/pii-without-anonymize.md), [`deploy-false-with-config`](https://docs.omniarc.eu/errors/deploy-false-with-config.md), [`overlay-parse`](https://docs.omniarc.eu/errors/overlay-parse.md)

There is no default, and it is required for every deployed service. A service with no volume can still hold personal data, for example an app that writes uploads into its own filesystem, and a branch copies that too.

Write `true` or `false`. The YAML words `yes`, `no`, `on` and `off`, and a quoted `"true"`, are not booleans here.

A service with `contains_pii: true` that branches must have `anonymize` rules. Otherwise real personal data would be copied into branches that developers and CI can reach.

## `services.*.anonymize`

How to remove personal data from this service's database when a branch is made.

- **Type:** map
- **Required:** no
- **Errors:** [`pii-without-anonymize`](https://docs.omniarc.eu/errors/pii-without-anonymize.md), [`anonymize-engine-unsupported`](https://docs.omniarc.eu/errors/anonymize-engine-unsupported.md), [`anonymize-empty-rules`](https://docs.omniarc.eu/errors/anonymize-empty-rules.md), [`anonymize-shape`](https://docs.omniarc.eu/errors/anonymize-shape.md), [`deploy-false-with-config`](https://docs.omniarc.eu/errors/deploy-false-with-config.md), [`removed-key`](https://docs.omniarc.eu/errors/removed-key.md), [`overlay-parse`](https://docs.omniarc.eu/errors/overlay-parse.md)

Needed when `contains_pii` is `true` and the service branches. There is one database per service. The rules run against the branch's copy, and the original is never touched.

Only Postgres is supported. A service that holds personal data in anything else needs `branch: false`.

## `services.*.anonymize.engine`

The database engine of the service.

- **Type:** string, `postgres`
- **Required:** yes
- **Errors:** [`anonymize-engine-unsupported`](https://docs.omniarc.eu/errors/anonymize-engine-unsupported.md)

`postgres` is the only value. A service with another engine is refused unless it has `branch: false`, in which case the engine is not looked at.

## `services.*.anonymize.database`

The name of the database to anonymize.

- **Type:** string
- **Required:** yes
- **Errors:** [`anonymize-shape`](https://docs.omniarc.eu/errors/anonymize-shape.md), [`anonymize-name`](https://docs.omniarc.eu/errors/anonymize-name.md)

Start with a letter or `_`, then use letters, digits, `_`, `$` or `-`, up to 63 characters. It cannot be empty.

## `services.*.anonymize.user`

The database role OmniArc connects as.

- **Type:** string
- **Required:** yes
- **Errors:** [`anonymize-shape`](https://docs.omniarc.eu/errors/anonymize-shape.md), [`anonymize-name`](https://docs.omniarc.eu/errors/anonymize-name.md)

Written the same way as `database`: a letter or `_` first, then letters, digits, `_`, `$` or `-`, up to 63 characters. OmniArc runs `psql` inside the service's container as this role.

## `services.*.anonymize.rules`

The columns to change and what to change them to.

- **Type:** list of rules
- **Required:** yes
- **Errors:** [`anonymize-empty-rules`](https://docs.omniarc.eu/errors/anonymize-empty-rules.md), [`anonymize-identifier`](https://docs.omniarc.eu/errors/anonymize-identifier.md), [`overlay-parse`](https://docs.omniarc.eu/errors/overlay-parse.md)

At least one rule. Each rule names a table, a column and a transform. List every column that holds personal data. If a rule cannot be applied, the branch is not created, so no half-anonymized copy is ever reachable.

## `services.*.anonymize.rules[].table`

The table to change.

- **Type:** string
- **Required:** yes
- **Errors:** [`anonymize-identifier`](https://docs.omniarc.eu/errors/anonymize-identifier.md)

Letters, digits and `_`, starting with a letter or `_`. You may write `schema.table`.

## `services.*.anonymize.rules[].column`

The column to change.

- **Type:** string
- **Required:** yes
- **Errors:** [`anonymize-identifier`](https://docs.omniarc.eu/errors/anonymize-identifier.md)

Letters, digits and `_`, starting with a letter or `_`. A column name has no schema part.

## `services.*.anonymize.rules[].transform`

What to do to the values in the column.

- **Type:** string, one of the transforms below
- **Required:** yes
- **Errors:** [`overlay-parse`](https://docs.omniarc.eu/errors/overlay-parse.md)

| Transform | Result |
| --- | --- |
| `redact` | Every value becomes a fixed marker |
| `hash` | Every value becomes a stable hash, so joins across tables still line up |
| `"null"` | Every value becomes SQL NULL. The branch fails if the column is `NOT NULL` |
| `fake-email` | A made-up email address |
| `fake-name` | A made-up name |
| `fake-phone` | A made-up phone number. Refused on a column that a unique index depends on, since made-up numbers can collide |

Write the SQL NULL transform in quotes, as `transform: "null"`. A bare `null` (or `~`, or `Null`) is YAML's "no value", not the word, so the file is refused.

## `services.*.quiesce`

A command that makes the service safe to snapshot.

- **Type:** map
- **Required:** no
- **Errors:** [`quiesce-empty`](https://docs.omniarc.eu/errors/quiesce-empty.md), [`quiesce-arg-empty`](https://docs.omniarc.eu/errors/quiesce-arg-empty.md), [`quiesce-timeout-zero`](https://docs.omniarc.eu/errors/quiesce-timeout-zero.md), [`deploy-false-with-config`](https://docs.omniarc.eu/errors/deploy-false-with-config.md), [`overlay-parse`](https://docs.omniarc.eu/errors/overlay-parse.md)

OmniArc runs the command inside the service's container, as `docker compose exec -T <service> <exec...>`, before it takes a branch and freezes the parent. Use it for a database checkpoint or a cache save, so the snapshot holds everything that was written.

A command that exits with an error, or runs longer than `timeout_secs`, fails the branch before any snapshot is taken.

## `services.*.quiesce.exec`

The command and its arguments, one list item for each.

- **Type:** list of strings
- **Required:** yes
- **Errors:** [`quiesce-empty`](https://docs.omniarc.eu/errors/quiesce-empty.md), [`quiesce-arg-empty`](https://docs.omniarc.eu/errors/quiesce-arg-empty.md), [`overlay-parse`](https://docs.omniarc.eu/errors/overlay-parse.md)

There is no shell, so write `[sh, -c, "..."]` if you need one. The list cannot be empty and no item in it can be empty.

## `services.*.quiesce.timeout_secs`

How many seconds the command may run before the branch fails.

- **Type:** integer, greater than 0
- **Required:** no
- **Default:** 30
- **Errors:** [`quiesce-timeout-zero`](https://docs.omniarc.eu/errors/quiesce-timeout-zero.md), [`overlay-parse`](https://docs.omniarc.eu/errors/overlay-parse.md)

**Example: shop/arc.yaml**

```yaml
arc: shop
edge: { service: web, port: 3000 }
resources: { cpu: "1", memory: 1Gi, disk: 5Gi }
services:
  web:
    kind: web-service
    contains_pii: false
  db:
    kind: database
    contains_pii: true
    anonymize:
      engine: postgres
      database: shop
      user: shop
      rules:
        - { table: customers, column: email, transform: fake-email }
```
