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
alpineimage for building the app instead of
node. Alpine uses
glibc; use Debian
slimif you need
- Use the
distrolessimage for serving the app instead of
- Use explicit (and optionally, deterministic) Docker base image tags.
- Use multistage builds to install dependencies and copy them to the runtime.
- Install only
productiondependencies in the Docker image. Use
npm ciif 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
# 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!