Skip to content

`atelier-spec.yaml` — declaring an app that is more than one container

Commit this file at the root of the app’s repo. It’s optional: without it, Atelier infers your app from its Dockerfile(s), which is right for most apps.

Reach for it when Dockerfiles alone can’t say what you mean:

  • your app needs a database (or cache, or any stock image) running alongside it
  • a service needs a persistent volume at a specific path (/var/lib/postgresql/data)
  • the app requires environment variables, some of them secret

You still don’t write Kubernetes YAML. The spec is a description of your app; the platform turns it into Deployments, Services, PVCs, and an Ingress.

The shape

name: myapp
description: A todo app with a Postgres database.
services:
# Built by Atelier from a Dockerfile in your repo.
- name: app
port: 3001
dockerfile: Dockerfile
# NOT built — a stock image, run as-is.
- name: db
port: 5432
image: postgres:16-alpine
volumes:
- mount_path: /var/lib/postgresql/data
size_gi: 5
env:
# Non-secret values with a default are applied automatically.
- name: POSTGRES_USER
default: myapp
- name: POSTGRES_DB
default: myapp
# ${APP} becomes the app's name. See "Talking to another service" below.
- name: PGHOST
default: "${APP}-db"
# Minted for you on first deploy. Nobody types it; nobody needs to read it.
- name: POSTGRES_PASSWORD
secret: true
generate: true
# Secret, but NOT generatable — Atelier can't invent someone else's API key.
# The operator is prompted for it in the app's Secrets tab.
- name: STRIPE_API_KEY
secret: true

Each service sets exactly one of dockerfile (Atelier builds it) or image (a published image, used verbatim). A prebuilt image never goes near the builder.

Talking to another service

A multi-service app gets one Kubernetes Service per service, named {app}-{service}, reachable in-cluster on its real port. So app reaches the database above at:

myapp-db:5432

You cannot hardcode that host, because the app’s name is chosen when it’s installed — the same repo might be deployed as myapp or todos-prod. That’s what ${APP} is for: write "${APP}-db" as the default and Atelier substitutes the real name at deploy time.

If your app expects a single connection URL, build it from the parts at startup rather than asking for the URL as one env var — a URL containing a generated password can’t be expressed as a default (see below).

Environment and secrets

DeclarationWhat happens
default: valueapplied automatically (goes in the app’s ConfigMap)
secret: truestored in the app’s Kubernetes Secret; never in a manifest, image, or the database
secret: true + generate: trueAtelier mints a strong random value on first deploy
secret: true alonethe operator is prompted for it (it shows as required in the Secrets tab)

A secret may not carry a default — that would defeat the point. generate: true is the counterpart: use it for values nobody chooses and nobody reads (a database password, a session-signing key). Use a plain secret: true for anything Atelier cannot invent, like a third-party API key.

Defaults seed; they never clobber. All of this runs on every build, so a value an operator has changed is left alone on the next git push.

Every var reaches every container in the app. That’s why POSTGRES_PASSWORD above is declared once and is seen by both the database (which initialises with it) and your app (which connects with it).

Postgres: set PGDATA to a subdirectory

If you give Postgres a volume, you must also set:

- name: PGDATA
default: /var/lib/postgresql/data/pgdata # a SUBDIRECTORY of the mount

A provisioned volume arrives formatted, so it already contains a lost+found directory — and initdb refuses to initialise into a non-empty directory:

initdb: error: directory "/var/lib/postgresql/data" exists but is not empty
initdb: hint: Using a mount point directly as the data directory is not recommended.
Create a subdirectory under the mount point.

Without it the database crash-loops on first deploy. This is standard for Postgres on Kubernetes, not an Atelier quirk. MySQL and MongoDB have the same trap.

Things worth knowing

  • Your app must retry its database connection on startup. Kubernetes has no depends_on. Your app may well start before Postgres is ready, crash, and be restarted until it isn’t. That’s self-healing but ugly — a short retry loop is better. (If it does crash-loop, the app’s Health tab says why.)
  • A rebuild does not restart your database. Pushing app code rebuilds and rolls only the services Atelier built. A stock image’s pod is left alone.
  • A service’s name must not collide with another app’s name. App myapp with a service db owns objects called myapp-db — so it can’t coexist with an app actually named myapp-db. Atelier refuses the deploy rather than overwrite.
  • The public URL routes to your app, never to a database. It targets a service called frontend if there is one, otherwise the first built service.
  • A broken spec fails the build. It does not quietly fall back to Dockerfile inference — deploying your app without the database it asked for isn’t a graceful degradation.

Per-service volumes vs. app storage

Two different things:

  • services[].volumes — a volume for one service, at any path. This is what a database needs.
  • The app’s Storage setting — a single volume mounted at /data in the app, managed from the UI or PUT /api/apps/{name}/storage. Good for a SQLite file or uploads.

Don’t try to put a database’s data on /data.