How to get a serverless SSR Nuxt.js app running on AWS Lambda

Published February 14, 2021 (10 min read)

A tutorial on how to get a server side rendered Nuxt.js application running on AWS Lambda using the serverless framework with a custom domain.


In the tutorial I will go through how to get a server side rendered Nuxt.js application running on AWS Lambda by using the serverless framework and then how to set it up under a custom domain name. The benefit of doing this is that we pay for only what we use and Lambda is automatically scalable, great for sites with lots of traffic. It is worth noting that it is cheaper to use EC2 instances if you are expecting a lot of traffic compared to AWS Lambda.

If you just want to see the working code look at the repo on GitHub.

Contents

Requirements

You'll only need a few things for this:

  • Nodejs
  • An AWS Account
  • An understanding of Nuxtjs and server side rendering
  • An understanding of AWS's system
  • A domain name (For setting up a custom domain for your lambda)

Setup and Configuration

We'll be starting from a fresh install of Nuxtjs but this tutorial can work with an existing Nuxtjs application, just skip to step 2 of setup.

1) Create a Nuxt app

Let's create a new Nuxt application, run the below in your terminal to start creating. If you wish to you can create the Nuxt application using other methods here. Choose the options you want for your project but as this tutorial is for a server side rendered application choose Universal for Rendering Mode and Server for Deployment Target.

npx create-nuxt-app nuxt-ssr-lambda

2) Install serverless

If you haven't already got serverless installed go ahead and run the below to install serverless globally

npm install -g serverless

Once installed we can set it up with AWS, we need to add our AWS credentials. Create an IAM user, for now grant admin permissions to the user. Once you are more comfortable with AWS you can modify these permissions.

If you are using the AWS Cli and have added your credentials already to it then you can skip this step, just make sure the IAM user has the correct permissions.

Once created you can add the generated key and secret details to serverless.

serverless config credentials \ 
    --provider aws \ 
    --key xxxxxxxxxxxxxx \ 
    --secret xxxxxxxxxxxxxx

3) Install dependencies

Now we need to install our dependencies so our application works in the serverless framework.

npm install serverless-apigw-binary serverless-http

Last dependancy we can install is serverless-offline, this allows serverless to run locally on your machine and test it without deploying it.

npm install --save-dev serverless-offline

Getting Nuxt to work with serverless

Lambda runs code using functions, what we are going to build is a function that Lambda runs with serverless. First lets build the main part of our functon, the Nuxt part. Create a nuxtBuild.js file.

Note: I found when I was building this that if the file is named nuxt.js then when running any build scripts all it'll do would open this file and stop instead of building the application.

nuxtBuild.js
const { Nuxt } = require('nuxt')

const config = require('./nuxt.config.js')

const nuxt = new Nuxt({ ...config, dev: false })

module.exports = (req, res) =>
    nuxt.ready().then(() => nuxt.server.app(req, res))

This file creates a new Nuxt instance and once ready starts the Nuxt app.

Next we have to specify what mime types are allowed to be used in our serverless application, create a binaryMimeTypes.js file which we will inject into our serverless instance.

binaryMimeTypes.js
module.exports = [
    'application/javascript',
    'application/json',
    'application/octet-stream',
    'application/xml',
    'font/eot',
    'font/opentype',
    'font/otf',
    'image/jpeg',
    'image/png',
    'image/svg+xml',
    'text/comma-separated-values',
    'text/css',
    'text/html',
    'text/javascript',
    'text/plain',
    'text/text',
    'text/xml'
]

In Lambda when the function is invoked it runs the handler method. We'll need to create the handler that exports our Nuxt function with serverless. We'll build an export of our serverless function bundled with Nuxt and the allowed binary types.

Note: Lambda turns off whenever not in use and so when a user visits the page it runs the handler export again, for what we are doing this is ok however I found that when using @nuxt/content this would mean start up takes much longer for each content file you have. This can be a problem if you don't have constant traffic as initial load times can be seconds long, 2-3 seconds per content file. Something to watch out for.

handler.js
const sls = require('serverless-http')
const binaryMimeTypes = require('./binaryMimeTypes')
const nuxt = require('./nuxtBuild')

module.exports.nuxt = sls(nuxt, {
    binary: binaryMimeTypes
})

Finally we create the main serverless.yaml file, this is where we tell serverless what's what. First we give it a name, then the provider which for us is AWS along with what it'll be running on.

Note: AWS Lambda has only just started supporting Nodejs 14 however at the time of writing this serverless doesn't recognize it and will throw a warning that 14 is not allowed when deploying, it is safe for you to ignore this. When running locally however it will not work. For now we're using Nodejs 12

Now we tell serverless our Lambda functions. In this case our function name is nuxt and its handler points to the nuxt export in our handler.js file. The events are for the API Gateway to help our routing.

We add the plugins that serverless will be using and in the custom field the binary types that the API Gateway will allow.

serverless.yaml
service: nuxt-ssr-lambda

useDotenv: true

provider:
  name: aws
  runtime: nodejs12.x
  stage: ${env:NODE_ENV}
  region: ${env:NUXT_AWS_REGION}
  lambdaHashingVersion: 20201221
  environment: 
    NODE_ENV: ${env:NODE_ENV}
  apiGateway:
    shouldStartNameWithService: true

functions:
  nuxt:
    handler: handler.nuxt
    events:
      - http: ANY /
      - http: ANY /{proxy+}

plugins: 
  - serverless-apigw-binary
  - serverless-offline

