Skip to Content
🎉 Bref 3.0 is released. Read more →
DocumentationLaravelMigrating from Vapor

Migrating from Laravel Vapor

Bref and Laravel Vapor  both run Laravel on AWS Lambda, in your own AWS account, with the same building blocks: Lambda, API Gateway, CloudFront, SQS, S3, RDS. Migrating from Vapor to Bref is more a configuration change than a rewrite.

This guide uses Bref Cloud as the main path, because it replaces what the Vapor dashboard does: deployments, environment variables and secrets, databases, logs, metrics, queues and commands. Bref Cloud is optional though, every section also mentions how to proceed without it.

What changes

What you gain:

  • A proven, actively maintained project: Bref was created in 2017 and is used by thousands of companies. It handles tens of billions of requests every month across all users, and it is referenced in the AWS Lambda documentation.
  • No lock-in: Bref is open source and deploys standard CloudFormation stacks to your AWS account. If you stop using Bref, everything keeps running. Bref Cloud is optional and has a free plan for personal projects (pricing).
  • Full access to AWS: you are not limited in the AWS services and features you can use. Functions can react to any Lambda event (S3, EventBridge, SNS…), and serverless.yml accepts any CloudFormation resource next to the application.
  • Simpler assets: assets are served on the same domain as the application, ASSET_URL is no longer needed.

What you lose:

  • Deploy hooks: Vapor runs the deploy hooks (for example migrations) before the new version receives traffic. With Bref, you run the migrations after bref deploy. That being said, in both cases old and new code use the same database for a short time: the web, artisan and queue functions are updated one after the other, and requests and jobs in progress finish on the old version. This is also true with Vapor: migrations should be backward-compatible.
  • Rollbacks: there is no vapor rollback command. To roll back, redeploy the previous commit.
  • Infrastructure commands: Vapor creates caches and firewalls. Bref Cloud creates databases and networks, but not caches and firewalls yet. You need to create these resources in the AWS console or in serverless.yml. This guide reuses the resources created by Vapor, so this only matters for new resources.
  • A shorter configuration file: serverless.yml is longer than vapor.yml, because it describes each function explicitly.

Need help with the migration? Bref support includes direct help from Matthieu, creator of Bref and AWS Serverless Hero .

Migration strategy

The recommended strategy is to keep the long-lived infrastructure and migrate the application:

  • Keep: the database (RDS or Aurora), the Redis cache (ElastiCache), the S3 storage bucket, the network (VPC and NAT gateway), the domain and its HTTPS certificate. Vapor created these resources in your AWS account and they will continue to work without Vapor.
  • Recreate with Bref: the Lambda functions, the HTTP layer (API Gateway and CloudFront), the SQS queues and the scheduler. These are cheap, stateless and quick to recreate.

The migration then happens without downtime (except in one case, see Domain and DNS switch):

  1. Deploy the application with Bref next to the Vapor environment, connected to the same database, cache and bucket, without the scheduler.
  2. Test it on the URL generated by Bref.
  3. Move the scheduler from Vapor to Bref (see Scheduler), and switch the DNS record of your domain to the Bref deployment.
  4. Delete the Vapor environment (see Clean up).

Recreating the database and the cache is also possible (dump and restore), but it requires a maintenance window to avoid losing data. Reusing them is simpler.

Lower the TTL of your DNS records a day before switching, so that the switch propagates quickly.

Before you start

  • Create a Bref Cloud account  and connect the AWS account where Vapor deploys your application. Bref Cloud connects via an IAM role, the same way Vapor does.

  • Install the bref CLI and run bref login.

  • Download the environment variables of the Vapor environment, you will need them later:

    vapor env:pull production

    This creates a .env.production file. Do not commit it.

  • Open the AWS Lambda console  and find the vapor-<project>-<environment> function. Its configuration is a useful reference during the migration: memory, timeout, VPC subnets and security groups, and the environment variables injected by Vapor (database host, Redis host, bucket name, etc.).

If you do not want to use Bref Cloud, follow the Setup guide to create AWS credentials, install the serverless CLI, and replace bref deploy with serverless deploy throughout this guide.

Install Bref

Replace the Vapor packages with the Bref packages:

