Generated Deployment Artifacts
Since the Wave 3 release train, no deployment artifact in a Productify install is hand-authored. Application Nomad jobs, the platform-core stack (Manager, proxy, identity provider, database), and the entire Caddyfile (prelude and per-application vhosts) all render from structured inputs. You maintain a spec and an install.yaml, not HCL and Caddyfiles.
One model: full render + structured overrides
Artifacts are always regenerated from their source. Hand edits are drift and are overwritten — CI fails on a modified GENERATED file. Customization happens through structured spec fields and the delimited extra_* escape hatches, never by editing output.
Where each artifact comes from
| Artifact | Source of truth | Command |
|---|---|---|
| Application Nomad job | Deployment spec on the Manager's Deployment entity | pfy nomad render | deploy |
Autoscaler scaling block | The same deployment spec (scaling) | rendered into the app job |
| Platform-core stack (Manager, proxy, Pocket ID, Postgres) | Local install.yaml | pfy platform render | deploy |
| Caddyfile prelude (globals, OAuth portal, Manager vhost) | Local install.yaml | pfy platform render |
| Per-application vhosts | Manager data (Deployment.domains + upstream) | pushed live via Caddy admin API |
The platform-core artifacts render from a local install.yaml, not from the Manager — the Manager cannot be its own prerequisite (the bootstrap exception). Everything else renders from the deployment spec that lives on the Manager and is UI-editable.
The GENERATED header and determinism
Every rendered file carries a header and is byte-identical for the same spec and template version (no timestamps):
# GENERATED by pfy vX.Y.Z from deployment spec <hash> — do not edit; edits are overwritten- No secrets, ever. Secrets are referenced — Nomad
templatestanzas /{env.*}in the Caddyfile — never inlined, so rendered files are safe to commit. - Spec hash is a canonical-JSON SHA-256 over the spec, exposed in the API and stamped into output headers.
- Escape hatches are structured spec fields (
extra_hcl,extra_caddy_global,extra_vhosts), appended verbatim and clearly delimited. Anything beyond them is a template feature request, not a hand edit.
The deployment spec
The spec is structured, validated data on the Deployment entity (per application × environment), edited in the Manager UI or via pfy. Every mutation is audit-logged (actor, before/after). Fields (all validated at write time):
| Field | Notes |
|---|---|
image | repo/name, no tag — the tag comes from the release being deployed |
ports | [{name, to, static?}] |
health | {liveness_path, readiness_path, interval, timeout} (paths start with /) |
resources | {cpu_mhz, memory_mb} (floors enforced) |
count | with min ≤ count ≤ max when scaling is on |
scaling | {enabled, min, max} — renders the Nomad scaling block with the productify-scaler strategy |
env | [{name, from_nomad_var}] — references only; literal values rejected for secret-pattern names |
migrate | {enabled, command} — prestart migration task |
datacenters, namespace | Nomad placement |
extra_hcl | delimited verbatim block |
domains | proxy hostnames for this (application, environment) |
upstream_override | for backends not deployed via pfy (external hosts) |
Access goes through the Manager's authorization helpers: write requires project maintainer or above; read is available to members.
Editing the spec from the CLI
pfy deployment get --env run -o yaml
pfy deployment set --env run --field resources.memory_mb=512
pfy deployment set --env run --file spec.run.yaml # bulk apply
# Opt-in git-review workflow
pfy deployment snapshot --env run # writes deploy/spec.run.yaml
pfy deployment apply deploy/spec.run.yaml # round-trips it backBecause the spec bypasses git review, snapshot/apply give teams a commit-anchored review trail, and deploys print the spec-hash delta since the last applied hash.
Rendering application jobs
# Render the full Nomad job HCL from the spec
pfy nomad render --env run --tag v1.4.2
# render → nomad job validate → nomad job plan → nomad job run
pfy nomad deploy --env run --use-project-versionpfy nomad deploy replaces the old in-place image = rewriter. The legacy path survives only behind --legacy-rewrite for one minor release and warns loudly. The rendered job includes ports, Nomad service registration (the name the vhost renderer consumes), liveness/readiness checks, internal-only networking, template stanzas for env references, an optional migration prestart task, the scaling block, and any extra_hcl.
Rendering the platform stack
The platform stack renders from a single install.yaml — domains (manager, id, apps wildcard), datacenter/region, volumes, pinned image versions, TLS mode, optimizer endpoint, and resource sizes:
pfy platform render --target nomad # or: --target compose
pfy platform deploy # applies (nomad)The Compose target output is the self-hosted bundle's Compose source. The rendered set includes the proxy job whose Caddyfile prelude comes from the same install.yaml. Operator extension points are structured (extra_caddy_global, extra_vhosts), matching the extra_hcl pattern.
Proxy configuration & drift
The Caddyfile is entirely rendered:
- Install-level parts (global options, the
productifyapp block with the Manager URL and{env.PFY_MANAGER_TOKEN}, the caddy-security OAuth2/OIDC portal, the Manager's own vhost, metrics) render frominstall.yamlat platform-deploy time. - Per-application vhost blocks render from Manager data (
Deployment.domains+ upstream) —productify_protect <app_id>+reverse_proxy <upstream>— and are pushed live to the running proxy via Caddy's admin API on any relevant change.
Drift is detected via a config hash (one hash, whole config) compared against the Manager's last-rendered hash:
pfy proxy diff # rendered-vs-running
pfy proxy sync # force-push current config
pfy deploy diff --env run # three-way: spec vs committed artifact vs running clusterA rejected config keeps the last good state (never a partial apply), surfaces the rejection, and retries with backoff; a fresh proxy triggers a startup reconcile push. The renderer stamps a minimum proxy version; a proxy that does not understand the config rejects it loudly — the version handshake that prevents silent misrouting across a version mismatch.
Migrating a hand-authored install
If you are coming from a hand-written deployment — for example the first consumer's deploy/nomad/mytask.hcl plus an in-repo proxy vhost Caddyfile:
- Adopt the job into a spec.
pfy nomad adoptreads the existing job file, extracts what maps onto the spec (image, ports, checks, resources), writes the spec to the Manager, renders, and prints the residual diff for manual triage intoextra_hclor a template request. - Move the vhost into Manager data. The in-repo Caddyfile vhost becomes
Deployment.domains+ upstream; the vhost block renders and pushes automatically — delete the hand-written block. - Delete the hand-authored artifacts. Once
pfy deploy diffis empty, removedeploy/nomad/mytask.hcland the in-repo vhost. From then on, the spec is the source of truth and CI fails on any re-introduced hand edit. - Convert the platform stack. Write
install.yamlfor your platform install,pfy platform render, diff against the live jobs, reconcile intentional differences into spec/template fields until the diff is empty, then delete the hand-maintained*.nomad.
The exit criterion is zero hand-written HCL or Caddyfile anywhere in the repo or on the box.
See Also
- Nomad Deployment — cluster setup and the rendered platform jobs
- Proxy Deployment — the rendered Caddyfile and admin-API push
- Application Routing — how vhosts route to applications
- Wave 3 Migration Guide — the coordinated upgrade