πŸͺ yoink

Configuration

Complete yoink.yaml schema reference β€” every field, its type, and its default.

Full schema for yoink.yaml. Canonical source: yoink/src/config.rs.

Top level

FieldTypeDefaultNotes
slugstring (optional)unsetBanner shown in the TUI's top chrome; use to mark a config (e.g. "PRODUCTION β€” TREAD CAREFULLY") so the operator can't miss which environment they're pointed at. Keep it short (single line).
hostslist of HostrequiredOne or more deploy targets.
deployDeploy{}Cross-service defaults (networks, etc.).
secretsSecretsunsetSecret provider config. Required if any service uses secrets: / env_from_secrets:.
registryRegistryunsetImage-pull credentials. Skip when every service is locally-built (yoink ships build artifacts from your docker daemon, no auth needed) or when pulling exclusively from public registries.
proxyProxyunsetBundled Caddy reverse proxy. Implicitly enabled when any service has a domain: set.
serviceslist of Service[]Services to deploy. Can be defined inline or split via include:.
hooksHooks{}Top-level hooks not scoped to a single service. Currently: pre_deploy:.
webhookslist of Webhook[]Outbound HTTP notifications fired on run-level events. Operator-side, best-effort.
includelist of glob[]Glob paths (relative to the config file) merged into services.

Host

