Skip to content
Manifest
Markdown

Services

Long-running processes: image or build, command, ports, health, public hosts, resources and the develop loop.

FieldMeaningDefault
imageA prebuilt image.One of image or build is required
build. (a directory with a Dockerfile), { context, dockerfile, args, paths }, or { from: <service> } to reuse another service's build.
commandReplaces the image's command. The entrypoint stays, as in Compose.The image's command
ports{ <name>: <number>, ... }. The first one is the service's port.{ http: 8080 } when the service needs a port, else none
health{ http: /path } on the first port, or { tcp: <port> }.Running means healthy
exposetrue (public on the first port, with the service's name as host), or a list of { port, host, path }.Not public
env_fromEnv groups the service gets.
externalsExternals the running service and its jobs call. Every service of the sandbox may reach them.
depends_onManaged entries (gives their standard variables) and services (start after they're healthy).
envVariables. See env.
user"<uid>[:<gid>]", numbers only and not 0.The image's user
resources{ requests: { cpu: 250m, memory: 512Mi }, limits: { memory: 1Gi } }100m CPU and 256Mi requested, 2 CPU and 2Gi limit
kubernetes{ patch: ... }, see the escape hatch.
developCompose's develop block: files copied into the running service instead of a rebuild, and a dev command. See Develop. Sandboxes only.Every edit rebuilds the image
  • Commands. A string of plain words (npm run worker) runs directly. A string that uses the shell (&&, |, $VAR, quotes, cd) runs as sh -c "<string>". A list ([node, server.js]) runs exactly as written, for images without a shell.
  • PORT is set to the service's first port, as on Cloud Run, Render and Heroku. An app that reads PORT needs no ports. An image that listens on a fixed port (nginx on 80) declares ports: { http: 80 } instead. Setting PORT to another number than the first port is refused, since traffic and health checks go to the port.
  • Non-root only. Containers can't run as root. If your image's user is root or a name rather than a number, use a numeric USER in the Dockerfile (USER 10001 rather than USER app, the uid your adduser -u made), or set user: "10001" on the service. Volumes are writable by that user. To find a named user's uid: docker run --rm --entrypoint id <image>. If you forget, the sandbox fails as soon as that container is created (after the setup jobs, and after the services it depends on are up), and the message says which of the two it is.
  • Public URLs are <host>--<sandbox>--<account>.<domain>, one DNS label, so the three together must fit in 63 characters. A host of up to 21 characters always fits. parity check prints them with <sandbox> and <account> in place of the IDs, refuses a host over 55 characters and warns over 21.
  • Paths aren't stripped. expose: [{ host: app, path: /api }] sends /api/... to the service with the path unchanged, so the app serves its routes under /api. Two services can share a host this way, which avoids CORS.
  • Ordering. depends_on: [api] holds this service until api passes its health check, like Compose's condition: service_healthy. It applies at start only. Cycles are refused.
  • Replicas. Sandboxes run one replica of each service. parity scale <service> <n> changes it for one sandbox, and 0 stops a service.
  • OpenTelemetry. OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES are set for you, so any OTel SDK tags traces, metrics and logs with the service and sandbox.
  • No persistent volumes. A service's filesystem resets when it restarts. Keep data in a managed entry: Postgres, Redis or S3.

Builds

parity up uploads your working directory (without .git and what .dockerignore leaves out, though each Dockerfile goes, as with docker build) and builds each build block, once per distinct block. Services built { from: web } share web's image. A build Parity has already done for the same files and args is reused across all your sandboxes, so a second sandbox on the same code starts without building. --image web=<ref> skips the build for that service and the services built from it. A failed build fails the sandbox and shows the build log.

  • context is a directory inside the one parity up uploads, and dockerfile a path inside context. Absolute paths and .. out of either are refused.
  • Network. A build reaches package registries (npm, PyPI, Go, crates.io, RubyGems, Maven Central, Packagist, Debian, Ubuntu and Alpine mirrors) and base images on Docker Hub, ghcr.io, quay.io, gcr.io and mcr.microsoft.com. Nothing else. To download from another host (Prisma engines, Playwright browsers, a GitHub release), add it to externals and name it in build.externals: [prisma]. A build that tries another host fails, and the error names the host.
  • Trigger paths. build.paths lists the files whose changes need a new image, as globs relative to parity.yaml with .dockerignore's syntax (** crosses directories). The default is everything under the context. Set it when a service also depends on files outside its context: build: { context: apps/web, paths: [apps/web/**, packages/ui/**] }. The default also covers the Dockerfile and the .dockerignore next to parity.yaml. On each push to your default branch, Parity works out which services changed since the last built push; the others keep their image.
  • Builds from a push use GitHub's archive of the commit, which differs from your working tree in three ways: files marked export-ignore in .gitattributes are left out, Git LFS files arrive as their pointer files, and submodules are empty. Build those in the Dockerfile instead (fetch LFS objects or clone the submodule there).
  • Builds see no secrets and no sandbox values, only build.args. parity check warns when an arg differs per sandbox (a public URL, a generated value), because that means one build per sandbox. Prefer reading such values at run time.

Develop

By default every edit rebuilds the service's image, which is what staging and production run. A develop block, Compose's develop spec, lets an edit skip the build in sandboxes. Parity knows no language or framework: your repo says which directories are copied and whether the process restarts.

services:
  api:
    build: { context: ./api }
    develop:
      watch:
        - { path: ./api/src, target: /app/src, action: sync+restart }
        - { path: ./api/src/requirements.txt, action: rebuild }
  web:
    build: { context: ./web }
    develop:
      command: npm run dev
      watch:
        - { path: ./web/src, target: /app/src, action: sync }
actionWhat parity up does when a file under path changes
syncCopies the changed files into the running container. Nothing restarts, so your app's own reloader, if it has one, picks them up.
sync+restartCopies them and restarts the service on the same image, in a new pod, so the process starts on the new files.
rebuildBuilds a new image. Every file outside a sync rule already does, so this matters only for a file inside a synced directory, such as a lock file.
  • path is a directory (a rebuild rule may also name a file), relative to the file that declares it, inside the upload. target is where it appears in the container, an absolute path. The target is replaced, not merged: what the image has there is hidden. So point it at a directory of source only (/app/src), never at one that also holds what the build installed (/app, with its node_modules).
  • No build for synced edits. The build ignores the files under sync paths, so an edit there reuses the image, and so do your other sandboxes whose code differs only there. A new sandbox still builds once if no sandbox has built that image yet.
  • The first copy comes before the start. A new pod, and each deploy job of a rollout, gets the whole directory before its process starts, so it never runs the image's older copy. Cron jobs and parity run use the image as built.
  • Dev mode is opt-in. parity up --dev web runs develop.command (your next dev, vite or air) instead of the image's command. Without a develop.command, --dev is an error rather than a guess. Each parity up states the services it runs in dev mode.
  • Check the real build. parity up --build ignores the sync rules and builds every image as staging and production run it. Run it before recording proof.
  • Not supported: ignore (leave files out of the upload with .dockerignore), and the restart and sync+exec actions. initial_sync is accepted and has no effect, since a new container always gets the whole directory.
  • Staging and production ignore develop.