diff --git a/content/guides/nodejs/_index.md b/content/guides/nodejs/_index.md index b7f04877a088..baa1b029d045 100644 --- a/content/guides/nodejs/_index.md +++ b/content/guides/nodejs/_index.md @@ -15,12 +15,41 @@ params: time: 20 minutes --- -The Node.js language-specific guide teaches you how to containerize a Node.js application using Docker. In this guide, you’ll learn how to: +[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. -- Containerize and run a Node.js application -- Set up a local environment to develop a Node.js application using containers -- Run tests for a Node.js application using containers -- Configure a CI/CD pipeline for a containerized Node.js application using GitHub Actions -- Deploy your containerized Node.js application locally to Kubernetes to test and debug your deployment +The sample application is a modern full-stack Todo application featuring: -Start by containerizing an existing Node.js application. +- **Backend**: Express.js with TypeScript, PostgreSQL database, and RESTful API +- **Frontend**: React.js with Vite and Tailwind CSS 4 + + +> **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. + +--- + +## What will you learn? + +In this guide, you will learn how to: + +- Containerize and run a Node.js application using Docker. +- 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. + +To begin, you’ll start by containerizing an existing 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, you’ll be ready to containerize your own Node.js application using the examples and instructions provided in this guide. diff --git a/content/guides/nodejs/configure-ci-cd.md b/content/guides/nodejs/configure-ci-cd.md deleted file mode 100644 index c951b37b5d7e..000000000000 --- a/content/guides/nodejs/configure-ci-cd.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Configure CI/CD for your Node.js application -linkTitle: Configure CI/CD -weight: 40 -keywords: ci/cd, github actions, node.js, node -description: Learn how to configure CI/CD using GitHub Actions for your Node.js application. -aliases: - - /language/nodejs/configure-ci-cd/ - - /guides/language/nodejs/configure-ci-cd/ ---- - -## Prerequisites - -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 [Docker](https://hub.docker.com/signup) account to complete this section. - -## Overview - -In this section, you'll learn how to set up and use GitHub Actions to build and test your Docker image as well as push it to Docker Hub. You will complete the following steps: - -1. Create a new repository on GitHub. -2. Define the GitHub Actions workflow. -3. Run the workflow. - -## Step one: Create the repository - -Create a GitHub repository, configure the Docker Hub credentials, and push your source code. - -1. [Create a new repository](https://github.com/new) on GitHub. - -2. Open the repository **Settings**, and go to **Secrets and variables** > - **Actions**. - -3. Create a new **Repository variable** named `DOCKER_USERNAME` and your Docker ID as a value. - -4. 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. - -5. Add the PAT as a **Repository secret** in your GitHub repository, with the name - `DOCKERHUB_TOKEN`. - -6. In your local repository on your machine, run the following command to change - the origin to the repository you just created. Make sure you change - `your-username` to your GitHub username and `your-repository` to the name of - the repository you created. - - ```console - $ git remote set-url origin https://github.com/your-username/your-repository.git - ``` - -7. Run the following commands to stage, commit, and push your local repository to GitHub. - - ```console - $ git add -A - $ git commit -m "my commit" - $ git push -u origin main - ``` - -## Step two: Set up the workflow - -Set up your GitHub Actions workflow for building, testing, and pushing the image -to Docker Hub. - -1. Go to your repository on GitHub and then select the **Actions** tab. - -2. Select **set up a workflow yourself**. - - This takes you to a page for creating a new GitHub actions workflow file in - your repository, under `.github/workflows/main.yml` by default. - -3. In the editor window, copy and paste the following YAML configuration. - - ```yaml - name: ci - - on: - push: - branches: - - main - - jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ vars.DOCKER_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and test - uses: docker/build-push-action@v6 - with: - target: test - load: true - - - name: Build and push - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - push: true - target: prod - tags: ${{ vars.DOCKER_USERNAME }}/${{ github.event.repository.name }}:latest - ``` - - For more information about the YAML syntax for `docker/build-push-action`, - refer to the [GitHub Action README](https://github.com/docker/build-push-action/blob/master/README.md). - -## Step three: Run the workflow - -Save the workflow file and run the job. - -1. Select **Commit changes...** and push the changes to the `main` branch. - - After pushing the commit, the workflow starts automatically. - -2. Go to the **Actions** tab. It displays the workflow. - - Selecting the workflow shows you the breakdown of all the steps. - -3. 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, it means the GitHub Actions - successfully pushed the image to Docker Hub. - -## Summary - -In this section, you learned how to set up a GitHub Actions workflow for your Node.js application. - -Related information: - -- [Introduction to GitHub Actions](/guides/gha.md) -- [Docker Build GitHub Actions](/manuals/build/ci/github-actions/_index.md) -- [Workflow syntax for GitHub Actions](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) - -## Next steps - -Next, learn how you can locally test and debug your workloads on Kubernetes before deploying. diff --git a/content/guides/nodejs/configure-github-actions.md b/content/guides/nodejs/configure-github-actions.md new file mode 100644 index 000000000000..3b9f4256d4e2 --- /dev/null +++ b/content/guides/nodejs/configure-github-actions.md @@ -0,0 +1,344 @@ +--- +title: Automate your builds with GitHub Actions +linkTitle: Automate your builds with GitHub Actions +weight: 50 +keywords: CI/CD, GitHub Actions, Node.js, Docker +description: Learn how to configure CI/CD using GitHub Actions for your Node.js application. +aliases: + - /language/nodejs/configure-ci-cd/ + - /guides/language/nodejs/configure-ci-cd/ +--- + +## Prerequisites + +Complete all the previous sections of this guide, starting with [Containerize a Node.js application](containerize.md). + +You must also have: + +- A [GitHub](https://github.com/signup) account. +- A [Docker Hub](https://hub.docker.com/signup) account. + +--- + +## Overview + +In this section, you'll set up a **CI/CD pipeline** using [GitHub Actions](https://docs.github.com/en/actions) to automatically: + +- 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). + +--- + +## Connect your GitHub repository to Docker Hub + +To enable GitHub Actions to build and push Docker images, you'll securely store your Docker Hub credentials in your new GitHub repository. + +### Step 1: Connect your GitHub repository to Docker Hub + +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. 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. + +3. Create a new [GitHub repository](https://github.com/new) for your Node.js project. + +4. Add Docker Hub credentials as GitHub repository secrets. + + 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: + +```yaml +name: CI/CD – Node.js Application with Docker + +on: + push: + branches: [main] + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + +jobs: + test: + name: Run Node.js Tests + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16-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@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache npm dependencies + uses: actions/cache@v4 + 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@v6 + 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 + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v4 + 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@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push multi-arch production image + uses: docker/build-push-action@v6 + 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 +``` + +This workflow performs the following tasks for your Node.js application: + +- 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. + +> [!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). + +--- + +### Step 3: Run the workflow + +After adding your workflow file, trigger the CI/CD process. + +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. + - `` – 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. + +--- + +## Summary + +In this section, you set up a comprehensive CI/CD pipeline for your containerized Node.js application using GitHub Actions. + +What you accomplished: + +- 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. + +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 + +--- + +## 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. diff --git a/content/guides/nodejs/containerize.md b/content/guides/nodejs/containerize.md index 0f54039de933..8d4178705283 100644 --- a/content/guides/nodejs/containerize.md +++ b/content/guides/nodejs/containerize.md @@ -1,9 +1,9 @@ --- title: Containerize a Node.js application -linkTitle: Containerize your app +linkTitle: Containerize weight: 10 keywords: node.js, node, containerize, initialize -description: Learn how to containerize a Node.js application. +description: Learn how to containerize a Node.js application with Docker by creating an optimized, production-ready image using best practices for performance, security, and scalability. aliases: - /get-started/nodejs/build-images/ - /language/nodejs/build-images/ @@ -14,44 +14,58 @@ aliases: ## Prerequisites -- You have installed the latest version of [Docker - Desktop](/get-started/get-docker.md). -- You have a [git client](https://git-scm.com/downloads). The examples in this - section use a command-line based git client, but you can use any client. +Before you begin, make sure the following tools are installed and available on your system: + +- You have installed the latest version of [Docker Desktop](/get-started/get-docker.md). +- You have a [git client](https://git-scm.com/downloads). The examples in this section use a command-line based git client, but you can use any client. + +> **New to Docker?** +> Start with the [Docker basics](/get-started/docker-concepts/the-basics/what-is-a-container.md) guide to get familiar with key concepts like images, containers, and Dockerfiles. + +--- ## Overview -This section walks you through containerizing and running a Node.js -application. +This guide walks you through the complete process of containerizing a Node.js application with Docker. You’ll learn how to create a production-ready Docker image using best practices that enhance performance, security, scalability, and operational efficiency. + +By the end of this guide, you will: + +- Containerize a Node.js application using Docker. +- Create and optimize a Dockerfile tailored for Node.js environments. +- Use multi-stage builds to separate dependencies and reduce image size. +- Configure the container for secure, efficient runtime using a non-root user. +- Follow best practices for building secure, lightweight, and maintainable Docker images. ## Get the sample application Clone the sample application to use with this guide. Open a terminal, change directory to a directory that you want to work in, and run the following command -to clone the repository: +to clone the git repository: ```console -$ git clone https://github.com/docker/docker-nodejs-sample && cd docker-nodejs-sample +$ git clone https://github.com/kristiyan-velkov/docker-nodejs-sample ``` -## Initialize Docker assets +## Generate a Dockerfile -Now that you have an application, you can create the necessary Docker assets to -containerize your application. You can use Docker Desktop's built-in Docker Init -feature to help streamline the process, or you can manually create the assets. +Docker provides an interactive CLI tool called `docker init` that helps scaffold the necessary configuration files for containerizing your application. This includes generating a `Dockerfile`, `.dockerignore`, `compose.yaml`, and `README.Docker.md`. -{{< tabs >}} -{{< tab name="Use Docker Init" >}} +To begin, navigate to the root of your project directory: + +```console +$ cd docker-nodejs-sample +``` -Inside the `docker-nodejs-sample` directory, run -the `docker init` command in a terminal. `docker init` provides some default -configuration, but you'll need to answer a few questions about your application. -Refer to the following example to answer the prompts from `docker init` and use -the same answers for your prompts. +Then run the following command: ```console $ docker init -Welcome to the Docker Init CLI! +``` + +You’ll see output similar to: + +```text +Welcome to the Docker Init CLI This utility will walk you through creating the following files with sensible defaults for your project: - .dockerignore @@ -60,228 +74,794 @@ This utility will walk you through creating the following files with sensible de - README.Docker.md Let's get started! +``` -? What application platform does your project use? Node -? What version of Node do you want to use? 18.0.0 -? Which package manager do you want to use? npm -? What command do you want to use to start the app: node src/index.js -? What port does your server listen on? 3000 +The CLI will prompt you with a few questions about your app setup. +For consistency, use the same responses shown in the example following when prompted: +| Question | Answer | +|------------------------------------------------------------|-----------------| +| What application platform does your project use? | Node | +| What version of Node do you want to use? | 24.11.1-alpine | +| Which package manager do you want to use? | npm | +| Do you want to run "npm run build" before starting server? | yes | +| What directory is your build output to? | dist | +| What command do you want to use to start the app? | npm run dev | +| What port does your server listen on? | 3000 | + +After completion, your project directory will contain the following new files: + +```text +├── docker-nodejs-sample/ +│ ├── Dockerfile +│ ├── .dockerignore +│ ├── compose.yaml +│ └── README.Docker.md ``` -{{< /tab >}} -{{< tab name="Manually create assets" >}} +## Create a Docker Compose file + +While `docker init` generates a basic `compose.yaml` file, you'll need to create a more comprehensive configuration for this full-stack application. Replace the generated `compose.yaml` with a production-ready configuration. -If you don't have Docker Desktop installed or prefer creating the assets manually, you can create the following files in your project directory. +Create a new file named `compose.yml` in your project root: -Create a file named `Dockerfile` with the following contents. +```yaml +# ======================================== +# Docker Compose Configuration +# Modern Node.js Todo Application +# ======================================== -```dockerfile {collapse=true,title=Dockerfile} -# syntax=docker/dockerfile:1 +services: + # ======================================== + # Development Service + # ======================================== + app-dev: + build: + context: . + dockerfile: Dockerfile + target: development + container_name: todoapp-dev + ports: + - '${APP_PORT:-3000}:3000' # API server + - '${VITE_PORT:-5173}:5173' # Vite dev server + - '${DEBUG_PORT:-9229}:9229' # Node.js debugger + environment: + NODE_ENV: development + DOCKER_ENV: 'true' + POSTGRES_HOST: db + POSTGRES_PORT: 5432 + POSTGRES_DB: todoapp + POSTGRES_USER: todoapp + POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-todoapp_password}' + ALLOWED_ORIGINS: '${ALLOWED_ORIGINS:-http://localhost:3000,http://localhost:5173}' + volumes: + - ./src:/app/src:ro + - ./package.json:/app/package.json + - ./vite.config.ts:/app/vite.config.ts:ro + - ./tailwind.config.js:/app/tailwind.config.js:ro + - ./postcss.config.js:/app/postcss.config.js:ro + depends_on: + db: + condition: service_healthy + develop: + watch: + - action: sync + path: ./src + target: /app/src + ignore: + - '**/*.test.*' + - '**/__tests__/**' + - action: rebuild + path: ./package.json + - action: sync + path: ./vite.config.ts + target: /app/vite.config.ts + - action: sync + path: ./tailwind.config.js + target: /app/tailwind.config.js + - action: sync + path: ./postcss.config.js + target: /app/postcss.config.js + restart: unless-stopped + networks: + - todoapp-network + + # ======================================== + # Production Service + # ======================================== + app-prod: + build: + context: . + dockerfile: Dockerfile + target: production + container_name: todoapp-prod + ports: + - '${PROD_PORT:-8080}:3000' + environment: + NODE_ENV: production + POSTGRES_HOST: db + POSTGRES_PORT: 5432 + POSTGRES_DB: todoapp + POSTGRES_USER: todoapp + POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-todoapp_password}' + ALLOWED_ORIGINS: '${ALLOWED_ORIGINS:-https://yourdomain.com}' + depends_on: + db: + condition: service_healthy + restart: unless-stopped + deploy: + resources: + limits: + memory: '${PROD_MEMORY_LIMIT:-2G}' + cpus: '${PROD_CPU_LIMIT:-1.0}' + reservations: + memory: '${PROD_MEMORY_RESERVATION:-512M}' + cpus: '${PROD_CPU_RESERVATION:-0.25}' + security_opt: + - no-new-privileges:true + read_only: true + tmpfs: + - /tmp + networks: + - todoapp-network + profiles: + - prod + + # ======================================== + # PostgreSQL Database Service + # ======================================== + db: + image: postgres:16-alpine + container_name: todoapp-db + environment: + POSTGRES_DB: '${POSTGRES_DB:-todoapp}' + POSTGRES_USER: '${POSTGRES_USER:-todoapp}' + POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-todoapp_password}' + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - '${DB_PORT:-5432}:5432' + restart: unless-stopped + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-todoapp} -d ${POSTGRES_DB:-todoapp}'] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + networks: + - todoapp-network + +# ======================================== +# Volume Configuration +# ======================================== +volumes: + postgres_data: + name: todoapp-postgres-data + driver: local + +# ======================================== +# Network Configuration +# ======================================== +networks: + todoapp-network: + name: todoapp-network + driver: bridge +``` + +This Docker Compose configuration includes: + +- **Development service** (`app-dev`): Full development environment with hot reload, debugging support, and bind mounts +- **Production service** (`app-prod`): Optimized production deployment with resource limits and security hardening +- **Database service** (`db`): PostgreSQL 16 with persistent storage and health checks +- **Networking**: Isolated network for secure service communication +- **Volumes**: Persistent storage for database data + +## Create environment configuration + +Create a `.env` file to configure your application settings: + +```console +$ cp .env.example .env +``` + +Update the `.env` file with your preferred settings: + +```env +# Application Configuration +NODE_ENV=development +APP_PORT=3000 +VITE_PORT=5173 +DEBUG_PORT=9229 + +# Production Configuration +PROD_PORT=8080 +PROD_MEMORY_LIMIT=2G +PROD_CPU_LIMIT=1.0 +PROD_MEMORY_RESERVATION=512M +PROD_CPU_RESERVATION=0.25 + +# Database Configuration +POSTGRES_HOST=db +POSTGRES_PORT=5432 +POSTGRES_DB=todoapp +POSTGRES_USER=todoapp +POSTGRES_PASSWORD=todoapp_password +DB_PORT=5432 + +# Security Configuration +ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173 +``` + +--- -# Comments are provided throughout this file to help you get started. -# If you need more help, visit the Dockerfile reference guide at -# https://docs.docker.com/go/dockerfile-reference/ +## Build the Docker image -# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 +The default Dockerfile generated by `docker init` provides a reliable baseline for standard Node.js applications. However, since this project is a full-stack TypeScript application that includes both a backend API and frontend React components, the Dockerfile should be customized to better support and optimize this specific architecture. -ARG NODE_VERSION=18.0.0 +### Review the generated files -FROM node:${NODE_VERSION}-alpine +In the following step, you’ll improve the Dockerfile and configuration files by following best practices: -# Use production node environment by default. -ENV NODE_ENV production +- Use multi-stage builds to keep the final image clean and small +- Improve performance and security by only including what’s needed +These updates make your app easier to deploy and faster to load. -WORKDIR /usr/src/app +> [!NOTE] +> A `Dockerfile` is a plain text file that contains step-by-step instructions to build a Docker image. It automates packaging your application along with its dependencies and runtime environment. +> For full details, see the [Dockerfile reference](/reference/dockerfile/). -# Download dependencies as a separate step to take advantage of Docker's caching. -# Leverage a cache mount to /root/.npm to speed up subsequent builds. -# Leverage a bind mounts to package.json and package-lock.json to avoid having to copy them into -# into this layer. -RUN --mount=type=bind,source=package.json,target=package.json \ - --mount=type=bind,source=package-lock.json,target=package-lock.json \ - --mount=type=cache,target=/root/.npm \ - npm ci --omit=dev +### Step 1: Configure the Dockerfile -# Run the application as a non-root user. -USER node +Before creating a Dockerfile, you need to choose a base image. You can either use the [Node.js Official Image](https://hub.docker.com/_/node) or a Docker Hardened Image (DHI) from the [Hardened Image catalog](https://hub.docker.com/hardened-images/catalog). -# Copy the rest of the source files into the image. +Choosing DHI offers the advantage of a production-ready image that is lightweight and secure. For more information, see [Docker Hardened Images](https://docs.docker.com/dhi/). + +> [!IMPORTANT] +> This guide uses a stable Node.js LTS image tag that is considered secure when the guide is written. Because new releases and security patches are published regularly, the tag shown here may no longer be the safest option when you follow the guide. Always review the latest available image tags and select a secure, up-to-date version before building or deploying your application. +> +> Official Node.js Docker Images: https://hub.docker.com/_/node + +{{< tabs >}} +{{< tab name="Using Docker Hardened Images" >}} +Docker Hardened Images (DHIs) are available for Node.js on [Docker Hub](https://hub.docker.com/hardened-images/catalog/dhi/node). Unlike using the Docker Official Image, you must first mirror the Node.js image into your organization and then use it as your base image. Follow the instructions in the [DHI quickstart](/dhi/get-started/) to create a mirrored repository for Node.js. + +Mirrored repositories must start with `dhi-`, for example: `FROM /dhi-node:`. In the following Dockerfile, the `FROM` instruction uses `/dhi-node:24-alpine3.22-dev` as the base image. + +```dockerfile +# ======================================== +# Optimized Multi-Stage Dockerfile +# Node.js TypeScript Application (Using DHI) +# ======================================== + +FROM /dhi-node:24-alpine3.22-dev AS base + +# Set working directory +WORKDIR /app + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 -G nodejs && \ + chown -R nodejs:nodejs /app + +# ======================================== +# Dependencies Stage +# ======================================== +FROM base AS deps + +# Copy package files +COPY package*.json ./ + +# Install production dependencies +RUN --mount=type=cache,target=/root/.npm,sharing=locked \ + npm ci --omit=dev && \ + npm cache clean --force + +# Set proper ownership +RUN chown -R nodejs:nodejs /app + +# ======================================== +# Build Dependencies Stage +# ======================================== +FROM base AS build-deps + +# Copy package files +COPY package*.json ./ + +# Install all dependencies with build optimizations +RUN --mount=type=cache,target=/root/.npm,sharing=locked \ + npm ci --no-audit --no-fund && \ + npm cache clean --force + +# Create necessary directories and set permissions +RUN mkdir -p /app/node_modules/.vite && \ + chown -R nodejs:nodejs /app + +# ======================================== +# Build Stage +# ======================================== +FROM build-deps AS build + +# Copy only necessary files for building (respects .dockerignore) +COPY --chown=nodejs:nodejs . . + +# Build the application +RUN npm run build + +# Set proper ownership +RUN chown -R nodejs:nodejs /app + +# ======================================== +# Development Stage +# ======================================== +FROM build-deps AS development + +# Set environment +ENV NODE_ENV=development \ + NPM_CONFIG_LOGLEVEL=warn + +# Copy source files COPY . . -# Expose the port that the application listens on. +# Ensure all directories have proper permissions +RUN mkdir -p /app/node_modules/.vite && \ + chown -R nodejs:nodejs /app && \ + chmod -R 755 /app + +# Switch to non-root user +USER nodejs + +# Expose ports +EXPOSE 3000 5173 9229 + +# Start development server +CMD ["npm", "run", "dev:docker"] + +# ======================================== +# Production Stage +# ======================================== +FROM /dhi-node:24-alpine3.22-dev AS production + +# Set working directory +WORKDIR /app + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 -G nodejs && \ + chown -R nodejs:nodejs /app + +# Set optimized environment variables +ENV NODE_ENV=production \ + NODE_OPTIONS="--max-old-space-size=256 --no-warnings" \ + NPM_CONFIG_LOGLEVEL=silent + +# Copy production dependencies from deps stage +COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules +COPY --from=deps --chown=nodejs:nodejs /app/package*.json ./ +# Copy built application from build stage +COPY --from=build --chown=nodejs:nodejs /app/dist ./dist + +# Switch to non-root user for security +USER nodejs + +# Expose port EXPOSE 3000 -# Run the application. -CMD node src/index.js -``` +# Start production server +CMD ["node", "dist/server.js"] -Create a file named `compose.yaml` with the following contents. +# ======================================== +# Test Stage +# ======================================== +FROM build-deps AS test -```yaml {collapse=true,title=compose.yaml} -# Comments are provided throughout this file to help you get started. -# If you need more help, visit the Docker Compose reference guide at -# https://docs.docker.com/go/compose-spec-reference/ +# Set environment +ENV NODE_ENV=test \ + CI=true -# Here the instructions define your application as a service called "server". -# This service is built from the Dockerfile in the current directory. -# You can add other services your application may depend on here, such as a -# database or a cache. For examples, see the Awesome Compose repository: -# https://github.com/docker/awesome-compose -services: - server: - build: - context: . - environment: - NODE_ENV: production - ports: - - 3000:3000 -# The commented out section below is an example of how to define a PostgreSQL -# database that your application can use. `depends_on` tells Docker Compose to -# start the database before your application. The `db-data` volume persists the -# database data between container restarts. The `db-password` secret is used -# to set the database password. You must create `db/password.txt` and add -# a password of your choosing to it before running `docker compose up`. -# depends_on: -# db: -# condition: service_healthy -# db: -# image: postgres -# restart: always -# user: postgres -# secrets: -# - db-password -# volumes: -# - db-data:/var/lib/postgresql/data -# environment: -# - POSTGRES_DB=example -# - POSTGRES_PASSWORD_FILE=/run/secrets/db-password -# expose: -# - 5432 -# healthcheck: -# test: [ "CMD", "pg_isready" ] -# interval: 10s -# timeout: 5s -# retries: 5 -# volumes: -# db-data: -# secrets: -# db-password: -# file: db/password.txt -``` +# Copy source files +COPY --chown=nodejs:nodejs . . + +# Switch to non-root user +USER nodejs -Create a file named `.dockerignore` with the following contents. - -```text {collapse=true,title=".dockerignore"} -# Include any files or directories that you don't want to be copied to your -# container here (e.g., local build artifacts, temporary files, etc.). -# -# For more help, visit the .dockerignore file reference guide at -# https://docs.docker.com/go/build-context-dockerignore/ - -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/.next -**/.cache -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/charts -**/docker-compose* -**/compose.y*ml -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -**/build -**/dist -LICENSE -README.md +# Run tests with coverage +CMD ["npm", "run", "test:coverage"] ``` {{< /tab >}} +{{< tab name="Using the Docker Official Image" >}} + +Now you need to create a production-ready multi-stage Dockerfile. Replace the generated Dockerfile with the following optimized configuration: + +```dockerfile +# ======================================== +# Optimized Multi-Stage Dockerfile +# Node.js TypeScript Application +# ======================================== + +ARG NODE_VERSION=24.11.1-alpine +FROM node:${NODE_VERSION} AS base + +# Set working directory +WORKDIR /app + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 -G nodejs && \ + chown -R nodejs:nodejs /app + +# ======================================== +# Dependencies Stage +# ======================================== +FROM base AS deps + +# Copy package files +COPY package*.json ./ + +# Install production dependencies +RUN --mount=type=cache,target=/root/.npm,sharing=locked \ + npm ci --omit=dev && \ + npm cache clean --force + +# Set proper ownership +RUN chown -R nodejs:nodejs /app + +# ======================================== +# Build Dependencies Stage +# ======================================== +FROM base AS build-deps + +# Copy package files +COPY package*.json ./ + +# Install all dependencies with build optimizations +RUN --mount=type=cache,target=/root/.npm,sharing=locked \ + npm ci --no-audit --no-fund && \ + npm cache clean --force + +# Create necessary directories and set permissions +RUN mkdir -p /app/node_modules/.vite && \ + chown -R nodejs:nodejs /app + +# ======================================== +# Build Stage +# ======================================== +FROM build-deps AS build + +# Copy only necessary files for building (respects .dockerignore) +COPY --chown=nodejs:nodejs . . + +# Build the application +RUN npm run build + +# Set proper ownership +RUN chown -R nodejs:nodejs /app + +# ======================================== +# Development Stage +# ======================================== +FROM build-deps AS development + +# Set environment +ENV NODE_ENV=development \ + NPM_CONFIG_LOGLEVEL=warn + +# Copy source files +COPY . . + +# Ensure all directories have proper permissions +RUN mkdir -p /app/node_modules/.vite && \ + chown -R nodejs:nodejs /app && \ + chmod -R 755 /app + +# Switch to non-root user +USER nodejs + +# Expose ports +EXPOSE 3000 5173 9229 + +# Start development server +CMD ["npm", "run", "dev:docker"] + +# ======================================== +# Production Stage +# ======================================== +ARG NODE_VERSION=24.11.1-alpine +FROM node:${NODE_VERSION} AS production + +# Set working directory +WORKDIR /app + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 -G nodejs && \ + chown -R nodejs:nodejs /app + +# Set optimized environment variables +ENV NODE_ENV=production \ + NODE_OPTIONS="--max-old-space-size=256 --no-warnings" \ + NPM_CONFIG_LOGLEVEL=silent + +# Copy production dependencies from deps stage +COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules +COPY --from=deps --chown=nodejs:nodejs /app/package*.json ./ +# Copy built application from build stage +COPY --from=build --chown=nodejs:nodejs /app/dist ./dist + +# Switch to non-root user for security +USER nodejs + +# Expose port +EXPOSE 3000 + +# Start production server +CMD ["node", "dist/server.js"] + +# ======================================== +# Test Stage +# ======================================== +FROM build-deps AS test + +# Set environment +ENV NODE_ENV=test \ + CI=true + +# Copy source files +COPY --chown=nodejs:nodejs . . + +# Switch to non-root user +USER nodejs + +# Run tests with coverage +CMD ["npm", "run", "test:coverage"] +``` +{{< /tab >}} + {{< /tabs >}} -You should now have at least the following contents in your -`docker-nodejs-sample` directory. +Key features of this Dockerfile: +- Multi-stage structure — Separate stages for dependencies, build, development, production, and testing to keep each phase clean and efficient. +- Lean production image — Optimized layering reduces size and keeps only what’s required to run the app. +- Security-minded setup — Uses a dedicated non-root user and excludes unnecessary packages. +- Performance-friendly design — Effective use of caching and well-structured layers for faster builds. +- Clean runtime environment — Removes files not needed in production, such as docs, tests, and build caches. +- Straightforward port usage — The app runs on port 3000 internally, exposed externally as port 8080. +- Memory-optimized runtime — Node.js is configured to run with a smaller memory limit than the default. + +### Step 2: Configure the .dockerignore file + +The `.dockerignore` file tells Docker which files and folders to exclude when building the image. + +> [!NOTE] +> This helps: +> +> - Reduce image size +> - Speed up the build process +> - Prevent sensitive or unnecessary files (like `.env`, `.git`, or `node_modules`) from being added to the final image. +> +> To learn more, visit the [.dockerignore reference](/reference/dockerfile.md#dockerignore-file). + +Copy and replace the contents of your existing `.dockerignore` with the optimized configuration: + +```dockerignore +# Optimized .dockerignore for Node.js + React Todo App +# Based on actual project structure + +# Version control +.git/ +.github/ +.gitignore + +# Dependencies (installed in container) +node_modules/ + +# Build outputs (built in container) +dist/ + +# Environment files +.env* + +# Development files +.vscode/ +*.log +coverage/ +.eslintcache + +# OS files +.DS_Store +Thumbs.db + +# Documentation +*.md +docs/ + +# Deployment configs +compose.yml +Taskfile.yml +nodejs-sample-kubernetes.yaml + +# Non-essential configs (keep build configs) +*.config.js +!vite.config.ts +!esbuild.config.js +!tailwind.config.js +!postcss.config.js +!tsconfig.json +``` + +### Step 3: Build the Node.js application image + +After creating all the configuration files, your project directory should now contain all necessary Docker configuration files: ```text ├── docker-nodejs-sample/ -│ ├── spec/ -│ ├── src/ -│ ├── .dockerignore -│ ├── .gitignore -│ ├── compose.yaml │ ├── Dockerfile -│ ├── package-lock.json -│ ├── package.json -│ └── README.md +│ ├── .dockerignore +│ ├── compose.yml +│ └── README.Docker.md ``` -To learn more about the files, see the following: +Now you can build the Docker image for your Node.js application. + +> [!NOTE] +> The `docker build` command packages your application into an image using the instructions in the Dockerfile. It includes all necessary files from the current directory (called the [build context](/build/concepts/context/#what-is-a-build-context)). + +Run the following command from the root of your project: + +```console +$ docker build --target production --tag docker-nodejs-sample . +``` -- [Dockerfile](/reference/dockerfile.md) -- [.dockerignore](/reference/dockerfile.md#dockerignore-file) -- [compose.yaml](/reference/compose-file/_index.md) +What this command does: -## Run the application +- Uses the Dockerfile in the current directory (.) +- Targets the production stage of the multi-stage build +- Packages the application and its dependencies into a Docker image +- Tags the image as docker-nodejs-sample so you can reference it later -Inside the `docker-nodejs-sample` directory, run the following command in a -terminal. +#### Step 4: View local images + +After building your Docker image, you can check which images are available on your local machine using either the Docker CLI or [Docker Desktop](/manuals/desktop/use-desktop/images.md). Since you're already working in the terminal, use the Docker CLI. + +To list all locally available Docker images, run the following command: + +```console +$ docker images +``` + +Example Output: + +```shell +REPOSITORY TAG IMAGE ID CREATED SIZE +docker-nodejs-sample latest 423525528038 14 seconds ago 237.46MB +``` + +This output provides key details about your images: + +- **Repository** – The name assigned to the image. +- **Tag** – A version label that helps identify different builds (e.g., latest). +- **Image ID** – A unique identifier for the image. +- **Created** – The timestamp indicating when the image was built. +- **Size** – The total disk space used by the image. + +If the build was successful, you should see `docker-nodejs-sample` image listed. + +--- + +## Run the containerized application + +In the previous step, you created a Dockerfile for your Node.js application and built a Docker image using the docker build command. Now it’s time to run that image in a container and verify that your application works as expected. + +Inside the `docker-nodejs-sample` directory, run the following command in a terminal. + +```console +$ docker compose up app-dev --build +``` + +The development application will start with both servers: + +- **API Server**: [http://localhost:3000](http://localhost:3000) - Express.js backend with REST API +- **Frontend**: [http://localhost:5173](http://localhost:5173) - Vite dev server with React frontend +- **Health Check**: [http://localhost:3000/health](http://localhost:3000/health) - Application health status + +For production deployment, you can use: ```console -$ docker compose up --build +$ docker compose up app-prod --build ``` -Open a browser and view the application at [http://localhost:3000](http://localhost:3000). You should see a simple todo application. +Which serves the full-stack app at [http://localhost:8080](http://localhost:8080) with the Express server running on port 3000 internally, mapped to port 8080 externally. + +You should see a modern Todo List application with React 19 and a fully functional REST API. -In the terminal, press `ctrl`+`c` to stop the application. +Press `CTRL + C` in the terminal to stop your application. ### Run the application in the background -You can run the application detached from the terminal by adding the `-d` -option. Inside the `docker-nodejs-sample` directory, run the following command -in a terminal. +You can run the application detached from the terminal by adding the `-d` option. Inside the `docker-nodejs-sample` directory, run the following command in a terminal. ```console -$ docker compose up --build -d +$ docker compose up app-dev --build -d ``` -Open a browser and view the application at [http://localhost:3000](http://localhost:3000). +Open a browser and view the application at [http://localhost:3000](http://localhost:3000) (API) or [http://localhost:5173](http://localhost:5173) (frontend). You should see the Todo application running. -You should see a simple todo application. +To confirm that the container is running, use `docker ps` command: -In the terminal, run the following command to stop the application. +```console +$ docker ps +``` + +This will list all active containers along with their ports, names, and status. Look for a container exposing ports 3000, 5173, and 9229 for the development app. + +Example Output: + +```shell +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +93f3faee32c3 docker-nodejs-sample-app-dev "docker-entrypoint.s…" 33 seconds ago Up 31 seconds 0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp, 0.0.0.0:5173->5173/tcp, [::]:5173->5173/tcp, 0.0.0.0:9230->9229/tcp, [::]:9230->9229/tcp todoapp-dev +``` + +### Run different profiles + +You can run different configurations using Docker Compose profiles: + +```console +# Run production +$ docker compose up app-prod -d + +# Run tests +$ docker compose up app-test -d +``` + +To stop the application, run: ```console $ docker compose down ``` -For more information about Compose commands, see the [Compose CLI -reference](/reference/cli/docker/compose/_index.md). +> [!NOTE] +> For more information about Compose commands, see the [Compose CLI +> reference](/reference/cli/docker/compose/_index.md). + +--- ## Summary -In this section, you learned how you can containerize and run your Node.js -application using Docker. +In this guide, you learned how to containerize, build, and run a Node.js application using Docker. By following best practices, you created a secure, optimized, and production-ready setup. + +What you accomplished: -Related information: +- Initialized your project using `docker init` to scaffold essential Docker configuration files. +- Created a `compose.yml` file with development, production, and database services. +- Set up environment configuration with a `.env` file for flexible deployment settings. +- Replaced the default `Dockerfile` with a multi-stage build optimized for TypeScript and React. +- Replaced the default `.dockerignore` file to exclude unnecessary files and keep the image clean and efficient. +- Built your Docker image using `docker build`. +- Ran the container using `docker compose up`, both in the foreground and in detached mode. +- Verified that the app was running by visiting [http://localhost:8080](http://localhost:8080) (production) or [http://localhost:3000](http://localhost:3000) (development). +- Learned how to stop the containerized application using `docker compose down`. + +You now have a fully containerized Node.js application, running in a Docker container, and ready for deployment across any environment with confidence and consistency. + +--- -- [Dockerfile reference](/reference/dockerfile.md) -- [.dockerignore file reference](/reference/dockerfile.md#dockerignore-file) -- [Docker Compose overview](/manuals/compose/_index.md) +## Related resources + +Explore official references and best practices to sharpen your Docker workflow: + +- [Multi-stage builds](/build/building/multi-stage/) – Learn how to separate build and runtime stages. +- [Best practices for writing Dockerfiles](/develop/develop-images/dockerfile_best-practices/) – Write efficient, maintainable, and secure Dockerfiles. +- [Build context in Docker](/build/concepts/context/) – Learn how context affects image builds. +- [`docker init` CLI reference](/reference/cli/docker/init/) – Scaffold Docker assets automatically. +- [`docker build` CLI reference](/reference/cli/docker/build/) – Build Docker images from a Dockerfile. +- [`docker images` CLI reference](/reference/cli/docker/images/) – Manage and inspect local Docker images. +- [`docker compose up` CLI reference](/reference/cli/docker/compose/up/) – Start and run multi-container applications. +- [`docker compose down` CLI reference](/reference/cli/docker/compose/down/) – Stop and remove containers, networks, and volumes. + +--- ## Next steps -In the next section, you'll learn how you can develop your application using -containers. +With your Node.js application now containerized, you're ready to move on to the next step. + +In the next section, you'll learn how to develop your application using Docker containers, enabling a consistent, isolated, and reproducible development environment across any machine. diff --git a/content/guides/nodejs/deploy.md b/content/guides/nodejs/deploy.md index d3fa1017ac9b..61234f9f1509 100644 --- a/content/guides/nodejs/deploy.md +++ b/content/guides/nodejs/deploy.md @@ -1,9 +1,9 @@ --- -title: Test your Node.js deployment -linkTitle: Test your deployment +title: Deploy your Node.js application +linkTitle: Deploy your app weight: 50 -keywords: deploy, kubernetes, node, node.js -description: Learn how to deploy locally to test and debug your Kubernetes deployment +keywords: deploy, kubernetes, node, node.js, production +description: Learn how to deploy your containerized Node.js application to Kubernetes with production-ready configuration aliases: - /language/nodejs/deploy/ - /guides/language/nodejs/deploy/ @@ -16,128 +16,579 @@ aliases: ## Overview -In this section, you'll learn how to use Docker Desktop to deploy your -application to a fully-featured Kubernetes environment on your development -machine. This allows you to test and debug your workloads on Kubernetes locally -before deploying. +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. -## Create a Kubernetes YAML file +You'll deploy a complete stack including: -In the cloned repository's directory, create a file named -`docker-node-kubernetes.yaml`. Open the file in an IDE or text editor and add -the following contents. Replace `DOCKER_USERNAME/REPO_NAME` with your Docker -username and the name of the repository that you created in [Configure CI/CD for -your Node.js application](configure-ci-cd.md). +- 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 Kubernetes deployment file + +Create a new file called `nodejs-sample-kubernetes.yaml` in your project root: ```yaml +# ======================================== +# Node.js Todo App - Kubernetes Deployment +# ======================================== + +apiVersion: v1 +kind: Namespace +metadata: + name: todoapp + labels: + app: todoapp + +--- +# ======================================== +# ConfigMap for Application Configuration +# ======================================== +apiVersion: v1 +kind: ConfigMap +metadata: + name: todoapp-config + namespace: todoapp +data: + NODE_ENV: 'production' + ALLOWED_ORIGINS: 'https://yourdomain.com' + POSTGRES_HOST: 'todoapp-postgres' + POSTGRES_PORT: '5432' + POSTGRES_DB: 'todoapp' + POSTGRES_USER: 'todoapp' + +--- +# ======================================== +# 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 +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: standard + +--- +# ======================================== +# PostgreSQL Deployment +# ======================================== apiVersion: apps/v1 kind: Deployment metadata: - name: docker-nodejs-demo - namespace: default + name: todoapp-postgres + namespace: todoapp + labels: + app: todoapp-postgres spec: replicas: 1 selector: matchLabels: - todo: web + app: todoapp-postgres template: metadata: labels: - todo: web + app: todoapp-postgres spec: containers: - - name: todo-site - image: DOCKER_USERNAME/REPO_NAME - imagePullPolicy: Always + - name: postgres + image: postgres:16-alpine + ports: + - containerPort: 5432 + name: postgres + env: + - name: POSTGRES_DB + valueFrom: + configMapKeyRef: + name: todoapp-config + key: POSTGRES_DB + - name: POSTGRES_USER + valueFrom: + configMapKeyRef: + name: todoapp-config + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: todoapp-secrets + key: postgres-password + volumeMounts: + - name: postgres-storage + mountPath: /var/lib/postgresql/data + livenessProbe: + exec: + command: + - pg_isready + - -U + - todoapp + - -d + - todoapp + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - pg_isready + - -U + - todoapp + - -d + - todoapp + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: postgres-storage + persistentVolumeClaim: + claimName: postgres-pvc + --- +# ======================================== +# PostgreSQL Service +# ======================================== apiVersion: v1 kind: Service metadata: - name: todo-entrypoint - namespace: default + name: todoapp-postgres + namespace: todoapp + labels: + app: todoapp-postgres +spec: + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 + protocol: TCP + name: postgres + selector: + app: todoapp-postgres + +--- +# ======================================== +# Application Deployment +# ======================================== +apiVersion: apps/v1 +kind: Deployment +metadata: + name: todoapp-deployment + namespace: todoapp + labels: + app: todoapp spec: - type: NodePort + replicas: 3 selector: - todo: web + matchLabels: + app: todoapp + template: + metadata: + labels: + app: todoapp + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1001 + fsGroup: 1001 + containers: + - name: todoapp + image: ghcr.io/your-username/docker-nodejs-sample:latest + imagePullPolicy: Always + ports: + - containerPort: 3000 + name: http + protocol: TCP + env: + - name: NODE_ENV + 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: POSTGRES_DB + valueFrom: + configMapKeyRef: + name: todoapp-config + key: POSTGRES_DB + - name: POSTGRES_USER + valueFrom: + configMapKeyRef: + name: todoapp-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 + readinessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 5 + 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 +spec: + type: ClusterIP ports: - - port: 3000 + - name: http + port: 80 targetPort: 3000 - nodePort: 30001 -``` + protocol: TCP + selector: + app: todoapp -In this Kubernetes YAML file, there are two objects, separated by the `---`: +--- +# ======================================== +# 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 -- A Deployment, describing a scalable group of identical pods. In this case, - you'll get just one replica, or copy of your pod. That pod, which is - described under `template`, has just one container in it. The container is - created from the image built by GitHub Actions in [Configure CI/CD for your - Node.js application](configure-ci-cd.md). -- A NodePort service, which will route traffic from port 30001 on your host to - port 3000 inside the pods it routes to, allowing you to reach your app - from the network. +--- +# ======================================== +# 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 -To learn more about Kubernetes objects, see the [Kubernetes documentation](https://kubernetes.io/docs/home/). +--- +# ======================================== +# PodDisruptionBudget +# ======================================== +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: todoapp-pdb + namespace: todoapp +spec: + minAvailable: 1 + selector: + matchLabels: + app: todoapp +``` -## Deploy and check your application +## Configure the deployment -1. In a terminal, navigate to where you created `docker-node-kubernetes.yaml` - and deploy your application to Kubernetes. +Before deploying, you need to customize the deployment file for your environment: - ```console - $ kubectl apply -f docker-node-kubernetes.yaml +1. **Image reference**: Replace `your-username` with your GitHub username or Docker Hub username: + + ```yaml + image: ghcr.io/your-username/docker-nodejs-sample:latest ``` - You should see output that looks like the following, indicating your Kubernetes objects were created successfully. +2. **Domain name**: Replace `yourdomain.com` with your actual domain in two places: + + ```yaml + # In ConfigMap + ALLOWED_ORIGINS: "https://yourdomain.com" - ```shell - deployment.apps/docker-nodejs-demo created - service/todo-entrypoint created + # In Ingress + - host: yourdomain.com ``` -2. Make sure everything worked by listing your deployments. +3. **Database password** (optional): The default password is already base64 encoded. To change it: ```console - $ kubectl get deployments + $ echo -n "your-new-password" | base64 ``` - Your deployment should be listed as follows: + Then update the Secret: - ```shell - NAME READY UP-TO-DATE AVAILABLE AGE - docker-nodejs-demo 1/1 1 1 6s + ```yaml + data: + postgres-password: ``` - This indicates all one of the pods you asked for in your YAML are up and running. Do the same check for your services. +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: + +```console +$ kubectl apply -f nodejs-sample-kubernetes.yaml +``` + +You should see output confirming all resources were created: + +```shell +namespace/todoapp created +secret/todoapp-secrets created +configmap/todoapp-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 +``` + +### Step 2: Verify the deployment + +Check that your deployments are running: + +```console +$ kubectl get deployments -n todoapp +``` + +Expected output: + +```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: + +```console +$ kubectl get services -n todoapp +``` + +Expected output: + +```shell +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +todoapp-service ClusterIP 10.111.101.229 80/TCP 45s +todoapp-postgres ClusterIP 10.111.102.130 5432/TCP 45s +``` + +Check that persistent storage is working: + +```console +$ kubectl get pvc -n todoapp +``` + +Expected output: + +```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: + +```console +$ kubectl port-forward -n todoapp service/todoapp-service 8080:80 +``` + +Open your browser and visit [http://localhost:8080](http://localhost:8080) to see your Todo application running in Kubernetes. + +### Step 4: Test the deployment + +Test that your application is working correctly: + +1. **Add some todos** through the web interface +2. **Check application pods**: ```console - $ kubectl get services + $ kubectl get pods -n todoapp -l app=todoapp ``` - You should get output like the following. +3. **View application logs**: - ```shell - NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE - kubernetes ClusterIP 10.96.0.1 443/TCP 7d22h - todo-entrypoint NodePort 10.111.101.229 3000:30001/TCP 33s + ```console + $ kubectl logs -f deployment/todoapp-deployment -n todoapp ``` - In addition to the default `kubernetes` service, you can see your `todo-entrypoint` service, accepting traffic on port 30001/TCP. - -3. Open a browser and visit your app at `localhost:30001`. You should see your - application. +4. **Check database connectivity**: -4. Run the following command to tear down your application. + ```console + $ kubectl get pods -n todoapp -l app=todoapp-postgres + ``` +5. **Monitor auto-scaling**: ```console - $ kubectl delete -f docker-node-kubernetes.yaml + $ kubectl describe hpa todoapp-hpa -n todoapp ``` +### Step 5: Clean up + +When you're done testing, remove the deployment: + +```console +$ kubectl delete -f nodejs-sample-kubernetes.yaml +``` + ## Summary -In this section, you learned how to use Docker Desktop to deploy your application to a fully-featured Kubernetes environment on your development machine. +You've deployed your containerized Node.js application to Kubernetes. You learned how to: + +- 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 + +Your application is now running in a production-like environment with enterprise-grade features including security contexts, resource management, and automatic scaling. + +--- + +## Related resources -Related information: +Explore official references and best practices to sharpen your Kubernetes deployment workflow: -- [Kubernetes documentation](https://kubernetes.io/docs/home/) -- [Deploy on Kubernetes with Docker Desktop](/manuals/desktop/use-desktop/kubernetes.md) -- [Swarm mode overview](/manuals/engine/swarm/_index.md) +- [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. diff --git a/content/guides/nodejs/develop.md b/content/guides/nodejs/develop.md index 0e892375182c..5f4d7026e6c1 100644 --- a/content/guides/nodejs/develop.md +++ b/content/guides/nodejs/develop.md @@ -1,7 +1,7 @@ --- title: Use containers for Node.js development linkTitle: Develop your app -weight: 20 +weight: 30 keywords: node, node.js, development description: Learn how to develop your Node.js application locally using containers. aliases: @@ -24,253 +24,192 @@ In this section, you'll learn how to set up a development environment for your c ## Add a local database and persist data -You can use containers to set up local services, like a database. In this section, you'll update the `compose.yaml` file to define a database service and a volume to persist data. - -1. Open your `compose.yaml` file in an IDE or text editor. -2. Uncomment the database related instructions. The following is the updated - `compose.yaml` file. - - > [!IMPORTANT] - > - > For this section, don't run `docker compose up` until you are instructed to. Running the command at intermediate points may incorrectly initialize your database. - - ```yaml {hl_lines="26-51",collapse=true,title=compose.yaml} - # Comments are provided throughout this file to help you get started. - # If you need more help, visit the Docker Compose reference guide at - # https://docs.docker.com/go/compose-spec-reference/ - - # Here the instructions define your application as a service called "server". - # This service is built from the Dockerfile in the current directory. - # You can add other services your application may depend on here, such as a - # database or a cache. For examples, see the Awesome Compose repository: - # https://github.com/docker/awesome-compose - services: - server: - build: - context: . - environment: - NODE_ENV: production - ports: - - 3000:3000 - - # The commented out section below is an example of how to define a PostgreSQL - # database that your application can use. `depends_on` tells Docker Compose to - # start the database before your application. The `db-data` volume persists the - # database data between container restarts. The `db-password` secret is used - # to set the database password. You must create `db/password.txt` and add - # a password of your choosing to it before running `docker compose up`. - - depends_on: - db: - condition: service_healthy - db: - image: postgres - restart: always - user: postgres - secrets: - - db-password - volumes: - - db-data:/var/lib/postgresql/data - environment: - - POSTGRES_DB=example - - POSTGRES_PASSWORD_FILE=/run/secrets/db-password - expose: - - 5432 - healthcheck: - test: ["CMD", "pg_isready"] - interval: 10s - timeout: 5s - retries: 5 - volumes: - db-data: - secrets: - db-password: - file: db/password.txt - ``` +The application uses PostgreSQL for data persistence. Add a database service to your Docker Compose configuration. - > [!NOTE] - > - > To learn more about the instructions in the Compose file, see [Compose file - > reference](/reference/compose-file/). - -3. Open `src/persistence/postgres.js` in an IDE or text editor. You'll notice - that this application uses a Postgres database and requires some environment - variables in order to connect to the database. The `compose.yaml` file doesn't - have these variables defined yet. -4. Add the environment variables that specify the database configuration. The - following is the updated `compose.yaml` file. - - ```yaml {hl_lines="16-19",collapse=true,title=compose.yaml} - # Comments are provided throughout this file to help you get started. - # If you need more help, visit the Docker Compose reference guide at - # https://docs.docker.com/go/compose-spec-reference/ - - # Here the instructions define your application as a service called "server". - # This service is built from the Dockerfile in the current directory. - # You can add other services your application may depend on here, such as a - # database or a cache. For examples, see the Awesome Compose repository: - # https://github.com/docker/awesome-compose - services: - server: - build: - context: . - environment: - NODE_ENV: production - POSTGRES_HOST: db - POSTGRES_USER: postgres - POSTGRES_PASSWORD_FILE: /run/secrets/db-password - POSTGRES_DB: example - ports: - - 3000:3000 - - # The commented out section below is an example of how to define a PostgreSQL - # database that your application can use. `depends_on` tells Docker Compose to - # start the database before your application. The `db-data` volume persists the - # database data between container restarts. The `db-password` secret is used - # to set the database password. You must create `db/password.txt` and add - # a password of your choosing to it before running `docker compose up`. - - depends_on: - db: - condition: service_healthy - db: - image: postgres - restart: always - user: postgres - secrets: - - db-password - volumes: - - db-data:/var/lib/postgresql/data - environment: - - POSTGRES_DB=example - - POSTGRES_PASSWORD_FILE=/run/secrets/db-password - expose: - - 5432 - healthcheck: - test: ["CMD", "pg_isready"] - interval: 10s - timeout: 5s - retries: 5 - volumes: - db-data: - secrets: - db-password: - file: db/password.txt - ``` +### Add database service to Docker Compose + +If you haven't already created a `compose.yml` file in the previous section, or if you need to add the database service, update your `compose.yml` file to include the PostgreSQL database service: + +```yaml +services: + # ... existing app services ... + + # ======================================== + # PostgreSQL Database Service + # ======================================== + db: + image: postgres:16-alpine + container_name: todoapp-db + environment: + POSTGRES_DB: '${POSTGRES_DB:-todoapp}' + POSTGRES_USER: '${POSTGRES_USER:-todoapp}' + POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-todoapp_password}' + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - '${DB_PORT:-5432}:5432' + restart: unless-stopped + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-todoapp} -d ${POSTGRES_DB:-todoapp}'] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + networks: + - todoapp-network + +# ======================================== +# Volume Configuration +# ======================================== +volumes: + postgres_data: + name: todoapp-postgres-data + driver: local + +# ======================================== +# Network Configuration +# ======================================== +networks: + todoapp-network: + name: todoapp-network + driver: bridge +``` + +### Update your application service + +Make sure your application service in `compose.yml` is configured to connect to the database: + +```yaml {hl_lines="18-20,42-44",collapse=true,title=compose.yml} +services: + app-dev: + build: + context: . + dockerfile: Dockerfile + target: development + container_name: todoapp-dev + ports: + - '${APP_PORT:-3000}:3000' # API server + - '${VITE_PORT:-5173}:5173' # Vite dev server + - '${DEBUG_PORT:-9229}:9229' # Node.js debugger + environment: + NODE_ENV: development + DOCKER_ENV: 'true' + POSTGRES_HOST: db + POSTGRES_PORT: 5432 + POSTGRES_DB: todoapp + POSTGRES_USER: todoapp + POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-todoapp_password}' + ALLOWED_ORIGINS: '${ALLOWED_ORIGINS:-http://localhost:3000,http://localhost:5173}' + volumes: + - ./src:/app/src:ro + - ./package.json:/app/package.json + - ./vite.config.ts:/app/vite.config.ts:ro + - ./tailwind.config.js:/app/tailwind.config.js:ro + - ./postcss.config.js:/app/postcss.config.js:ro + depends_on: + db: + condition: service_healthy + develop: + watch: + - action: sync + path: ./src + target: /app/src + ignore: + - '**/*.test.*' + - '**/__tests__/**' + - action: rebuild + path: ./package.json + - action: sync + path: ./vite.config.ts + target: /app/vite.config.ts + - action: sync + path: ./tailwind.config.js + target: /app/tailwind.config.js + - action: sync + path: ./postcss.config.js + target: /app/postcss.config.js + restart: unless-stopped + networks: + - todoapp-network + + db: + image: postgres:16-alpine + container_name: todoapp-db + environment: + POSTGRES_DB: '${POSTGRES_DB:-todoapp}' + POSTGRES_USER: '${POSTGRES_USER:-todoapp}' + POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-todoapp_password}' + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - '${DB_PORT:-5432}:5432' + restart: unless-stopped + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-todoapp} -d ${POSTGRES_DB:-todoapp}'] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + networks: + - todoapp-network + +volumes: + postgres_data: + name: todoapp-postgres-data + driver: local + +networks: + todoapp-network: + name: todoapp-network + driver: bridge +``` + +1. The PostgreSQL database configuration is handled automatically by the application. The database is created and initialized when the application starts, with data persisted using the `postgres_data` volume. -5. Add the `secrets` section under the `server` service so that your application securely handles the database password. The following is the updated `compose.yaml` file. - - ```yaml {hl_lines="33-34",collapse=true,title=compose.yaml} - # Comments are provided throughout this file to help you get started. - # If you need more help, visit the Docker Compose reference guide at - # https://docs.docker.com/go/compose-spec-reference/ - - # Here the instructions define your application as a service called "server". - # This service is built from the Dockerfile in the current directory. - # You can add other services your application may depend on here, such as a - # database or a cache. For examples, see the Awesome Compose repository: - # https://github.com/docker/awesome-compose - services: - server: - build: - context: . - environment: - NODE_ENV: production - POSTGRES_HOST: db - POSTGRES_USER: postgres - POSTGRES_PASSWORD_FILE: /run/secrets/db-password - POSTGRES_DB: example - ports: - - 3000:3000 - - # The commented out section below is an example of how to define a PostgreSQL - # database that your application can use. `depends_on` tells Docker Compose to - # start the database before your application. The `db-data` volume persists the - # database data between container restarts. The `db-password` secret is used - # to set the database password. You must create `db/password.txt` and add - # a password of your choosing to it before running `docker compose up`. - - depends_on: - db: - condition: service_healthy - secrets: - - db-password - db: - image: postgres - restart: always - user: postgres - secrets: - - db-password - volumes: - - db-data:/var/lib/postgresql/data - environment: - - POSTGRES_DB=example - - POSTGRES_PASSWORD_FILE=/run/secrets/db-password - expose: - - 5432 - healthcheck: - test: ["CMD", "pg_isready"] - interval: 10s - timeout: 5s - retries: 5 - volumes: - db-data: - secrets: - db-password: - file: db/password.txt +1. Configure your environment by copying the example file: + + ```console + $ cp .env.example .env ``` -6. In the `docker-nodejs-sample` directory, create a directory named `db`. -7. In the `db` directory, create a file named `password.txt`. This file will - contain your database password. - - You should now have at least the following contents in your - `docker-nodejs-sample` directory. - - ```text - ├── docker-nodejs-sample/ - │ ├── db/ - │ │ └── password.txt - │ ├── spec/ - │ ├── src/ - │ ├── .dockerignore - │ ├── .gitignore - │ ├── compose.yaml - │ ├── Dockerfile - │ ├── package-lock.json - │ ├── package.json - │ └── README.md + Update the `.env` file with your preferred settings: + + ```env + # Application Configuration + NODE_ENV=development + APP_PORT=3000 + VITE_PORT=5173 + DEBUG_PORT=9230 + + # Database Configuration + POSTGRES_HOST=db + POSTGRES_PORT=5432 + POSTGRES_DB=todoapp + POSTGRES_USER=todoapp + POSTGRES_PASSWORD=todoapp_password + + # Security Configuration + ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173 ``` -8. Open the `password.txt` file in an IDE or text editor, and specify a password - of your choice. Your password must be on a single line with no additional - lines. Ensure that the file doesn't contain any newline characters or other - hidden characters. -9. Ensure that you save your changes to all the files that you have modified. -10. Run the following command to start your application. +1. Run the following command to start your application in development mode: - ```console - $ docker compose up --build - ``` + ```console + $ docker compose up app-dev --build + ``` -11. Open a browser and verify that the application is running at - [http://localhost:3000](http://localhost:3000). -12. Add some items to the todo list to test data persistence. -13. After adding some items to the todo list, press `ctrl+c` in the terminal to - stop your application. -14. In the terminal, run `docker compose rm` to remove your containers. +1. Open a browser and verify that the application is running at [http://localhost:5173](http://localhost:5173) for the frontend or [http://localhost:3000](http://localhost:3000) for the API. The React frontend is served by Vite dev server on port 5173, with API calls proxied to the Express server on port 3000. - ```console - $ docker compose rm - ``` +1. Add some items to the todo list to test data persistence. -15. Run `docker compose up` to run your application again. +1. After adding some items to the todo list, press `CTRL + C` in the terminal to stop your application. - ```console - $ docker compose up --build - ``` +1. Run the application again: + ```console + $ docker compose up app-dev + ``` -16. Refresh [http://localhost:3000](http://localhost:3000) in your browser and verify that the todo items persisted, even after the containers were removed and ran again. +1. Refresh [http://localhost:5173](http://localhost:5173) in your browser and verify that the todo items persisted, even after the containers were removed and ran again. ## Configure and run a development container @@ -280,145 +219,301 @@ In addition to adding a bind mount, you can configure your Dockerfile and `compo ### Update your Dockerfile for development -Open the Dockerfile in an IDE or text editor. Note that the Dockerfile doesn't -install development dependencies and doesn't run nodemon. You'll -need to update your Dockerfile to install the development dependencies and run -nodemon. - -Rather than creating one Dockerfile for production, and another Dockerfile for -development, you can use one multi-stage Dockerfile for both. +Your Dockerfile should be configured as a multi-stage build with separate stages for development, production, and testing. If you followed the previous section, your Dockerfile already includes a development stage that has all development dependencies and runs the application with hot reload enabled. -Update your Dockerfile to the following multi-stage Dockerfile. +Here's the development stage from your multi-stage Dockerfile: ```dockerfile {hl_lines="5-26",collapse=true,title=Dockerfile} -# syntax=docker/dockerfile:1 +# ======================================== +# Development Stage +# ======================================== +FROM build-deps AS development -ARG NODE_VERSION=18.0.0 +# Set environment +ENV NODE_ENV=development \ + NPM_CONFIG_LOGLEVEL=warn -FROM node:${NODE_VERSION}-alpine as base -WORKDIR /usr/src/app -EXPOSE 3000 - -FROM base as dev -RUN --mount=type=bind,source=package.json,target=package.json \ - --mount=type=bind,source=package-lock.json,target=package-lock.json \ - --mount=type=cache,target=/root/.npm \ - npm ci --include=dev -USER node -COPY . . -CMD npm run dev - -FROM base as prod -RUN --mount=type=bind,source=package.json,target=package.json \ - --mount=type=bind,source=package-lock.json,target=package-lock.json \ - --mount=type=cache,target=/root/.npm \ - npm ci --omit=dev -USER node +# Copy source files COPY . . -CMD node src/index.js -``` -In the Dockerfile, you first add a label `as base` to the `FROM -node:${NODE_VERSION}-alpine` statement. This lets you refer to this build stage -in other build stages. Next, you add a new build stage labeled `dev` to install -your development dependencies and start the container using `npm run dev`. -Finally, you add a stage labeled `prod` that omits the dev dependencies and runs -your application using `node src/index.js`. To learn more about multi-stage -builds, see [Multi-stage builds](/manuals/build/building/multi-stage.md). +# Ensure all directories have proper permissions +RUN mkdir -p /app/node_modules/.vite && \ + chown -R nodejs:nodejs /app && \ + chmod -R 755 /app -Next, you'll need to update your Compose file to use the new stage. +# Switch to non-root user +USER nodejs -### Update your Compose file for development +# Expose ports +EXPOSE 3000 5173 9229 + +# Start development server +CMD ["npm", "run", "dev:docker"] +``` + +The development stage: -To run the `dev` stage with Compose, you need to update your `compose.yaml` -file. Open your `compose.yaml` file in an IDE or text editor, and then add the -`target: dev` instruction to target the `dev` stage from your multi-stage -Dockerfile. +- Installs all dependencies including dev dependencies +- Exposes ports for the API server (3000), Vite dev server (5173), and Node.js debugger (9229) +- Runs `npm run dev` which starts both the Express server and Vite dev server concurrently +- Includes health checks for monitoring container status -Also, add a new volume to the server service for the bind mount. For this application, you'll mount `./src` from your local machine to `/usr/src/app/src` in the container. +Next, you'll need to update your Compose file to use the new stage. -Lastly, publish port `9229` for debugging. +### Update your Compose file for development -The following is the updated Compose file. All comments have been removed. +Update your `compose.yml` file to run the development stage with bind mounts for hot reloading: -```yaml {hl_lines=[5,8,20,21],collapse=true,title=compose.yaml} +```yaml {hl_lines=[5,8-10,20-27],collapse=true,title=compose.yml} services: - server: + app-dev: build: context: . - target: dev + dockerfile: Dockerfile + target: development + container_name: todoapp-dev ports: - - 3000:3000 - - 9229:9229 + - '${APP_PORT:-3000}:3000' # API server + - '${VITE_PORT:-5173}:5173' # Vite dev server + - '${DEBUG_PORT:-9229}:9229' # Node.js debugger environment: - NODE_ENV: production + NODE_ENV: development + DOCKER_ENV: 'true' POSTGRES_HOST: db - POSTGRES_USER: postgres - POSTGRES_PASSWORD_FILE: /run/secrets/db-password - POSTGRES_DB: example + POSTGRES_PORT: 5432 + POSTGRES_DB: todoapp + POSTGRES_USER: todoapp + POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-todoapp_password}' + ALLOWED_ORIGINS: '${ALLOWED_ORIGINS:-http://localhost:3000,http://localhost:5173}' + volumes: + - ./src:/app/src:ro + - ./package.json:/app/package.json + - ./vite.config.ts:/app/vite.config.ts:ro + - ./tailwind.config.js:/app/tailwind.config.js:ro + - ./postcss.config.js:/app/postcss.config.js:ro depends_on: db: condition: service_healthy - secrets: - - db-password - volumes: - - ./src:/usr/src/app/src - db: - image: postgres - restart: always - user: postgres - secrets: - - db-password - volumes: - - db-data:/var/lib/postgresql/data - environment: - - POSTGRES_DB=example - - POSTGRES_PASSWORD_FILE=/run/secrets/db-password - expose: - - 5432 - healthcheck: - test: ["CMD", "pg_isready"] - interval: 10s - timeout: 5s - retries: 5 -volumes: - db-data: -secrets: - db-password: - file: db/password.txt + develop: + watch: + - action: sync + path: ./src + target: /app/src + ignore: + - '**/*.test.*' + - '**/__tests__/**' + - action: rebuild + path: ./package.json + - action: sync + path: ./vite.config.ts + target: /app/vite.config.ts + - action: sync + path: ./tailwind.config.js + target: /app/tailwind.config.js + - action: sync + path: ./postcss.config.js + target: /app/postcss.config.js + restart: unless-stopped + networks: + - todoapp-network ``` +Key features of the development configuration: + +- **Multi-port exposure**: API server (3000), Vite dev server (5173), and debugger (9229) +- **Comprehensive bind mounts**: Source code, configuration files, and package files for hot reloading +- **Environment variables**: Configurable through `.env` file or defaults +- **PostgreSQL database**: Production-ready database with persistent storage +- **Docker Compose watch**: Automatic file synchronization and container rebuilds +- **Health checks**: Database health monitoring with automatic dependency management + ### Run your development container and debug your application -Run the following command to run your application with the new changes to the `Dockerfile` and `compose.yaml` file. +Run the following command to run your application with the development configuration: + +```console +$ docker compose up app-dev --build +``` + +Or with file watching for automatic updates: + +```console +$ docker compose up app-dev --watch +``` + +For local development without Docker: ```console -$ docker compose up --build +$ npm run dev:with-db ``` -Open a browser and verify that the application is running at [http://localhost:3000](http://localhost:3000). +Or start services separately: -Any changes to the application's source files on your local machine will now be -immediately reflected in the running container. +```console +$ npm run db:start # Start PostgreSQL container +$ npm run dev # Start both server and client +``` -Open `docker-nodejs-sample/src/static/js/app.js` in an IDE or text editor and update the button text on line 109 from `Add Item` to `Add`. +### Using Task Runner (alternative) -```diff -+ {submitting ? 'Adding...' : 'Add'} -- {submitting ? 'Adding...' : 'Add Item'} +The project includes a Taskfile.yml for advanced workflows: + +```console +# Development +$ task dev # Start development environment +$ task dev:build # Build development image +$ task dev:run # Run development container + +# Production +$ task build # Build production image +$ task run # Run production container +$ task build-run # Build and run in one step + +# Testing +$ task test # Run all tests +$ task test:unit # Run unit tests with coverage +$ task test:lint # Run linting + +# Kubernetes +$ task k8s:deploy # Deploy to Kubernetes +$ task k8s:status # Check deployment status +$ task k8s:logs # View pod logs + +# Utilities +$ task clean # Clean up containers and images +$ task health # Check application health +$ task logs # View container logs ``` -Refresh [http://localhost:3000](http://localhost:3000) in your browser and verify that the updated text appears. +The application will start with both the Express API server and Vite development server: + +- **API Server**: [http://localhost:3000](http://localhost:3000) - Express.js backend with REST API +- **Frontend**: [http://localhost:5173](http://localhost:5173) - Vite dev server with hot module replacement +- **Health Check**: [http://localhost:3000/health](http://localhost:3000/health) - Application health status + +Any changes to the application's source files on your local machine will now be immediately reflected in the running container thanks to the bind mounts. + +Try making a change to test hot reloading: + +1. Open `src/client/components/TodoApp.tsx` in an IDE or text editor. +1. Update the main heading text: + + ```diff + -

+ - Modern Todo App + -

+ +

+ + My Todo App + +

+ ``` + +1. Save the file and the Vite dev server will automatically reload the page with your changes. + +**Debugging support:** + +You can connect a debugger to your application on port 9229. The Node.js inspector is enabled with `--inspect=0.0.0.0:9230` in the development script (`dev:server`). + +### VS Code debugger setup + +1. Create a launch configuration in `.vscode/launch.json`: + + ```json + { + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Docker Container", + "type": "node", + "request": "attach", + "port": 9229, + "address": "localhost", + "localRoot": "${workspaceFolder}", + "remoteRoot": "/app", + "protocol": "inspector", + "restart": true, + "sourceMaps": true, + "skipFiles": ["/**"] + } + ] + } + ``` + +1. Start your development container: + + ```console + docker compose up app-dev --build + ``` + +1. Attach the debugger: + - Open VS Code + - From the Debug panel (Ctrl/Cmd + Shift + D), select **Attach to Docker Container** from the drop-down + - Select the green play button or press F5 + +### Chrome DevTools (alternative) + +You can also use Chrome DevTools for debugging: + +1. Start your container (if not already running): + + ```console + docker compose up app-dev --build + ``` + +1. Open Chrome and go to `chrome://inspect`. + +1. From the **Configure** option, add: + + ```text + localhost:9229 + ``` + +1. When your Node.js target appears, select **inspect**. + +### Debugging configuration details + +The debugger configuration: + +- **Container port**: 9230 (internal debugger port) +- **Host port**: 9229 (mapped external port) +- **Script**: `tsx watch --inspect=0.0.0.0:9230 src/server/index.ts` + +The debugger listens on all interfaces (`0.0.0.0`) inside the container on port 9230 and is accessible on port 9229 from your host machine. + +### Troubleshooting debugger connection + +If the debugger doesn't connect: + +1. Check if the container is running: + + ```console + docker ps + ``` + +1. Check if the port is exposed: + + ```console + docker port todoapp-dev + ``` + +1. Check container logs: + + ```console + docker compose logs app-dev + ``` + + You should see a message like: + + ```text + Debugger listening on ws://0.0.0.0:9230/... + ``` + +Now you can set breakpoints in your TypeScript source files and debug your containerized Node.js application. -You can now connect an inspector client to your application for debugging. For -more details about inspector clients, see the [Node.js -documentation](https://nodejs.org/en/docs/guides/debugging-getting-started). +For more details about Node.js debugging, see the [Node.js documentation](https://nodejs.org/en/docs/guides/debugging-getting-started). ## Summary -In this section, you took a look at setting up your Compose file to add a mock -database and persist data. You also learned how to create a multi-stage -Dockerfile and set up a bind mount for development. +You've set up your Compose file with a PostgreSQL database and data persistence. You also created a multi-stage Dockerfile and configured bind mounts for development. Related information: diff --git a/content/guides/nodejs/run-tests.md b/content/guides/nodejs/run-tests.md index b59072f9becf..41f069a83429 100644 --- a/content/guides/nodejs/run-tests.md +++ b/content/guides/nodejs/run-tests.md @@ -15,159 +15,213 @@ Complete all the previous sections of this guide, starting with [Containerize a ## Overview -Testing is an essential part of modern software development. Testing can mean a -lot of things to different development teams. There are unit tests, integration -tests and end-to-end testing. In this guide you take a look at running your unit -tests in Docker when developing and when building. +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. ## Run tests when developing locally -The sample application already has the Jest package for running tests and has tests inside the `spec` directory. When developing locally, you can use Compose to run your tests. +The sample application uses Vitest for testing, and it already includes tests for React components, custom hooks, API routes, database operations, and utility functions. -Run the following command to run the test script from the `package.json` file inside a container. +### Run tests locally (without Docker) ```console -$ docker compose run server npm run test +$ npm run test ``` -To learn more about the command, see [docker compose run](/reference/cli/docker/compose/run/). +### 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 -You should see output like the following. +To generate a coverage report: ```console -> docker-nodejs@1.0.0 test -> jest +$ npm run test:coverage +``` - PASS spec/routes/deleteItem.spec.js - PASS spec/routes/getItems.spec.js - PASS spec/routes/addItem.spec.js - PASS spec/routes/updateItem.spec.js - PASS spec/persistence/sqlite.spec.js - ● Console +You should see output like the following: - console.log - Using sqlite database at /tmp/todo.db +```console +> docker-nodejs-sample@1.0.0 test +> vitest --run + + ✓ 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) +``` - at Database.log (src/persistence/sqlite.js:18:25) +### Test structure - console.log - Using sqlite database at /tmp/todo.db +The test suite covers: - at Database.log (src/persistence/sqlite.js:18:25) +- **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 + +## Run tests when building - console.log - Using sqlite database at /tmp/todo.db +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: - at Database.log (src/persistence/sqlite.js:18:25) +```dockerfile +# ======================================== +# Test Stage +# ======================================== +FROM build-deps AS test - console.log - Using sqlite database at /tmp/todo.db +# Set environment +ENV NODE_ENV=test \ + CI=true + +# Copy source files +COPY --chown=nodejs:nodejs . . + +# Switch to non-root user +USER nodejs + +# Run tests with coverage +CMD ["npm", "run", "test:coverage"] +``` - at Database.log (src/persistence/sqlite.js:18:25) +This test stage: - console.log - Using sqlite database at /tmp/todo.db +- **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 - at Database.log (src/persistence/sqlite.js:18:25) +### 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: -Test Suites: 5 passed, 5 total -Tests: 9 passed, 9 total -Snapshots: 0 total -Time: 2.008 s -Ran all test suites. +```console +$ docker build --target test -t node-docker-image-test . ``` -## Run tests when building +### 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 +``` -To run your tests when building, you need to update your Dockerfile to add a new test stage. - -The following is the updated Dockerfile. - -```dockerfile {hl_lines="27-35"} -# syntax=docker/dockerfile:1 - -ARG NODE_VERSION=18.0.0 - -FROM node:${NODE_VERSION}-alpine as base -WORKDIR /usr/src/app -EXPOSE 3000 - -FROM base as dev -RUN --mount=type=bind,source=package.json,target=package.json \ - --mount=type=bind,source=package-lock.json,target=package-lock.json \ - --mount=type=cache,target=/root/.npm \ - npm ci --include=dev -USER node -COPY . . -CMD npm run dev - -FROM base as prod -RUN --mount=type=bind,source=package.json,target=package.json \ - --mount=type=bind,source=package-lock.json,target=package-lock.json \ - --mount=type=cache,target=/root/.npm \ - npm ci --omit=dev -USER node -COPY . . -CMD node src/index.js - -FROM base as test -ENV NODE_ENV test -RUN --mount=type=bind,source=package.json,target=package.json \ - --mount=type=bind,source=package-lock.json,target=package-lock.json \ - --mount=type=cache,target=/root/.npm \ - npm ci --include=dev -USER node -COPY . . -RUN npm run test +Or run it as a one-off container: + +```console +$ docker compose run --rm app-test ``` -Instead of using `CMD` in the test stage, use `RUN` to run the tests. The reason is that the `CMD` instruction runs when the container runs, and the `RUN` instruction runs when the image is being built and the build will fail if the tests fail. +### Run tests with coverage in CI/CD -Run the following command to build a new image using the test stage as the target and view the test results. Include `--progress=plain` to view the build output, `--no-cache` to ensure the tests always run, and `--target test` to target the test stage. +For continuous integration, you can run tests with coverage: ```console -$ docker build -t node-docker-image-test --progress=plain --no-cache --target test . +$ 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. +You should see output containing the following: ```console -... - -#11 [test 3/3] RUN npm run test -#11 1.058 -#11 1.058 > docker-nodejs@1.0.0 test -#11 1.058 > jest -#11 1.058 -#11 3.765 PASS spec/routes/getItems.spec.js -#11 3.767 PASS spec/routes/deleteItem.spec.js -#11 3.783 PASS spec/routes/updateItem.spec.js -#11 3.806 PASS spec/routes/addItem.spec.js -#11 4.179 PASS spec/persistence/sqlite.spec.js -#11 4.207 -#11 4.208 Test Suites: 5 passed, 5 total -#11 4.208 Tests: 9 passed, 9 total -#11 4.208 Snapshots: 0 total -#11 4.208 Time: 2.168 s -#11 4.208 Ran all test suites. -#11 4.265 npm notice -#11 4.265 npm notice New major version of npm available! 8.6.0 -> 9.8.1 -#11 4.265 npm notice Changelog: -#11 4.265 npm notice Run `npm install -g npm@9.8.1` to update! -#11 4.266 npm notice -#11 DONE 4.3s - -... + ✓ 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) ``` ## Summary -In this section, you learned how to run tests when developing locally using Compose and how to run tests when building your image. +In this section, you learned how to run tests when developing locally using Docker Compose and how to run tests when building your image. Related information: -- [docker compose run](/reference/cli/docker/compose/run/) +- [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. ## Next steps