custom:
  apigwBinary:
    types:
      - '*/*'
  serverless-offline:
    noPrependStageInUrl: true

Now let's create our .env file which will hold our environment variables used in serverless.yaml. You can use secrets however you want in serverless.yaml, I just prefer using .env.

.env
NODE_ENV=prod
NUXT_AWS_REGION={your_region}

We use NUXT_AWS_REGION here as AWS_REGION is a reserved name in AWS.

Now that all the code has been setup all we need to do is add the scripts to our package.json file to run our deployments.

package.json
"deploy": "npm run build && sls deploy",
"start-sls": "npm run build && sls offline start"

deploy will deploy the application to Lambda and start-sls will run our serverless application locally. Lets first test it locally. Important We'll need to update our nuxt.config.js file to use module.exports = {} instead of export default {}.

Note: Before we do that set telemetry property to false in our nuxt.config.js file. This will stop Nuxt asking for error reports during our testing as this stops serverless from showing our site offline until we respond to it.

npm run start-sls

Now that we can see serverless is running smoothly we can deploy.

npm run deploy

If no errors occur we should be a given a nice AWS domain where our site is hosted under the stage we gave it, go ahead and navigate to it and our application should be running.

Note: There currently is a problem with routing in Nuxt for Lambda when the url is suffixed with our stage and so only the homepage will display correctly, any static files will get a 403 error. This isn't an issue if you're going to create a custom domain. I have created a stackoverflow question here explaining the problem. Check this to see if an answer has been given or if you manage to fix the issue yourself it would mean a lot if you post your answer there. Thanks.

Reducing the Lambda Upload size

You may have noticed that once you deploy to Lambda the upload size is quite big, if you've started from a fresh Nuxt install this can be close to 40MB. Serverless does what it can to reduce this, for example it checks what dependencies we have and only uploads those from the node_modules folder. We can however help reduce this by telling what serverless can include/exclude.

We can add a package field to our serverless.yaml file using exclude/include fields to specify the files. For ease we can move all the Nuxt folders (pages, plugins, etc...) into it's own directory. We will be moving into src, then we need to tell Nuxt that we have moved it.

nuxt.config.js
srcDir: 'src/',

Now we can add the fields we want to exclude/include.

serverless.yaml
package:
  exclude:
    - src/**
    - .nuxt/**
    - package.json
    - package-lock.json
    - .gitignore
    - README.md
    - .env.example
  include:
    - src/static/**
    - .nuxt/dist/**

We will be ignoring the .nuxt build directory and only including the dist folder which contains all our distribution code. We'll also need to include our static files.

We can go one step further to reduce the size, we can install a production ready version of Nuxt called nuxt-start.

npm install nuxt-start

Install this and move nuxt in your package.json to the devDependencies. We'll then need to update our nuxtBuild.js file to use this production ready build. So instead of using require('nuxt') we can use nuxt-start.

nuxtBuild.js
const { Nuxt } = require('nuxt-start')

With these changes we have greatly reduced the size of our upload, if you are looking to reduce the size even more you could look into using serverless-finch. This allows you to upload all client side files to an s3 bucket, meaning you would then only need to upload the server distribution to Lambda.

Using a Custom Domain

Now we can use our custom domain, you will have to have AWS setup with this domain. If you use a different provider you can create a public hosted zone within AWS Route53 that will work as your DNS management. This will generate name servers in Route53 for your domain which you can update your domain provider with, for example if you are using GoDaddy to register your domain.

1) Set up an SSL certificate

API Gateway requests are served over HTTPS, so we need an SSL certificate.

The great thing about AWS is that it can set you up with a free SSL certifcate, all you have to do is enter which domain it's for. You can follow the steps here to generate an SSL certifcate. It does a great job of describing the steps you need to take. AWS may default to region us-east-1 but you can change it to the region of your choice.

Once you have your certificate generated and validated you'll be given a certifcateArn. If you used the AWS cli to generate it then you'll be given this back in the output, if you went through the manager then the ARN can be found under the details section of your certificate.

Copy this ARN and create a new environment variable as CERTIFICATE_ARN in .env.

2) Add the custom domain

We need to install the dependencies for domain management in serverless.

npm install --save-dev serverless-domain-manager

And then add the plugin to your list of plugins in the serverless.yaml file.

serverless.yaml
plugins:
  - serverless-domain-manager

Now we can add the details of our custom domain under the custom field in serverless.yaml. Add the following for the options of your custom domain. Creating the domain in your .env file.

serverless.yaml
customDomain:
    domainName: ${env:DOMAIN}
    basePath: ''
    stage: ${self:provider.stage}
    createRoute53Record: true
    endpointType: 'regional'
    certificateArn: ${env:CERTIFICATE_ARN}

You can see what the options do here but essentially you are giving it the domain name you want at what basePath. The createRoute53Record will allow serverless to create an A Alias and AAAA Alias records in Route53 mapping the domainName to the generated distribution domain name. If you already have these set up you can set to false.

Important: If you want an SSL certicate in a region other than us-east-1 then you also need to add the endpointType as regional.

If your domain isn't set up run the below and serverless will create everything for you.

sls create_domain

If all goes well you now should now have a Nuxt application running on Lambda at your custom domain. Don't worry if you don't see anything immediately, it can take some time to get working. Take a break for about 40 minutes.

Summary

Now you have a a server side rendered Nuxt application running on AWS Lambda under your custom domain, I used this same technique to help build my own youtube clone. I hope you found it useful and built something really cool, thanks for reading.