Docker lets you package your app and its runtime into a portable image that runs the same anywhere. This article shows how to containerize a minimal Node.js web app, run it locally, and introduce docker-compose and practical best practices so you can iterate confidently.
What is Docker?
Docker is a platform for building, shipping, and running containers. A container packages your application code together with its runtime, system tools, libraries, and settings so it runs reliably across environments.
Why containerize your web app?
- Consistency: “Works on my machine” problems disappear.
- Isolation: Dependencies and processes are isolated per container.
- Portability: Same image runs locally, in CI, or in production.
- Faster CI/CD: Images are cached and easily deployed.
Prerequisites
- Docker Desktop or Docker Engine installed
- Basic knowledge of Node.js and the command line
Create a simple Node.js web app
Create two files: index.js and package.json. This tiny Express server demonstrates a web app to containerize.
// index.js
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => res.send('Hello from Dockerized Node.js app!'));
app.listen(port, () => console.log(`Listening on ${port}`));
{
"name": "docker-demo",
"version": "1.0.0",
"main": "index.js",
"scripts": { "start": "node index.js" },
"dependencies": { "express": "^4.18.2" }
}
Write a Dockerfile
This Dockerfile uses the lightweight Node Alpine image, installs dependencies, copies source, and runs the app.
# Use official lightweight Node image
FROM node:18-alpine
# Create app directory
WORKDIR /usr/src/app
# Install dependencies
COPY package.json package-lock.json* ./
RUN npm ci --only=production
# Bundle app source
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
Tip: add a .dockerignore to exclude node_modules, logs, and local files to keep image builds fast.
Build and run the image
Build the Docker image and run it, mapping port 3000 to the host.
# Build
docker build -t docker-demo:latest .
# Run
docker run --rm -p 3000:3000 --name docker-demo docker-demo:latest
# Verify
# open http://localhost:3000
If you change code, rebuild with docker build or use the next section for a developer-friendly workflow.
Using docker-compose for local development
docker-compose simplifies running multi-service apps and maps volumes for fast iteration.
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules
environment:
- NODE_ENV=development
Run with docker-compose up –build and your local edits will reflect immediately (because of the bind mount). Keep node_modules in the container to avoid host/OS mismatch issues.
Best practices
- Use .dockerignore to skip unnecessary files (node_modules, .git, .env, logs).
- Pin base image versions (e.g., node:18-alpine) to avoid surprises.
- Use multistage builds for compiled languages or when you need smaller production images.
- Avoid running as root inside containers; create a non-root user where appropriate.
- Store secrets outside images (environment variables, secret managers).
- Add HEALTHCHECK for production images.
Debugging and iteration
- docker logs
for logs - docker exec -it
sh (or bash) to inspect the container - docker-compose up –build –force-recreate to ensure fresh builds
- Use layered Dockerfile ordering: copy package.json first, install deps, then copy source to leverage build cache
Next steps
- Push images to a registry (Docker Hub, GitHub Container Registry)
- Add Docker build and push to CI (GitHub Actions, GitLab CI)
- Explore orchestrators: Kubernetes, Docker Swarm
- Add automated tests and vulnerability scanning to your pipeline
Conclusion
Containerizing a simple Node.js app with Docker takes a few clear steps: create a lightweight Dockerfile, build an image, and run it locally. docker-compose helps with development and multi-service orchestration. Follow best practices (pin images, use .dockerignore, manage secrets) to make containerization safe and repeatable. You’ll gain portability, reliability, and a smoother path to CI/CD and production deployments.
