Skip to content

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

ArtifactSource of truthCommand
Application Nomad jobDeployment spec on the Manager's Deployment entitypfy nomad render | deploy
Autoscaler scaling blockThe same deployment spec (scaling)rendered into the app job
Platform-core stack (Manager, proxy, Pocket ID, Postgres)Local install.yamlpfy platform render | deploy
Caddyfile prelude (globals, OAuth portal, Manager vhost)Local install.yamlpfy platform render
Per-application vhostsManager 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 template stanzas / {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):

FieldNotes
imagerepo/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)
countwith 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, namespaceNomad placement
extra_hcldelimited verbatim block
domainsproxy hostnames for this (application, environment)
upstream_overridefor 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

bash
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 back

Because 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

bash
# 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-version

pfy 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:

bash
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 productify app block with the Manager URL and {env.PFY_MANAGER_TOKEN}, the caddy-security OAuth2/OIDC portal, the Manager's own vhost, metrics) render from install.yaml at 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:

bash
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 cluster

A 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:

  1. Adopt the job into a spec. pfy nomad adopt reads 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 into extra_hcl or a template request.
  2. 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.
  3. Delete the hand-authored artifacts. Once pfy deploy diff is empty, remove deploy/nomad/mytask.hcl and the in-repo vhost. From then on, the spec is the source of truth and CI fails on any re-introduced hand edit.
  4. Convert the platform stack. Write install.yaml for 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