> ## Content Index
> Fetch the complete content index at: https://blog.hostly.ninja/llms.txt
> Use this file to discover other available public pages before exploring further.

# How do I package my NodeJS app for production? A Docker Guide.
- URL: https://blog.hostly.ninja/how-do-i-package-my-nodejs-app-for-production-a-docker-guide/
- Published: 2026-11-02T18:03:00.000Z
- Updated: 2026-11-02T18:06:41.000Z
- Description: Learn how to Dockerize a Node.js Express app for production: write a lean, secure Dockerfile, test the image locally, publish it to Docker Hub, and deploy it on Hostly.
- Author: Andreas Knipl
- Tags: tutorial

You have written a Node.js application, it works on your machine, and now you want to run it somewhere else. To do that on a container-hosting platform such as Hostly, you first need to package the application as a container image.

# How to dockerize a Node.js Application

This article walks through the complete process using a small Express API written in JavaScript. We will create the application, write a practical Dockerfile, test the image locally, publish it to Docker Hub, and finally deploy it on **Hostly**.

The application itself will remain intentionally simple. When you open it in a browser, it returns a small JSON greeting. This keeps the focus on creating and publishing the image rather than on the application code.

## What you will need

Before starting, make sure you have:

- Node.js and npm installed
- Docker Desktop or Docker Engine installed
- A free Docker Hub account (optional)
- A text editor or IDE

The examples use Node.js 24, which is the current LTS release line at the time of writing.

## 1\. Create a small Node.js project

Create a new directory for the application and move into it:

```Shell
$ mkdir hello-node-api && cd hello-node-api
```

Initialize a new npm project:

```
$ npm init -y
```

We will use Express to create the HTTP server:

```
$ npm install --save express
```

The application will use modern JavaScript modules, so enable ES modules in `package.json`:

```
$ npm pkg set type=module
```

Add a start command:

```
$ npm pkg set scripts.start="node src/main.js"
```

Create a directory for the source code:

```
$ mkdir src
```

Your project should now contain roughly the following files:

```
hello-node-api/
├── node_modules/
├── src/
├── package.json
└── package-lock.json
```

The `package-lock.json` file is important for the Docker build later on. It records the dependency versions installed by npm and allows us to use `npm ci` inside the image.

## 2\. Create the Express application

Create a file named `src/main.js`:

```JS
import express from "express";

const app = express();
const port = Number(process.env.PORT) || 3000;

app.get("/", (_request, response) => {
  response.json({
    message: "Hello from a Dockerized Node.js application!",
  });
});

app.listen(port, "0.0.0.0", () => {
  console.log(`Server listening on port ${port}`);
});
```

There are two details in this small application that matter when it is run inside a container.

First, the port can be supplied through the `PORT` environment variable. If no value is provided, the application uses port `3000`.

Second, the server listens on `0.0.0.0` rather than `localhost`. Inside a container, binding only to `localhost` would make the server unreachable from outside that container. Listening on `0.0.0.0` allows Docker (and later Hostly) to send traffic to it.

You can test the application before creating the image:

```Shell
$ npm start
```

Then open the following address in your browser:

```
http://localhost:3000
```

The browser should show a message similar to this:

```JSON
{
  "message": "Hello from a Dockerized Node.js application!"
}
```

Stop the server with `Ctrl+C`.

## 3\. Add a `.dockerignore` file

When Docker builds an image, it receives the contents of the project directory as its build context. We do not want to send local dependencies, Git metadata, environment files, or other unnecessary files.

Create a file named `.dockerignore` in the project root:

```Dockerignore
node_modules
npm-debug.log*
.git
.gitignore
.env
.env.*
!.env.example
coverage
```

The most important entry is `node_modules`.

Dependencies installed on your computer may have been built for a different operating system or CPU architecture than the container. Instead of copying them, the Docker build should install a clean set of dependencies inside the image.

