multiapi / arco
Deploy PHP applications to AWS Lambda Web Functions
Requires
- php: ^8.3
- ext-json: *
- ext-zip: *
- aws/aws-sdk-php: ^3.401.1
- guzzlehttp/guzzle: ^7.8 || ^8.0
- multiapi/arco-secrets: v0.11.3
- symfony/console: ^7.1 || ^8.0
- symfony/filesystem: ^7.1 || ^8.0
- symfony/process: ^7.1 || ^8.0
Requires (Dev)
None
Suggests
- psy/psysh: Required by arco tinker; laravel/tinker installs it
Provides
None
Conflicts
None
Replaces
None
README
The bridge between PHP and AWS Lambda Web Functions.
Run Laravel and PHP on AWS Lambda Web Functions.
Arco is an open-source PHP CLI by Multiapi. It builds your app, uploads it and deploys it to Lambda Web Functions with FrankenPHP, with environments, secrets, custom domains and rollback built in.
Status: 0.9, the API freeze before 1.0. Lambda Web Functions launched in October 2026 and Arco is new. It already runs a production Laravel app. 0.9 is the last release allowed to break
arco.php, the CLI or the--jsonoutput; from 1.0 they stay compatible until 2.0 (what is covered). Upgrading from an earlier version: read Upgrading.
Requirements
- PHP >= 8.3 and Composer on your machine (no Docker, no Node.js needed locally)
- An AWS account and a profile for it (SSO, assume-role and static-key profiles all work)
- For Laravel: Laravel >= 11 with
laravel/octane
Quick start
composer require multiapi/arco-laravel laravel/octane composer require --dev multiapi/arco vendor/bin/arco init vendor/bin/arco secret:set app-key --env=production vendor/bin/arco deploy --env=production
init asks for the app name, region, environments and (optionally) a custom domain, and writes
arco.php. The first deploy asks which AWS profile to use and remembers it in .arco/local.php.
What you get
- One command deploys.
arco deploybuilds a copy of the project, packages it with a pinned, attestation-verified FrankenPHP arm64 binary, uploads it, creates a revision, shifts traffic, checks the health route and rolls back if it fails. - Laravel with Octane workers on FrankenPHP. Node.js only launches FrankenPHP at start-up; it is not in the request path.
- Environments (
production,staging, …), each its own Web Function, all in onearco.php. - Secrets as
arco-ssm:/arco-secret:references, resolved when the function starts and scoped in IAM to exactly what you reference. - Operations.
arco logsinspects the deployed app from your terminal. With the opt-in console function (->console()),arco commandandarco tinkerrun on it and migrations can run on every deploy before traffic moves;->scheduler(N)runs Laravel's scheduler on a UTC clock-aligned EventBridge rule. - Queues.
->queue('emails')creates an SQS queue with a dead-letter queue and a worker function that runs the jobs with Laravel's own worker (retries,failed()andfailed_jobsas usual);->onQueue('emails')works without app changes. - Storage, cache and failed jobs.
->storage('uploads')creates a private S3 bucket that is a Laravel disk (the first one is the default disk, unless every one saysdefault: false), with optional public prefixes, expiry rules and CORS;->cache()and->failedJobsTable()create DynamoDB tables for Laravel's cache and failed jobs, and->table('state')one (PK/SK) for your own code, named inARCO_TABLES. Each can point at one you already have (existing:), and removed ones stay in AWS with their data. - Custom domains in one line: certificate, CloudFront and DNS records, with the DNS zone in the same AWS account, another account, Cloudflare, or outside AWS.
Configuring arco.php
arco init writes this file; edit it by hand or with arco set / arco domain:add. It is plain
PHP returning an App:
<?php use Multiapi\Arco\Config\App; use Multiapi\Arco\Config\Environment; use Multiapi\Arco\Config\Runtime; return App::make('shop') // function name: shop-<environment> ->region('us-east-1') ->runtime(fn (Runtime $r) => $r->php('8.5')) // PHP minor; an Arco upgrade never changes it // App level: defaults for every environment ->timeout(28) ->variables(['APP_LOCALE' => 'es']) ->exclude(['docs/**']) // never packaged ->environment('production', fn (Environment $env) => $env ->profile('my-company-prod') // AWS profile used to deploy this environment ->domain(['shop.example.com', 'api.shop.example.com']) ->dns('my-company-dns') // Route 53 zone lives in another account (optional) ->maxInstances(10) // cap on running instances (and DB connections) ->variables([ 'APP_ENV' => 'production', 'APP_KEY' => 'arco-ssm:/shop/production/app-key', // SSM parameter 'DB_PASSWORD' => 'arco-secret:shop/production/db#password', // Secrets Manager JSON key ]) ->permissions([ // extra IAM for the function (S3, SES, …) ['Effect' => 'Allow', 'Action' => 's3:GetObject', 'Resource' => 'arn:aws:s3:::shop-files/*'], ]) ->runtime(fn (Runtime $r) => $r->phpIni(['memory_limit' => '512M']))) ->environment('staging', fn (Environment $env) => $env ->variables(['APP_ENV' => 'staging', 'APP_KEY' => 'arco-ssm:/shop/staging/app-key']));
Inheritance: a value set on an environment wins over the app level, which wins over Arco's
default. variables() and permissions() are merged (environment entries added to the app's).
arco info --env=production shows every resolved value and where it came from.
The main options:
| Option | Default | What it does |
|---|---|---|
region() |
— (required) | AWS region of the app (one region per app) |
concurrency(int) |
AWS default (64) | Cap on requests per instance (2 vCPU / 2 GB); Lambda adds instances as traffic grows |
maxInstances(int) |
no cap | Maximum running instances |
rateLimit(int) |
no limit | Requests per second; extra requests get 429 |
timeout(int) |
30 | Seconds per request (3–900) |
variables([...]) |
preset defaults | Environment variables; values may be secret references |
permissions([...]) |
— | Extra IAM statements for the function's role |
domain(...) / dns(...) |
— | Custom domains and where their DNS zone is |
profile() / account() |
— | AWS profile for the environment (without it, the first deploy asks and remembers the answer in .arco/local.php), and an account lock so a wrong profile can't deploy (offered after the first deploy) |
auth('public'|'iam') |
public |
iam requires signed requests |
runtime(fn ...) |
PHP 8.5 | PHP version and FrankenPHP/Octane: php(), frankenphp(), build(), binary(), workers(), maxThreads(), threads(), phpIni() |
deployCommands([...]) |
— | Commands run on every deploy before traffic moves, e.g. ['migrate --force'] |
console(bool = true) |
off | Deploy the console function (arco command, arco tinker, deploy commands); console(false) deletes it |
scheduler(int|false) |
off | Run schedule:run every N minutes (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60), clock-aligned in UTC; needs the console |
logRetention(int|false) |
30 | Days CloudWatch keeps the logs; false = never expire |
queue(name, ...) |
none | An SQS queue and its worker function; named options timeout, memory, maxInstances, batchSize, tries, dlq, fifo, existing, default |
storage(name, ...) |
none | An S3 bucket used as a Laravel disk; named options public, expire, cors, existing, default |
cache() / failedJobsTable() |
off | DynamoDB tables for Laravel's cache and failed jobs; existing: names your own table |
table(name, ...) |
none | A DynamoDB table (PK/SK strings, on demand) for your code, in ARCO_TABLES; named options ttl, existing |
console(timeout:, memory:) |
900 s, 2048 MB | Console function timeout in seconds (3–900) and memory in MB (128–10240) |
protected() |
production |
Ask before arco tinker, arco command, arco remove and arco queue:dlq --redrive on this environment (--yes answers; required without a terminal) |
Every option, the packaging rules and the Laravel preset are in the configuration reference.
Commands
| Command | What it does |
|---|---|
arco init |
Create arco.php (asks name, region, environments, custom domain) |
arco deploy --env=<env> |
Build, upload, deploy commands, deploy, verify, attach domains; --migrate runs migrations first; --package=<zip> deploys a prebuilt zip; --json streams NDJSON events and a result |
arco console:add --env=<env> / console:remove |
Turn the console function on or off in arco.php (off deletes it on the next deploy) |
arco command "<args>" --env=<env> |
Run an artisan (or PHP) command, given as one quoted string, on the console function (needs console()); exits with its exit code (--async to start it and return; --yes on a protected environment without a terminal) |
arco tinker --env=<env> |
Tinker on your machine, statements run in Lambda (needs console(); --execute="<code>" for one statement) |
arco scheduler:set [N] --env=<env> / scheduler:remove |
Run Laravel's scheduler every N minutes (suggests N from your tasks; turns the console on) / turn it off; --apply (or answer yes) applies it in AWS without a deploy |
arco scheduler:pause --env=<env> / scheduler:resume |
Pause or resume the scheduler in AWS at once, without editing arco.php (deploys keep it paused) |
arco logs:retention <days|never> --env=<env> |
Set how long CloudWatch keeps the logs (--remove stops managing it) |
arco queue:add <name> --env=<env> / queue:set / queue:remove |
Add, change or remove a queue in arco.php (removed queues stay in AWS with their messages) |
arco queue:list --env=<env> |
Each queue's messages, dead-letter queue and worker version (--json too) |
arco queue:dlq <name> --env=<env> |
Show the messages in a queue's dead-letter queue; --redrive moves them back |
arco failed-jobs:add --env=<env> / failed-jobs:remove |
Turn the DynamoDB failed-jobs table on (--existing=<table>) or off in arco.php |
arco storage:add <name> --env=<env> / storage:remove |
Add or remove a storage in arco.php (--existing=<bucket>, --default; removed buckets stay in AWS with their files) |
arco storage:public / storage:expire / storage:cors |
Public prefixes, expiry rules and CORS origins of a storage (--remove takes one out) |
arco storage:list --env=<env> |
Each storage's bucket, URL and options, and the retained buckets no longer in arco.php (--json too) |
arco cache:add --env=<env> / cache:remove |
Turn the DynamoDB cache table on (--existing=<table>) or off in arco.php |
arco table:add <name> --env=<env> / table:remove |
Add (--ttl=<attribute>, --existing=<table>) or remove a table() in arco.php (removed tables stay in AWS with their items) |
arco logs --env=<env> |
Web, console and queue worker logs merged, filtered and colored (--follow, --since, --level, --request, --filter, --queue) |
arco package --env=<env> |
Build the zip only (for CI: build once, deploy later) |
arco info --env=<env> |
Resolved settings with their source, URL, active revision (--json too); --resources lists every AWS resource tagged for the environment |
arco secret:set <name> --env=<env> |
Store a secret in SSM and print the reference to use |
arco set <key> <value> --env=<env> / arco unset <key> |
Change concurrency, max-instances, rate-limit, timeout, log-level, console-timeout, console-memory in arco.php |
arco domain:add <host> --env=<env> / domain:remove |
Change an environment's domains in arco.php |
arco profile --env=<env> |
Show or change the AWS profile remembered for an environment (--json too) |
arco remove --env=<env> |
Delete the function, its endpoint, role, workers and domain resources (queues, buckets, tables and log groups are kept); without a terminal it needs --yes; --json too |
Messages are in English or Spanish (--lang=es or ARCO_LANG=es).
Documentation
- Getting started
- Configuration reference
- Upgrading: every breaking change per version, and what 1.x keeps stable
- Try it locally with Floci
Limits (of Lambda Web Functions)
- 2 vCPU and 2 GB per instance, 512 MB unzipped package, 512 MB
/tmp, arm64 only - Regions: us-east-1, us-east-2, us-west-2, ap-northeast-1, eu-west-1
- The console function (commands, tinker, scheduler) is opt-in and a classic Lambda function: 250 MB unzipped package, with a static PHP CLI instead of the FrankenPHP binary
- The queue workers are classic Lambda functions running the console package (250 MB unzipped), one per
queue; a batch of jobs runs for at most the worker's
timeout(up to 900 s) - Arco deploys HTTP functions, an optional console function for commands and the scheduler, SQS queue workers, S3 buckets and DynamoDB tables for the cache and failed jobs, in one region per app
Packages
| Package | Install as | Purpose |
|---|---|---|
multiapi/arco |
require-dev |
The CLI |
multiapi/arco-laravel |
require |
Laravel preset and service provider (ships with your app) |
multiapi/arco-secrets |
dependency | arco-ssm: / arco-secret: reference parsing |
The packages are published from one monorepo. Please open issues in
multi-api/arco.
License
MIT