composer remove laravel/vapor-core laravel/vapor-cli composer require bref/bref bref/laravel-bridge --update-with-dependencies npm install --save-dev serverless-lift

The Lift plugin  provides the constructs section of serverless.yml, used below for queues and the website.

If your project uses them, also remove laravel/vapor-ui (Bref Cloud replaces it, see Monitoring) and the laravel-vapor npm package (see Assets and File uploads).

Then create the serverless.yml file, which replaces vapor.yml:

php artisan vendor:publish --tag=serverless-config

The bref/laravel-bridge package does what laravel/vapor-core did at runtime: it moves the storage directory to /tmp, sends logs to stderr (CloudWatch), trusts the API Gateway proxy headers, and uses the cookie session driver when the file driver is configured (details). No code change is needed for that.

Vapor-specific files can be deleted once the migration is complete: vapor.yml, config/vapor.php and .vaporignore. Keep the uploadFiles method of your UserPolicy if you use file uploads, Bref uses the same gate.

From vapor.yml to serverless.yml

Here is a typical vapor.yml:

vapor.yml
id: 12345 name: my-app environments: production: runtime: 'php-8.4:al2023' memory: 1024 cli-memory: 512 queue-memory: 1024 timeout: 28 domain: example.com database: my-app-db cache: my-app-cache storage: my-app-storage queues: - default scheduler: true build: - 'composer install --no-dev' - 'npm ci && npm run build' deploy: - 'php artisan migrate --force'

And the equivalent serverless.yml, based on the file published by bref/laravel-bridge:

serverless.yml
service: my-app bref: team: my-team # your Bref Cloud team provider: name: aws region: us-east-1 # the region of your Vapor project environment: APP_ENV: ${param:appEnv} APP_KEY: ${ssm:/my-app/${sls:stage}/APP_KEY} # ... see the "Environment variables and secrets" section # Run in the Vapor network to reach the private database and cache # (see the "Database" section) vpc: securityGroupIds: - sg-0123456789abcdef0 subnetIds: - subnet-0123456789abcdef0 - subnet-abcdef0123456789 functions: web: handler: public/index.php runtime: php-84-fpm memorySize: 1024 # vapor.yml `memory` timeout: 28 # vapor.yml `timeout` events: - httpApi: '*' artisan: handler: artisan runtime: php-84-console memorySize: 1024 # vapor.yml `cli-memory` timeout: 720 # vapor.yml `cli-timeout` events: # vapor.yml `scheduler: true` # Add it when you switch from Vapor (see the "Scheduler" section) - schedule: rate: rate(1 minute) input: '"schedule:run"' constructs: # vapor.yml `queues` jobs: type: queue worker: handler: Bref\LaravelBridge\Queue\QueueHandler runtime: php-84 memorySize: 1024 # vapor.yml `queue-memory` timeout: 60 # vapor.yml `queue-timeout` # vapor.yml `domain` and assets (see the "Assets" and "Domain" sections) website: type: server-side-website versionedAssets: true assets: '/build/*': public/build '/favicon.ico': public/favicon.ico '/robots.txt': public/robots.txt domain: example.com certificate: arn:aws:acm:us-east-1:123456789012:certificate/... params: default: appEnv: ${sls:stage} prod: appEnv: production package: patterns: # replaces `.vaporignore` - '!.env*' - '!node_modules/**' - '!public/build/**' - 'public/build/manifest.json' - '!resources/js/**' - '!resources/css/**' - '!storage/**' - '!tests/**' plugins: - ./vendor/bref/bref - serverless-lift

Vapor deploys one Lambda function for HTTP, one for the CLI and one for queues. Bref does the same, but each function is explicit in serverless.yml: web, artisan and the queue worker.

Vapor environments (production, staging) become Bref environments: bref deploy --env=production. Values that differ between environments go in the params section (Laravel environments).

vapor.yml reference

