Skip to main content

component-tasks

Renders CronJobs, worker Deployments, manual jobs and the migrations job for an app. All four share the parent app's image, pull secret, and envFrom secret via the global: block.

When to use it​

  • Migrations: one command that runs once per deploy, before any new pod starts (./craft up, prisma migrate deploy).
  • Cron: run a command on a schedule (./craft cleanup, …).
  • Worker: long-running process that consumes a queue (./craft queue:work).
  • Manual: one-off command you trigger by hand (backfills, cache warm). Rendered as a suspended CronJob so it never auto-fires.

Syntax​

component-tasks:
migrations:
commands:
- ["./craft", "up"]
- ["./craft", "cacheable/flush/config"]
cron:
- name: nightly-cleanup
schedule: "0 3 * * *"
command: ["./craft", "cleanup"]
resources:
requests:
cpu: 50m
memory: 64Mi
workers:
- name: queue-default
command: ["./craft", "queue:work"]
replicas: 2
- name: queue-emails
command: ["./craft", "queue:work", "emails"]
replicas: 1
manual:
- name: backfill-slugs
command: ["./craft", "resave/entries", "--update-slugs"]
- name: cache-warm
command: ["./craft", "cache:warm"]
  • Empty lists and a migrations block without commands (the defaults) produce no resources.
  • Nothing here is rendered while the effective image tag is the bootstrap placeholder initial (the app charts skip their web Deployment/StatefulSet the same way): that image does not exist, and the migrations hook would hold the sync until it times out. Everything appears with the first real tag Kargo writes.
  • Generated names: <global.nameOverride>-migrations, <global.nameOverride>-cron-<name>, <global.nameOverride>-worker-<name>, and <global.nameOverride>-manual-<name>.

Migrations​

One batch/v1 Job annotated as an ArgoCD Sync hook in sync wave -1. ArgoCD applies the waves in order and waits for each to be healthy, so the Job only starts once everything it needs is in place: the app-secrets ExternalSecret (wave -5), shared volume PVCs (-3), and, when enabled, Redis and the MySQLApp claim (-2, healthy once the database user and grants are Ready). It runs to completion with the new image before wave 0: the web StatefulSet, the cron jobs and the workers are only updated once the Job has succeeded. It is not a PreSync hook because that phase runs before any of those resources exist, so on a first deploy the Job would start without its secret or database. A failed Job fails the sync and leaves the previous version serving, with the log on the Job. The previous Job is deleted when the next sync starts, so the last run's log stays available in between.

The hook fires on every sync (image bump, values change), so the commands must be idempotent. craft up, migrate/all, doctrine:migrations:migrate and prisma migrate deploy are. Pods of the previous version keep running while the Job executes, so migrations must be backward compatible for that window, the usual expand/contract rule.

Remove the migration from the app's .container-init.sh when you set this, and delete the script if migrating was all it did. The init container is for per-pod steps only, such as publishing assets to a shared volume or flushing a cache. Running migrations from the init container means they run on every pod start, and a failing migration turns into a crash loop that retries the same DDL against the database every backoff.

FieldRequiredDefaultNotes
commandsyes—List of commands, each in array form, run in order and chained with &&: the first failure stops the Job. Nothing is rendered without it.
backoffLimitno0Retries before the Job, and with it the sync, is failed
activeDeadlineSecondsno900Hard timeout for the Job
imagenoparent{ repository, tag, pullPolicy } override
resourcesno—Standard resource requests/limits

Cron fields​

FieldRequiredDefaultNotes
nameyes—Used in resource name + labels
scheduleyes—Standard cron expression
timeZonenoEurope/AmsterdamIANA zone the schedule is evaluated in; the chart-level cronTimeZone value sets the default for all cron entries of an app
commandyes—Container command (array form)
concurrencyPolicynoForbidSkip new run if previous still running
successfulJobsHistoryLimitno3
failedJobsHistoryLimitno1
startingDeadlineSecondsno—Drop jobs that miss their slot by this much
backoffLimitno—Retries before the Job is failed
activeDeadlineSecondsno—Hard timeout for the Job
restartPolicynoNeverPod restart policy
imagenoparent{ repository, tag, pullPolicy } per-task override
resourcesno—Standard resource requests/limits

Schedules are Amsterdam wall-clock time by default, including DST changes; nodes, pods and logs stay on UTC. Set component-tasks.cronTimeZone to change the default for an app, or timeZone on a single entry.

Worker fields​

FieldRequiredDefaultNotes
nameyes—
commandyes—
replicasno1
terminationGracePeriodSecondsno30
imagenoparentPer-worker override
resourcesno—
livenessProbeno—Raw probe spec (e.g. exec: { command: [...] })

Manual fields​

Same shape as cron, minus schedule and startingDeadlineSeconds.

FieldRequiredDefaultNotes
nameyes—Used in resource name + labels
commandyes—Container command (array form)
concurrencyPolicynoForbid
successfulJobsHistoryLimitno3
failedJobsHistoryLimitno1
backoffLimitno—Retries before the Job is failed
activeDeadlineSecondsno—Hard timeout for the Job
restartPolicynoNeverPod restart policy
imagenoparentPer-task override
resourcesno—

Triggering a manual job​

The rendered CronJob has suspend: true and a placeholder schedule (0 0 31 2 * — Feb 31, never fires). Run it on demand with:

kubectl create job <app>-manual-<name>-$(date +%s) \
--from=cronjob/<app>-manual-<name> \
-n <namespace>

Example:

kubectl create job myapp-manual-db-migrate-$(date +%s) \
--from=cronjob/myapp-manual-db-migrate \
-n myapp-staging

Generated resources​

A migrations.commands list → one batch/v1 Job with the ArgoCD Sync hook annotations (wave -1). Each cron entry → one batch/v1 CronJob. Each worker entry → one apps/v1 Deployment with restartPolicy: Always. Each manual entry → one suspended batch/v1 CronJob. All inherit image and pull secret from global.image / global.imagePullSecrets, with envFrom pointing at global.secretName.

Labels and the app Service​

Cron pods are labelled app: <global.nameOverride>, task: cron, task-name: <name>; worker pods the same with task: worker; manual job pods the same with task: manual; the migrations pod with task: migrations. They share the app label with the main workload but intentionally lack the component: web label, so the app's Service — which selects app and component: web (see App charts → Service and pod labels) — never sends HTTP traffic to a queue worker, cron, or manual pod.