Creating serverless PHP websites

Before reading this article we assume that you have read Bref's introduction and that you are familiar with Bref's HTTP runtime.


Websites usually contain 2 parts:

You will find below different architectures for creating websites.

Separate domains

This architecture is the simplest. Assets are hosted on a separate domain than the PHP code.

However, by default API Gateway only supports HTTPS. That means there is no redirection from HTTP to HTTPS: will work but not

To solve that, we can switch from API Gateway's "HTTP API" to "REST API" (which provides a HTTP -> HTTPS redirection). Despite the name, REST API work fine for websites.

        # ...
-            - httpApi: '*'
+            - http: 'ANY /'
+            - http: 'ANY /{proxy+}'

Finally, when setting up a custom domain make sure to select Edge deployment:

Same domain

This architecture is more complex, as CloudFront (the AWS CDN) serves as a HTTP/HTTPS proxy.

This lets us host everything under the same domain and support both HTTP and HTTPS.

On top of that, CloudFront can act as a CDN and cache assets and web pages all over the world. This is however not documented in this page.

Hosting static files with S3

This section explains how to host assets on S3.

Creating a S3 bucket

S3 stores files in "buckets". You will need to create one for your website.

If you plan on using a custom domain for S3 URLs, the bucket must be named after the domain. For example Learn more about this in the official AWS documentation.

If you plan to use CloudFront, you can use any name for the bucket.

In order to automate everything let's create and configure the bucket using serverless.yml:


        # The S3 bucket that stores the assets
            Type: AWS::S3::Bucket
                BucketName: <bucket-name>
        # The policy that makes the bucket publicly readable
            Type: AWS::S3::BucketPolicy
                Bucket: !Ref Assets # References the bucket we defined above
                        -   Effect: Allow
                            Principal: '*' # everyone
                            Action: 's3:GetObject' # to read
                            Resource: !Join ['/', [!GetAtt Assets.Arn, '*']] # things in the bucket
                            # alternatively you can write out Resource: 'arn:aws:s3:::<bucket-name>/*'

Don't forget to replace <bucket-name> with the bucket name of your choice. Note that the name must be universally unique within Amazon (so you can't use assets) otherwise you'll get this error when you deploy: Assets - assets already exists.

After deploying with serverless deploy, the static files will be served from https://<bucket> Read the next section to upload your files.

You can either setup a custom domain to point to this URL or setup CloudFront as explained below.

In this section we explained how to host assets on S3. It is also possible to host static websites on S3 (i.e. HTML files). In that cas you will need to enable this feature and use different S3 URLs. See the AWS documentation to learn more.

If you need to enable CORS, for example if a CSS or JavaScript file needs to load additional resources like fonts from S3:

            Type: AWS::S3::Bucket
                BucketName: <bucket-name>
                        -   AllowedHeaders: ["*"]
                            AllowedMethods: [GET]
                            AllowedOrigins: ["*"]

More options can be set on the bucket, read more here.

Uploading files to S3

It is not possible to use serverless deploy to upload files to S3, you need to upload them separately. To do this, you can use the aws s3 sync command:

aws s3 sync <your-assets-directory> s3://<bucket-name>/<your-assets-folder> --delete

Please note that the assets would normally need to be inside a folder, and not in the root of your bucket.

Be aware that the content of the bucket is public!

Serving PHP and static files via CloudFront

This section assumes you have already deployed your static files on S3 as shown above.

As explained at the beginning of this page, CloudFront will let us:

  • serve the PHP application and static files under the same domain
  • support both HTTP and HTTPS

This diagram helps understand how CloudFront works:

CloudFront forwards HTTP requests to "Origins" (API Gateway/Lambda, S3, etc.) based on "Cache Behaviors".

A cache behavior can match a specific URL pattern, and can have a specific caching configuration (e.g. cache the responses for 10 days).

The serverless.yml example below:

  • forwards URLs that start with /assets/ to S3 (static files)
  • forwards all the other requests to Lambda
