Docker for Beginners: Containerize Your First Web App

Posted November 21, 2025 by Karol Polakowski

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.