vapor.ymlserverless.yml
runtime: php-8.4:al2023runtime: php-84-fpm (web), php-84-console (artisan), php-84 (queue worker). Runtimes
runtime: php-8.4:al2023-armarchitecture: arm64 on each function. ARM runtimes
runtime: dockerDeploy the bref/php-84 Docker image. Deploying Docker images
memory, cli-memory, queue-memorymemorySize on the web, artisan and queue worker functions (1024MB is the recommended default). Function configuration 
timeout, cli-timeout, queue-timeouttimeout on each function (max 28 seconds for web, because of API Gateway). Function configuration 
concurrencyreservedConcurrency on the web function. Function configuration 
queue-concurrencymaxConcurrency on the queue construct. Lift queue documentation 
queuesOne queue construct per queue. Laravel Queues
scheduler: trueschedule event on the artisan function. Cron tasks
warmwarmer schedule event, or provisioned concurrency. Cold starts
database, cache, network, subnets, security-groupsEnvironment variables and provider.vpc. Database
storageAWS_BUCKET and IAM permissions. File storage
domain, asset-domaindomain and certificate on the server-side-website construct. Domain
octane: truehandler: Bref\LaravelBridge\Http\OctaneHandler with runtime: php-84. Laravel Octane
octane-database-session-persistOCTANE_PERSIST_DATABASE_SESSIONS environment variable. Persistent database connections
gateway-version: 2httpApi event (the default). gateway-version: 1 is the http event. HTTP applications
balanceralb event. Bref does not create the load balancer, create it in the AWS console or in the resources section. Application Load Balancer
firewallAWS WAF. Bref does not create it, attach a WAF web ACL to the CloudFront distribution or the API Gateway in the AWS WAF console 
tmp-storageephemeralStorageSize on the function. Ephemeral storage 
buildSteps of your CI pipeline before bref deploy. Deploying from GitHub Actions
deploybref command "migrate --force" after bref deploy. Database migrations
ignore and .vaporignorepackage.patterns. Exclusions
separate-vendorNot needed. Use Docker images if the application exceeds the 250MB limit
dockerfile, docker-build-argsprovider.ecr.images. Deploying Docker images

Bref supports Laravel Octane, Docker and ARM, like Vapor. Note that extra PHP extensions are not available for ARM.

Environment variables and secrets

Vapor stores environment variables and secrets in its dashboard and injects them at deployment. With Bref, they live in serverless.yml and in your AWS account:

  • Plain values go in provider.environment in serverless.yml, with parameters for values that change between environments.

  • Secrets (APP_KEY, database password, API tokens) are created in Bref Cloud, in the “Secrets” tab of the environment or with bref secret:create. Bref Cloud stores them as SSM parameters in your AWS account, under /<app>/<environment>/<name>, and you reference them in serverless.yml. They are resolved at deployment time:

    serverless.yml
    provider: environment: APP_KEY: ${ssm:/my-app/${sls:stage}/APP_KEY} DB_PASSWORD: ${ssm:/my-app/${sls:stage}/DB_PASSWORD}

Go through the .env.production file pulled from Vapor and sort the variables into these two groups. Keep the same APP_KEY: it encrypts sessions, cookies and any encrypted data in your database.

Drop the variables that Vapor injected or that only make sense for Vapor: VAPOR_*, ASSET_URL, MIX_URL, SQS_PREFIX, DYNAMODB_CACHE_TABLE (unless sessions are stored in DynamoDB, see Cache and sessions). Replace SQS_QUEUE with the URL of the new queue (see Queues).

Without Bref Cloud: create the SSM parameters with the AWS CLI or the AWS console, the ${ssm:...} syntax works the same. Read more about secrets.

A few details:

  • Lambda limits environment variables to 4KB in total. If large secrets (long tokens, private keys) exceed that limit, load them at runtime instead of at deployment time with the bref/secrets-loader package and the bref-ssm: prefix.
  • If you use an encrypted environment file (.env.production.encrypted with LARAVEL_ENV_ENCRYPTION_KEY), the simplest is to move its values to serverless.yml and Bref Cloud secrets. Alternatively, decrypt it in your CI pipeline with php artisan env:decrypt and deploy the resulting .env file with the application.
  • Vapor sets MYSQL_ATTR_SSL_CA=/var/task/rds-combined-ca-bundle.pem for RDS MySQL databases, using a certificate bundle shipped by Vapor. If your database requires SSL connections, download the RDS certificate bundle  into your project (for example certs/global-bundle.pem) and set MYSQL_ATTR_SSL_CA=/var/task/certs/global-bundle.pem.

