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.
You'll only need a few things for this:
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.
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
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
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
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.
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.
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.
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 usingNodejs 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.
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
.
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.
"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 tofalse
in ournuxt.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.
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.
srcDir: 'src/',
Now we can add the fields we want to exclude/include.
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
.
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.
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.
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
.
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.
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.
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 theendpointType
asregional
.
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.
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.