Environment files are also excluded because they frequently contain credentials or other secrets. Configuration values should be supplied when the container is deployed rather than embedded permanently in the image.

## 4\. Create the Dockerfile

Create a file named `Dockerfile` in the project root. The filename has no extension.

Add the following content:

```Docker
FROM node:24-alpine

ENV NODE_ENV=production

WORKDIR /app

COPY package.json package-lock.json ./

RUN npm ci --omit=dev \
    && npm cache clean --force

COPY --chown=node:node src ./src

USER node

EXPOSE 3000

CMD ["node", "src/main.js"]
```

The finished project structure should now look like this:

```
hello-node-api/
├── src/
│   └── main.js
├── .dockerignore
├── Dockerfile
├── package.json
└── package-lock.json
```

Let us go through the Dockerfile one section at a time.

### Choose the base image

```Docker
FROM node:24-alpine
```

Every image starts from a base image. In this case, we use the official Node.js 24 image based on the alpine linux distribution. This is typically the smallest and most light-weight base image available.

> Alpine is a good fit for this simple application and produces a smaller image. Some Node.js packages with native components expect the `glibc` system library used by Debian. If you encounter native dependency or binary compatibility problems, use `node:24-bookworm-slim` instead.

Using a specific major Node.js version makes the build more predictable than using a floating tag such as `node:latest`. Patch releases within Node.js 24 can still be included when the base image is updated.

### Set the production environment

```Docker
ENV NODE_ENV=production
```

Many Node.js libraries use `NODE_ENV` to distinguish production from development behaviour.

### Select the working directory

```Docker
WORKDIR /app
```

This creates or selects `/app` as the working directory for the remaining Dockerfile instructions and for the running application.

### Copy and install the dependencies

```Docker
COPY package.json package-lock.json ./

RUN npm ci --omit=dev \
    && npm cache clean --force
```

The package files are copied before the application source. Docker can reuse this dependency layer when the source code changes but `package.json` and `package-lock.json` remain the same.

`npm ci` installs the versions recorded in the lock file. The `--omit=dev` option leaves out development-only dependencies because they are not needed to run the production container. Docker’s Node.js guide similarly recommends switching to `npm ci` once a lock file exists.

### Copy the application

```Docker
COPY --chown=node:node src ./src
```

This copies the source code into `/app/src`.

The `--chown` option assigns the files to the `node` user included in the official Node.js image.

### Stop running as root

```Docker
USER node
```

Containers run as the root user unless the image or Dockerfile specifies otherwise. Our application does not need root privileges, so we switch to the existing `node` user before starting it.

Running the application as a non-root user limits what it can modify inside the container if the application or one of its dependencies is compromised. The official Node.js image defines this unprivileged `node` user for this purpose.

### Document the application port

```Docker
EXPOSE 3000
```

`EXPOSE` documents the port used by the application. It does not publish the port automatically, nor does it force the application to use that port.

The actual port still comes from the application configuration. When deploying on Hostly, the port entered in the service configuration must match the port on which the application listens.

### Start Node.js directly

```Docker
CMD ["node", "src/main.js"]
```

This is the command that runs when a container is started from the image.

We start Node.js directly instead of using `npm start`, which avoids placing an additional npm process between the container runtime and the application.

## 5\. Build the image

From the directory containing the Dockerfile, run:

```Shell
$ docker build -t hello-node-api:1.0.0 .
```

The final period is important. It tells Docker to use the current directory as the build context.

The `-t` option assigns a name and tag to the image:

```
hello-node-api:1.0.0
```

Docker executes the instructions in the Dockerfile and creates a container image containing Node.js, the production dependencies, and the application source.

After the build finishes, you can confirm that the image exists:

```Shell
$ docker image ls
```

## 6\. Run the image locally

Start a container from the image:

```Shell
$ docker run --rm -p 3000:3000 hello-node-api:1.0.0
```

The `-p` option maps a port on your computer to a port inside the container. Its format is:

```
HOST_PORT:CONTAINER_PORT
```

In this example, traffic sent to port `3000` on your computer is forwarded to port `3000` in the container.

The `--rm` option removes the stopped container automatically. It does not delete the image.

Test the running container:

```Shell
$ curl http://localhost:3000
```

You should receive the same JSON response as before in the browser:

```JSON
{
  "message": "Hello from a Dockerized Node.js application!"
}
```

You have now confirmed that the application works independently of the Node.js installation on your computer.

Stop the container with `Ctrl+C`.

## 7\. Publish the image to Docker Hub

The image currently exists only on your machine. For Hostly to deploy it, the image must be available from a container registry.

We will use a public Docker Hub repository for this tutorial.

Sign in to Docker Hub and create a public repository named:

```
hello-node-api
```

Then authenticate from your terminal:

```Shell
$ docker login
```

Tag the local image with your Docker Hub username:

```Shell
$ docker tag hello-node-api:1.0.0 YOUR_USERNAME/hello-node-api:1.0.0
```

Replace `YOUR_USERNAME` with your actual Docker Hub username.

Push the tagged image:

```Shell
$ docker push YOUR_USERNAME/hello-node-api:1.0.0
```

Docker Hub image names include the account namespace, repository name, and tag. Docker’s documented publishing workflow is to tag the local image with that complete name and then upload it using `docker push`.

After the upload finishes, the complete image address is:

```
docker.io/YOUR_USERNAME/hello-node-api:1.0.0
```

Using a version such as `1.0.0` instead of relying only on `latest` makes it clear which image a deployment is running. When you publish a new version later, you can use a new tag such as `1.0.1`.

### A note about CPU architectures

A normal `docker build` creates an image for the architecture used by your Docker environment. For example, an Apple Silicon computer will ordinarily produce an ARM64 image.

When you need one image that supports both common server and desktop architectures, Docker Buildx can publish a multi-platform image:

```Shell
$ docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t YOUR_USERNAME/hello-node-api:1.0.0 \
  --push .
```

A multi-platform image contains variants for the specified architectures, allowing the container runtime to select the appropriate one when it pulls the image.

For this pure JavaScript example, creating both variants should normally be straightforward. Applications with native Node.js dependencies may require additional attention during a cross-platform build.

## 8\. Deploy the image on Hostly

The final deployment is deliberately short because all of the application packaging work has already been completed.

Create a new service in Hostly and provide:

- Your desired `hostly.ninja` subdomain
- The image URL: `docker.io/YOUR_USERNAME/hello-node-api:1.0.0`
- Container port: `3000`

This example does not require environment variables, secrets, volumes, or configuration files, so those sections can remain empty.

Click **Deploy**.

Hostly pulls the image, starts the container, routes traffic to it, and provides SSL for the selected subdomain. Service health is checked by opening a TCP connection to the configured port, so the application does not need a special health-check endpoint.

Once the deployment is ready, opening the assigned `hostly.ninja` address should return:

```JSON
{
  "message": "Hello from a Dockerized Node.js application!"
}
```

For this tutorial, we used a public Docker Hub repository so that the image can be pulled without authentication. Hostly can also deploy images from private repositories when the appropriate registry credentials are supplied.

## What you have built

You started with a few lines of JavaScript and turned them into a portable container image that can be run locally or on a hosting platform.

The important parts were not specific to the Hello World response. The same process applies to a larger Express or Fastify application:

1. Make the server listen on a configurable port and `0.0.0.0`.
2. Keep dependencies locked in `package-lock.json`.
3. Exclude local dependencies and secrets using `.dockerignore`.
4. Install only production dependencies in the image.
5. Run the application as a non-root user.
6. Build and test the image locally.
7. Publish it to a container registry.
8. Give Hostly the image address and application port.

The application may become more complex, but the basic deployment contract remains small: an image, a port, and the configuration the application needs to run.