Database

Keep the database created by Vapor. It is a regular RDS or Aurora database in your AWS account.

In Bref Cloud, open your AWS account page and click “Scan for apps and databases”: existing databases are imported and listed in the Databases page  with their hostname and port, next to the databases created by Bref Cloud.

Then configure Laravel with the same variables Vapor injected:

serverless.yml
provider: environment: DB_CONNECTION: mysql DB_HOST: my-app-db.abc123.us-east-1.rds.amazonaws.com DB_PORT: 3306 DB_DATABASE: vapor DB_USERNAME: vapor DB_PASSWORD: ${ssm:/my-app/${sls:stage}/DB_PASSWORD}

The values are in the .env.production file, in the Vapor dashboard (in the database details), or in the environment variables of the Vapor Lambda function.

Private databases: Vapor places serverless databases, and optionally fixed-size databases, in a private network. The Lambda functions must then run in that VPC. Copy the subnet IDs and the security group of the Vapor Lambda function (in the Lambda console, “Configuration” then “VPC”) into serverless.yml:

serverless.yml
provider: vpc: securityGroupIds: - sg-0123456789abcdef0 subnetIds: - subnet-0123456789abcdef0 - subnet-abcdef0123456789

This reuses the Vapor network, including its NAT gateway, so the functions keep their internet access. Read more about VPC databases.

Without Bref Cloud: the configuration is identical, only the database will not appear in a dashboard.

Once deployed, run the migrations as you did with the Vapor deploy hook:

bref command "migrate --force"

Vapor calls Schema::defaultStringLength(191) when the database is Aurora MySQL 5.7 (VAPOR_SERVERLESS_DB=true). If your application relies on it, add that line to the boot() method of AppServiceProvider.

Cache and sessions

Redis: if the Vapor environment has a Redis cache (ElastiCache), keep it. It lives in the Vapor network, so the provider.vpc configuration above is required. Vapor configured the Redis connection automatically (VAPOR_CACHE=true), you now have to do it in config/database.php. Vapor caches are Redis clusters, so declare it as a cluster:

config/database.php
'redis' => [ 'client' => 'phpredis', 'options' => [ 'cluster' => 'redis', ], 'clusters' => [ 'default' => [ [ 'host' => env('REDIS_HOST'), 'port' => 6379, 'database' => 0, ], ], 'cache' => [ [ 'host' => env('REDIS_HOST'), 'port' => 6379, 'database' => 0, ], ], ], ],

Set REDIS_HOST to the value injected by Vapor (in the Lambda console). The redis PHP extension is enabled by default in Bref.

DynamoDB: when no Redis cache is attached, Vapor uses a DynamoDB table as the default cache store. Cache data is disposable, so the simplest is to deploy a new table with serverless.yml. Follow the Laravel caching guide. The database cache driver is also a good option: it’s simple, fast, and avoids opening another HTTPS connection to DynamoDB.

Sessions: to keep users logged in through the migration, keep the same SESSION_DRIVER and the same session store (database, Redis or cookie). With the same APP_KEY, existing sessions remain valid.

If sessions are stored in DynamoDB (SESSION_DRIVER=dynamodb), they live in the Vapor DynamoDB table. A new table logs out all users. To avoid it, keep the Vapor table for sessions: keep the DYNAMODB_CACHE_TABLE variable and allow the functions to access the table:

serverless.yml
provider: environment: SESSION_DRIVER: dynamodb DYNAMODB_CACHE_TABLE: vapor_cache # the value from .env.production iam: role: statements: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:BatchGetItem - dynamodb:PutItem - dynamodb:UpdateItem - dynamodb:DeleteItem Resource: arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/vapor_cache

You can move sessions to another store later, when logging out users is acceptable.

Failed jobs: Vapor tracks job attempts in the cache. Bref uses Laravel’s native failed_jobs table, make sure it exists (it is part of the default Laravel migrations).

Queues

Vapor creates SQS queues for each environment and runs jobs in a queue Lambda function. Bref does the same with the Lift queue construct and the Bref\LaravelBridge\Queue\QueueHandler worker. Follow the Laravel Queues guide:

