A Stateless Node.js App, Containerized

Dockerizing a Node.js application brings consistency across environments, simplifies debugging, and removes dependency headaches. When the resulting container runs on a managed platform like Kinsta, you get the added benefits of automatic scaling and a more controlled security posture without having to manage the underlying infrastructure.

Stateless architecture is a natural fit for this workflow. A server that stores no session information is easier to scale horizontally, simpler to maintain, and more resilient to failure because there is no state to lose or recover. Here’s how to build one and package it in a Docker container.

Setting Up The Node.js Project

Start by creating a directory for the project and moving into it:

mkdir smashing-app && cd smashing-app

Then initialize a new Node.js project:

npm init -y

Node.js is a platform built on Chrome's JavaScript engine, designed for server-side applications. Its lightweight, asynchronous model makes it a common choice for this kind of work. Express provides the web framework for our app:

npm install express

Create a file named app.js with the following code:

const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
app.get("/", (req, res) => {
  res.send("Welcome to our smashing stateless Node.js app!");
});
app.listen(port, () => {
  console.log(`Smashing app listening at http://localhost:${port}`);
});

Here’s what that code does:

  • const express = require("express"); imports the Express framework.
  • const app = express(); creates the application instance where routes and configuration are defined.
  • const port = process.env.PORT || 3000; reads the port from the PORT environment variable, defaulting to 3000 if it’s not set.
  • app.get("/", (req, res) => {} defines a route for GET requests to the root URL.
  • res.send("Welcome to our smashing stateless Node.js app!"); is the response sent for that request.
  • app.listen(port, () => {}) starts the server on the specified port.

Run the app locally with:

node app.js

The app will be available at http://localhost:3000.

Layering Docker On Top

Docker packages an application with its runtime and dependencies into a single container image, so it runs the same way on any platform that supports Docker. This removes the class of problems that come from differences in local environments. Before proceeding, make sure Docker is installed on your machine.

Create a Dockerfile in the project directory:

FROM node:18-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
ENV PORT=3000
CMD [ "node", "app.js" ]

Breaking that down:

  • FROM node:18-alpine uses the official Node.js image based on Alpine Linux as the starting point.
  • WORKDIR /usr/src/app sets the working directory inside the container.
  • COPY . . copies everything from the local directory into the container.
  • RUN npm install installs the dependencies in package.json.
  • ENV PORT=3000 sets the port as an environment variable, making the app more configurable for different hosting environments.
  • CMD [ "node", "app.js" ] is the command executed when the container starts.

Build the image and run the container to verify everything works:

docker build -t smashing-app
docker run -p 3000:3000 smashing-app

The -p 3000:3000 flag maps port 3000 on the host to port 3000 in the container. The first number is the host port, the second the container port. For example, you could map your machine’s port 1234 to the container’s port 3000, and localhost:1234 would still reach the app. The smashing-app argument names the image you built.

You can also pass a different port at runtime using an environment variable:

docker run -p 8080:5713 -d -e PORT=5713 smashing-app

That command maps the container’s port 5713 to the host’s port 8080, with PORT set to 5713 inside the container. Supporting environment variables in the Dockerfile makes the app adaptable to different hosting providers that may assign ports dynamically.

More Reasons To Containerize

Beyond the basic setup, Docker adds practical advantages during development and deployment.

Dependencies That Travel With The App

All dependencies are encapsulated in the image, so there’s no need to reconcile versions between machines. A package.json entry like

{
  "dependencies": {
    "lodash": "4.17.21"
  }
}

gets installed and bundled inside the container automatically when the Dockerfile runs npm install.

Versioning And Rollbacks

Images can be tagged for specific versions, making rollbacks or parallel deployments straightforward. Build a versioned image like this:

docker build -t smashing-app:v2 .

Then run different versions side by side:

docker run -p 3000:3000 -d smashing-app:v1

docker run -p 3001:3000 -d smashing-app:v2

Environment-Specific Configuration

Environment variables let the same image behave differently in development, staging, and production. In app.js you might read NODE_ENV:

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
const env = process.env.NODE_ENV || 'development';
app.get('/', (req, res) => {
  res.send(`Welcome to our smashing stateless Node.js app running in ${env} mode!`);
});
app.listen(port, () => {
  console.log(`Smashing app listening at http://localhost:${port}`);
});

You can set it in the Dockerfile:

FROM node:18-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
ENV NODE_ENV=production
CMD [ "node", "app.js" ]

Or when starting the container:

docker run -p 3000:3000 -d -e NODE_ENV=production smashing-app

Deploying To Kinsta

Kinsta handles the infrastructure once the container is ready. From the Kinsta dashboard, navigate to Applications in the sidebar and add a service of type application. The platform will prompt you to connect a GitHub account so that code pushes can trigger automated deployments. Select the repository, set the application name and any environment variables, and specify the build environment. The final step is to tell Kinsta where the Dockerfile lives in the repo, allocate compute resources, and provide payment information.

Kinsta then builds the application and gives you a public, secure URL where the container is reachable. Combined with stateless design, this setup holds up well under changing traffic and makes deployments predictable rather than an exercise in guessing what will break this time.