Building & Deploying ​
With your infrastructure provisioned by yolo sync, shipping code is one command:
yolo deploy productiondeploy builds the image and ships it. You rarely need build on its own — deploy runs it for you.
What deploy does ​
yolo deploy runs the build, then the rollout, end to end:
- Build — runs the full
buildpipeline below (image built and pushed to ECR). - Push assets — uploads compiled assets to the S3 asset bucket (served via CloudFront when configured).
- Register task definition — registers a new ECS task definition revision pointing at the freshly pushed image.
- Run deploy hooks — runs your manifest's
deploycommands (e.g.php artisan migrate --force) as a one-off ECS task before traffic shifts. - Update the service — points the ECS service at the new revision and starts the rolling deployment.
- Wait for healthy — polls until the new tasks pass their health checks.
- Point DNS — UPSERTs the Route 53 record(s) once the deployment is healthy.
- Recap — prints a summary of what's now running (each service's task spec, count, scaling, and new revision) plus a link to the app's CloudWatch dashboard — the same view
yolo statusshows.
To watch a rollout as it happens, or check what's running at any time, run yolo status <env> — a live dashboard of services, load, scaling, and any in-progress deploy.
The build ​
yolo build production prepares and packages the image:
- Purge the build directory and stage a clean copy of your app (see the staged copy).
- Pull
.env.<environment>from S3 and stamp inAPP_VERSION(andASSET_URL, mirrored intoVITE_ASSET_URLfor Vite, if a CDN is configured). - Run your manifest's
buildhooks (composer install,npm run build, …). With Inertia SSR enabled, this is also wherenpm run buildproduces the SSR bundle that gets baked into the image. - Generate the entrypoint and supervisord config (see The Container Image), gated by an Octane preflight: YOLO hard-fails the build if
laravel/octaneisn't in your committedcomposer.lockproduction requirements — the web role runsoctane:start, so a missing (orrequire-dev-only) octane would crash-loop the container on boot. (Skipped whentasks.web.octane: false, where the web tier runs FrankenPHP classic mode and needs no octane package.) - Log in to ECR, build the Docker image (re-resolving the Dockerfile's base image — see the base image), probe it for the runtimes it needs —
supercronicalways (the scheduler's cron),nodewhentasks.web.ssris on, the metrics-enableddocker/Caddyfilewhen the web tier autoscales — hard-failing the build if one is missing (see Runtime checks), then push it.
The staged copy ​
The build never runs in your working tree. YOLO rsyncs the app into .yolo/build first, dropping everything the image has no business carrying: .git, CI config, editor and tooling caches, node_modules, tests, nested agent worktrees, and the runtime contents of storage. Your build hooks then run in that copy, so a hook can scribble on the tree without touching your checkout.
One nuance of staging the tree without its .git: tools that discover their input files by walking the directory and honouring .gitignore only do so inside a git repository, so in a bare copy they fall back to scanning everything — node_modules and vendor included. Tailwind v4's automatic source detection works this way, and on a dependency-heavy app the difference is a vite build that takes minutes instead of seconds. YOLO therefore creates an empty .git directory in the build copy after staging, which restores the repository boundary (the staged .gitignore files do the rest) and produces identical compiled output. The placeholder is removed again before the image is built, so it never reaches a layer, and the real .git is never copied — its history can run to hundreds of megabytes.
TIP
If your manifest works around this with a mkdir -p .git build hook, you can drop it.
The image-building steps (4–5) only run when your manifest declares tasks — with none declared, the build stops after staging the build directory (env file baked, build hooks run) and no image is produced.
Zero-downtime rollout ​
The rollout uses the ECS deployment circuit breaker. New tasks must pass their ALB health checks before the old ones are drained and stopped; if the new version fails to stabilise, ECS automatically rolls back to the previous task definition. Combined with the graceful-shutdown drain window, a healthy deploy serves traffic with no dropped requests — and a broken one never takes the service down.
App version ​
Every build is stamped with an APP_VERSION. By default it's a timestamp in the form y.W.N.Hi (two-digit year, ISO week, ISO weekday, hour-minute) — for example 26.22.5.1430. Override it to stamp a meaningful tag, such as a GitHub release name:
yolo deploy production --app-version=26.22.1The version must start with the current year.week prefix (e.g. 26.22) — this keeps versions monotonic and prevents accidental stale tags. Both 26.22 and the non-zero-padded 26.<week> forms are accepted.
TIP
The week prefix is computed in your manifest's timezone (defaulting to UTC). Set timezone to your team's timezone so a version cut just before midnight on a Sunday doesn't trip the validation. See timezone.
Rolling back ​
To return to a version you've already shipped, use yolo rollback — it re-deploys an image that's still in ECR, skipping the build entirely:
yolo rollback productionThe interactive picker lists your recent deployments (newest first, the running one marked (current)); pick one and it re-points the service through the same circuit-breaker-guarded rollout as a deploy — re-running your deploy hooks against the rolled-back image. Code and assets are versioned and immutable, so they revert cleanly — but the database does not (migrate is forward-only and never reverts the schema), so rollback warns before continuing: a rollback past a destructive migration can break against the old code.
WARNING
Rollback reverts code and assets, never the schema. Roll back across additive migrations freely; across a destructive one, only after confirming the old code runs against the new schema.
Hooks: build vs deploy vs start ​
Three manifest arrays run shell commands at different points in an image's lifecycle:
| Hook | Runs | Where | Use for |
|---|---|---|---|
build | At build time, on your machine | the build context | composer install, npm run build, asset compilation |
deploy | Once per deploy, before traffic shifts | a one-off ECS task | php artisan migrate --force |
start | On every container start — a deploy, a scale-out replica, a replaced task, a one-off yolo run | the entrypoint, before the role's process | php artisan optimize (cache config/routes/views) |
The deploy task templates on your management-tier service — a dedicated scheduler if you've extracted one, else a standalone queue, else web (the same scheduler → queue → web order yolo run uses). It's a one-off task, so it just runs the hooks once and exits. It runs at a fixed 1 vCPU / 2 GiB regardless of that service's task size, so migrations and other hooks aren't throttled by a thin management tier (a queue/scheduler defaults to 0.25 vCPU).
The start hooks run inside every container the image boots, so keep them idempotent and fast — they also run ahead of the deploy task itself, since that one-off task boots through the same entrypoint.
build:
- composer install --no-dev --optimize-autoloader
- npm ci
- npm run build
deploy:
- php artisan migrate --force
start:
- php artisan optimizeHiding progress output ​
All stepped commands accept --no-progress to suppress the live progress UI — useful in CI logs:
yolo deploy production --no-progressRunning commands in the container ​
To open a shell or run a one-off command inside a running task (via ECS Exec):
yolo run production # interactive shell
yolo run production --command="php artisan tinker" # one-off command
yolo run production --command="php artisan queue:restart" --group=web,queueECS Exec is on by default (tasks.web.enable-execute-command defaults to true), so this just needs the AWS Session Manager plugin installed locally (yolo init offers to install it). See yolo run.
