Containerize and Deploy a Web App
Package a Node.js app into a Docker container and run it locally.
Introduction
30 Second Summary
Every developer has heard the phrase "it works on my machine." The reason code breaks when it moves between computers is that each machine has a different setup. Containers solve this by packaging your app alongside everything it needs to run, so it behaves the same everywhere.
In this project, you will build a web application and package it into a Docker container that runs on your local machine. You will write a Dockerfile, build a custom image, and manage a running container from start to finish.
What You'll Build
A simple Node.js web application that displays a "Hello from Docker!" page with dynamic information, running entirely inside a Docker container you build from scratch.
By the end of this project, you'll have:
- A working Node.js Express web server that displays container information like hostname and current time.
- A custom Docker image you built from a Dockerfile you wrote yourself.
- Hands-on experience with the core Docker workflow: write, build, run, and manage containers.
- Secret Mission: Add a second container running Nginx as a reverse proxy using Docker Compose.
Are there any prerequisites?
You only need basic familiarity with the command line (navigating folders, running commands). No prior Docker or Node.js experience is needed.
Install Docker Desktop
Docker packages your application and everything it needs to run into a single portable unit called a container. Containers run the same way on every machine, eliminating the "it works on my machine" problem.
In this step, you'll install Docker Desktop (the tool that runs containers on your Mac or PC) and set up the project folder for your web app.
In this step, get ready to:
- Install Docker Desktop and confirm it's running.
- Verify Docker can pull and run container images.
- Set up your code editor with a new project folder.
Download and install Docker Desktop
Docker Desktop is the application that runs Docker on your local machine. It provides the Docker engine, CLI tools, and a graphical dashboard for managing containers.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Check if Docker is already installed by running this command:
docker version
This command shows version information for both the Docker Client and Server (daemon). Look at your terminal output and pick the tab that matches what you see.
✔️ I see Client and Server version info
Docker Desktop is already installed and running. You're all set to move on to the next substep.
ⓧ I see an error about the Docker daemon
Docker is installed, but the daemon isn't running yet. This happens when Docker Desktop hasn't been started.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Docker and press Enter to open Docker Desktop.
- Wait for the whale icon in the menu bar to stop animating. This means the Docker engine is ready.
- Run docker version again in your terminal. You should now see both Client and Server sections.
ⓧ Command not found
Docker isn't installed on your machine yet. Let's set it up.
- Visit the Docker Desktop download page and download Docker Desktop for your operating system.
- Open the downloaded .dmg file (macOS) and drag Docker to your Applications folder.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Docker and press Enter to launch Docker Desktop.
- Accept the license agreement when prompted.
- Wait for Docker Desktop to finish starting. The whale icon in the menu bar will stop animating when it's ready.
- Run docker version again in your terminal. You should now see both Client and Server sections with version numbers.
What are the system requirements?
Docker Desktop requires macOS Sonoma (version 14) or later and at least 4 GB of RAM.
Docker Desktop is free for personal use
Docker Desktop is free for personal use, small businesses, education, and non-commercial open source projects. You won't need to pay anything for this project.
Verify Docker can pull images
Now that Docker is running, let's confirm it can download and run a container image from Docker Hub.
- Test Docker's full workflow by running this command:
docker run hello-world
What does this command do?
This command pulls the official hello-world image from Docker Hub (Docker's public image registry), creates a container from it, and runs it.
If everything is working, the container prints a confirmation message and exits. This proves that Docker can pull images, create containers, and run them.
You should see a message starting with Hello from Docker! followed by an explanation of what just happened.
Not seeing the hello-world output?
- Make sure Docker Desktop is fully started. The whale icon in the menu bar should be stable (not animating).
- Check your internet connection. Docker needs to download the image from Docker Hub the first time.
- If you see a permission error, try closing and reopening your terminal.
Still stuck? Help me troubleshoot my docker run hello-world command
Set up your code editor
With Docker working, you need a code editor and a project folder for your web app's files.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder by running this command:
mkdir docker-hello
- Confirm the folder was created by running:
ls
You should see docker-hello listed in the output.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Cursor and press Enter to open it. If you prefer VS Code, open that instead.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the docker-hello folder, and click Open.
Don't see the docker-hello folder?
- Make sure you ran cd ~/Desktop before mkdir docker-hello. Without this, the folder may have been created in a different location.
- Check your Desktop visually. You should see a docker-hello folder there.
Need help? Help me find my docker-hello folder
Your environment is ready. Next up, you'll build the web application that Docker will run inside a container.
Build the Web Application
Your Docker environment is ready to go. Now it is time to build the application that you will eventually containerize.
In this step, you will create a simple web server using Node.js and Express. The app serves an HTML page showing dynamic information like the container hostname and current time.
In this step, get ready to:
- Initialize a Node.js project with npm.
- Create an Express web server that serves an HTML page.
- Test the app locally to confirm it works before containerizing.
Initialize the Node.js project
Every Node.js project starts with a package.json file that tracks your project's name, version, and dependencies. You will use npm (the Node package manager) to generate this file automatically.
- In your terminal, make sure you are inside the docker-hello folder by running:
cd ~/Desktop/docker-hello
- Generate a default package.json file by running:
npm init -y
Seeing command not found?
If your terminal says npm: command not found, you need to install Node.js first. Download and install it from https://nodejs.org (choose the LTS version), then restart your terminal and try again.
help me install Node.js so I can use npm
- Install the Express framework by running:
npm install express@4.22.2
You should see output confirming that packages were added. A node_modules folder and a package-lock.json file now appear in your project.
What did these commands do?
npm init -y created a package.json with default values (project name based on your folder, version 1.0.0, and so on).
npm install express@4.22.2 downloaded the Express library into node_modules and recorded it as a dependency in package.json.
The auto-generated package.json contains extra fields you do not need. You will now replace its contents with a cleaner version that only includes what the project requires.
- Open package.json in Cursor by clicking on it in the file explorer sidebar.
- Select all the content and replace it with the following:
{
"name": "docker-hello",
"version": "1.0.0",
"description": "A simple web app to learn Docker",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "4.22.2"
}
}
What does this file do?
name and version identify your project. main tells Node.js which file to run.
The scripts.start field defines what happens when you run npm start. The dependencies section lists the exact version of Express your project needs. When Docker builds your image later, it uses this file to install the right packages.
- Save package.json by pressing Cmd+S (macOS) or Ctrl+S (Windows).
File not saving correctly?
Make sure you replaced ALL the contents of the file, not just part of it. The file should start with { and end with } with no leftover fields from the auto-generated version.
help me fix my package.json
Create the web server
Your project needs an application to serve. You will create a simple Express server that responds with an HTML page showing dynamic system information.
- Create a new file called server.js in the docker-hello folder. In Cursor, right-click in the file explorer sidebar and select New File.
- Name the file server.js and press Enter.
- Paste the following code into server.js:
const express = require("express");
const os = require("os");
const app = express();
const PORT = 3000;
app.get("/", (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Hello from Docker!</title>
<style>
body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
h1 { color: #0db7ed; }
.info { background: #f0f0f0; padding: 15px; border-radius: 8px; margin: 20px 0; }
.info p { margin: 8px 0; }
code { background: #e0e0e0; padding: 2px 6px; border-radius: 4px; }
</style>
</head>
<body>
<h1>Hello from Docker!</h1>
<p>This page is being served from inside a Docker container.</p>
<div class="info">
<p><strong>Container Hostname:</strong> <code>${os.hostname()}</code></p>
<p><strong>Current Time:</strong> <code>${new Date().toISOString()}</code></p>
<p><strong>Node.js Version:</strong> <code>${process.version}</code></p>
<p><strong>Platform:</strong> <code>${process.platform} (${process.arch})</code></p>
</div>
</body>
</html>
`);
});
app.listen(PORT, "0.0.0.0", () => {
console.log(`Server running at http://localhost:${PORT}`);
});
What does this code do?
The server creates an Express app that listens on port 3000. When someone visits the root URL (/), it responds with an HTML page.
The page displays four pieces of dynamic information: the machine's hostname (os.hostname()), the current time, the Node.js version, and the platform. When this runs inside a Docker container, the hostname will show the container ID instead of your computer's name.
The server binds to 0.0.0.0 so it accepts connections from outside the container. This is essential for Docker's port mapping to work.
- Save server.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Seeing red squiggly lines in the editor?
If Cursor shows syntax errors, make sure you copied the entire code block from the very first line (const express) through the closing }); at the end. Template literals (the backtick characters) are easy to miss.
help me fix syntax errors in my server.js
✔️ Awesome, I've got everything!
Great. Make sure both package.json and server.js are saved before moving on.
ⓧ I'd like to double check the full code
Here is the final package.json:
{
"name": "docker-hello",
"version": "1.0.0",
"description": "A simple web app to learn Docker",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "4.22.2"
}
}
Here is the final server.js:
const express = require("express");
const os = require("os");
const app = express();
const PORT = 3000;
app.get("/", (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Hello from Docker!</title>
<style>
body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
h1 { color: #0db7ed; }
.info { background: #f0f0f0; padding: 15px; border-radius: 8px; margin: 20px 0; }
.info p { margin: 8px 0; }
code { background: #e0e0e0; padding: 2px 6px; border-radius: 4px; }
</style>
</head>
<body>
<h1>Hello from Docker!</h1>
<p>This page is being served from inside a Docker container.</p>
<div class="info">
<p><strong>Container Hostname:</strong> <code>${os.hostname()}</code></p>
<p><strong>Current Time:</strong> <code>${new Date().toISOString()}</code></p>
<p><strong>Node.js Version:</strong> <code>${process.version}</code></p>
<p><strong>Platform:</strong> <code>${process.platform} (${process.arch})</code></p>
</div>
</body>
</html>
`);
});
app.listen(PORT, "0.0.0.0", () => {
console.log(`Server running at http://localhost:${PORT}`);
});
Test the app locally (optional)
Before containerizing, you can quickly confirm the app works on your machine. This is an optional step that helps you distinguish between app bugs and Docker configuration issues later.
- Start the server by running:
node server.js
You should see Server running at http://localhost:3000 printed in the terminal.
- Open your browser and visit http://localhost:3000.
You should see a page with a blue "Hello from Docker!" heading and a grey info box showing your computer's hostname, the current time, your Node.js version, and your platform.
- Return to your terminal and press Ctrl+C to stop the server.
Page not loading in the browser?
Make sure the terminal still shows the "Server running" message. If the process crashed, check server.js for typos. Also confirm you are visiting http://localhost:3000 (not https).
If you see an error about port 3000 already being in use, another application is occupying that port. Close other dev servers or applications that might be using it, then try again.
help me get my Express server running on localhost:3000
Your web application is complete. Next up, you will write a Dockerfile that packages this app into a portable container image.
Write the Dockerfile
Your web app works locally, but it only runs on your machine with your specific setup. A Dockerfile solves this by giving Docker a step-by-step recipe for packaging your app into a portable image.
In this step, you will write the Dockerfile that tells Docker how to build your app into an image. You will also create a .dockerignore file to keep unnecessary files out of the image.
In this step, get ready to:
- Create a Dockerfile that packages your app into a container image.
- Create a .dockerignore file to keep the image lean and fast.
Create the Dockerfile
A Dockerfile is a text file containing instructions that Docker reads top-to-bottom to build an image. Each instruction creates a layer in the final image.
- In Cursor, create a new file called Dockerfile (no file extension) inside your docker-hello folder.
- Paste the following code into Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
What does this Dockerfile do?
- FROM node:22-alpine starts from a lightweight Linux image that already has Node.js installed. Alpine is a minimal Linux distribution, which keeps your image small.
- WORKDIR /app sets the working directory inside the container. All following commands run from this folder.
- COPY package.json package-lock.json ./ copies only the dependency files first. This is a caching optimization.
- RUN npm install --production installs your dependencies inside the image. Because this layer only changes when your dependencies change, Docker can cache it and skip re-installing on future builds.
- COPY . . copies the rest of your source code into the image. This layer changes every time you edit your code, but the cached dependency layer above stays intact.
- EXPOSE 3000 documents that the container listens on port 3000.
- CMD ["node", "server.js"] sets the default command that runs when the container starts.
Why copy package.json separately from the rest?
Docker builds images in layers. If a layer has not changed since the last build, Docker reuses it from cache instead of rebuilding it. By copying package.json first and running npm install before copying the source code, Docker only re-installs dependencies when package.json actually changes.
If you copied everything at once, any change to server.js would force Docker to re-run npm install from scratch. The separate-copy pattern saves significant time on repeated builds.
- Save Dockerfile.
- Confirm the file exists by running:
ls Dockerfile
You should see Dockerfile printed in the output.
Don't see the file?
- Make sure the file is named exactly Dockerfile with a capital D and no file extension. It is not Dockerfile.txt or dockerfile.
- Confirm your terminal is in the docker-hello folder. Run pwd to check your current directory.
Why isn't Docker finding my Dockerfile?
Create a .dockerignore file
When Docker builds an image, it sends all the files in your project folder to the Docker engine as the "build context." A .dockerignore file tells Docker which files to skip.
- Create a new file called .dockerignore inside your docker-hello folder.
- Paste the following into .dockerignore:
node_modules
npm-debug.log
.git
.gitignore
What does this file do?
- node_modules is excluded because Docker installs fresh dependencies inside the image using npm install. Sending your local node_modules would bloat the build context and could conflict with the container's Linux-based packages.
- npm-debug.log, .git, and .gitignore are development files that the running app does not need.
- Save .dockerignore.
- Confirm both new files exist by running:
ls -a
You should see both Dockerfile and .dockerignore listed alongside your existing package.json and server.js.
Don't see .dockerignore?
Files starting with a dot are hidden by default. The -a flag in ls -a shows hidden files. If you ran ls without -a, the file exists but is not displayed.
Help me find my .dockerignore file.
✔️ Awesome, I've got everything!
Great. Double check that you have saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the complete Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
And here is the complete .dockerignore:
node_modules
npm-debug.log
.git
.gitignore
Your Dockerfile is ready. Next up, you will build this into an actual Docker image and run your first container.
Build and Run Your Container
In the previous step, you wrote a Dockerfile that describes how to package your app into an image.
Now it is time to put that Dockerfile to work. You will use Docker to build an image, launch it as a running container, and see your app served from inside that container.
In this step, get ready to:
- Build a Docker image from your Dockerfile.
- Run the image as a container and verify the app in your browser.
- Inspect and manage the container lifecycle.
Build the Docker image
A Docker image is a snapshot of your app plus everything it needs to run. The docker build command reads your Dockerfile and creates this snapshot layer by layer.
- Build your Docker image by running this command in your terminal (make sure you are inside the docker-hello folder):
docker build -t docker-hello .
What does this command do?
- docker build tells Docker to create a new image from a Dockerfile.
- -t docker-hello tags (names) the image docker-hello so you can reference it later.
- . sets the build context to the current directory. Docker sends these files to the build engine.
- Watch the terminal output as Docker executes each instruction in your Dockerfile.
- You should see each layer being processed and the build completing successfully.
Seeing an error during the build?
- If you see a message about the Docker daemon not running, open Docker Desktop and wait for the whale icon in the menu bar to stop animating before retrying.
- If you see a file-not-found error for package-lock.json, make sure you ran npm install express@4.22.2 in the previous step. That command generates the lock file.
- Make sure your terminal is inside the docker-hello folder where your Dockerfile lives.
Help me debug my Docker build error.
Run the container
Your image is built. Now you will launch it as a running container and access the app in your browser.
- Start a container from your image by running:
docker run -d -p 127.0.0.1:3000:3000 --name my-app docker-hello
What do these flags mean?
- -d runs the container in detached mode (in the background), so your terminal stays free.
- -p 127.0.0.1:3000:3000 maps port 3000 inside the container to port 3000 on your machine. The 127.0.0.1 part binds it to localhost only for security.
- --name my-app gives the container a friendly name so you can reference it instead of using a random ID.
- docker-hello at the end tells Docker which image to use.
- Open your browser and visit http://localhost:3000.
- You should see a page titled "Hello from Docker!" with dynamic information including the container hostname, current time, Node.js version, and platform.
Don't see the page in your browser?
- If port 3000 is already in use (from testing the app locally earlier), stop the local Node.js process first with Ctrl+C in the terminal where it was running.
- Run docker ps to confirm the container is actually running. If it is not listed, try removing the old container with docker rm my-app and running the docker run command again.
- Make sure Docker Desktop is still running (check the whale icon in your menu bar).
My container started but I cannot see the page in my browser.
Inspect and manage the container
Your app is running inside Docker. Now you will learn the core commands to check on containers, view their logs, and shut them down cleanly.
- View your running containers by running:
docker ps
- You should see a table listing your my-app container with its ID, image name, status, and port mapping.
- View the container's log output by running:
docker logs my-app
- You should see the message Server running at http://localhost:3000 printed by your Express server.
What are docker ps and docker logs?
The docker ps command lists all running containers on your machine. Think of it like a task manager for Docker.
The docker logs command lets you see everything a container has printed to its console. This is essential for debugging when something goes wrong inside a container.
- Stop the running container by running:
docker stop my-app
- Remove the stopped container by running:
docker rm my-app
- Confirm the container is gone by running:
docker ps -a
- You should no longer see my-app in the list.
Why stop and remove separately?
Stopping a container shuts down the process but keeps the container's file system on disk. Removing it deletes that file system entirely.
This two-step process lets you inspect a stopped container's logs or files before deciding to delete it permanently.
Seeing 'No such container'?
- If docker stop my-app says the container does not exist, it may have already stopped on its own. Run docker ps -a to see all containers including stopped ones.
- If you already removed it, that is fine. The goal is to end this step with no my-app container listed in docker ps -a.
Help me fix my docker stop or docker rm error.
You have now experienced the full Docker workflow from start to finish. Next up, you will take on a Secret Mission to level up your container skills even further.
Secret mission
Add Nginx Reverse Proxy with Docker Compose
Your app runs in a single container. But production web apps sit behind a reverse proxy for routing and security. Can you run two containers together to create a real production-like setup?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally using Docker Desktop, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- docker-hello project folder on your Desktop.
- docker-hello Docker image (built locally).
- hello-world Docker image (pulled during setup).
- Docker Desktop application.
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend the project.
- Your docker-hello folder stays on your Desktop with all project files.
- Your Docker images remain cached locally for fast container starts.
- Docker Desktop continues running in the background (uses some memory and CPU).
Pause - I'll come back to this later
Shut down Docker Desktop to free up memory. Your project files and images stay intact for next time.
- Click the Docker whale icon in your menu bar.
- Select Quit Docker Desktop.
- Your docker-hello folder and all Docker images remain on disk.
- When you are ready to continue, relaunch Docker Desktop and your images will still be available.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow these steps to clean everything up.
- Remove the Docker images by running these commands in Terminal:
docker rmi docker-hello
docker rmi hello-world
- Delete the project folder from your Desktop by running:
cd ~/Desktop
rm -rf docker-hello
- Confirm the folder is gone by running:
ls ~/Desktop | grep docker-hello
You should see no output, confirming the folder has been removed.
- To uninstall Docker Desktop, open Docker Desktop.
- Click the Troubleshoot icon (bug icon) in the top-right corner of the Dashboard.
- Select Uninstall.
- Confirm by selecting Uninstall again when prompted.
- Drag the Docker application from your Applications folder to the Trash.
What about residual Docker files?
After uninstalling Docker Desktop, you can remove leftover configuration files by running:
rm -rf ~/Library/Group\ Containers/group.com.docker
rm -rf ~/.docker
This clears Docker's cached settings and credentials from your system.
Nice Work!
Nice Work!
Nice work! You just containerized a web application with Docker. You went from an empty folder to a fully packaged app running inside a container on your machine.
You've learned how to:
- Built a Node.js Express web server that displays dynamic container information.
- Wrote a Dockerfile to package your app into a portable container image with optimized layer caching.
- Used Docker commands to build, run, inspect, and manage containers with port mapping.
- Added a multi-container setup with Docker Compose and an Nginx reverse proxy serving your app on port 80.
Ready to quiz yourself?