`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: myappdescription: 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: trueEach 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:5432You 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
| Declaration | What happens |
|---|---|
default: value | applied automatically (goes in the app’s ConfigMap) |
secret: true | stored in the app’s Kubernetes Secret; never in a manifest, image, or the database |
secret: true + generate: true | Atelier mints a strong random value on first deploy |
secret: true alone | the 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 mountA 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 emptyinitdb: 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
myappwith a servicedbowns objects calledmyapp-db— so it can’t coexist with an app actually namedmyapp-db. Atelier refuses the deploy rather than overwrite. - The public URL routes to your app, never to a database. It targets a service
called
frontendif 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
/datain the app, managed from the UI orPUT /api/apps/{name}/storage. Good for a SQLite file or uploads.
Don’t try to put a database’s data on /data.