service: app
    name: aws
    runtime: provided.al2

        handler: public/index.php
            - ${bref:layer.php-74-fpm}
            - httpApi: '*'

    - ./vendor/bref/bref

        # The S3 bucket that stores the assets
            # [...] see the previous section for details
            # [...] see the previous section for details

            Type: AWS::CloudFront::Distribution
                    Enabled: true
                    # Cheapest option by default (
                    PriceClass: PriceClass_100
                    # Enable http2 transfer for better performances
                    HttpVersion: http2
                    # Origins are where CloudFront fetches content
                        # The website (AWS Lambda)
                        -   Id: Website
                            DomainName: !Join ['.', [!Ref HttpApi, 'execute-api', !Ref AWS::Region, '']]
                                OriginProtocolPolicy: 'https-only' # API Gateway only supports HTTPS
                            # CloudFront does not forward the original `Host` header. We use this
                            # to forward the website domain name to PHP via the `X-Forwarded-Host` header.
                            # Learn more:
                            #    -   HeaderName: 'X-Forwarded-Host'
                            #        HeaderValue: # our custom domain
                        # The assets (S3)
                        -   Id: Assets
                            DomainName: !GetAtt Assets.RegionalDomainName
                            S3OriginConfig: {} # this key is required to tell CloudFront that this is an S3 origin, even though nothing is configured
                            # If you host a static website, like a SPA, use s3-website URLs instead of the config above
                            # See
                            # DomainName: !Select [2, !Split ["/", !GetAtt Assets.WebsiteURL]]
                            # CustomOriginConfig:
                            #     OriginProtocolPolicy: 'http-only' # S3 websites only support HTTP
                            # You'll also need to enable website hosting on your s3 bucket by configuring the WebsiteConfiguration property
                    # The default behavior is to send everything to AWS Lambda
                        AllowedMethods: [GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE]
                        TargetOriginId: Website # the PHP application
                        # Disable caching for the PHP application
                        DefaultTTL: 0
                        MinTTL: 0
                        MaxTTL: 0
                            QueryString: true
                                Forward: all # Forward cookies to use them in PHP
                            # We must *not* forward the `Host` header else it messes up API Gateway
                                - 'Accept'
                                - 'Accept-Encoding'
                                - 'Accept-Language'
                                - 'Authorization'
                                - 'Origin'
                                - 'Referer'
                        # CloudFront will force HTTPS on visitors (which is more secure)
                        ViewerProtocolPolicy: redirect-to-https
                        # Assets will be served under the `/assets/` prefix
                        -   PathPattern: 'assets/*'
                            TargetOriginId: Assets # the static files on S3
                            AllowedMethods: [GET, HEAD]
                                # No need for all that with assets
                                QueryString: 'false'
                                    Forward: none
                            ViewerProtocolPolicy: redirect-to-https
                            Compress: true # Serve files with gzip for browsers that support it (
                        # Force CloudFront to not cache HTTP errors
                        -   ErrorCode: 500
                            ErrorCachingMinTTL: 0
                        -   ErrorCode: 504
                            ErrorCachingMinTTL: 0

Feel free to customize the /asset/ path. If your application is a JS application backed by a PHP API, you will want to invert API Gateway and S3 (set S3 as the DefaultCacheBehavior and serve API Gateway under a /api/ path).

The first deployment takes a lot of time (5 to 10 minutes) because CloudFront is a distributed service. The next deployments that do not modify CloudFront's configuration will not suffer from this delay. You will know it is finished when the Status column changes from In Progress to Deployed in your CloudFront dashboard.

You can also follow the deployment in real time using the Bref Dashboard.

The URL of the deployed CloudFront distribution can be found in the CloudFront dashboard under Domain Name.

Setting up a domain name

Just like in the "Custom domains" guide, you need to register your domain in ACM (AWS Certificate Manager) to get a HTTPS certificate.

If you have already set up this domain as a custom domain in API Gateway (by following the Custom domain guide), you will need to remove it before continuing.

  • open this link or manually go in the ACM Console and click "Request a new certificate" in the us-east-1 region (CloudFront requires certificates from us-east-1)
  • add your domain name and click "Next"
  • choose the domain validation of your choice
    • domain validation will require you to add entries to your DNS configuration
    • email validation will require you to click a link you will receive in an email sent to

After validating the domain and the certificate we can now configure it in template.yaml.

Copy the ARN of the ACM certificate. It should look like this:


Add your domain name under Aliases and configure ViewerCertificate to use your custom HTTPS certificate:

            Type: AWS::CloudFront::Distribution
                    # Custom domain name
                        - <custom-domain> # e.g. (This is your application's domain, not your assets' domain)
                        # ARN of the certificate created in ACM
                        AcmCertificateArn: <certificate-arn>
                        # See
                        SslSupportMethod: 'sni-only'
                        MinimumProtocolVersion: TLSv1.1_2016

The last step will be to point your domain name to the CloudFront URL:

More examples

Complete deployable examples for:

  • server-side websites with assets
  • single-page applications with a backend API

are available in the Serverless Visually Explained course.