guides: refresh nodejs (#25319)

<!--Delete sections as needed -->

## Description

Refreshed Node.js guide

- Updated all examples to DHI. DHI Community is now free so the DOI
fallback is no longer needed.
- Replaced the git clone pattern with the file scaffolding component.
- Simplified the sample app to a Node.js backend API. Added links at the
start of the guide to dedicated frontend framework guides.
- Added "Secure your Node.js image supply chain" topic to showcase DHI.
- Refreshed topic intros and related links.

https://deploy-preview-25319--docsdocker.netlify.app/guides/nodejs/


## Related issues or tickets

ENGDOCS-3319
Closes #25280

## Reviews

<!-- Notes for reviewers here -->
<!-- List applicable reviews (optionally @tag reviewers) -->

- [ ] Technical review
- [ ] Editorial review
- [ ] Product review

Signed-off-by: Craig Osterhout <craig.osterhout@docker.com>
This commit is contained in:
Craig Osterhout
2026-06-11 08:41:08 -07:00
committed by GitHub
parent f9c884674f
commit 8f225794d6
7 changed files with 1377 additions and 2038 deletions
+11 -23
View File
@@ -1,7 +1,7 @@
---
title: Node.js language-specific guide
linkTitle: Node.js
description: Containerize and develop Node.js apps using Docker
description: Containerize and develop Node.js applications using Docker
keywords: getting started, node, node.js
summary: |
This guide explains how to containerize Node.js applications using Docker.
@@ -16,41 +16,29 @@ params:
time: 20 minutes
---
[Node.js](https://nodejs.org/en) is a JavaScript runtime for building web applications. This guide shows you how to containerize a TypeScript Node.js application with a React frontend and PostgreSQL database.
The sample application is a modern full-stack Todo application featuring:
- **Backend**: Express.js with TypeScript, PostgreSQL database, and RESTful API
- **Frontend**: React.js with Vite and Tailwind CSS 4
[Node.js](https://nodejs.org/en) is a JavaScript runtime for building server-side applications. This guide shows you how to containerize a TypeScript Node.js application using Docker, starting from a simple Express API and progressively adding features like a database and CI/CD.
This guide focuses on a backend Node.js API. If you're building a standalone frontend application, Docker has dedicated guides for [React.js](/guides/reactjs/), [Vue.js](/guides/vuejs/), [Angular](/guides/angular/), and [Next.js](/guides/nextjs/).
> **Acknowledgment**
>
> Docker extends its sincere gratitude to [Kristiyan Velkov](https://www.linkedin.com/in/kristiyan-velkov-763130b3/) for authoring this guide. As a Docker Captain and experienced Full-stack engineer, his expertise in Docker, DevOps, and modern web development has made this resource invaluable for the community, helping developers navigate and optimize their Docker workflows.
---
> Docker thanks [Kristiyan Velkov](https://www.linkedin.com/in/kristiyan-velkov-763130b3/) for his contribution to this guide.
## What will you learn?
In this guide, you will learn how to:
In this guide, you'll learn how to:
- Containerize and run a Node.js application using Docker.
- Set up a local development environment using containers.
- Run tests inside a Docker container.
- Set up a development container environment.
- Configure GitHub Actions for CI/CD with Docker.
- Deploy your Dockerized Node.js app to Kubernetes.
- Inspect and generate supply chain attestations for your image.
- Deploy your containerized Node.js application to Kubernetes.
To begin, youll start by containerizing an existing Node.js application.
---
Start by containerizing a Node.js application.
## Prerequisites
Before you begin, make sure you're familiar with the following:
- Basic understanding of [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) and [TypeScript](https://www.typescriptlang.org/).
- Basic knowledge of [Node.js](https://nodejs.org/en), [npm](https://docs.npmjs.com/about-npm), and [React](https://react.dev/) for modern web development.
- Understanding of Docker concepts such as images, containers, and Dockerfiles. If you're new to Docker, start with the [Docker basics](/get-started/docker-concepts/the-basics/what-is-a-container.md) guide.
- Familiarity with [Express.js](https://expressjs.com/) for backend API development.
Once you've completed the Node.js getting started modules, youll be ready to containerize your own Node.js application using the examples and instructions provided in this guide.
- Basic knowledge of [Node.js](https://nodejs.org/en) and [npm](https://docs.npmjs.com/about-npm).
- Familiarity with Docker concepts such as images, containers, and Dockerfiles. If you're new to Docker, start with the [Docker basics](/get-started/docker-concepts/the-basics/what-is-a-container.md) guide.
+68 -277
View File
@@ -1,7 +1,7 @@
---
title: Automate your builds with GitHub Actions
linkTitle: GitHub Actions CI
weight: 50
weight: 40
keywords: CI/CD, GitHub Actions, Node.js, Docker
description: Learn how to configure CI/CD using GitHub Actions for your Node.js application.
aliases:
@@ -11,334 +11,125 @@ aliases:
## Prerequisites
Complete all the previous sections of this guide, starting with [Containerize a Node.js application](containerize.md).
Complete all the previous sections of this guide, starting with [Containerize a Node.js application](containerize.md). You must have a [GitHub](https://github.com/signup) account and a verified [Docker](https://hub.docker.com/signup) account to complete this section.
You must also have:
If you haven't created a [GitHub repository](https://github.com/new) for your project yet, do that now. After creating the repository, [add a remote](https://docs.github.com/en/get-started/getting-started-with-git/managing-remote-repositories) and make sure you can commit and [push your code](https://docs.github.com/en/get-started/using-git/pushing-commits-to-a-remote-repository#about-git-push) to GitHub.
- A [GitHub](https://github.com/signup) account.
- A verified [Docker Hub](https://hub.docker.com/signup) account.
1. In your project's GitHub repository, open **Settings**, and go to **Secrets and variables** > **Actions**.
---
2. Under the **Variables** tab, create a new **Repository variable** named `DOCKER_USERNAME` with your Docker ID as the value.
3. Create a new [Personal Access Token (PAT)](/manuals/security/access-tokens.md#create-an-access-token) for Docker Hub. You can name this token `docker-tutorial`. Make sure access permissions include Read and Write.
4. Add the PAT as a **Repository secret** in your GitHub repository, with the name `DOCKERHUB_TOKEN`.
## Overview
In this section, you'll set up a CI/CD pipeline using [GitHub Actions](https://docs.github.com/en/actions) to automatically:
GitHub Actions is a CI/CD automation tool built into GitHub. A workflow is a YAML file that tells GitHub which jobs to run when something happens in your repository, like a push to a branch or a pull request opening. Workflows live in the `.github/workflows/` directory of your repository.
- Build your Node.js application inside a Docker container.
- Run unit and integration tests, and make sure your application meets solid code quality standards.
- Perform security scanning and vulnerability assessment.
- Push production-ready images to [Docker Hub](https://hub.docker.com).
In this section, you'll add a workflow that runs your tests on every push to the main branch, then builds your Docker image and pushes it to Docker Hub.
---
## Define the GitHub Actions workflow
## Connect your GitHub repository to Docker Hub
You can create a GitHub Actions workflow by creating a YAML file in the `.github/workflows/` directory of your repository. Use your favorite text editor or the GitHub web interface.
To enable GitHub Actions to build and push Docker images, you'll securely store your Docker Hub credentials in your new GitHub repository.
If you prefer to use the GitHub web interface:
### Step 1: Connect your GitHub repository to Docker Hub
1. Go to your repository on GitHub and select the **Actions** tab.
1. Create a Personal Access Token (PAT) from [Docker Hub](https://hub.docker.com).
1. From your Docker Hub account, go to **Account Settings → Security**.
2. Generate a new Access Token with **Read/Write** permissions.
3. Name it something like `docker-nodejs-sample`.
4. Copy and save the token — you'll need it in Step 4.
2. Select **set up a workflow yourself**.
2. Create a repository in [Docker Hub](https://hub.docker.com/repositories/).
1. From your Docker Hub account, select **Create a repository**.
2. For the Repository Name, use something descriptive — for example: `nodejs-sample`.
3. Once created, copy and save the repository name — you'll need it in Step 4.
This takes you to a page for creating a new GitHub Actions workflow file in your repository. By default, the file is created under `.github/workflows/main.yml`. Change the filename to `build.yml`.
3. Create a new [GitHub repository](https://github.com/new) for your Node.js project.
If you prefer to use your text editor, create a new file named `build.yml` in the `.github/workflows/` directory of your repository.
4. Add Docker Hub credentials as GitHub repository secrets.
Add the following content to the file:
In your newly created GitHub repository:
1. From **Settings**, go to **Secrets and variables → Actions → New repository secret**.
2. Add the following secrets:
| Name | Value |
| ------------------------ | ------------------------------------------------ |
| `DOCKER_USERNAME` | Your Docker Hub username |
| `DOCKERHUB_TOKEN` | Your Docker Hub access token (created in Step 1) |
| `DOCKERHUB_PROJECT_NAME` | Your Docker Project Name (created in Step 2) |
These secrets let GitHub Actions to authenticate securely with Docker Hub during automated workflows.
5. Connect your local project to GitHub.
Link your local project `docker-nodejs-sample` to the GitHub repository you just created by running the following command from your project root:
```console
$ git remote set-url origin https://github.com/{your-username}/{your-repository-name}.git
```
> [!IMPORTANT]
> Replace `{your-username}` and `{your-repository}` with your actual GitHub username and repository name.
To confirm that your local project is correctly connected to the remote GitHub repository, run:
```console
$ git remote -v
```
You should see output similar to:
```console
origin https://github.com/{your-username}/{your-repository-name}.git (fetch)
origin https://github.com/{your-username}/{your-repository-name}.git (push)
```
This confirms that your local repository is properly linked and ready to push your source code to GitHub.
6. Push your source code to GitHub.
Follow these steps to commit and push your local project to your GitHub repository:
1. Stage all files for commit.
```console
$ git add -A
```
This command stages all changes — including new, modified, and deleted files — preparing them for commit.
2. Commit your changes.
```console
$ git commit -m "Initial commit with CI/CD pipeline"
```
This command creates a commit that snapshots the staged changes with a descriptive message.
3. Push the code to the `main` branch.
```console
$ git push -u origin main
```
This command pushes your local commits to the `main` branch of the remote GitHub repository and sets the upstream branch.
Once completed, your code will be available on GitHub, and any GitHub Actions workflow you've configured will run automatically.
> [!NOTE]
> Learn more about the Git commands used in this step:
>
> - [Git add](https://git-scm.com/docs/git-add) Stage changes (new, modified, deleted) for commit
> - [Git commit](https://git-scm.com/docs/git-commit) Save a snapshot of your staged changes
> - [Git push](https://git-scm.com/docs/git-push) Upload local commits to your GitHub repository
> - [Git remote](https://git-scm.com/docs/git-remote) View and manage remote repository URLs
---
### Step 2: Set up the workflow
Now you'll create a GitHub Actions workflow that builds your Docker image, runs tests, and pushes the image to Docker Hub.
1. From your repository on GitHub, select the **Actions** tab in the top menu.
2. When prompted, select **Set up a workflow yourself**.
This opens an inline editor to create a new workflow file. By default, it will be saved to:
`.github/workflows/main.yml`
3. Add the following workflow configuration to the new file:
{{< files name="nodejs-docker-example" >}}
{{< file path=".github/workflows/build.yml" status="new" >}}
```yaml
name: CI/CD Node.js Application with Docker
# GitHub Actions workflow that runs on every push to main.
# - test: runs Vitest unit tests inside a container.
# - build_and_push: signs in to Docker Hub and the DHI registry, then
# builds and pushes the image.
name: Build and push Docker image
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
branches:
- main
jobs:
test:
name: Run Node.js Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18-alpine
env:
POSTGRES_DB: todoapp_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@{{% param "checkout_action_version" %}}
- uses: actions/checkout@{{% param "checkout_action_version" %}}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@{{% param "setup_buildx_action_version" %}}
- name: Run tests
run: docker build --target test -t nodejs-test . && docker run --rm nodejs-test
- name: Cache npm dependencies
uses: actions/cache@{{% param "cache_action_version" %}}
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-npm-
- name: Build test image
uses: docker/build-push-action@{{% param "build_push_action_version" %}}
with:
context: .
target: test
tags: nodejs-app-test:latest
platforms: linux/amd64
load: true
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
- name: Run tests inside container
run: |
docker run --rm \
--network host \
-e NODE_ENV=test \
-e POSTGRES_HOST=localhost \
-e POSTGRES_PORT=5432 \
-e POSTGRES_DB=todoapp_test \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
nodejs-app-test:latest
env:
CI: true
timeout-minutes: 10
build-and-push:
name: Build and Push Docker Image
build_and_push:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@{{% param "checkout_action_version" %}}
- uses: actions/checkout@{{% param "checkout_action_version" %}}
- name: Login to Docker Hub
uses: docker/login-action@{{% param "login_action_version" %}}
with:
username: ${{ vars.DOCKER_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Docker Hardened Images
uses: docker/login-action@{{% param "login_action_version" %}}
with:
registry: dhi.io
username: ${{ vars.DOCKER_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@{{% param "setup_buildx_action_version" %}}
- name: Cache Docker layers
uses: actions/cache@{{% param "cache_action_version" %}}
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: ${{ runner.os }}-buildx-
- name: Extract metadata
id: meta
run: |
echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> "$GITHUB_OUTPUT"
echo "SHORT_SHA=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- name: Log in to Docker Hub
uses: docker/login-action@{{% param "login_action_version" %}}
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push multi-arch production image
- name: Build and push
uses: docker/build-push-action@{{% param "build_push_action_version" %}}
with:
context: .
target: production
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKERHUB_PROJECT_NAME }}:latest
${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKERHUB_PROJECT_NAME }}:${{ steps.meta.outputs.SHORT_SHA }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
tags: ${{ vars.DOCKER_USERNAME }}/${{ github.event.repository.name }}:latest
```
{{< /file >}}
This workflow performs the following tasks for your Node.js application:
{{< /files >}}
- Triggers on every `push` or `pull request` to the `main` branch.
- Builds a test Docker image using the `test` stage.
- Runs tests in a containerized environment.
- Stops the workflow if any test fails.
- Caches Docker build layers and npm dependencies for faster runs.
- Authenticates with Docker Hub using GitHub secrets.
- Builds an image using the `production` stage.
- Tags and pushes the image to Docker Hub with `latest` and short SHA tags.
The workflow has two jobs:
> [!NOTE]
> For more information about `docker/build-push-action`, refer to the [GitHub Action README](https://github.com/docker/build-push-action/blob/master/README.md).
1. **test**: Builds the `test` stage of the Dockerfile and runs it. If tests fail, the workflow stops and `build_and_push` doesn't run.
2. **build_and_push**: Signs in to Docker Hub and the DHI registry, then builds and pushes the image.
---
## Run the workflow
### Step 3: Run the workflow
Commit the changes and push them to the `main` branch. This workflow runs every time you push changes to `main`. You can find more information about workflow triggers in the [GitHub documentation](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows).
After adding your workflow file, trigger the CI/CD process.
Go to the **Actions** tab of your GitHub repository. It displays the workflow. Selecting the workflow shows you the breakdown of all the steps.
1. Commit and push your workflow file
From the GitHub editor, select **Commit changes…**.
- This push automatically triggers the GitHub Actions pipeline.
2. Monitor the workflow execution
1. From your GitHub repository, go to the **Actions** tab.
2. Select the workflow run to follow each step: `test`, `build`, `security`, and (if successful) `push` and `deploy`.
3. Verify the Docker image on Docker Hub
- After a successful workflow run, visit your [Docker Hub repositories](https://hub.docker.com/repositories).
- You should see a new image under your repository with:
- Repository name: `${your-repository-name}`
- Tags include:
- `latest` represents the most recent successful build; ideal for quick testing or deployment.
- `<short-sha>` a unique identifier based on the commit hash, useful for version tracking, rollbacks, and traceability.
> [!TIP] Protect your main branch
> To maintain code quality and prevent accidental direct pushes, enable branch protection rules:
>
> - From your GitHub repository, go to **Settings → Branches**.
> - Under Branch protection rules, select **Add rule**.
> - Specify `main` as the branch name.
> - Enable options like:
> - _Require a pull request before merging_.
> - _Require status checks to pass before merging_.
>
> This ensures that only tested and reviewed code is merged into `main` branch.
---
When the workflow is complete, go to your [repositories on Docker Hub](https://hub.docker.com/repositories). If you see the new repository in that list, the GitHub Actions workflow successfully pushed the image to Docker Hub.
## Summary
In this section, you set up a comprehensive CI/CD pipeline for your containerized Node.js application using GitHub Actions.
In this section, you learned how to set up a GitHub Actions workflow for your Node.js application that includes:
What you accomplished:
- Running Vitest unit tests inside a container
- Building and pushing Docker images
- Created a new GitHub repository specifically for your project.
- Generated a Docker Hub access token and added it as a GitHub secret.
- Created a GitHub Actions workflow that:
- Builds your application in a Docker container.
- Run tests in a containerized environment.
- Pushes an image to Docker Hub if tests pass.
- Verified the workflow runs successfully.
Related information:
Your Node.js application now has automated testing and deployment.
---
## Related resources
Deepen your understanding of automation and best practices for containerized apps:
- [Introduction to GitHub Actions](/guides/gha.md) Learn how GitHub Actions automate your workflows
- [Docker Build GitHub Actions](/manuals/build/ci/github-actions/_index.md) Set up container builds with GitHub Actions
- [Workflow syntax for GitHub Actions](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) Full reference for writing GitHub workflows
- [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) Learn about GHCR features and usage
- [Best practices for writing Dockerfiles](/develop/develop-images/dockerfile_best-practices/) Optimize your image for performance and security
---
- [Introduction to GitHub Actions](/guides/gha.md)
- [Docker Build GitHub Actions](/manuals/build/ci/github-actions/_index.md)
- [docker/login-action](https://github.com/docker/login-action)
- [docker/build-push-action](https://github.com/docker/build-push-action)
- [Create a Docker Hub access token](/manuals/security/access-tokens.md#create-an-access-token)
## Next steps
Next, learn how you can deploy your containerized Node.js application to Kubernetes with production-ready configuration. This helps you ensure your application behaves as expected in a production-like environment, reducing surprises during deployment.
In the next section, you'll learn how to inspect and generate supply chain
attestations for your image. See [Secure your supply chain](secure-supply-chain.md).
File diff suppressed because it is too large Load Diff
+92 -412
View File
@@ -3,7 +3,7 @@ title: Deploy your Node.js application
linkTitle: Deploy your app
weight: 50
keywords: deploy, kubernetes, node, node.js, production
description: Learn how to deploy your containerized Node.js application to Kubernetes with production-ready configuration
description: Learn how to deploy your containerized Node.js application to Kubernetes.
aliases:
- /language/nodejs/deploy/
- /guides/language/nodejs/deploy/
@@ -16,143 +16,88 @@ aliases:
## Overview
In this section, you'll learn how to deploy your containerized Node.js application to Kubernetes using Docker Desktop. This deployment uses production-ready configurations including security hardening, auto-scaling, persistent storage, and high availability features.
In this section, you'll deploy your containerized Node.js application to a local Kubernetes cluster using Docker Desktop. You'll create a Kubernetes manifest that describes how the application should run, including the application deployment, the PostgreSQL database, and the services that connect them.
You'll deploy a complete stack including:
## Create a Kubernetes manifest
- Node.js Todo application with 3 replicas.
- PostgreSQL database with persistent storage.
- Auto-scaling based on CPU and memory usage.
- Ingress configuration for external access.
- Security settings.
Create a new file called `nodejs-docker-example-kubernetes.yaml` in your project root:
## Create a Kubernetes deployment file
Create a new file called `nodejs-sample-kubernetes.yaml` in your project root:
{{< files name="nodejs-docker-example" >}}
{{< file path="nodejs-docker-example-kubernetes.yaml" status="new" >}}
```yaml
# ========================================
# Node.js Todo App - Kubernetes Deployment
# ========================================
apiVersion: v1
kind: Namespace
metadata:
name: todoapp
labels:
app: todoapp
name: nodejs-docker-example
---
# ========================================
# ConfigMap for Application Configuration
# ========================================
apiVersion: v1
kind: ConfigMap
metadata:
name: todoapp-config
namespace: todoapp
name: app-config
namespace: nodejs-docker-example
data:
NODE_ENV: 'production'
ALLOWED_ORIGINS: 'https://yourdomain.com'
POSTGRES_HOST: 'todoapp-postgres'
POSTGRES_PORT: '5432'
POSTGRES_DB: 'todoapp'
POSTGRES_USER: 'todoapp'
POSTGRES_SERVER: 'postgres'
POSTGRES_DB: 'example'
POSTGRES_USER: 'postgres'
---
# ========================================
# Secret for Database Credentials
# ========================================
apiVersion: v1
kind: Secret
metadata:
name: todoapp-secrets
namespace: todoapp
type: Opaque
data:
postgres-password: dG9kb2FwcF9wYXNzd29yZA== # base64 encoded "todoapp_password"
---
# ========================================
# PostgreSQL PersistentVolumeClaim
# ========================================
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: todoapp
namespace: nodejs-docker-example
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: standard
storage: 1Gi
---
# ========================================
# PostgreSQL Deployment
# ========================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: todoapp-postgres
namespace: todoapp
labels:
app: todoapp-postgres
name: postgres
namespace: nodejs-docker-example
spec:
replicas: 1
selector:
matchLabels:
app: todoapp-postgres
app: postgres
template:
metadata:
labels:
app: todoapp-postgres
app: postgres
spec:
containers:
- name: postgres
image: postgres:18-alpine
image: dhi.io/postgres:18
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: todoapp-config
name: app-config
key: POSTGRES_DB
- name: POSTGRES_USER
valueFrom:
configMapKeyRef:
name: todoapp-config
name: app-config
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: todoapp-secrets
key: postgres-password
name: app-secrets
key: db-password
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql
livenessProbe:
exec:
command:
- pg_isready
- -U
- todoapp
- -d
- todoapp
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- todoapp
- -d
- todoapp
command: [pg_isready]
initialDelaySeconds: 5
periodSeconds: 5
volumes:
@@ -161,434 +106,169 @@ spec:
claimName: postgres-pvc
---
# ========================================
# PostgreSQL Service
# ========================================
apiVersion: v1
kind: Service
metadata:
name: todoapp-postgres
namespace: todoapp
labels:
app: todoapp-postgres
name: postgres
namespace: nodejs-docker-example
spec:
type: ClusterIP
ports:
- port: 5432
targetPort: 5432
protocol: TCP
name: postgres
selector:
app: todoapp-postgres
app: postgres
---
# ========================================
# Application Deployment
# ========================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: todoapp-deployment
namespace: todoapp
labels:
app: todoapp
name: server
namespace: nodejs-docker-example
spec:
replicas: 3
replicas: 2
selector:
matchLabels:
app: todoapp
app: server
template:
metadata:
labels:
app: todoapp
app: server
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
fsGroup: 1001
containers:
- name: todoapp
image: ghcr.io/your-username/docker-nodejs-sample:latest
imagePullPolicy: Always
- name: server
image: DOCKER_USERNAME/nodejs-docker-example:latest
ports:
- containerPort: 3000
name: http
protocol: TCP
env:
- name: NODE_ENV
- name: POSTGRES_SERVER
valueFrom:
configMapKeyRef:
name: todoapp-config
key: NODE_ENV
- name: ALLOWED_ORIGINS
valueFrom:
configMapKeyRef:
name: todoapp-config
key: ALLOWED_ORIGINS
- name: POSTGRES_HOST
valueFrom:
configMapKeyRef:
name: todoapp-config
key: POSTGRES_HOST
- name: POSTGRES_PORT
valueFrom:
configMapKeyRef:
name: todoapp-config
key: POSTGRES_PORT
name: app-config
key: POSTGRES_SERVER
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: todoapp-config
name: app-config
key: POSTGRES_DB
- name: POSTGRES_USER
valueFrom:
configMapKeyRef:
name: todoapp-config
name: app-config
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: todoapp-secrets
key: postgres-password
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
name: app-secrets
key: db-password
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
initialDelaySeconds: 10
periodSeconds: 5
resources:
requests:
memory: '256Mi'
cpu: '250m'
limits:
memory: '512Mi'
cpu: '500m'
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
---
# ========================================
# Application Service
# ========================================
apiVersion: v1
kind: Service
metadata:
name: todoapp-service
namespace: todoapp
labels:
app: todoapp
name: server
namespace: nodejs-docker-example
spec:
type: ClusterIP
ports:
- name: http
port: 80
- port: 3000
targetPort: 3000
protocol: TCP
selector:
app: todoapp
---
# ========================================
# Ingress for External Access
# ========================================
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: todoapp-ingress
namespace: todoapp
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
cert-manager.io/cluster-issuer: 'letsencrypt-prod'
spec:
tls:
- hosts:
- yourdomain.com
secretName: todoapp-tls
rules:
- host: yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: todoapp-service
port:
number: 80
---
# ========================================
# HorizontalPodAutoscaler
# ========================================
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: todoapp-hpa
namespace: todoapp
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: todoapp-deployment
minReplicas: 1
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
---
# ========================================
# PodDisruptionBudget
# ========================================
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: todoapp-pdb
namespace: todoapp
spec:
minAvailable: 1
selector:
matchLabels:
app: todoapp
app: server
```
{{< /file >}}
## Configure the deployment
{{< /files >}}
Before deploying, you need to customize the deployment file for your environment:
Before applying the manifest, replace `DOCKER_USERNAME` in the `server` deployment's image field with your Docker Hub username.
1. Image reference: Replace `your-username` with your GitHub username or Docker Hub username:
## Deploy to Kubernetes
```yaml
image: ghcr.io/your-username/docker-nodejs-sample:latest
```
2. Domain name: Replace `yourdomain.com` with your actual domain in two places:
```yaml
# In ConfigMap
ALLOWED_ORIGINS: "https://yourdomain.com"
# In Ingress
- host: yourdomain.com
```
3. Database password (optional): The default password is already base64 encoded. To change it:
```console
$ echo -n "your-new-password" | base64
```
Then update the Secret:
```yaml
data:
postgres-password: <your-base64-encoded-password>
```
4. Storage class: Adjust based on your cluster (current: `standard`)
## Understanding the deployment
The deployment file creates a complete application stack with multiple components working together.
### Architecture
The deployment includes:
- Node.js application: Runs 3 replicas of your containerized Todo app
- PostgreSQL database: Single instance with 10Gi of persistent storage
- Services: Kubernetes services handle load balancing across application replicas
- Ingress: External access through an ingress controller with SSL/TLS support
### Security
The deployment uses several security features:
- Containers run as a non-root user (UID 1001)
- Read-only root filesystem prevents unauthorized writes
- Linux capabilities are dropped to minimize attack surface
- Sensitive data like database passwords are stored in Kubernetes secrets
### High availability
To keep your application running reliably:
- Three application replicas ensure service continues if one pod fails
- Pod disruption budget maintains at least one available pod during updates
- Rolling updates allow zero-downtime deployments
- Health checks on the `/health` endpoint ensure only healthy pods receive traffic
### Auto-scaling
The Horizontal Pod Autoscaler scales your application based on resource usage:
- Scales between 1 and 5 replicas automatically
- Triggers scaling when CPU usage exceeds 70%
- Triggers scaling when memory usage exceeds 80%
- Resource limits: 256Mi-512Mi memory, 250m-500m CPU per pod
### Data persistence
PostgreSQL data is stored persistently:
- 10Gi persistent volume stores database files
- Database initializes automatically on first startup
- Data persists across pod restarts and updates
## Deploy your application
### Step 1: Deploy to Kubernetes
Deploy your application to the local Kubernetes cluster:
Apply the manifest to your local Kubernetes cluster:
```console
$ kubectl apply -f nodejs-sample-kubernetes.yaml
$ kubectl apply -f nodejs-docker-example-kubernetes.yaml
```
You should see output confirming all resources were created:
You should see output confirming that the resources were created:
```shell
namespace/todoapp created
secret/todoapp-secrets created
configmap/todoapp-config created
```console
namespace/nodejs-docker-example created
configmap/app-config created
persistentvolumeclaim/postgres-pvc created
deployment.apps/todoapp-postgres created
service/todoapp-postgres created
deployment.apps/todoapp-deployment created
service/todoapp-service created
ingress.networking.k8s.io/todoapp-ingress created
poddisruptionbudget.policy/todoapp-pdb created
horizontalpodautoscaler.autoscaling/todoapp-hpa created
deployment.apps/postgres created
service/postgres created
deployment.apps/server created
service/server created
```
### Step 2: Verify the deployment
Check that your deployments are running:
Then create the database secret from your password file:
```console
$ kubectl get deployments -n todoapp
$ kubectl create secret generic app-secrets \
--namespace nodejs-docker-example \
--from-file=db-password=db/password.txt
```
Expected output:
## Verify the deployment
```shell
NAME READY UP-TO-DATE AVAILABLE AGE
todoapp-deployment 3/3 3 3 30s
todoapp-postgres 1/1 1 1 30s
```
Verify your services are created:
Check that your pods are running:
```console
$ kubectl get services -n todoapp
$ kubectl get pods -n nodejs-docker-example
```
Expected output:
```shell
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
todoapp-service ClusterIP 10.111.101.229 <none> 80/TCP 45s
todoapp-postgres ClusterIP 10.111.102.130 <none> 5432/TCP 45s
```
Check that persistent storage is working:
Wait until all pods show `Running` in the STATUS column. Then verify your services:
```console
$ kubectl get pvc -n todoapp
$ kubectl get services -n nodejs-docker-example
```
Expected output:
## Access the application
```shell
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
postgres-pvc Bound pvc-12345678-1234-1234-1234-123456789012 10Gi RWO standard 1m
```
### Step 3: Access your application
For local testing, use port forwarding to access your application:
Use port forwarding to access the application from your local machine:
```console
$ kubectl port-forward -n todoapp service/todoapp-service 8080:80
$ kubectl port-forward -n nodejs-docker-example service/server 3000:3000
```
Open your browser and visit [http://localhost:8080](http://localhost:8080) to see your Todo application running in Kubernetes.
Open a new terminal and make a request to the application:
### Step 4: Test the deployment
```console
$ curl http://localhost:3000
{"message":"Hello World"}
```
Test that your application is working correctly:
You can also create a hero:
1. Add some todos through the web interface
2. Check application pods:
```console
$ curl -X POST http://localhost:3000/heroes/ \
-H 'Content-Type: application/json' \
-d '{"name": "my hero", "secret_name": "austing", "age": 12}'
```
```console
$ kubectl get pods -n todoapp -l app=todoapp
```
3. View application logs:
```console
$ kubectl logs -f deployment/todoapp-deployment -n todoapp
```
4. Check database connectivity:
```console
$ kubectl get pods -n todoapp -l app=todoapp-postgres
```
5. Monitor auto-scaling:
```console
$ kubectl describe hpa todoapp-hpa -n todoapp
```
### Step 5: Clean up
## Clean up
When you're done testing, remove the deployment:
```console
$ kubectl delete -f nodejs-sample-kubernetes.yaml
$ kubectl delete -f nodejs-docker-example-kubernetes.yaml
```
## Summary
You've deployed your containerized Node.js application to Kubernetes. You learned how to:
In this section, you deployed your containerized Node.js application to Kubernetes. You created a manifest that defines the application and database deployments, applied it to a local cluster, and verified the application is accessible.
- Create a comprehensive Kubernetes deployment file with security hardening
- Deploy a multi-tier application (Node.js + PostgreSQL) with persistent storage
- Configure auto-scaling, health checks, and high availability features
- Test and monitor your deployment locally using Docker Desktop's Kubernetes
Related information:
Your application is now running in a production-like environment with enterprise-grade features including security contexts, resource management, and automatic scaling.
---
## Related resources
Explore official references and best practices to sharpen your Kubernetes deployment workflow:
- [Kubernetes documentation](https://kubernetes.io/docs/home/) Learn about core concepts, workloads, services, and more.
- [Deploy on Kubernetes with Docker Desktop](/manuals/desktop/use-desktop/kubernetes.md) Use Docker Desktop's built-in Kubernetes support for local testing and development.
- [`kubectl` CLI reference](https://kubernetes.io/docs/reference/kubectl/) Manage Kubernetes clusters from the command line.
- [Kubernetes Deployment resource](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) Understand how to manage and scale applications using Deployments.
- [Kubernetes Service resource](https://kubernetes.io/docs/concepts/services-networking/service/) Learn how to expose your application to internal and external traffic.
- [Kubernetes documentation](https://kubernetes.io/docs/home/)
- [Deploy on Kubernetes with Docker Desktop](/manuals/desktop/use-desktop/kubernetes.md)
- [`kubectl` CLI reference](https://kubernetes.io/docs/reference/kubectl/)
- [Kubernetes Deployment resource](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)
- [Kubernetes Service resource](https://kubernetes.io/docs/concepts/services-networking/service/)
File diff suppressed because it is too large Load Diff
+210 -174
View File
@@ -2,7 +2,7 @@
title: Run Node.js tests in a container
linkTitle: Run your tests
weight: 30
keywords: node.js, node, test
keywords: node.js, node, test, vitest
description: Learn how to run your Node.js tests in a container.
aliases:
- /language/nodejs/run-tests/
@@ -15,214 +15,250 @@ Complete all the previous sections of this guide, starting with [Containerize a
## Overview
Testing is a core part of building reliable software. Whether you're writing unit tests, integration tests, or end-to-end tests, running them consistently across environments matters. Docker makes this easy by giving you the same setup locally, in CI/CD, and during image builds.
Testing is a core part of building reliable software. Docker makes it easy to
run your tests in the same environment used in CI and production, so failures
are caught before they reach your users.
## Run tests when developing locally
In this section, you'll add [Vitest](https://vitest.dev/) to the project and
run tests both locally and inside a container.
The sample application uses Vitest for testing, and it already includes tests for React components, custom hooks, API routes, database operations, and utility functions.
## Update the application
### Run tests locally (without Docker)
You'll refactor `src/index.ts` to export the Express `app` instance so tests
can import it without starting a server. Add a test file and update
`package.json` to add Vitest and a test runner for HTTP requests. The file browser shows only the files that change in this step.
{{< files name="nodejs-docker-example" >}}
{{< file path="src/index.ts" status="modified" hl_lines="10,31,70-75" >}}
```typescript
// Express application backed by a PostgreSQL database.
// Creates a heroes table at startup.
// Endpoints: GET / (greeting), GET /health (health check), POST /heroes/ (create), GET /heroes/ (list).
// See https://expressjs.com/ and https://node-postgres.com/
import express, { type Request, type Response } from 'express';
import { Pool } from 'pg';
import { readFileSync } from 'fs';
export const app = express();
const port = parseInt(process.env.PORT ?? '3000', 10);
app.use(express.json());
function getPassword(): string {
const passwordFile = process.env.POSTGRES_PASSWORD_FILE;
if (passwordFile) {
return readFileSync(passwordFile, 'utf8').trim();
}
return process.env.POSTGRES_PASSWORD ?? '';
}
const pool = new Pool({
host: process.env.POSTGRES_SERVER,
port: 5432,
database: process.env.POSTGRES_DB,
user: process.env.POSTGRES_USER,
password: getPassword(),
});
if (process.env.POSTGRES_SERVER) {
pool
.query(
`CREATE TABLE IF NOT EXISTS heroes (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
secret_name TEXT NOT NULL,
age INTEGER
)`,
)
.catch(console.error);
}
app.get('/', (_req: Request, res: Response) => {
res.json({ message: 'Hello World' });
});
app.get('/health', (_req: Request, res: Response) => {
res.json({ status: 'ok' });
});
app.post('/heroes/', async (req: Request, res: Response) => {
const { name, secret_name, age } = req.body as {
name: string;
secret_name: string;
age?: number;
};
const result = await pool.query(
'INSERT INTO heroes (name, secret_name, age) VALUES ($1, $2, $3) RETURNING *',
[name, secret_name, age],
);
res.json(result.rows[0]);
});
app.get('/heroes/', async (_req: Request, res: Response) => {
const result = await pool.query('SELECT * FROM heroes');
res.json(result.rows);
});
// Only start the server when this file is run directly.
if (require.main === module) {
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
}
```
{{< /file >}}
{{< file path="src/index.test.ts" status="new" >}}
```typescript
// Unit tests for the Express application.
// Tests the root endpoint without starting a server.
// See https://vitest.dev/ for the test framework reference.
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import { app } from './index';
describe('GET /', () => {
it('returns a JSON greeting', async () => {
const response = await request(app).get('/');
expect(response.status).toBe(200);
expect(response.body).toEqual({ message: 'Hello World' });
});
});
```
{{< /file >}}
{{< file path="package.json" status="modified" hl_lines="10,20-22" >}}
```json
{
"name": "nodejs-docker-example",
"version": "1.0.0",
"description": "A minimal Node.js TypeScript application.",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"test": "vitest run"
},
"dependencies": {
"express": "^4.21.2",
"pg": "^8.16.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"@types/pg": "^8.11.0",
"supertest": "^7.0.0",
"@types/supertest": "^6.0.0",
"tsx": "^4.19.3",
"typescript": "^5.8.3",
"vitest": "^3.0.0"
}
}
```
{{< /file >}}
{{< /files >}}
## Run tests locally
Run the following command to run the tests locally:
```console
$ npm run test
```
### Add test service to Docker Compose
To run tests in a containerized environment, you need to add a dedicated test service to your `compose.yml` file. Add the following service configuration:
```yaml
services:
# ... existing services ...
# ========================================
# Test Service
# ========================================
app-test:
build:
context: .
dockerfile: Dockerfile
target: test
container_name: todoapp-test
environment:
NODE_ENV: test
POSTGRES_HOST: db
POSTGRES_PORT: 5432
POSTGRES_DB: todoapp_test
POSTGRES_USER: todoapp
POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-todoapp_password}'
depends_on:
db:
condition: service_healthy
command: ['npm', 'run', 'test:coverage']
networks:
- todoapp-network
profiles:
- test
```
This test service configuration:
- Builds from test stage: Uses the `test` target from your multi-stage Dockerfile
- Isolated test database: Uses a separate `todoapp_test` database for testing
- Profile-based: Uses the `test` profile so it only runs when explicitly requested
- Health dependency: Waits for the database to be healthy before starting tests
### Run tests in a container
You can run tests using the dedicated test service:
```console
$ docker compose up app-test --build
```
Or run tests against the development service:
```console
$ docker compose run --rm app-dev npm run test
```
For a one-off test run with coverage:
```console
$ docker compose run --rm app-dev npm run test:coverage
```
### Run tests with coverage
To generate a coverage report:
```console
$ npm run test:coverage
$ npm install
$ npm test
```
You should see output like the following:
```console
> docker-nodejs-sample@1.0.0 test
> vitest --run
RUN v3.0.0 /app
✓ src/server/__tests__/routes/todos.test.ts (5 tests) 16ms
✓ src/shared/utils/__tests__/validation.test.ts (15 tests) 6ms
✓ src/client/components/__tests__/LoadingSpinner.test.tsx (8 tests) 67ms
✓ src/server/database/__tests__/postgres.test.ts (13 tests) 136ms
✓ src/client/components/__tests__/ErrorMessage.test.tsx (8 tests) 127ms
✓ src/client/components/__tests__/TodoList.test.tsx (8 tests) 147ms
✓ src/client/components/__tests__/TodoItem.test.tsx (8 tests) 218ms
✓ src/client/__tests__/App.test.tsx (13 tests) 259ms
✓ src/client/components/__tests__/AddTodoForm.test.tsx (12 tests) 323ms
✓ src/client/hooks/__tests__/useTodos.test.ts (11 tests) 569ms
✓ src/index.test.ts (1)
✓ GET / (1)
✓ returns a JSON greeting
Test Files 9 passed (9)
Tests 88 passed (88)
Start at 20:57:19
Duration 4.41s (transform 1.79s, setup 2.66s, collect 5.38s, tests 4.61s, environment 14.07s, prepare 4.34s)
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 12:00:00
Duration 500ms
```
### Test structure
## Run tests in a container
The test suite covers:
Run the tests using the dev stage of your Dockerfile:
- Client Components (`src/client/components/__tests__/`): React component testing with React Testing Library
- Custom Hooks (`src/client/hooks/__tests__/`): React hooks testing with proper mocking
- Server Routes (`src/server/__tests__/routes/`): API endpoint testing
- Database Layer (`src/server/database/__tests__/`): PostgreSQL database operations testing
- Utility Functions (`src/shared/utils/__tests__/`): Validation and helper function testing
- Integration Tests (`src/client/__tests__/`): Full application integration testing
```console
$ docker compose run --build --rm --no-deps server npm test
```
The `--no-deps` flag skips starting the database, since the unit tests don't require it. The `--rm` flag removes the container when the tests finish.
You should see the same test output as when running locally.
## Run tests when building
To run tests during the Docker build process, you need to add a dedicated test stage to your Dockerfile. If you haven't already added this stage, add the following to your multi-stage Dockerfile:
To run tests during the Docker build process, add a `test` stage to your Dockerfile that runs after the dev stage.
```dockerfile
# ========================================
# Test Stage
# ========================================
FROM build-deps AS test
```dockerfile {hl_lines="32-36"}
FROM dhi.io/node:24-alpine3.23-dev AS dev
# Set environment
ENV NODE_ENV=test \
CI=true
WORKDIR /app
# Copy source files
COPY --chown=nodejs:nodejs . .
RUN --mount=type=cache,target=/root/.npm \
--mount=type=bind,source=package.json,target=package.json \
npm install
# Switch to non-root user
USER nodejs
COPY . .
RUN npm run build
# Run tests with coverage
CMD ["npm", "run", "test:coverage"]
EXPOSE 3000
CMD ["npm", "run", "dev"]
FROM dhi.io/node:24-alpine3.23-dev AS deps
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm \
--mount=type=bind,source=package.json,target=package.json \
npm install --omit=dev
FROM dhi.io/node:24-alpine3.23 AS runner
ENV PATH=/app/node_modules/.bin:$PATH
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --from=dev --chown=node:node /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
FROM dev AS test
ENV CI=true
CMD ["npm", "test"]
```
This test stage:
- Test environment: Sets `NODE_ENV=test` and `CI=true` for proper test execution
- Non-root user: Runs tests as the `nodejs` user for security
- Flexible execution: Uses `CMD` instead of `RUN` to allow running tests during build or as a separate container
- Coverage support: Configured to run tests with coverage reporting
### Build and run tests during image build
To build an image that runs tests during the build process, you can create a custom Dockerfile or modify the existing one temporarily:
Then build and run the test stage:
```console
$ docker build --target test -t node-docker-image-test .
```
### Run tests in a dedicated test container
The recommended approach is to use the test service defined in `compose.yml`:
```console
$ docker compose --profile test up app-test --build
```
Or run it as a one-off container:
```console
$ docker compose run --rm app-test
```
### Run tests with coverage in CI/CD
For continuous integration, you can run tests with coverage:
```console
$ docker build --target test --progress=plain --no-cache -t test-image .
$ docker run --rm test-image npm run test:coverage
```
You should see output containing the following:
```console
✓ src/server/__tests__/routes/todos.test.ts (5 tests) 16ms
✓ src/shared/utils/__tests__/validation.test.ts (15 tests) 6ms
✓ src/client/components/__tests__/LoadingSpinner.test.tsx (8 tests) 67ms
✓ src/server/database/__tests__/postgres.test.ts (13 tests) 136ms
✓ src/client/components/__tests__/ErrorMessage.test.tsx (8 tests) 127ms
✓ src/client/components/__tests__/TodoList.test.tsx (8 tests) 147ms
✓ src/client/components/__tests__/TodoItem.test.tsx (8 tests) 218ms
✓ src/client/__tests__/App.test.tsx (13 tests) 259ms
✓ src/client/components/__tests__/AddTodoForm.test.tsx (12 tests) 323ms
✓ src/client/hooks/__tests__/useTodos.test.ts (11 tests) 569ms
Test Files 9 passed (9)
Tests 88 passed (88)
Start at 20:57:19
Duration 4.41s (transform 1.79s, setup 2.66s, collect 5.38s, tests 4.61s, environment 14.07s, prepare 4.34s)
$ docker build --target test -t nodejs-app-test .
$ docker run --rm nodejs-app-test
```
## Summary
In this section, you learned how to run tests when developing locally using Docker Compose and how to run tests when building your image.
In this section, you learned how to run tests when developing locally and inside a container.
Related information:
- [Dockerfile reference](/reference/dockerfile/) Understand all Dockerfile instructions and syntax.
- [Best practices for writing Dockerfiles](/develop/develop-images/dockerfile_best-practices/) Write efficient, maintainable, and secure Dockerfiles.
- [Compose file reference](/compose/compose-file/) Learn the full syntax and options available for configuring services in `compose.yaml`.
- [`docker compose run` CLI reference](/reference/cli/docker/compose/run/) Run one-off commands in a service container.
- [Dockerfile reference](/reference/dockerfile/)
- [Compose file reference](/compose/compose-file/)
- [`docker compose run` CLI reference](/reference/cli/docker/compose/run/)
## Next steps
Next, youll learn how to set up a CI/CD pipeline using GitHub Actions.
In the next section, you'll learn how to set up a CI/CD pipeline using GitHub Actions.
@@ -0,0 +1,141 @@
---
title: Secure your Node.js image supply chain
linkTitle: Secure your supply chain
weight: 45
keywords: node.js, node, sbom, provenance, attestations, docker scout, supply chain, security
description: Learn how to inspect, generate, and verify supply chain attestations for your Node.js container image.
---
## Prerequisites
Complete [Automate your builds with GitHub Actions](configure-github-actions.md).
## Overview
When you ship a container image, what's inside it and where it came from
matters. Supply chain attestations are signed records that answer questions
like which packages are in the image, what vulnerabilities affect them, how
the image was built, and what security checks it passed.
In this section, you'll inspect the attestations that ship with your Docker
Hardened Image base, generate your own SBOM and provenance attestations
during CI, and pin the base image by digest so your builds are reproducible.
The inspection commands in this topic are shown manually so you can see what
each one returns. In a real workflow you'd automate these checks with
[Docker Scout](/scout/), which runs the same scans on every push,
enforces policies in CI, and surfaces results in your registry and pull
requests.
## Inspect the base image attestations
Docker Hardened Images are built to SLSA Build Level 3 and ship with a set of
signed attestations covering bill-of-materials, vulnerabilities, build
provenance, and security scans. See
[DHI attestations](/manuals/dhi/core-concepts/attestations.md) for the full
list of types and how to verify their signatures with Cosign.
List all the attestations available on the Node.js DHI:
```console
$ docker scout attest list registry://dhi.io/node:24-alpine3.23-dev
```
View the SBOM:
```console
$ docker scout sbom registry://dhi.io/node:24-alpine3.23-dev
```
Check known vulnerabilities:
```console
$ docker scout cves registry://dhi.io/node:24-alpine3.23-dev
```
> [!NOTE]
>
> The `registry://` prefix forces `docker scout` to fetch the image and its
> attestations from the registry instead of reading a locally pulled copy. If
> you've already pulled or built against the base image, the local copy
> doesn't have the attached attestations, so the prefix is required to see
> them.
When you base your own image on a DHI image, these attestations stay attached to the base layer in the registry. Tools that inspect your image can follow the chain back to the DHI source.
## Generate attestations for your image
Update your GitHub Actions workflow to attach SBOM and provenance attestations to the image you push.
Edit `.github/workflows/build.yml` and update the build-and-push step:
```yaml {hl_lines="6-7"}
- name: Build and push Docker image
uses: docker/build-push-action@{{% param "build_push_action_version" %}}
with:
context: .
push: true
sbom: true
provenance: mode=max
tags: ${{ vars.DOCKER_USERNAME }}/${{ github.event.repository.name }}:latest
```
- `sbom: true` tells BuildKit to scan the built image and attach an SBOM attestation.
- `provenance: mode=max` records detailed build provenance, including the source repository, commit, and build parameters.
The next time your workflow runs, the pushed image will carry these attestations alongside the image manifest in the registry.
## Inspect your pushed image's attestations
After your workflow pushes the image, inspect it the same way you inspected the base image:
```console
$ docker scout attest list registry://DOCKER_USERNAME/REPO_NAME:latest
$ docker scout sbom registry://DOCKER_USERNAME/REPO_NAME:latest
```
The SBOM includes packages from every layer, including those inherited from `dhi.io/node:24-alpine3.23-dev`. The provenance record references the DHI base image by digest, so consumers of your image can trace the build chain back to the DHI source.
## Pin the base image by digest
Image tags like `dhi.io/node:24-alpine3.23-dev` move over time as new patches land. For reproducible builds, pin to an immutable digest.
Look up the digest for each image:
```console
$ docker buildx imagetools inspect dhi.io/node:24-alpine3.23-dev --format "{{ .Manifest.Digest }}"
sha256:2bf01111c7dfe429362f64b3977f0cd6e63ff39023012f88487dec7e83aa26ca
$ docker buildx imagetools inspect dhi.io/node:24-alpine3.23 --format "{{ .Manifest.Digest }}"
sha256:868827fd45c6a01f7f3337ba7ff3f48ebb14da10d8cf3d347f98ded5481317a5
```
Each digest is a 64-character hex string. Update your `Dockerfile` to reference each digest on its corresponding `FROM` line:
```dockerfile
FROM dhi.io/node:24-alpine3.23-dev@sha256:2bf01111c7dfe429362f64b3977f0cd6e63ff39023012f88487dec7e83aa26ca AS dev
# ...
FROM dhi.io/node:24-alpine3.23@sha256:868827fd45c6a01f7f3337ba7ff3f48ebb14da10d8cf3d347f98ded5481317a5 AS runner
```
> [!TIP]
>
> Pinning by digest also pins you to that image's vulnerabilities. Use [Dependabot](https://docs.github.com/en/code-security/dependabot) or [Renovate](https://docs.renovatebot.com/) to automate digest updates so you get a PR when a new patched image is available, with a changelog to review before merging.
## Summary
In this section, you learned how to:
- Inspect the supply chain attestations that ship with the DHI base image, including SBOMs, CVE reports, and build provenance
- Generate SBOM and provenance attestations for your own image in CI
- Pin base images by digest for reproducible builds
Related information:
- [DHI attestations](/manuals/dhi/core-concepts/attestations.md)
- [Verify a Docker Hardened Image](/manuals/dhi/how-to/verify.md)
- [Docker Scout](/scout/)
- [Build attestations](/manuals/build/metadata/attestations/_index.md)
## Next steps
In the next section, you'll deploy your application to Kubernetes.