serverless.yml
provider: environment: QUEUE_CONNECTION: sqs SQS_QUEUE: ${construct:jobs.queueUrl} constructs: jobs: type: queue worker: handler: Bref\LaravelBridge\Queue\QueueHandler runtime: php-84 memorySize: 1024 # vapor.yml `queue-memory` timeout: 60 # vapor.yml `queue-timeout` maxConcurrency: 50 # vapor.yml `queue-concurrency`

Declare one construct per queue listed in vapor.yml, and fifo: true for FIFO queues (Lift queue documentation ).

All Laravel Queues features work as usual: retries, failed_jobs, queue:retry, etc. The vapor:work command and the Vapor worker are no longer involved. Laravel Horizon is not applicable on Lambda, same as on Vapor.

During the switch: jobs already in the Vapor queues are processed by the Vapor environment as long as it exists. Wait for the Vapor queues to be empty (in the SQS console) before deleting the Vapor environment.

Scheduler

scheduler: true in vapor.yml runs php artisan schedule:run every minute. In serverless.yml, this is a schedule event on the artisan function:

serverless.yml
functions: artisan: handler: artisan runtime: php-84-console timeout: 720 events: - schedule: rate: rate(1 minute) input: '"schedule:run"'

Like on Vapor, the scheduler is invoked once per minute, from a single place, so onOneServer() is not needed. A task that runs longer than a minute can still overlap with the next run: use withoutOverlapping() to prevent it. Sub-minute tasks work as long as the function timeout is longer than 60 seconds: schedule:run keeps running until the end of the minute.

While the Vapor environment exists, its scheduler runs every minute. If the Bref scheduler also runs, every scheduled task runs twice (emails, billing, reports…).

Deploy the Bref application without the schedule event first. When you switch the domain to Bref:

  1. Set scheduler: false in vapor.yml and run vapor deploy production. Do this before you switch the DNS record, see Domain and DNS switch.
  2. Add the schedule event to the artisan function and run bref deploy.

Scheduled tasks do not run between the two deployments. Do it at a time when no important task is scheduled.

File storage

Keep the S3 bucket configured with storage in vapor.yml. It is a regular S3 bucket in your AWS account: the files, URLs and lifecycle rules remain unchanged.

Configure Laravel to use it and grant the Lambda functions access to it:

