Skip to main content
This is Part 1 of the Upsun Task Containers series, where we build up — episode by episode — from a simple background job to a fleet of autonomous AI agents. In this first episode, no AI yet: we focus on the foundation, using a cron to trigger an on-demand task container for a heavy workload.
Every team eventually hits the same wall: a nightly job that almost fits in a cron. A database import, a full search reindex, a bulk export — something that runs for several minutes, eats memory, and competes with the very application that is supposed to be serving traffic. You bump the cron, you cross your fingers, and you hope it finishes before the next schedule fires. There is a cleaner way. Instead of running the heavy work inside your application container, you can keep the cron tiny and let it trigger a dedicated task container — an on-demand, run-to-completion workload with its own image, its own resources, and its own timeout. The cron becomes a trigger; the task does the work.
Task containers are currently in prerelease. To enable them on your project, open this prepopulated support ticket and add your project ID before submitting.

Prerequisites

Before starting, make sure you have:
  • An Upsun account with task containers enabled (see the note above)
  • The Upsun CLI installed
  • An existing application deployed on Upsun (this guide uses a PHP/Symfony app, but the pattern is language-agnostic)
  • A heavy application command you want to offload (for example bin/console app:reindex or a database import script)

Why not just run it in the cron?

Crons are the right tool for scheduling, but they run inside your application container. That means the heavy job shares CPU and memory with the process serving HTTP traffic, and it is bound by the app’s resource profile. A long reindex can starve your web requests, and a deploy will cancel a running cron mid-flight. A task container flips the model. It is a separate, on-demand workload that is injected into your environment’s cluster only while it runs, then removed when the command exits. Here is how the four workload types compare: The pattern we build in this episode keeps the best of both worlds: a lightweight cron does nothing but fire an API call, and a task container with its own resources does the heavy lifting. Upsun Task Container Flow Notice how the heavy CPU and memory live entirely in the task container’s own profile: the application container stays small and responsive, while the reindex gets the resources it needs only for the seconds it runs.

Step 1 - Define the task

Tasks are declared at the top level of .upsun/config.yaml, alongside applications: and services:. Let’s define a task that runs a heavy search reindex command against the same database the app uses.
.upsun/config.yaml
A few things worth calling out:
  • type uses the same runtime syntax as an application. Here the task reuses the app’s PHP image so the application command and its dependencies are available.
  • run.command is the heavy job. It runs to completion, not as a daemon. If you need setup steps that require service access, chain them at the start of the command, for example php bin/console cache:warmup && php bin/console app:reindex.
  • run.timeout caps the run at one hour (the default). The maximum is 86400 (one day). On timeout, the platform sends SIGTERM, then SIGKILL after a short grace period.
  • relationships give the task its own connection to the database. Inside the task container, these appear in PLATFORM_RELATIONSHIPS exactly as they do for an application.
  • mounts declare writable directories. Remember that instance and tmp mounts are reset between runs — use a storage or service mount if you need to persist data across runs.
A task gets its own resources, configured against the task name with upsun resources:set. Give the reindex task the CPU and memory it needs without inflating your always-on application — billing is per-second for the duration of each run.

Step 2 - Authorize the app to trigger the task

A task does not trigger itself. Something has to call the Upsun API — in our case, a cron running in the application container. For that, the application needs permission to operate the task. Declare it with a workload authorization:
.upsun/config.yaml
With this in place, the application container can request a short-lived token and call the task run endpoint — no long-lived credentials stored anywhere.

Step 3 - Trigger the task from a cron

Now the lightweight part. The cron does not run the reindex itself; it mints a token and POSTs to the task run endpoint. Every Upsun container exposes a local credential broker at http://localhost:8200/oauth2/token that exchanges the container’s ambient credentials for a short-lived access token. Add a small trigger script to your repository:
bin/trigger-reindex.sh
Then wire it into a cron that runs on schedule but returns in milliseconds:
.upsun/config.yaml
The cron itself is now trivial: it makes one HTTP call and exits. The reindex runs in its own container, with its own resources, completely decoupled from the app serving traffic.
Multiple task runs can execute in parallel up to a default limit of 3. Further triggers queue behind running ones, so a cron that fires while a previous run is still going will not pile up unbounded.

Step 4 - Pass run-time variables

Often the same task needs to do slightly different work depending on when it runs. Pass run-time variables in the POST body and read them as environment variables inside the task:
bin/trigger-reindex.sh
Inside the task, BATCH_SIZE and INDEX are available as regular environment variables your command can read. In a Symfony command, that is simply $_ENV (or getenv()):
src/Command/ReindexCommand.php
Only use variables for non-sensitive values. Set secrets such as API keys or passwords ahead of time with the CLI so they stay out of version control and request bodies, for example:
Terminal

Step 5 - Deploy and observe

Commit your configuration and push:
Terminal
Each task invocation creates an activity — the same mechanism used for deploys, backups, and crons — which gives you a unique activity ID, live status (pending, in_progress, complete, cancelled), and streamed logs. You can watch the activity feed while the task runs:
Terminal
You can also trigger the task manually to test it, without waiting for the cron, by running the same script over SSH:
Terminal

Wrapping up

You have moved a heavy, long-running job out of your application’s critical path. The cron stays lightweight and predictable, while the actual work runs in a dedicated task container with its own image, resources, and timeout — billed only for the seconds it runs. This is the foundation for everything that follows. In Part 2, we keep the exact same cron-triggers-a-task pattern, but instead of a database command we launch an OpenCode AI agent that reads your cron and application logs and writes you a health report. See you in the next episode.
Last modified on August 20, 2026