For the past few hours, I've been working on dockerizing a Nuxt.js server-side rendering app to be deployed to AWS Elastic Container Service (ECS). I hit a stone wall when environment variables are not properly injected on runtime on ECS.
In this post, I will walk us through steps to properly dockerize Nuxt.js SSR apps and share some of the lessons I learnt.
There are 2 major ways to dockerize Nuxt.js apps, and each of them has its shortcomings:
Nuxt build on run: The Nuxt app is built after Docker image is built, when the container is spawned. The downside of this is the container boot time takes significantly longer as it involves building the artifacts on run. This also violates the build/release/run principle of a 12-factor app.
Nuxt build on Docker build: For this, sensitive environment variables are injected during
docker build
stage. The upside is the boot time is much shorter now, hence, quicker revert time in case you need to revert to an older version. However, the downside is the sensitive variables are now bundled into our Docker image and it requires a rebuild every time a variable is updated.
For this post, I will be using the first method, "Nuxt build on run".
How environment variables works in Nuxt.js apps?
Let's revisit how environment variables work in Nuxt apps. Some of us may already be familiar with the env
property in nuxt.config.js
, which allows us to inject sensitive values from your terminal environment to the app on runtime.
// in nuxt.config.js
export default {
env: {
/**
* dbUri is accessible from both client and server side.
*/
dbUri: process.env.DB_URI
}
}
From the snippet above, DB_URI
is read and injected to dbUri
when we run the app for any purpose. (nuxt start
, nuxt build
or nuxt
)
When serving a production build, our environment variables are most likely different from environment to environment. Thus, if we build our app (nuxt build
) as part of the building steps of our Docker image, any sensitive environment variables that is injected when we run our Docker image (docker run ...
) will not work as the artifact has been built before these variables are injected.
How to safely inject environment variables into Dockerized Nuxt.js on runtime?
By adopting "Nuxt build on run", we adhere to:
Principle 1: Build and serve on runtime!
Principle 2: Never embed any sensitive value in Dockerfile
In my package.json
, I added a script to build and run my app:
{
...
"scripts": {
...
"build": "nuxt build --modern=server",
"start": "nuxt start",
"prod": "yarn build && yarn start"
...
}
...
}
To serve a production build, all we need to do is to run yarn prod
.
In my Dockerfile
:
FROM node:10.16.3
ENV APP_DIR /app/
WORKDIR ${APP_DIR}
COPY . ./
RUN yarn install
ENV HOST 0.0.0.0 # Insensitive environment variable
EXPOSE 3000
CMD ["yarn", "prod"]
Here we go! Our dockerized Nuxt app is ready.
We can run our Docker image using docker run
or alternatively with Docker Compose. I personally use Docker Compose a lot. In my case, I created a docker-compose.production.yml
that inject environment variables from the .env
file in same directory.
My docker-compose.production.yml
:
version: "3"
services:
web:
build: .
restart: always
env_file:
- .env
ports:
- "3000:3000"
Once ready, we run docker-compose -f docker-compose.production.yml up -d
to bring up our containerized Nuxt app.
How to safely inject environment variable for dockerized Nuxt.js on AWS ECS
This section is only applicable if you choose ECS as your deployment platform of choice.
In this section, I will walk us through on:
- storing sensitive values using AWS SSM Parameter Store
- building and pushing image to image registry
- creating an ECS task
Step 1: Storing Sensitive Environment Variables in Parameter Store
AWS provides several products for the purpose of storing sensitive configurations, namely AWS SSM Parameter Store and AWS Secrets Manager. I use Parameter Store because of its' free 10,000 Standard tier parameters.
To add a parameter, click "Create Parameter" in the Parameter Store Management Console:
Fill up the form, then submit:
We should see the parameters created:
We will leave the parameters created for now. These will be consumed in a latter step.
Step 2: Building and Pushing Image to Image Registry
To deploy our Nuxt.js app to AWS ECS, we first require a repository to store our Docker image. For the sake of simplicity, I use AWS Elastic Container Registry (ECR).
The official guide provides a comprehensive list of steps to create a repository on AWS ECR.
Once the image repository is created, we can then build and push our image:
$(aws ecr get-login --no-include-email --region <region>)
docker build -t <image_name> .
docker tag <image_name>:latest <remote_repository_url>
docker push <remote_repository_url>
Once it's pushed, we should see a similar output on our terminal:
Navigating to our repository with AWS Management Console, we should see our image pushed:
Step 3: Create an ECS Task
This guide details the steps to create a Task definition in ECS. It is highly suggested to read this if you are not familiar with the concepts of Task and Service in ECS before moving forward.
While creating an ECS Task with the Management Console, you can now reference to parameters created in SSM Parameter Store by using the ARN of the parameters.
Once the Task is created, we can then run it on ECS. Navigating the the endpoint of your Task, you should be able to see your app is up and running.
Voila!
Now we have a dockerized Nuxt.js SSR app that is running on ECS with sensitive environment variables stored on SSM Parameter Store.
Have fun hacking around!