Containerize a Node.js API with CI/CD
Package a Node.js API in Docker and automate builds with GitHub Actions.
Introduction
30 Second Summary
Every app you use gets updated constantly. Behind the scenes, code travels from a developer's laptop to production through automated pipelines that test and package it without human intervention.
In this project, you will build a Node.js REST API, package it inside a Docker container, and set up a GitHub Actions pipeline that automatically tests and builds your app every time you push code.
What You'll Build
A containerized Express.js API with an automated CI/CD pipeline that mirrors how professional engineering teams ship software.
By the end of this project, you'll have:
- A working Express.js API with a health check endpoint and an automated test suite.
- A Dockerfile that packages your API into a portable container you can run anywhere.
- A GitHub Actions workflow that automatically tests and builds your Docker image on every push.
- Secret Mission: Add automated container security scanning to your pipeline with Docker Scout.
Are there any prerequisites?
You need basic JavaScript knowledge and a GitHub account. No prior Docker or CI/CD experience is required.
Before We Start
Every production app needs tools for running code, packaging it, and shipping it. For this project, those tools are Node.js, Docker, and Git.
You'll install all three on your Windows machine and confirm they're ready to go. Once verified, you'll have everything you need to build, containerize, and automate your API.
In this step, get ready to:
- Install Node.js v24 LTS.
- Install Docker Desktop with WSL 2.
- Verify Git is ready.
Install Node.js v24 LTS
Node.js is the runtime that executes JavaScript outside of a browser. Your API code will run on Node.js both locally and inside your Docker container.
- Open your browser and go to the Node.js download page:
- https://nodejs.org/en/download
- Click the Windows installer option and choose the LTS version (v24.18.0).
- Run the downloaded installer.
- Accept all defaults.
- If you see a checkbox for Automatically install necessary tools, check it.
Why the LTS version?
LTS stands for Long-Term Support. It's the version that receives security updates and bug fixes for the longest period, making it the safest choice for production projects.
- Once the installer finishes, open a new PowerShell window.
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open it.
- Verify Node.js is installed by running this command:
node --version
You should see a version number starting with v24 in the output.
✔️ I see v24 or higher
Node.js is installed and ready. Confirm npm (Node's package manager) also works by running:
npm --version
You should see a version number in the output. You're good to move on.
ⓧ I see an older version
A previous installation of Node.js is taking precedence. You need to replace it with v24.18.0.
- Go back to https://nodejs.org/en/download and download the v24.18.0 LTS installer for Windows.
- Run the installer again. It will overwrite the older version.
- Close your current PowerShell window and open a fresh one.
- Run node --version again to confirm it now shows v24.
ⓧ Command not found
The installer did not add Node.js to your system PATH, or you are using the same PowerShell window from before the install.
- Close your current PowerShell window completely.
- Open a brand-new PowerShell window (press the Windows key, type PowerShell, press Enter).
- Run node --version again.
If it still doesn't work, re-run the Node.js installer from https://nodejs.org/en/download and make sure you accept all defaults.
Install Docker Desktop
Docker Desktop is the application that lets you build and run containers on your machine. It needs a Linux layer called WSL 2 to work on Windows.
You'll enable WSL 2 first, then install Docker Desktop on top of it.
- Open PowerShell as Administrator.
- Press the Windows key, type PowerShell, then press Ctrl+Shift+Enter to launch it with admin privileges.
- Click Yes on the User Account Control prompt that appears.
- Enable WSL 2 by running this command:
wsl --install
What does this command do?
This installs the Windows Subsystem for Linux (version 2), which gives Docker a lightweight Linux environment to run containers in. Without it, Docker Desktop cannot function on Windows.
- If the output tells you to restart your computer, do so now and reopen PowerShell afterwards.
Seeing an error about virtualization?
Some machines have hardware virtualization disabled in the BIOS. If you see an error mentioning virtualization or Hyper-V, you may need to enable it in your BIOS settings.
Search your computer manufacturer's support site for instructions on enabling virtualization. Help me fix a WSL virtualization error
- Open your browser and go to the Docker Desktop install page:
- https://docs.docker.com/desktop/setup/install/windows-install/
- Download and run the Docker Desktop installer.
- During installation, make sure Use WSL 2 instead of Hyper-V is checked.
- After installation completes, open Docker Desktop.
- Press the Windows key, type Docker Desktop, and press Enter.
- Wait for Docker Desktop to finish starting. The whale icon in your system tray should stop animating when it's ready.
First launch takes a minute
Docker Desktop needs to set up its Linux backend on first launch. Give it a minute or two before verifying.
- Return to your PowerShell window (a regular one, not Administrator) and verify Docker is working by running:
docker --version
✔️ I see a version number
Docker is installed and running. You're ready to move on.
ⓧ Command not found
Docker Desktop may not have finished starting, or it isn't running yet.
- Check your system tray (bottom-right corner of the screen) for the Docker whale icon.
- If the whale icon is not there, open Docker Desktop again (press the Windows key, type Docker Desktop, press Enter).
- Wait until the whale icon stops animating, then close and reopen PowerShell.
- Run docker --version again.
If it still fails, help me troubleshoot Docker Desktop not being recognized in PowerShell
Verify Git is installed
Git tracks your code changes and pushes them to GitHub. The Node.js installer may have already included it on your machine.
- In your PowerShell window, check if Git is available by running:
git --version
✔️ I see a version number
Git is ready. You have all three tools installed.
ⓧ Command not found
Git is not installed yet. You'll need to download and install it.
- Open your browser and go to:
- https://git-scm.com/download/win
- Download the installer and run it.
- Accept all default settings throughout the installer.
- Close your current PowerShell window and open a fresh one.
- Run git --version again to confirm it works.
- As a final check, run all three version commands to confirm everything is working:
node --version
npm --version
docker --version
git --version
You should see version numbers for all four commands with no errors.
One of the commands still failing?
Make sure Docker Desktop is running (whale icon in the system tray) and that you opened a fresh PowerShell window after each installation. Installers modify your PATH, but existing terminal sessions don't pick up the change until reopened.
Help me fix a version command that isn't working after installation
Your machine is fully set up. Next up, you'll build the Express.js API that will eventually run inside your Docker container.
Install Node.js and Docker Desktop
Before you can containerize an API and automate it with a pipeline, you need three core tools working together on your machine. Node.js runs your JavaScript code outside the browser. Docker packages your app into a portable container. Git connects your local code to GitHub where your CI/CD pipeline will live.
This step confirms each tool is installed and responding correctly in PowerShell.
In this step, get ready to:
- Confirm Node.js v24 LTS returns a version number.
- Confirm Docker Desktop is running and responds to commands.
- Confirm Git is ready for version control.
Confirm your Node.js installation
Node.js is the runtime that powers your Express.js API. It also ships with npm, the package manager you will use to install dependencies like Express.
- Verify your Node.js version by running this command in PowerShell:
node --version
- You should see v24.x.x (for example, v24.18.0).
- Now check that npm is available by running:
npm --version
- You should see a version number (such as 11.x.x).
What are Node.js and npm?
Node.js lets you run JavaScript on your computer instead of only inside a browser. Your API code is written in JavaScript, so Node.js is what executes it.
npm (Node Package Manager) downloads and manages third-party libraries. You will use it to install Express, the framework for building your API.
Not seeing a version number?
If you see an error like "node is not recognized", PowerShell cannot find Node.js in your system PATH. Close PowerShell, open a fresh window, and try again. A fresh window picks up PATH changes made by the installer.
help me troubleshoot why node --version is not working in PowerShell on Windows
Confirm Docker Desktop is running
Docker commands only work when Docker Desktop is actively running in the background. Look for the whale icon in your system tray (bottom-right of your screen). The icon should be still, not animating.
- Verify Docker is responding by running this command:
docker --version
- You should see a version string starting with Docker version followed by a number.
Why does Docker Desktop need to be running?
Docker Desktop manages the background engine that builds and runs containers. Without it running, the docker command has nothing to talk to and returns an error.
Docker Desktop uses WSL 2 (Windows Subsystem for Linux) under the hood to run a lightweight Linux kernel. This is what makes Linux containers work natively on Windows.
Seeing "docker is not recognized"?
Make sure Docker Desktop is open and fully started (whale icon is still, not animating). If you just installed it, you may need to restart your computer first.
If Docker Desktop is running but the command still fails, close PowerShell and open a fresh window.
help me troubleshoot why docker --version is not working on Windows 11
Confirm Git is installed
Git is the version control tool that tracks your code changes and pushes them to GitHub. Your CI/CD pipeline triggers every time you push code, so Git is essential.
- Verify Git by running this command:
git --version
- You should see a version string starting with git version followed by a number.
Command not found?
If git is not recognized, download it from https://git-scm.com/download/win and install with default settings. After installing, close and reopen PowerShell before retrying.
help me troubleshoot why git --version is not working in PowerShell on Windows
Your development environment is locked and loaded. Next up, you will build the Express.js API that your Docker container will eventually package and your pipeline will automatically test.
Build the Express.js API
Every containerized application starts with the application itself. Before you can package anything in Docker, you need working code that does something useful.
In this step, you will build a simple Express.js REST API with two endpoints. You will also write automated tests that prove your API works correctly.
In this step, get ready to:
- Create a project folder and define its dependencies.
- Write an Express API with a root endpoint and a health check endpoint.
- Write and run automated tests to verify your API works.
Create the project structure
Your project needs a home folder, a manifest file that lists its dependencies, the API source code, and a test file. You will create each of these one at a time.
- Open PowerShell by pressing the Windows key, typing PowerShell, and pressing Enter.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir node-docker-cicd
cd node-docker-cicd
- Open this folder in VS Code by running:
code .
A new VS Code window opens with the node-docker-cicd folder loaded.
VS Code didn't open?
If PowerShell says code is not recognized, close and reopen PowerShell so it picks up the updated PATH from your VS Code installation. Then run cd ~/Desktop/node-docker-cicd and try code . again.
Help me get the code command working in PowerShell.
- Open the integrated terminal in VS Code by pressing Ctrl+` (the backtick key, above Tab).
- Create a new file called package.json by clicking the new file icon in the Explorer sidebar (or pressing Ctrl+N then Ctrl+S and saving as package.json).
- Paste the following into package.json:
{
"name": "node-docker-cicd",
"version": "1.0.0",
"description": "A containerized Node.js API with CI/CD pipeline",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "node --test"
},
"keywords": ["docker", "cicd", "express", "github-actions"],
"author": "",
"license": "MIT",
"dependencies": {
"express": "5.2.1"
}
}
What does this file do?
The package.json file is the manifest for any Node.js project. It tells npm what your project is called, what scripts it can run, and what packages it depends on.
The scripts section defines two shortcuts: npm start runs your server, and npm test runs the built-in Node.js test runner with node --test. The dependencies section pins Express at version 5.2.1.
- Save package.json with Ctrl+S.
- Install Express by running this command in the VS Code integrated terminal:
npm install
You should see output showing packages being added, ending with a summary like added X packages. A node_modules folder and package-lock.json file now appear in your Explorer sidebar.
Seeing permission errors or network issues?
Make sure you are running the command inside the node-docker-cicd folder. Run pwd to check your current directory. If you see a network timeout, check your internet connection and try again.
Help me fix my npm install error.
- Create a new file called index.js in the root of your project folder.
- Paste the following code into index.js:
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
// Root endpoint - returns project info as JSON
app.get('/', (req, res) => {
res.json({
project: 'node-docker-cicd',
version: '1.0.0',
message: 'Welcome to my containerized API!',
endpoints: ['/health']
});
});
// Health check endpoint - returns server status
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
});
// Start the server and export for testing
const server = app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
module.exports = { app, server };
What does this code do?
This file creates an Express web server with two GET endpoints. The root endpoint (/) returns JSON describing the project. The /health endpoint returns the server's current status, uptime, and a timestamp.
The last line exports both app and server so your test file can import them and make requests against the running server without needing to start it separately.
- Save index.js.
✔️ Awesome, I've got everything!
Great. Make sure you have saved index.js before continuing.
ⓧ I'd like to double check the full code
Here is the complete index.js for comparison:
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({
project: 'node-docker-cicd',
version: '1.0.0',
message: 'Welcome to my containerized API!',
endpoints: ['/health']
});
});
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
});
const server = app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
module.exports = { app, server };
- Create a folder called test inside your project. You can do this by right-clicking in the Explorer sidebar and selecting New Folder.
- Inside the test folder, create a new file called api.test.js.
- Paste the following code into test/api.test.js:
const { describe, it, after } = require('node:test');
const assert = require('node:assert');
const { app, server } = require('../index.js');
describe('API Endpoints', () => {
// Close the server after all tests finish
after(() => {
server.close();
});
it('GET / should return project info', async () => {
const response = await fetch(`http://localhost:${server.address().port}/`);
const data = await response.json();
assert.strictEqual(response.status, 200);
assert.strictEqual(data.project, 'node-docker-cicd');
assert.strictEqual(data.version, '1.0.0');
});
it('GET /health should return healthy status', async () => {
const response = await fetch(`http://localhost:${server.address().port}/health`);
const data = await response.json();
assert.strictEqual(response.status, 200);
assert.strictEqual(data.status, 'healthy');
assert.ok(data.uptime >= 0);
assert.ok(data.timestamp);
});
});
What does this test file do?
This file uses Node.js's built-in test runner (node:test) to verify your API. It imports server from index.js, which starts the server automatically. Then it makes real HTTP requests and checks that the responses match what you expect.
The after() hook calls server.close() to shut down the server once all tests finish. This prevents the process from hanging open.
- Save test/api.test.js.
✔️ Awesome, I've got everything!
Great. Make sure you have saved test/api.test.js before continuing.
ⓧ I'd like to double check the full code
Here is the complete test/api.test.js for comparison:
const { describe, it, after } = require('node:test');
const assert = require('node:assert');
const { app, server } = require('../index.js');
describe('API Endpoints', () => {
after(() => {
server.close();
});
it('GET / should return project info', async () => {
const response = await fetch(`http://localhost:${server.address().port}/`);
const data = await response.json();
assert.strictEqual(response.status, 200);
assert.strictEqual(data.project, 'node-docker-cicd');
assert.strictEqual(data.version, '1.0.0');
});
it('GET /health should return healthy status', async () => {
const response = await fetch(`http://localhost:${server.address().port}/health`);
const data = await response.json();
assert.strictEqual(response.status, 200);
assert.strictEqual(data.status, 'healthy');
assert.ok(data.uptime >= 0);
assert.ok(data.timestamp);
});
});
Run and test locally
Your code is written. Now you need to confirm the server actually starts and that your tests pass.
- Start your server by running this command in the VS Code terminal:
npm start
You should see Server running on port 3000 printed in the terminal.
- Open your web browser and navigate to http://localhost:3000.
You should see a JSON response with your project name, version, a welcome message, and the list of endpoints.
- Navigate to http://localhost:3000/health in your browser.
You should see a JSON response with status set to healthy, along with the uptime and a timestamp.
Browser shows an error or refuses to connect?
Make sure the terminal still shows Server running on port 3000 without any error messages above it. If you see a syntax error, double check your index.js matches the code above exactly.
Help me figure out why my server won't start.
- Switch back to your VS Code terminal.
- Stop the running server by pressing Ctrl+C.
- Run your automated tests by running:
npm test
You should see output showing a checkmark next to each test name, followed by a summary confirming all tests passed.
Why stop the server before testing?
Your test file imports index.js, which automatically starts the server on port 3000. If the server is already running from npm start, the test will fail because the port is already in use.
Tests failing or hanging?
If you see a port-in-use error, make sure you pressed Ctrl+C to stop the server before running npm test. If you see an import error, confirm that test/api.test.js uses require('../index.js') with the correct relative path.
Help me debug my failing tests.
Your API is built and tested. Next up, you will package it into a Docker container so it can run anywhere.
Containerize with Docker
Your API works perfectly on your machine. But what happens when you want to run it somewhere else, like a teammate's laptop or a cloud server?
Docker solves this by packaging your app and all its dependencies into a portable container. In this step, you'll write a Dockerfile, build an image, and run your API inside a container.
In this step, get ready to:
- Write a Dockerfile and .dockerignore to define your container.
- Build a Docker image and run your API inside a container.
- Verify the containerized API returns the same JSON as the local version.
Create the Dockerfile
A Dockerfile is a recipe that tells Docker how to build your image. Each line is an instruction that adds a layer to the final image.
- In VS Code, create a new file called Dockerfile (no extension) in the root of your node-docker-cicd folder.
- Add the following content to your Dockerfile:
# Use a lightweight Node.js base image
FROM node:24-alpine
# Set the working directory inside the container
WORKDIR /app
# Copy dependency files first for layer caching
COPY package.json package-lock.json* ./
# Install production dependencies only
RUN npm ci --omit=dev
# Copy the rest of the source code
COPY . .
# Document the port the app listens on
EXPOSE 3000
# Start the server
CMD ["node", "index.js"]
What does this Dockerfile do?
- FROM node:24-alpine starts from a minimal Node.js image (Alpine Linux is only ~5 MB).
- WORKDIR /app creates and sets the working directory for all subsequent instructions.
- COPY package.json first, then RUN npm ci means Docker can cache the install step. If your code changes but dependencies don't, Docker skips the install on the next build.
- --omit=dev skips test frameworks and dev tools, keeping the image lean.
- CMD ["node", "index.js"] tells Docker what command to run when the container starts.
Why Alpine instead of the default Node image?
The default node:24 image is based on Debian and weighs over 1 GB. The node:24-alpine image is under 200 MB because Alpine Linux is a stripped-down distribution.
Express has no native module dependencies, so the smaller Alpine image works perfectly for this project.
- Save Dockerfile.
VS Code adding an extension?
Make sure the file is named exactly Dockerfile with a capital D and no file extension. If VS Code saved it as Dockerfile.txt, rename it to remove the extension.
Help me fix my Dockerfile name in VS Code
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
Next, you need a .dockerignore file. This works like .gitignore but for Docker builds. It prevents unnecessary files from being copied into the image.
- Create a new file called .dockerignore in the root of your node-docker-cicd folder.
- Add the following content:
node_modules
npm-debug.log
.git
.gitignore
.github
test
Dockerfile
.dockerignore
Why exclude these files?
- node_modules is excluded because the Dockerfile installs dependencies fresh inside the container.
- test is excluded because production doesn't need test files.
- .git and .github are excluded to keep the image lean. Git history and workflow files aren't needed at runtime.
- Save .dockerignore.
- Confirm both files exist in your Explorer sidebar. You should see Dockerfile and .dockerignore at the top level of your project.
Don't see .dockerignore in the sidebar?
Files starting with a dot are sometimes hidden. In VS Code's Explorer, click the three-dot menu and confirm Show Hidden Files is enabled.
Help me find my .dockerignore file in VS Code
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
node_modules
npm-debug.log
.git
.gitignore
.github
test
Dockerfile
.dockerignore
Build and run the container
With your Dockerfile ready, you can now build a Docker image. The image is a snapshot of your app and everything it needs to run.
- Make sure Docker Desktop is running. You should see the whale icon in your system tray.
- Open a terminal in VS Code by pressing Ctrl+` (backtick key).
- Build your Docker image by running this command:
docker build -t node-docker-cicd .
What does this command do?
- docker build reads your Dockerfile and assembles an image layer by layer.
- -t node-docker-cicd gives the image a human-readable name (a "tag").
- . tells Docker to use the current folder as the build context (the files it can access).
- Wait for the build to finish. You should see output showing each step being executed, ending with a line like naming to docker.io/library/node-docker-cicd.
Seeing a "Cannot connect to the Docker daemon" error?
This means Docker Desktop is not running. Open Docker Desktop from the Start menu and wait for the whale icon to appear in your system tray. Then try the build command again.
Help me get Docker Desktop running so I can build my image
Now that the image is built, you can run a container from it. A container is a running instance of your image.
- Start a container from your image by running this command:
docker run -d -p 3000:3000 --name node-api node-docker-cicd
What do these flags mean?
- -d runs the container in detached mode (in the background), so you keep your terminal.
- -p 3000:3000 maps port 3000 on your machine to port 3000 inside the container.
- --name node-api gives the container a friendly name so you can refer to it easily.
- You should see a long string of characters (the container ID) printed in your terminal. This confirms the container started successfully.
Seeing a port conflict error?
If port 3000 is already in use, you might still have the server running from Step 2. Stop it by pressing Ctrl+C in the terminal where it's running. Then try the docker run command again.
Help me free up port 3000 so my container can start
Verify the container works independently
Your API is now running inside a Docker container, completely isolated from your local Node.js installation. Let's prove it returns the same response.
- Open your browser and navigate to http://localhost:3000.
You should see the exact same JSON response you saw in Step 2:
{
"project": "node-docker-cicd",
"version": "1.0.0",
"message": "Welcome to my containerized API!",
"endpoints": ["/health"]
}
Why does this matter?
The response is identical whether you run the API with Node.js directly or inside a Docker container. This proves your container is portable. Anyone with Docker can run your image and get the exact same result, regardless of what's installed on their machine.
Now let's clean up the running container and verify the image persists.
- Stop and remove the container by running these commands:
docker stop node-api
docker rm node-api
- Confirm your image still exists by running:
docker images
- You should see node-docker-cicd listed in the output with a latest tag.
What is the difference between a container and an image?
An image is a read-only template (like a blueprint). A container is a running instance created from that image. You can stop and delete containers without losing the image. Spin up a new container from the same image any time you want.
Don't see your image in the list?
Make sure the docker build command completed successfully earlier. Scroll up in your terminal and look for any error messages during the build. If there was an error, fix it and run the build command again.
Help me figure out why my Docker image didn't build correctly
Your API is now containerized and portable. Next up, you'll push this project to GitHub and set up an automated pipeline that tests and builds your Docker image every time you push code.
Create the GitHub Actions CI/CD Pipeline
Your API is containerized and running locally. The next challenge is automation.
Right now, every time you change code, you have to manually re-run tests and rebuild the Docker image. In a real software team, this manual process slows everyone down and introduces human error. GitHub Actions solves this by running your tests and builds automatically every time you push code.
In this step, get ready to:
- Initialize a Git repository and push your project to GitHub.
- Write a GitHub Actions workflow file that automates testing and building.
- Watch the pipeline run and verify all steps pass.
Create the GitHub repository
Before your code can trigger an automated pipeline, it needs to live on GitHub. You will create a .gitignore file, initialize a local Git repository, and connect it to a new public repository on GitHub.
- In VS Code, create a new file called .gitignore in the root of your node-docker-cicd folder.
- Add the following content to .gitignore:
node_modules/
npm-debug.log
.env
What does this file do?
The .gitignore file tells Git which files and folders to exclude from version control.
You never want to commit node_modules/ because it contains thousands of downloaded dependency files that anyone can regenerate with npm ci. The .env exclusion protects any future environment variables with secrets from accidentally being pushed.
- Save .gitignore.
- Initialize a Git repository in your project folder by running this command in the VS Code terminal:
git init
- Now create a new public repository on GitHub. Open your browser and go to github.com.
- In the upper-right corner, click the + icon, then select New repository.
- In the Repository name field, type node-docker-cicd.
- Select Public for the repository visibility.
- Leave all other options unchecked (no README, no .gitignore template, no license).
- Click Create repository.
Why leave everything unchecked?
You already have a local project with its own .gitignore. If GitHub creates files like a README on the remote, your first push will fail with a merge conflict. Starting with an empty remote avoids this.
After creating the repository, GitHub shows a Quick Setup page with your repository URL.
- Copy the HTTPS URL from the Quick Setup page (it looks like https://github.com/your-username/node-docker-cicd.git).
- Connect your local repository to GitHub by running this command:
git remote add origin [[YOUR_REPO_URL="https://github.com/your-username/node-docker-cicd.git"]]
What does this command do?
This tells Git that your remote server (called origin) lives at the GitHub URL you just created. From now on, when you push code, Git knows where to send it.
Getting a 'remote origin already exists' error?
If you accidentally ran this command twice, remove the existing remote and re-add it:
git remote remove origin
git remote add origin [[YOUR_REPO_URL="https://github.com/your-username/node-docker-cicd.git"]]
Help me fix a 'remote origin already exists' error in my Git setup.
Write the workflow file
A CI/CD pipeline is defined by a YAML file inside a special directory in your repository. GitHub Actions automatically detects any workflow file in .github/workflows/ and runs it when the configured trigger fires.
- Create the workflow directory by running this command in your terminal:
mkdir -p .github/workflows
- In VS Code, create a new file at .github/workflows/ci.yml.
- Add the following workflow configuration to ci.yml:
name: CI Pipeline
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
test-and-build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build Docker image
run: docker build -t node-docker-cicd:${{ github.sha }} .
What does this workflow do?
- on: push / pull_request triggers the pipeline whenever you push to main or open a pull request targeting main.
- runs-on: ubuntu-latest spins up a fresh Linux virtual machine in GitHub's cloud to run your pipeline.
- actions/checkout@v6 clones your repository code into the virtual machine.
- actions/setup-node@v4 installs Node.js 24 on the runner so your project can build.
- npm ci installs dependencies from the lockfile for a reproducible build.
- npm test runs your test suite. If any test fails, the pipeline stops here.
- docker build builds your Docker image, tagging it with the unique commit SHA so every build is traceable.
- Save .github/workflows/ci.yml.
YAML indentation errors?
YAML is whitespace-sensitive. Every indentation level must use exactly 2 spaces (not tabs). If you see parse errors later, check that each line aligns with the examples above.
Help me fix YAML indentation in my ci.yml file.
✔️ Awesome, I've got everything!
Great. Double check you have saved both .gitignore and .github/workflows/ci.yml.
ⓧ I'd like to double check the full code
Here is the complete .gitignore file:
node_modules/
npm-debug.log
.env
Here is the complete .github/workflows/ci.yml file:
name: CI Pipeline
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
test-and-build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build Docker image
run: docker build -t node-docker-cicd:${{ github.sha }} .
Push and watch the pipeline run
Your workflow file is ready. Now you will commit all your project files and push them to GitHub. The moment the code lands on the main branch, GitHub Actions picks up the workflow and starts running it.
- Stage, commit, and push your project by running these commands:
git add .
git commit -m "Initial commit: Express API with Docker and CI/CD"
git branch -M main
git push -u origin main
What do these commands do?
- git add . stages all files in your project (except those in .gitignore).
- git commit saves a snapshot of your staged files with a descriptive message.
- git branch -M main renames your branch to main so it matches the branch your workflow triggers on.
- git push -u origin main uploads your code to GitHub and sets origin main as the default upstream for future pushes.
Seeing a browser window asking you to sign in?
This is normal. Git Credential Manager opens a browser window the first time you push to GitHub. Sign in with your GitHub account and authorize the OAuth app when prompted. After this, your credentials are saved and future pushes will not require sign-in.
- After the push completes, open your browser and go to your repository on GitHub.
- Click the Actions tab at the top of your repository page.
- You should see a workflow run named after your commit message. Click on it to see the details.
- Click the test-and-build job to expand it and watch each step execute in real time.
All five steps (Checkout code, Setup Node.js, Install dependencies, Run tests, Build Docker image) should complete with green checkmarks.
Seeing a red X on your workflow run?
Click the failed step to see the error log. Common causes:
- YAML indentation error in ci.yml. Compare your file character-for-character with the double-check tab above.
- Test failures. If the Run tests step fails, the same tests that pass locally should pass in CI. Check for hardcoded ports or missing files.
- Dockerfile syntax issue. If Build Docker image fails, double-check your Dockerfile matches the version from the previous step.
Help me debug my failing GitHub Actions workflow.
Secret mission
Add Docker Scout Security Scanning
Your pipeline builds and tests automatically. But what about security? In real-world DevOps, every image gets scanned for known vulnerabilities before it ships. Add Docker Scout to your pipeline so critical CVEs get caught before deployment.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and uses free services, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Local node-docker-cicd project folder on your Desktop.
- Docker image (node-docker-cicd) built locally.
- GitHub repository (public).
- Docker Hub account (free, created for Secret Mission).
Keep everything running
No action needed. Choose this if you're still actively building or want to revisit the project later.
- Your project folder stays on your Desktop and is ready to use anytime.
- Your GitHub repository remains public and continues to run the CI/CD pipeline on every push.
- Docker Desktop uses system memory while running. You can quit it from the system tray when not in use.
- Node.js, Docker Desktop, and Git remain installed on your machine for future projects.
Pause - I'll come back to this later
Shut down running processes to free up memory while keeping all your files intact.
- Stop any running Docker containers by opening PowerShell and running:
- Right-click the Docker Desktop whale icon in your system tray and select Quit Docker Desktop.
- Your project folder, GitHub repository, and Docker Hub account all stay as they are.
docker stop $(docker ps -q)
Quick restart
When you come back, open Docker Desktop and wait for it to finish starting. Then open PowerShell, navigate to your project folder, and pick up where you left off.
Delete - I don't want to use this again
Remove all project resources and start fresh. This is permanent.
- Remove your local Docker image by running this in PowerShell:
docker rmi node-docker-cicd
- Remove any stopped containers and unused images with:
docker system prune -a
- Delete the local project folder by running:
Remove-Item -Recurse -Force ~/Desktop/node-docker-cicd
- Delete your GitHub repository:
- Go to your repository page on GitHub.
- Click Settings in the top menu.
- Scroll down to the Danger Zone section.
- Click Delete this repository and follow the confirmation prompts.
- Delete your Docker Hub account (optional, only if you created it solely for this project):
- Sign in to Docker Home.
- Navigate to Account settings.
- Select Deactivate and confirm the action.
What about Node.js, Docker Desktop, and Git?
These are general-purpose developer tools you will likely use in future projects. If you want to remove them, uninstall each through Settings > Apps > Installed apps on Windows.
Nice Work!
Nice Work!
You just built a fully automated DevOps pipeline from scratch. Your Express.js API lives inside a Docker container, and every time you push code to GitHub, your CI/CD pipeline automatically runs tests and builds a fresh image.
You've learned how to:
- Build and containerize a Node.js API using Docker with production best practices like Alpine base images, layer caching, and production-only dependencies.
- Create a CI/CD pipeline with GitHub Actions that automatically runs tests and builds a Docker image on every push to main.
- Publish a public GitHub repository that demonstrates real-world DevOps workflow from code to container.
- Secret Mission: Added automated container security scanning with Docker Scout to catch vulnerabilities before deployment.
Ready to quiz yourself?