Serverless Symfony - Getting started
This guide helps you run Symfony applications on AWS Lambda using Bref. These instructions are kept up to date to target the latest Symfony version.
Setup
First, follow the Setup guide to create an AWS account and install the necessary tools.
Next, in an existing Symfony project, install Bref and the Symfony Bridge package .
composer require bref/bref bref/symfony-bridge --update-with-dependenciesNext, create a serverless.yml configuration file at the root of your project:
service: app # your application name (lowercase without spaces)
bref:
# Uncomment and set your team slug if you are using Bref Cloud (`bref teams` lists them)
#team: my-team
provider:
name: aws
region: us-east-1 # AWS region to deploy to
environment: # Environment variables
APP_ENV: prod
functions:
# This function runs the Symfony website/API
web:
handler: public/index.php
runtime: php-84-fpm
timeout: 28 # in seconds (API Gateway HTTP APIs time out after 30 seconds)
events:
- httpApi: '*'
# This function lets us run console commands in Lambda
console:
handler: bin/console
runtime: php-84-console
timeout: 120 # in seconds
package:
patterns:
# Excluded files and folders for deployment
- '!assets/**'
- '!node_modules/**'
- '!public/build/**'
- '!tests/**'
- '!var/**'
# If you want to include files and folders that are part of excluded folders,
# add them at the end
- 'var/cache/prod/**'
- 'public/build/entrypoints.json'
- 'public/build/manifest.json'
plugins:
- ./vendor/bref/brefYou will also want to add .serverless to your .gitignore.
You still have a few modifications to do on the application to make it compatible with AWS Lambda.
Since the filesystem is readonly except for /tmp we need to customize where the cache and logs are stored in the src/Kernel.php file. This is automatically done by the bridge, you just need to use the BrefKernel class instead of the default BaseKernel:
namespace App;
+ use Bref\SymfonyBridge\BrefKernel;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
-use Symfony\Component\HttpKernel\Kernel as BaseKernel;
- class Kernel extends BaseKernel
+ class Kernel extends BrefKernel
{
use MicroKernelTrait;
}Deployment
Let’s deploy the application to AWS Lambda:
Bref Cloud
bref deployWhen finished, the deploy command will show the URL of the application.
Deploying for production
At the moment, we deployed our local codebase to Lambda. When deploying for production, we probably don’t want to deploy:
- development dependencies,
- our local
.envfiles, - or any other dev artifact.
Follow the deployment guide for more details.
Separately, on cold starts, Symfony will boot with an empty cache directory. It will build the cache on the fly, which can take a few seconds depending on the complexity of the application.
To optimize cold starts, you can deploy the application with a warm cache. In a simple application it means that the deployment script should include cache:warmup to look something like this:
Bref Cloud
# Install dependencies
composer install --classmap-authoritative --no-dev --no-scripts
# Warmup the cache
bin/console cache:clear --env=prod
# Disable use of Dotenv component
echo "<?php return [];" > .env.local.php
bref deployWith Dotenv disabled, Symfony no longer reads the .env files on Lambda: define every variable your application needs (APP_SECRET, DATABASE_URL, etc.) as environment variables of the function (environment in serverless.yml), or store it as a secret (read the environment variables documentation). For example, recent Symfony skeletons (like Symfony 8) need DEFAULT_URI, else every request fails with Environment variable not found: "DEFAULT_URI":
provider:
# ...
environment:
DEFAULT_URI: 'https://example.com'Optimizing caches
When running Symfony on Lambda you should avoid writing to the filesystem. If you pre-warm the cache before deploying you are mostly fine. But you should also make sure you never write to a filesystem cache like cache.system or use a pool like:
framework:
cache:
pools:
my_pool:
adapter: cache.adapter.filesystemIf you don’t write to such cache pool you can optimize your setup by not copying the var/cache/pools directory. The change below will make sure to symlink the pools directory.
Symfony’s application cache (cache.app) is also a filesystem cache by default. Only apply this change if framework.cache.app uses another adapter (for example Redis or the database, see Caching), otherwise cache.app stops caching anything.
class Kernel extends BrefKernel
{
// ...
+ protected function getWritableCacheDirectories(): array
+ {
+ return [];
+ }
}Troubleshooting
In case your application is showing a blank page after being deployed, have a look at the logs.
Logs
Thanks to the Bref integration, Symfony will automatically log to CloudWatch via stderr. You don’t have to do anything.
You can learn more about logs in the Logs guide.
Website assets
Have a look at the Website guide to learn how to deploy a website with assets.
Symfony Console
As you may have noticed, we define a function named “console” in serverless.yml. That function is using the Console runtime, which lets us run the Symfony Console on AWS Lambda.
For example, to execute a bin/console command on Lambda, run the command below:
Bref Cloud
bref command "{console command and options}"For example:
bref command "doctrine:migrations:migrate"For more details follow the “Console” guide.
Trust API Gateway
When hosting a website on Lambda, API Gateway acts as a proxy between the client and your Lambda function.
By default, Symfony doesn’t trust proxies for security reasons, but it’s safe to do it when using API Gateway and Lambda. This is needed because otherwise, Symfony will not be able to generate URLs properly.
Add the following lines to config/packages/framework.yaml:
framework:
# trust the remote address because API Gateway has no fixed IP or CIDR range that we can target
trusted_proxies: '127.0.0.1'
# trust "X-Forwarded-*" headers coming from API Gateway
trusted_headers: [ 'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-port' ]Note that API Gateway doesn’t set the X-Forwarded-Host header, so we don’t trust it by default. You should only whitelist this header if you set it manually,
for example in your CloudFront configuration (this is done automatically
in the CloudFront distribution deployed by Lift).
You can get more details in the Symfony documentation .
Be careful with these settings if your app is also executed outside a Lambda environment (for example on a public server).
Getting the user IP
When using CloudFront on top of API Gateway, you will not be able to retrieve the client IP address, and you will instead get one of CloudFront’s IP when
calling Request::getClientIp(). If you really need this, you will need to
whitelist every CloudFront IP
in trusted_proxies.
Sessions
By default, Symfony stores sessions in files, which end up in /tmp on Lambda. Each Lambda instance has its own /tmp: once the application runs on several instances, users are logged out at random and forms fail with “Invalid CSRF token” errors.
Store sessions in a service shared by all instances instead:
- the database, with
PdoSessionHandler, - Redis, with
RedisSessionHandler, - DynamoDB, with the session handler of the DynamoDB cache bundle.
Stateless APIs that do not use sessions can skip this.
The kernel.terminate event
The kernel.terminate event runs synchronously on Lambda.
That means that if you use this event, its listeners will be executed before the Lambda function returns its response. That will add latency to your response.
To run asynchronous tasks, use the Symfony Messenger instead.