SPAs hosting

Our method to deploy static Single-Page Applications


Our SPAs are mostly Vue.js applications. We deploy them with the following architecture:

  • AWS S3 static website hosting

  • AWS CloudFront CDN + SSL resolver

  • Cloudflare DNS

Although all of this is super simple to setup, there is weird stuff that is good to know whenever you want to deploy a new website this way. Read very thoroughly the instructions in this section, because any deviation from the proven way of doing things can quickly break your setup.

Detailed instructions

AWS S3 static website hosting

Bucket creation

Create a bucket to host the website. Preferably name it as per the host under which it will be accessible (e.g., Do not enable the "use this bucket to host a website" option, as it would make the bucket accessible directly, bypassing Cloudfront. We do not want this as Cloudfront can enforce security policies for the bucket (e.g. geographical restrictions). Allowing the bucket to be accessed directly would defeat this entire purpose.

Bucket admin access

Once the bucket is setup, you need to grant it admin access for deployment purposes. For this, go to the AWS IAM console, then to Policies, and update the s3-spa-deployer policy to include the 2 new lines under the Resource array:

"Version": "2012-10-17",
"Statement": [
"Effect": "Allow",
"Action": "s3:*",
"Resource": [

Don't worry about the "version" flag, it has no impact and is outdated even on AWS blogs. Note that both the bucket and its content must be allowed (hence the line with /* after the root resource).

Save the policy and ensure it is attached to a Group to which your user has access (typically, internal_developers)

Note that you need to have aws-cli installed and that your AWS user should be stored in ~/.aws/credentials:

aws_access_key_id = your_access_key_here
aws_secret_access_key = your_secret_access_key_here
region = eu-central-1

You know have an AWS profile fully setup on your local machine, and you'll be able to use its privileges to upload your SPA files to the S3 bucket.

Pushing your SPA files to S3

You can now use a script along the following lines to deploy to S3 (adapt for your own bucket name) :

"scripts": {
"build": "node build/build.js",
"build-production": "NODE_ENV=production npm run build",
"preproduction": "npm run build-production",
"production": "aws s3 cp dist/ s3://bucket-name --recursive --profile seraphin"

This script should typically be included in your package.json file. Note the --recursive flag to let aws-cli know that the whole set of files should be uploaded and the --profile seraphin that specifies the profile to use.

AWS Cloudfront CDN + SSL resolver

Creating or loading an SSL certificate

To ensure your site runs on SSL, you need the AWS Certificate Manager to either generate a cert for you or host a pre-generated cert. We at Seraphin use an existing certificate that has been uploaded. Open the AWS Certificate Manager in the US East (N. Virginia) region and make sure there is a certificate available covering the required domain.

CloudFront will only work with certs from the US East region! This is weird but good to know.

Creating a CloudFront distribution

Create a new distribution on CloudFront, specifying the S3 URL endpoint as the origin. Under CNAME, indicate the requested domain (e.g., If multiple domains redirect to the same CloudFront distribution (e.g. &, you can add multiple CNAMEs separated by a coma. Ensure you select "Redirect HTTP to HTTPS". Finally, make sure that you input index.html as the root object for the distribution (so that when the bare URL is input, this file gets loaded).

Note the CloudFront domain name (e.g.,, you will need it for the DNS setup.

Similarly, note the CloudFront distribution ID, as it will be used for the deployment script to clear the cache by creating a cache invalidation at each deployment.

Handling routing

Finally, setup custom error pages to handle your SPA routes: if one tries to visit, say, the /comments route, CloudFront will initially try to serve the comments object from S3. Since this object does actually not exist (because in a Vue.js SPA all we have is an index.html file), this will yield either a 403 or 404 error depending on the permissions you have setup. To circumvent this, we must tell CloudFront to serve the /index.html page for any and all of these errors, replacing the status code by 200. This way, CloudFront will actually still pass the route path to Vue.js, which will be able to serve the correct router component accordingly.

Cloudflare DNS

Go to the Cloudflare DNS console, and create a new CNAME, redirecting your domain (the CNAME indicated in CloudFront) to the CloudFront domain (e.g., cars => Make sure to click the little cloud so that it becomes grey: Cloudflare should only be used as a DNS for this, because otherwise it would be redundant with CloudFront's CDN and SSL resolution capabilities.

Do not click on anything else in the Cloudflare console: this thing is way too easy to break

If the little cloud is orange (Cloudflare used as proxy), then unexpected things could happen and your browser could throw an error when navigating your website.

After some time (from 10 minutes to a few hours), the CloudFront distribution should now be active, and you should be capable of navigating a nice shiny SSL-enabled website, all hosted on S3. Well done!

The DNS propagation of CloudFront + S3 can be quite slow, and there is a temporary redirection in place that crashes the SSL checks of your browser. Don't worry, it will disappear in due time. You can check that it is temporary by querying your website with curl and inspecting the response text:curl -i