Most apps are one container, and for those Yard writes the compose file for you. But sometimes an app wants company: a background worker, a database, a little sidecar. Then you write the compose file yourself, and Yard runs it as it is.
If you've written a compose.yml before, this chapter is short. There's a contract of four lines, and one thing about healthchecks that will save you an afternoon.
The file Yard writes for you#
Start by looking at what you get without a compose file. This is Yard's default, slightly trimmed:
services: app: build: context: ${CHECKOUT} dockerfile: ${DOCKERFILE:-Containerfile} image: ${IMAGE} pull_policy: never restart: always networks: [yard] volumes: - ${DATA_DIR}:/data env_file: - ${APP_ENV} environment: PROJECT_NAME: ${PROJECT_NAME} PROJECT_DATA_DIR: /data SHA: ${SHA} PORT: "8000" expose: ["8000"]networks: yard: external: true name: yardEverything in ${…} is filled in by Yard before Compose runs:
CHECKOUTis Yard's clone of your repo.IMAGEis the tag for this deploy,localhost/<name>:<sha12>.SHAis the full commit.DATA_DIRis/srv/yard/apps/<name>/dataon the host.APP_ENVis the app's env file, the oneyard env setwrites.PROJECT_NAMEis the app's name.
The full file is ops/apply/default.compose.yml in the Yard repo.
The contract#
Put a compose.yml (or compose.yaml) at the repo root or in ops/, and Yard uses it instead of the default. Four lines in it matter:
Service
appon networkyard— required, and the deploy fails without it. Caddy dials<name>-app-1, and it can only reach containers onyard.image: ${IMAGE}andbuild.context: ${CHECKOUT}on the service that builds — that's how the image gets its commit tag, and how a rollback finds the old one.env_file: [${APP_ENV}]— without it,yard env sethas no effect. The deploy log warns.${DATA_DIR}:/dataundervolumes:— without it, the app has no safe place for files. The deploy log warns.
Yard does not merge your file with the default one, so the environment: values from the default (PROJECT_DATA_DIR, SHA, …) are yours to add, if your app reads them.
Example: an app and a worker#
Two processes, one image:
services: app: build: context: ${CHECKOUT} image: ${IMAGE} pull_policy: never restart: always networks: [yard] env_file: [${APP_ENV}] volumes: ["${DATA_DIR}:/data"] environment: SHA: ${SHA} worker: image: ${IMAGE} pull_policy: never restart: always command: ["python", "-m", "myapp.worker"] env_file: [${APP_ENV}] volumes: ["${DATA_DIR}:/data"] networks: yard: external: true name: yardOnly app has a build: section, so the image is built once and both services run it. The worker isn't on yard, because nothing needs to reach it from outside.
Example: a database next to the app#
services: app: build: context: ${CHECKOUT} image: ${IMAGE} pull_policy: never restart: always networks: [yard, default] env_file: [${APP_ENV}] environment: DATABASE_URL: postgresql://app:app@db/app db: image: docker.io/library/postgres:17 restart: always environment: POSTGRES_USER: app POSTGRES_PASSWORD: app volumes: ["${DATA_DIR}/postgres:/var/lib/postgresql/data"] networks: yard: external: true name: yardLook at the networks. app is on two: yard, so Caddy can reach it, and default, the project's private network, so it can reach db. The database is only on default.
Warning. Every container on yard can reach every other container on yard, across all apps. Keep databases and internal services off it. Put them on the project's default network, which only that app's containers share.
The database files go under ${DATA_DIR}, so they survive deploys like any other app data.
Healthchecks#
A healthcheck is what makes a deploy careful. With one, Compose waits until the new container reports healthy, and Yard brings the old image back when it never does. Without one, a container that starts and then breaks goes live anyway.
Here's the afternoon-saver: declare the healthcheck in the compose file, not in the Containerfile. Podman builds images in the OCI format by default, and that format has no place for a HEALTHCHECK line, so Podman drops it with only a warning in the build log.
services: app: # … healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"] start_period: 30s start_interval: 1s interval: 5mstart_interval: 1s checks every second while the app starts, so a good deploy goes live fast. interval: 5m keeps the check quiet afterwards. The command runs inside the container, so use a tool the image has: curl if it's installed, or the language runtime as above.
Yard waits up to two minutes for healthy. A healthcheck is the only reason to write a compose file for a one-container app, and it's a good one.
Any language, any server#
Yard never looks inside your image. It builds what the repo describes, starts it, and waits for healthy. So a Go service, a Node app, or nginx serving a static site deploys exactly like a Stario app, as long as it keeps the contract.
A repo that already runs with docker compose somewhere else usually needs three changes to move in: name the web service app, add the yard network, and attach env_file: [${APP_ENV}].
By the way#
Yard runs
docker-composeagainst Podman's Docker-compatible API. So the Compose you know is the Compose you get, includingdepends_on,profiles, andcommand.--remove-orphansis on. Delete a service from the file, and the next deploy removes its container.Each deploy builds with
docker-compose buildwhen the repo has its own compose file, and with a quickerpodman buildfor the default one. The result is the same image.