Dockerize a Node.js App using a Distroless Image
In a previous post, I covered the deployment of a simple Node.js application on Railway. Today, I'll explain how to dockerize the app using a distroless container image for better security, performance, and manageability.
What is a Google Distroless Image?
Google's Distroless container images are minimal Docker images designed for running applications. These images are built from scratch, signed using Sigstore Cosign, and contain only the minimal set of libraries and packages needed to run an application, making them smaller and more secure than other images like Debian or Alpine. The smallest distroless image, gcr.io/distroless/static-debian11
, is around 2 MiB. That's approximately 50% of the size of alpine
(~5 MiB), and less than 2% of the size of debian
(124 MiB). Distroless images do not contain package managers, shells or other similar programs; since they have a smaller attack surface, they are also less vulnerable to security exploits.
Dockerize a Simple Node.js App using a Distroless Image
First, let's fork the vanilla Node.js web application repository on GitHub. This is a simple "Hello World!" application, and I don't intend to make any structural changes to it. Since distroless images exclude several packages, they are better suited to serving the application than building it. Hence, we'll use multi-stage Docker builds to deploy the Node.js app. Add a Dockerfile
file to the repository with the contents below; I'm splitting the stages for readability.
A vanilla, but functional, Dockerfile
using the node
image would be as below.
FROM node
WORKDIR /app
COPY * /app
RUN npm install
CMD ["node", "index.js"]
Snyk has good posts on choosing a Node.js Docker image and configuring the images; while I'm using some of the guidance to create a hardened image with a smaller footprint, there's obviously a lot more that you should consider before deploying this image into production. Some of my considerations below are:
- Use the
alpine
image for building the app instead ofnode
. Alpine usesmusl
instead ofglibc
; use Debianslim
if you needglibc
compatibility. - Use the
distroless
image for serving the app instead ofnode
. - Use explicit (and optionally, deterministic) Docker base image tags.
- Use multistage builds to install dependencies and copy them to the runtime.
- Install only
production
dependencies in the Docker image. Usenpm ci
if integration with a continuous integration system. - Run the container as a non-root user account.
The first stage builds the application artifacts using a node:18.15.0-alpine
image, sets the environment to production, and installs the required dependencies.
# Stage 1
FROM node:18.15.0-alpine AS base
ENV NODE_ENV production
RUN mkdir -p /app
WORKDIR /app
COPY * /app
RUN npm install --production
The second stage inserts the application artifacts built in the first stage into the runtime distroless image, and launches the index.js
file as a nonroot
user.
# Stage 2
FROM gcr.io/distroless/nodejs18-debian11
WORKDIR /app
COPY --from=base /app /app
USER nonroot
CMD ["index.js"]
You can use this repository to deploy the app in the next section; I'll be using a pre-built template with the PORT
variable configured instead.
Deploy the Dockerized Node.js App on Railway
Railway is a modern app hosting platform that makes it easy to deploy production-ready apps quickly. Railway offers persistent database services for PostgreSQL, MySQL, MongoDB, and Redis, as well as application services with a GitHub repository as the deployment source. For the latter, Railway can automatically determine the application runtime and deploy the service. Railway offers several one-click starters for popular applications, including Node.js. Since we are just testing the waters, Railway's free tier should be sufficient to host the service.
Sign up for an account with Railway using GitHub, and click Authorize Railway App
when redirected. Review and agree to Railway's Terms of Service and Fair Use Policy if prompted. Launch the Node.js Distroless one-click starter template (or click the button below) to deploy a dockerized Node.js app instantly on Railway.
You'll be given an opportunity to change the default repository name and set it private, if you'd like. Accept the defaults and click Deploy
; the deployment will kick off immediately.
Once the deployment completes, a simple Node.js app will be available at a default xxx.up.railway.app
domain - launch this URL to access the app. If you are interested in setting up a custom domain, I covered it at length in a previous post - see the final section here. In conclusion, deploying a dockerized Node.js app on Railway using a distroless image is a straightforward process, made even easier by the one-click template. Go forth and deploy!