serverless.yml
provider: environment: FILESYSTEM_DISK: s3 AWS_BUCKET: my-app-storage iam: role: statements: - Effect: Allow Action: s3:* Resource: - arn:aws:s3:::my-app-storage - arn:aws:s3:::my-app-storage/*

This replaces the storage construct of the File storage guide, which creates a new bucket. AWS credentials are provided by Lambda, there is no AWS_ACCESS_KEY_ID to set.

File uploads

Vapor provides the Vapor.store() JavaScript helper to upload files from the browser to S3, because Lambda limits request payloads to about 4.5MB. It calls the /vapor/signed-storage-url route (provided by laravel/vapor-core and protected by the uploadFiles gate), uploads the file to a tmp/ key in the bucket, and your code then moves the file with Storage::copy().

Bref has the same limit and the same solution, built into bref/laravel-bridge (Large files):

  • Keep the uploadFiles gate (or UserPolicy::uploadFiles()): the Bref route /signed-upload-url uses the same ability.

  • Replace Vapor.store() with the store() function of the bref-upload.js helper shipped by the package (import it from vendor/ with a Vite alias or publish it with php artisan vendor:publish --tag=bref-upload, see Large files):

    import { store } from 'bref-upload'; // Same signature and same `progress` option as Vapor.store() const { uuid, key, bucket, extension } = await store(file, { progress: (ratio) => console.log(`${Math.round(ratio * 100)}%`), });

    The response contains the same uuid, key, bucket, url and headers fields as Vapor’s, plus extension. Keys are tmp/{user id}/{uuid}.{extension} instead of tmp/{uuid}. The httpClient option is supported too, to keep using your configured axios instance; bucket, expires, visibility and data are not.

  • Validate the key sent by the browser with the UploadedToS3 rule (it checks that the file was uploaded by the current user, exists, and matches the expected extension and size), then copy it to its final location with Storage::copy(), exactly like with Vapor.

Two things to check on the existing bucket:

  • CORS: Vapor configured CORS for your domain. To test uploads on the temporary URL generated by Bref, add that origin to the CORS configuration of the bucket in the S3 console.
  • Lifecycle: Vapor configured the tmp/ prefix to expire after 24 hours, this rule stays in place.

Assets

Vapor uploads the public/ directory to a separate S3 bucket, serves it through CloudFront and injects ASSET_URL. Bref uses the server-side-website construct instead: assets are uploaded to S3 and served by CloudFront under the same domain as the application. This removes the need for ASSET_URL:

  • Remove ASSET_URL and MIX_URL from the environment variables.
  • Remove Vapor.asset() and Vapor.withBaseAssetUrl() calls from your JavaScript, and the ASSET_URL or VITE_VAPOR_ASSET_URL configuration from vite.config.js. The asset() helper in Blade keeps working, with URLs on the application domain.
  • The serve_assets and redirect_robots_txt options of config/vapor.php are replaced by the assets list of the construct: list the files and directories that CloudFront should serve from S3 (public/build, favicon.ico, robots.txt, etc.). Anything not listed is handled by Laravel.
  • Build assets (npm run build) before running bref deploy, as you did in the Vapor build hook.

Domain and DNS switch

Vapor manages domains with Route 53 hosted zones and ACM certificates created in your AWS account. Both can be reused.

  1. Deploy with Bref without the domain option first, and test the application on the URL displayed by bref deploy.

  2. Find the HTTPS certificate in the ACM console : CloudFront requires a certificate in the us-east-1 region. Vapor certificates for API Gateway v1 are usually in us-east-1, otherwise request a new one, which is free.

  3. Check if the Vapor domain is edge-optimized: in the API Gateway console , open “Custom domain names” and look at the endpoint type of example.com. An edge-optimized domain uses a CloudFront distribution managed by AWS, and CloudFront allows a domain on only one distribution. The Bref deployment then fails with a CNAMEAlreadyExists error. In that case, the switch causes a short downtime (a few minutes), do it at a quiet time:

    • Delete the example.com custom domain in the API Gateway console. The Vapor application is no longer reachable on example.com.
    • Continue immediately with the next steps.

    A regional custom domain does not have this problem, the switch has no downtime.

  4. Add the domain and the certificate ARN to the website construct and deploy again:

    serverless.yml
    constructs: website: type: server-side-website # ... domain: example.com certificate: arn:aws:acm:us-east-1:123456789012:certificate/...
  5. Point the DNS record of example.com to the CloudFront domain of the website construct (displayed in the outputs of the deployment, or in the CloudFront console). If Vapor manages the zone, edit the record in the Route 53 console  as an alias to the CloudFront distribution, and do not deploy the Vapor environment again. If you manage DNS elsewhere, update the CNAME.

The redirect_to_root option of config/vapor.php becomes redirectToMainDomain: true on the construct, with both domains listed (Lift documentation ).

Monitoring, logs and commands

VaporBref Cloud
vapor deploy productionbref deploy --env=production
vapor env:pull, vapor env:push, vapor secretserverless.yml and bref secret:create (Environment variables)
vapor command production "migrate"bref command "migrate" --env=production (Console runtime)
vapor tinker productionbref tinker --env=production
vapor down and vapor upMAINTENANCE_MODE environment variable (Maintenance mode)
vapor rollbackDeploy the previous commit. Failed deployments are rolled back automatically by CloudFormation
Vapor dashboard and Vapor UI (logs, metrics, failed jobs)Bref Cloud dashboard: logs, metrics, traces, queues and failed jobs (Monitoring)
vapor database:shell, vapor jump, vapor cache:tunnel7777 , an SSH tunnel to private databases made by Bref maintainers
vapor env:deletebref remove --env=production

Without Bref Cloud: serverless deploy, serverless bref:cli --args="migrate", CloudWatch logs and the Bref Dashboard  (Monitoring).

Clean up

Once the domain points to Bref and the Vapor queues are empty, you are free to delete the Vapor environment. Make sure not to delete resources that are still in use (like the database, buckets, etc.).

Last updated on