Templates and environment profiles
Templates are how the platform team defines the golden path: with a good
template, a production deployment is four meaningful lines of
pagifier.yaml. This page is the complete reference for authoring them.
What a template is
A template is a pagifier.yaml fragment stored in the config bucket
at templates/<name>/<version>.yaml. There is no separate template
language: anything a project file can say, a template can pre-say — and
the project can override. Templates typically set:
| Concern | Keys |
|---|---|
| Runtime selection | runtime |
| Build recipe | build.enabled, build.workDir, build.commands |
| What ships | artifact.directory |
| How it starts | runtimeConfig.command, runtimeConfig.port |
| Caching policy | cache.static, cache.html |
| Probes | healthCheck |
They can also set resources, headers, env, autoscaling — anything —
but environment concerns belong in environment profiles (below), and
identity (name, environment, ingress.host) always comes from the
project.
A complete template, annotated
# templates/react-static/2.yaml
runtime: static # selects runtime-images/static.yaml too
build:
enabled: true
workDir: .
commands: # run verbatim in the toolchain image
- npm ci
- npm run build
artifact:
directory: dist # ONLY this directory enters the image
cache:
static: {maxAge: 365d} # hashed assets are immutable
html: {maxAge: 5m}
# templates/go-api/1.yaml — server runtime with binary-only shipping
runtime: go
build:
enabled: true
commands:
- mkdir -p bin
- CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o bin/server .
artifact:
directory: bin # sources never enter the image
runtimeConfig:
command: /app/server # never inferred from go.mod
port: 8080
healthCheck: {path: /healthz}
Versioning and the publishing workflow
Version files are immutable once published; latest.yaml is a
floating pointer:
template: react-staticresolvestemplates/react-static/latest.yamltemplate: react-static@2pinstemplates/react-static/2.yaml
The safe rollout sequence:
# 1. Publish the new version alongside the old ones
aws s3 cp react-static-2.yaml s3://pagifier-platform/templates/react-static/2.yaml
# 2. Verify with a pinned project (template: react-static@2) or a dry run:
curl -H "Authorization: Bearer $TOKEN" --data-binary @test-project.yaml \
https://pagifier.company.com/v1/validate
# 3. Promote it as the default
aws s3 cp react-static-2.yaml s3://pagifier-platform/templates/react-static/latest.yaml
Nothing running changes at any of these steps: resolution happens at
upload, so version 2 applies to each project's next deploy, and the
Release records "template:react-static": "2" in templateVersions.
Rolling a bad template back is re-copying the previous file over
latest.yaml. Teams that need stability during a migration pin
(@1) and unpin on their own schedule.
How the merge actually behaves
Five layers, lowest to highest precedence — later layers win per key:
platform/defaults.yaml → runtime-images/<runtime>.yaml
→ templates/<name>/<version>.yaml → defaults/<environment>.yaml
→ the project's pagifier.yaml
Three rules govern every collision:
- Maps merge recursively. A project overriding
resources.memorykeeps the template'sresources.cpu. - Scalars and lists replace wholesale. A project's
build.commandsreplaces the template's list — commands never concatenate. - Explicit
nulldeletes.compression: nullin a project removes whatever any lower layer set.
Worked example — these five inputs:
# platform/defaults.yaml # runtime-images/static.yaml
port: 80 runtime: static
security: {profile: recommended}
images:
# templates/react-static/2.yaml runtime: nginx:1.27-alpine
build: {enabled: true, build: node:22-alpine
commands: [npm ci, npm run build]}
artifact: {directory: dist}
cache: {static: {maxAge: 365d}}
# defaults/prod.yaml # project pagifier.yaml
ha: true version: 1
replicas: 3 name: frontend
resources: {cpu: 250m, template: react-static@2
memory: 256Mi} environment: prod
deploy: canary ingress: app.company.com
resources: {memory: 512Mi}
resolve to this snapshot (abbreviated):
name: frontend # project
runtime: static # template
port: 80 # platform defaults
build: {enabled: true, commands: [npm ci, npm run build]} # template
artifact: {directory: dist} # template
images: {runtime: nginx:1.27-alpine, build: node:22-alpine} # runtime def
ha: true # env profile
replicas: 3 # env profile
resources: {cpu: 250m, memory: 512Mi} # cpu from profile, memory from project
deploy: {strategy: canary} # env profile
ingress: {host: app.company.com, tls: true} # host from project, tls from defaults
security: {profile: recommended, headers: {…}} # defaults, expanded from security/
Anyone can inspect this for a real release —
GET …/releases/{id}/config returns the exact snapshot.
Environment profiles
defaults/<environment>.yaml is a pagifier.yaml fragment plus
three profile-only keys (logging, podDisruptionBudget,
rollingUpdate). Profiles are where operational policy lives:
# defaults/prod.yaml
ha: true
replicas: 3
autoRollback: true
resources: {cpu: 250m, memory: 256Mi}
podDisruptionBudget: {enabled: true, minAvailable: "50%"}
rollingUpdate: {maxSurge: "25%", maxUnavailable: "0"}
deploy:
strategy: canary
canary: {steps: [10, 50], stepInterval: 5m, analysis: [error-rate, latency-p95]}
# defaults/dev.yaml — fast and cheap
ha: false
replicas: 1
resources: {cpu: 50m, memory: 64Mi}
logging: {level: debug}
Developers get metric-gated canaries in prod and instant rolling updates in dev without writing a line of strategy configuration — and can still override any of it per project when justified.
Guardrails on template authors
The project file always wins, with two deliberate exceptions that protect the platform's guarantees:
- Auth middleware annotations (
middleware/authentication-*.yaml) override project ingress annotations — apps cannot annotate around authentication. - Validation applies to the merged result — a template cannot make an invalid configuration deployable (ascending canary steps, port ranges, mandatory runtime commands are all enforced post-merge).
And a failure guarantee for authors: a template with a typo'd key is rejected at upload with a did-you-mean error, never silently dropped — strict decoding applies to every layer, not just user files.