Each host needs an address; that can be either a literal address: or, for cases where the address itself is sensitive (a public IP that you front through Cloudflare, a tailnet hostname you'd rather not commit), a sealed-secret reference via address_secret:. Set exactly one; neither or both is rejected at config load.

FieldTypeNotes
addressstringHostname or IP. Anything your local SSH client accepts: a raw IP, a DNS name, a ~/.ssh/config alias, or a tailnet hostname. Mutually exclusive with address_secret.
address_secretstring (optional)Name of an entry in your sealed-secrets bundle holding the host's address (IP or hostname). Resolved at config-load time after the bundle decrypts. Use this when the deploy config sits in a public repo and the host's address is sensitive. Mutually exclusive with address.
userstringSSH user. Must be in the docker group on the host (or be root).
ssh_key_secretstring (optional)Name of an entry in your sealed-secrets bundle holding a PEM-formatted SSH private key. When set, yoink decrypts the key into a per-process tempfile (mode 0o600) and uses it for this host's SSH connections, both the docker daemon connection and the pre-flight ssh probe. Lets you ship the deploy key with the repo (encrypted at rest in secrets.age) instead of relying on every operator's personal ssh-agent.
hosts:
  - { address: prod-eu-1, user: deploy }
  - { address: prod-us-1, user: deploy }

  # Deploy-key-in-repo flavour (for fresh VPS hosts without
  # ssh-agent already configured for them):
  - address: 1.2.3.4
    user: root
    ssh_key_secret: PROD_HOST_SSH_KEY

  # Address-in-repo-too flavour: the IP lives in `secrets.age`,
  # never in the committed yaml. Resolved at deploy time.
  - address_secret: DOCS_HOST_IP
    user: deploy
    ssh_key_secret: DOCS_DEPLOY_SSH_KEY

yoink hosts add --address-secret <KEY> --user <USER> --name <NAME> writes the sealed-address fragment shape. --name is required (no literal address to derive a fragment filename from).

Deploy

FieldTypeDefaultNotes
networkslist of string[]Declared docker networks. Services attach via services[].networks:. Yoink creates each one if missing.

Secrets

Two providers, selected by the provider: tag.

provider: age (default, batteries-included)

A single sealed dotenv file committed alongside yoink.yaml, decrypted at deploy time with the operator's age identity (private half) resolved in priority order:

  1. YOINK_AGE_KEY env (raw key): CI / managed-env contexts
  2. YOINK_AGE_KEY_FILE env (path): explicit override
  3. ~/.config/yoink/keys/*.key: laptop default; yoink scans the dir and picks whichever key's public half matches one of recipients: below
  4. ~/.config/yoink/age.key: legacy single-key fallback

The keys-dir scan (#3) is what makes yoink secrets key generate work without per-project env-var setup; multiple projects with distinct identities coexist there, picked by recipient match.

FieldTypeDefaultNotes
providerstringrequiredage
recipientslist of string[]Public age recipients (age1...), one per principal that needs to decrypt. New writes are sealed against every entry; decryption only needs one matching identity. Validates non-empty at config-load.
filestringsecrets.ageSealed file path, relative to the config file's directory. .. and absolute paths are rejected.
profilesmap of SecretsProfile{}Named env-shape recipes for downstream consumers (Terraform state-backend creds, provider tokens, CI shell exports). Consumed by yoink secrets env --profile <NAME> and by hooks.pre_deploy[].secrets_profile. See Secrets profiles.

The recipient/identity split is the asymmetric-keypair mental model; see Sealed secrets (age) in the secrets guide for the full explanation.

Secrets profiles

A profile names "what env shape does one downstream tool expect" and lets the same recipe drive both an operator shell (Taskfile, CI workflow) and a yoink pre-deploy hook. Without profiles, the same bundle-key β†’ env-var-name mapping is hand-encoded in N Taskfiles plus N CI workflows, with a separate unset block per consumer for env-leak cleanup; profiles consolidate it next to the bundle that supplies the values.

SecretsProfile
FieldTypeDefaultNotes
includelist of string[]Allowlist of bundle keys this consumer cares about. Empty = "every key in the bundle." A listed key missing from the bundle is a hard error at resolve time β€” loud-fail beats silently feeding empty creds.
renamemap of string→string{}bundle_key → env_var_name. Renames apply after include; keys not in the map keep their original name. Targets must be POSIX env-var names ([A-Za-z_][A-Za-z0-9_]*); mixed case is allowed because Terraform's TF_VAR_<varname> puts the (lowercase) variable name in the suffix.
unsetlist of string[]Env vars to clear in the importing shell before the exports land. Solves the "devbox init_hook leaks B2_ENDPOINT and the b2 SDK underneath the terraform provider mis-routes auth" class of issue. Operator-shell-only β€” a hook runs in a fresh container with no parent env to clear; referencing an unset profile from a hook is a config-validation error.

Profile names must match [a-z0-9-]+ (lowercase letters, digits, hyphens).

secrets:
  provider: age
  recipients: [age1…]
  profiles:
    terraform-cloudflare:
      include: [CLOUDFLARE_API_TOKEN,
                TFSTATE_B2_KEY_ID, TFSTATE_B2_APPLICATION_KEY]
      rename:
        TFSTATE_B2_KEY_ID:          AWS_ACCESS_KEY_ID
        TFSTATE_B2_APPLICATION_KEY: AWS_SECRET_ACCESS_KEY
      unset: [B2_ENDPOINT, B2_BUCKET_NAME]

Use it from a Taskfile:

env:
  YOINK_TF:
    sh: cd ../../deploy-prod && yoink secrets env --profile terraform-cloudflare
tasks:
  tf:plan:
    cmds:
      - eval "$YOINK_TF" && terraform plan

Or from a yoink pre-deploy hook (subprocess flavor β€” runs on the operator's machine):

hooks:
  pre_deploy:
    - name: cloudflare-tf-apply
      working_dir: ../terraform/cloudflare
      cmd: ["sh", "-c",
            "terraform init -input=false && terraform apply -auto-approve"]
      secrets_profile: terraform-cloudflare

The profile's unset: applies on subprocess hooks (yoink calls Command::env_remove for each entry); container hooks reject it because there's no parent env to clear. See Run Terraform from a hook for the full walkthrough.

In CI, when GITHUB_ENV is set in the environment, yoink secrets env auto-prepends echo '::add-mask::<value>' lines for every revealed value β€” no per-key copy-paste needed; new keys added to the profile are masked automatically.

Bootstrap: yoink secrets key generate writes a fresh identity to ~/.config/yoink/keys/<recipient>.key (mode 0600) by default and prints the matching recipient (age1…, public; paste into recipients: above). --out PATH writes to a specific file at mode 0600 instead. --print sends the secret to stdout for piping into a CI secret store (… --print | gh secret set YOINK_AGE_KEY). yoink secrets key public re-derives the recipient from whichever identity yoink would use right now (handy "is the key in my shell the same one yoink.yaml expects?" check). See the secrets guide.

provider: command (bring-your-own-tool)

Yoink invokes the configured command and reads a secrets bundle from stdout. Format auto-detects between dotenv and JSON. Lets operators wire any external manager (Doppler, 1Password, HashiCorp Vault, AWS Secrets Manager, the Infisical CLI, Bitwarden, …) without yoink growing first-party integrations for each.

FieldTypeDefaultNotes
providerstringrequiredcommand
commandlist of stringrequiredArgv to spawn. First element is the binary, the rest are arguments. No shell interpretation; wrap in ["sh", "-c", "..."] if you need pipes.
formatstringautoauto inspects the first non-whitespace byte ({ β†’ JSON, else dotenv). dotenv / json force the parser.
secrets:
  provider: command
  command: ["doppler", "secrets", "download", "--no-file", "--format", "env"]

See the secrets guide for per-tool wiring.

Registry

FieldTypeNotes
serverstringRegistry hostname (e.g. ghcr.io).
username_secretstringName of the secret in your provider holding the username.
password_secretstringName of the secret holding the token/password.

Yoink resolves these at deploy time and passes them as X-Registry-Auth on every pull.

Proxy

The bundled Caddy reverse proxy. See the proxy guide for the full operator-side picture.

FieldTypeDefaultNotes
enabledboolimplicit when any service has domain:Force-on when no service has a domain (e.g. routes are only configured via global_handlers:).
emailstringunsetLet's Encrypt registration email. Required when any service uses tls: auto. Not used when proxy.tls: is set.
imagestringcaddy:2Pre-built caddy image (registry-pulled). Mutually exclusive with xcaddy:.
xcaddyXcaddyunsetCompile a custom caddy on each proxy host with plugins. Mutually exclusive with image:.
cert_volumestringyoink_caddy_dataNamed volume for ACME state and certs. Don't delete casually β€” Let's Encrypt rate-limits aggressively.
bindstringunset (all interfaces)Host IP to bind :80 and :443 to. Common use: bind to a Tailscale IP so the proxy is reachable only over the tailnet. The admin port stays on 127.0.0.1 regardless.
tlsProxyTlsunsetProxy-level TLS β€” every routed service inherits this cert (and optional mTLS) by default. ACME implicitly off when set.
config_extrastring (JSON)unsetTop-level Caddy JSON snippet, deep-merged into the rendered config before /load. Escape hatch for global settings yoink doesn't model as typed fields β€” trusted_proxies, storage, plugin app blocks.
global_handlerslist of string (JSON)[]Caddy handlers (and/or routes) that run for every request before any per-service route matches. The natural place for proxy-wide concerns: CrowdSec bouncer, Coraza WAF, fleet-wide rate limiting.
global_handlers_afterlist of string (JSON)[]Same shape as global_handlers:, but the chain runs after per-service routes match. Use for concerns that need the upstream's response (response headers, access-log shaping, post-processing).

Xcaddy

Builds a custom caddy on each proxy host using xcaddy. Content-addressed by build inputs, tagged locally as yoink-caddy:<hash>; subsequent up runs short-circuit until plugins or version change. No registry needed.

FieldTypeDefaultNotes
pluginslist of stringrequired (non-empty)One entry per caddy module. Bare module path or pinned (module@version) β€” same syntax as xcaddy build --with. Sorted alphabetically before hashing/rendering.
caddy_versionstringxcaddy's latest tagged releaseCaddy git tag to compile (e.g. v2.8.4). Pinned values pass verbatim to xcaddy build.
base_imagestringcaddy:2Runtime image. Override only to bind to a specific caddy patch version.
builder_imagestringcaddy:2-builderBuild stage (carries xcaddy + Go toolchain). Pin to caddy:<v>-builder to also pin the xcaddy CLI.
replacelist of string[]Forwarded as xcaddy build --replace entries. Useful for testing a local fork of a plugin.

ProxyTls

When set, every routed service uses this cert by default. Per-service tls_cert_secret: / tls_key_secret: overrides remain available for the rare different-cert-per-service case. :80 β†’ :443 redirect auto-emitted.

FieldTypeDefaultNotes
cert_secretstringrequiredSealed-secret name holding the PEM cert (full chain).
key_secretstringrequiredSealed-secret name holding the PEM private key.
client_authClientAuthunsetOptional mTLS (e.g. Cloudflare origin-pull).

ClientAuth

FieldTypeDefaultNotes
modeenumrequire_and_verifyrequest / require / verify_if_given / require_and_verify. Most setups want the strict default.
trust_pool_secretstringrequiredSealed-secret name holding the PEM CA bundle that signs accepted client certs.

Service

FieldTypeDefaultNotes
namestringrequiredUsed as container name and DNS alias. Unique within the config.
imagestringrequiredImage reference. Bare names (no /) trigger no-registry mode.
tagstringunsetLiteral tag. Overridable at deploy via --tag <name>=<value>.
buildBuildunsetLocal-build config. Required for --build flow.
hostslist of stringall hostsRestrict which hosts run this service. Strings match hosts[].address.
depends_onlist of string[]Other service names. Topological-sort drives wave ordering. Must be a DAG.
networkslist of string[<deploy.network default>]Docker networks to attach. Must be declared in deploy.networks.
secretslist of string[]Secret names exposed as env vars of the same name.
env_from_secretsmap of string{}ENV_VAR_NAME: SECRET_NAME β€” exposes a secret under a different env name.
envmap of string{}Plain env vars. Don't put secrets here β€” they end up in container labels.
labelsmap of string{}Extra docker labels.
descriptionstringunsetWritten to the container as org.opencontainers.image.description; surfaced in the TUI.
kindenumunsetInternal marker used by yoink-synthesized services (the proxy). Operators don't set this.
pre_deploylist of Hook[]One-shot hooks that run before the swap (per service-wave; see Hook for the once-per-up vs once-per-wave distinction).
domainstring | list of stringunsetHostname(s) for the bundled proxy to route. Presence enables proxying. List form is the apex+www / multi-domain pattern. See the proxy guide.
path_prefixstringunsetMatch this path glob in addition to domain: so multiple services can share a hostname. Yoink orders routes so path-constrained ones come before catch-alls.
tlsenumauto (when domain: set)auto = ACME via Let's Encrypt; off = HTTP only; cert = inline cert from sealed secrets (set tls_cert_secret: / tls_key_secret:).
tls_cert_secretstringunsetSealed-secret name holding the PEM cert. Required when tls: cert.
tls_key_secretstringunsetSealed-secret name holding the PEM private key. Required when tls: cert.
upstream_h2cboolfalseTalk to backend over HTTP/2 cleartext. Required for native gRPC backends (Tonic, grpc-go, grpc-java).
compressionboolfalseEmit encode gzip zstd. No-op behind a CDN.
canonical_domainstringunsetOne of the domain: entries. Yoink 308-redirects every other entry to it (apex/www patterns).
hstsbooltrue for TLS sitesEmit Strict-Transport-Security: max-age=31536000; includeSubDomains.
caddy_extra_jsonstring (JSON)unsetRaw Caddy handler JSON merged into the route. Routes auto-wrapped in subroute. See the snippets cookbook.
caddy_extra_caddyfilestring (Caddyfile)unsetSame as above but in Caddyfile syntax. Yoink shells out to caddy adapt at render time (needs docker on operator). Mutually exclusive with caddy_extra_json:.
runRunrequiredRuntime config (port, replicas, healthcheck, options).

Build

FieldTypeDefaultNotes
contextstring.Build context, relative to the config file.
dockerfilestringDockerfileDockerfile path, relative to the context.
targetstringunsetMulti-stage build target.
argsmap of string{}--build-arg KEY=VALUE pairs.
extra_argslist of string[]Raw extra args passed to the build command.

Run

FieldTypeDefaultNotes
portu16requiredContainer port to healthcheck and route to.
healthcheck_pathstringunsetHTTP path. If unset, yoink does a TCP-connect probe instead.
healthcheck_timeoutduration60sHow long to wait for a healthy response on the new container.
replicasu321Number of containers per host. β‰₯2 enables rolling swap.
drain_timeoutduration30sGrace period before SIGKILL on the old container.
entrypointlist of stringimage defaultOverride ENTRYPOINT.
cmdlist of stringimage defaultOverride CMD.
publishlist of string[]Host port mappings ("80:80", "443:443/udp").
bindslist of string[]Host bind mounts ("src:dst:mode"). Default mode is :ro.
volumeslist of string[]Named docker volumes.
fileslist of string[]Same shape as binds, but the source content is hashed into spec_hash (config files trigger a redeploy on edit).
optionsRunOptionshardened defaultsResource limits + security flags.

RunOptions

The hardened defaults make new containers prod-safe out of the box. Override per-service when needed.

FieldTypeDefaultNotes
memorystringunsetMemory limit ("512Mi", "1Gi").
cpusstringunsetCPU limit ("1.5" = 1.5 cores; "2" = 2 dedicated cores).
pids_limitu321024Fork-bomb bound.
cap_droplist of string["ALL"]Linux capabilities dropped.
cap_addlist of string[]Capabilities re-added. Use sparingly (e.g. NET_BIND_SERVICE for Caddy).
security_optlist of string["no-new-privileges:true"]Docker security options.
read_onlybooltrueRead-only rootfs. Combine with tmpfs: for writable scratch.
initbooltrueRun with tini as PID 1 (zombie reaping + proper SIGTERM).
tmpfsmap of string{}mount_path: "size=N,mode=NNNN". Auto-applies noexec,nosuid,nodev.
restartstringunset (docker default no)Docker restart policy. Common values: unless-stopped (recommended for long-running services), on-failure, always. Set explicitly; yoink doesn't impose a default.
userstring"65534:65534" (nobody)UID/GID. Default runs non-root. Override with "0:0" for images that genuinely need root, or a specific uid:gid ("1000:1000", "redis") when the image has pre-baked file ownership.
network_aliaseslist of string[]Extra DNS aliases on the attached networks. The container always gets the service name as an alias regardless; this field adds more names (e.g. for legacy hostname compat).
deviceslist of string[]Host devices to expose. Each entry is <host-path>[:<container-path>[:<perms>]] (docker's --device syntax). <perms> is some combination of r, w, m; defaults to rwm. Both bind-mounts the device file and adds it to the cgroup devices.allow list. Narrower than --privileged β€” only the listed devices become accessible. Common uses: /dev/nvidia* (GPU), /dev/dri (Intel/AMD VAAPI), /dev/ttyUSB* (USB serial), /dev/fuse (with cap_add: [SYS_ADMIN]), /dev/snd (audio).

See Security defaults for the full picture.

Hooks

Two places hooks live:

  • Per-service services[].pre_deploy: runs before that service's wave starts (so a database migration finishes before the api container that depends on it is even pulled). Once per up, on the first host that has the service.
  • Top-level hooks.pre_deploy: runs before any service-wave starts. Once per up, on the first host overall. Use for cluster-wide concerns that don't belong to a single service.
hooks:
  pre_deploy:
    - name: cluster-bootstrap
      image: my/bootstrap-tool
      tag: v1

Both shapes use the same Hook entry shape below. A non-zero exit aborts the deploy before the swap; the old container stays live.

Hook

A hook runs in one of two flavors, distinguished by the presence of image::

  • Container hook (image: + tag: set): yoink pulls the image and runs cmd: inside docker on the first host. Use for pinned tool images or isolation from the operator's filesystem.
  • Subprocess hook (no image:, no tag:): yoink invokes cmd[0] directly on the operator's machine (or the CI runner). Inherits the parent env. Use for tools already on PATH (terraform, dbmate, alembic, gcloud, …) β€” see Run Terraform from a hook.
FieldTypeDefaultNotes
namestringrequiredHook identifier (used in logs, lock label).
imagestringunsetContainer hook only. Usually the same as the runtime service.
tagstring | {service: <name>}unsetContainer hook only. Literal tag, or {service: api} to mirror the runtime tag. Required when image: is set; rejected otherwise.
entrypointlist of stringimage defaultContainer hook only. Override ENTRYPOINT.
cmdlist of stringimage defaultContainer: override CMD. Subprocess: required; cmd[0] is the binary, the rest are args.
working_dirstring (path)yoink.yaml's dirSubprocess hook only. Resolved relative to yoink.yaml's directory.
envmap of string{}Plain env vars.
secretslist of string[]Secret names exposed as env vars of the same name. Often a different role than the runtime (e.g. a migrate role).
env_from_secretsmap of string{}ENV_NAME: SECRET_NAME mapping.
secrets_profilestringunsetReference a named recipe from secrets.profiles. Profile's include joins secrets; profile's rename joins env_from_secrets. Profile's unset: applies on subprocess hooks (via Command::env_remove); rejected on container hooks (no parent env to clear). See Secrets profiles.

Webhooks

Outbound HTTP calls fired on RunStarted / RunFinished. Operator-side: the request goes from the box running yoink up, not from each host. Best-effort: every fire records a WebhookFired audit event whose ok field carries the outcome; a failure never aborts the deploy. See Notify chat / paging / observability on deploy for templating, secrets, and worked receiver examples (ntfy, Slack, Grafana).

webhooks:
  - name: slack-deploys
    url: "https://hooks.slack.com/${secret:SLACK_WEBHOOK_PATH}"
    on: [run_succeeded, run_failed]
    headers:
      Content-Type: application/json
    body: '{ "text": "{{ command }} {{ services | join(\", \") }}{% if error %} β€” {{ error }}{% endif %}" }'
FieldTypeDefaultNotes
namestringrequiredUnique across the webhooks: list. Appears in WebhookFired.name.
urlstringrequiredTemplated. Must resolve to http(s)://… after ${secret:NAME} substitution.
onlist of triggerrequiredSubset of run_started, run_succeeded, run_failed. At least one entry required.
methodGET | POST | PUTPOST
timeoutduration10shumantime syntax.
headersmap of string{}Each value templated.
bodystringunsetTemplated. Omit for GET-style receivers.

Templating runs in two passes per field: ${secret:NAME} literal substitution from the sealed bundle, then minijinja rendering with UndefinedBehavior::Strict. Available template variables: event, ok, command, services, deploy_id, actor, host, git_sha, yoink_version, error, ts. See Notify chat / paging / observability on deploy: Template context for the full table.

Tag overrides at deploy time

Any service or hook with tag: unset (or set to a literal that you want to override) accepts --tag at the CLI:

yoink up --tag api=$(git rev-parse HEAD) --tag web=$(git rev-parse HEAD)

tag: { service: api } mirrors the runtime tag, useful for pre_deploy migrations that must use the exact image being deployed.

Includes

include: globs are resolved relative to the config file's directory. Each matched file is parsed as a partial config; only services: and hooks.pre_deploy: are permitted in fragments and they merge into the entry config's lists. All other top-level fields (hosts, deploy, secrets, registry, proxy, slug) belong only in the entry config.

# yoink.prod.yaml
hosts: [...]
deploy: { networks: [...] }
include:
  - services/prod/*.yaml
# services/prod/api.yaml
services:
  - name: api
    image: ghcr.io/you/api
    # ...

Environment-variable substitution

Any ${VAR} reference in yoink.yaml (or in a fragment loaded via include:) is substituted against the process environment at config-load time, before YAML parsing.

hosts:
  - address: ${HOST_IP}
    user: root
services:
  - name: hello
    domain:
      - ${HOST_IP}.nip.io

Run with HOST_IP=1.2.3.4 yoink up …. Useful for throwaway-host recipes, ephemeral preview environments, and any other case where one of the values isn't known until invocation time.

Rules:

  • Only the unambiguous ${NAME} form is recognized. Bare $NAME and lone $ characters pass through unchanged so values containing literal dollar signs are unaffected.
  • NAME must match [A-Za-z_][A-Za-z0-9_]*. Anything else (including ${HOST-IP}, ${1BAD}, or unterminated ${) errors with a snippet pointing at the bad reference.
  • Missing variables are a hard error rather than silent empty substitution; a quietly-empty address: produces baffling failures further down the deploy.
  • The POSIX ${NAME:-default} form supplies a fallback when NAME is unset. Useful for commands that parse the full config but don't actually use the value (e.g. yoink secrets seal against a config whose hosts/domains reference ${HOST_IP}). Set the var explicitly when you do mean to use it.
  • For long-lived secrets, prefer secrets: (sealed or provider: command) over passing values via env. The substitution path is intended for routing parameters (host IPs, hostnames, port numbers), not credentials.

See also

  • CLI reference β€” every subcommand and flag.
  • Examples β€” three complete annotated yoink.yamls (hobby, polyglot, production).
  • Secrets β€” the mental model behind the secrets: block.
  • Reverse proxy β€” the schema and behavior of the proxy: block.

On this page