laravel/vapor-cli
The official CLI client for Laravel Vapor, the auto-scaling serverless deployment platform for Laravel powered by AWS Lambda. Manage your infrastructure, queues, databases, Redis clusters, and CDN from the command line.
Install it as a tool, not a dependency. The CLI is an operator client for the closed-source Vapor API, so it does not belong in require (it never loads in a request). Two valid placements:
# On a workstation: global, so `vapor` is on PATH anywhere
composer global require laravel/vapor-cli
# Per project: pinned and reproducible for CI
composer require --dev laravel/vapor-cli
./vendor/bin/vapor --version
Pin the major/minor in CI (laravel/vapor-cli:^1.70) — see Gotchas for why.
Authenticate once, then use tokens. vapor login stores a token in your home directory for interactive use; CI should instead export VAPOR_API_TOKEN from a secret store. Both are team-scoped credentials with infrastructure-destroying power, so treat them like AWS keys.
Where to look first, in this order:
vapor.yml in the repo root — the manifest is the infrastructure. Everything about memory, runtime, database, cache, queue workers, build and deploy hooks lives here, not in the CLI flags.vapor <command> --help — the CLI is self-documenting and is the fastest way to see current flag names for your pinned version.docs.vapor.build — the README deliberately defers to it. Bookmark it; there is no in-repo usage documentation, and no OSS support channel (support runs through the subscription).First use case — a staging deploy. The realistic first day is not "learn Vapor", it is "ship one environment":
vapor init # interactive: creates/links the project, writes vapor.yml
vapor env:pull staging # writes .env.staging locally from the remote secrets
vapor deploy staging # builds a Docker image, pushes to ECR, deploys to Lambda
vapor logs staging # tail CloudWatch output
vapor init is interactive and single-purpose, which is correct for onboarding but means it never appears in a pipeline. Get the manifest committed and reviewed before the first production deploy — that file is the thing your team will actually argue about in PRs.
Treat vapor.yml as reviewed infrastructure-as-code. It is the only durable artifact; CLI invocations should be thin wrappers over it.
id: 12345
name: my-app
environments:
staging:
memory: 1024
cli-memory: 512
runtime: php-8.3:al2
database: my-app-staging
cache: my-app-staging
build:
- 'composer install --no-dev --optimize-autoloader'
deploy:
- 'php artisan migrate --force'
production:
memory: 2048
cli-memory: 1024
runtime: php-8.3:al2
database: my-app-production
cache: my-app-production
queues:
- default
- emails
warm: 2
Duplicate environments deliberately rather than templating them: Vapor's per-environment blocks are the contract, and a generator between you and the manifest makes drift harder to spot.
Separate build hooks from deploy hooks. build runs inside the image (composer, asset compilation); deploy runs against the live environment (migrations, cache warmup). Anything in deploy executes on every deploy to that environment — so keep it idempotent and fast, and never put a destructive one-off in there.
Keep secrets out of the manifest; use env:pull / env:push. Environment variables live encrypted on Vapor. The daily loop is:
vapor env:pull production # refresh local values when a teammate rotates a key
vapor env:push staging # push a new key after adding it to .env.staging
Add .env.* to .gitignore on day one. env:pull writes a real file containing real credentials into the working tree, and it will overwrite what is there.
Use remote execution instead of SSH archaeology. Vapor has no persistent box to log into, so the commands you'll actually reach for are:
vapor command production "cache:clear"
vapor command production "queue:restart"
vapor tinker production
vapor jump production # ephemeral jumpbox when you truly need a shell
This is the biggest mental shift for developers coming from Forge/EC2: there is no server to fix, only commands to run and logs to read.
Verify runtime parity locally before deploying, which is what vapor local and vapor test exist for — both run your app inside the Lambda-style container rather than your host PHP:
vapor local # serve the app in the runtime container
vapor test # run PHPUnit or Pest inside the runtime image
This catches the classic Lambda-only failures (missing extensions, read-only filesystem assumptions, storage path expectations) that pass on a laptop and fail on deploy.
Make CI a single non-interactive command. No init, no login, no prompts:
- uses: shivammathur/setup-php@v2
with: { php-version: '8.3', tools: composer }
- run: composer global require laravel/vapor-cli:^1.70 --no-interaction
- run: echo "$(composer global config home)/vendor/bin" >> $GITHUB_PATH
- run: vapor deploy production --commit="${{ github.sha }}"
env:
VAPOR_API_TOKEN: ${{ secrets.VAPOR_API_TOKEN }}
Docker must be available on the runner — vapor deploy shells out to docker build and pushes the image to ECR. On self-hosted runners, ensure the daemon is running and the build context can reach the registry.
Stamp deploys with the commit SHA. --commit="${{ github.sha }}" makes the Vapor dashboard a usable audit log and gives you the join key back to the PR when something breaks. Use --debug on the deploy command when a build fails opaquely — it surfaces the underlying build output.
Pair the CLI with per-environment approval gates. Because tokens are team-scoped (see Gotchas), the only real protection around a production deploy is the CI layer: GitHub Environments with required reviewers, or an equivalent gate in GitLab. The CLI will not stop a misdirected vapor deploy production.
Provision backing services from the CLI, but record the outcome. vapor database, vapor cache, and vapor domain create and manage RDS/Aurora, ElastiCache, and CloudFront/Route 53 resources. These are one-time commands, so capture the resulting names in vapor.yml immediately — an untracked database name is an orphaned Aurora cluster waiting to happen.
composer global bin path varies. Older Composer uses ~/.composer/vendor/bin; Composer 2.2+ uses ~/.config/composer/vendor/bin. Resolve it with composer global config home rather than hardcoding, or you'll get "vapor: command not found" on a runner that has it installed.VAPOR_API_TOKEN can delete databases and environments. Rotate on team changes, store in a secret manager, never in vapor.yml or a Dockerfile.environment:delete, domain:delete, and database/network teardown can remove production infrastructure. Some commands accept --force to skip confirmation (added for vanity domain deletion) — that is a footgun in scripts; treat --force as production-only and gate it.testing is rejected outright. Since 1.63.1 the CLI aborts on that name. Pick staging, qa, preview-<branch> instead — a surprising number of naming conventions collide here.php-8.0:al2, and the default has moved 8.2 → 8.3. If a warning appears in CI, schedule the runtime bump in the same sprint; the image will be pulled eventually.vapor.yml id must match the linked project. A copy-pasted manifest from another repo deploys to the wrong project or fails confusingly. First thing to check when a deploy "succeeds" but nothing changes..git, and node_modules bloat the image and can leak files. Verify what's in the image with a failed-build log rather than assuming.vapor deploy <env> --debug → vapor logs <env> → vapor command <env> "about". The unhealthy-environment message now includes a hint pointing at the relevant log, which saves the round trip.vapor test needs your test runner to match. Pest support landed in 1.60.0; on older pins the command assumes PHPUnit. Cheap way to lose an afternoon.How can I help you explore Laravel packages today?