Building a Development Environment with Docker

beginner | 90 min read | 2025.12.02

What You’ll Learn in This Tutorial

  • How to write Dockerfiles and the meaning of each instruction
  • Building images and starting containers
  • Managing multiple containers with docker-compose
  • Building a Node.js + PostgreSQL development environment

Prerequisites: Docker Desktop must be installed. If docker --version displays a version, you’re good to go.

What is Docker? Why Was It Created?

History of Container Technology

The roots of container technology date back to chroot in UNIX V7 in 1979. It has evolved as follows:

YearTechnologyOverview
1979chrootFilesystem isolation
2000FreeBSD JailProcess isolation
2006cgroups (Google)Resource limits
2008LXC (Linux Containers)Lightweight containers for Linux
2013DockerContainer standardization and adoption

The Birth of Docker

In 2013, Solomon Hykes (dotCloud) announced Docker.

“Docker made it possible to easily package applications and run them identically anywhere.” — Docker Official

Why Docker Was Revolutionary

  1. Share “working environments” as-is: Solved the “it works on my machine” problem
  2. Lightweight: Shares OS unlike VMs, starts in seconds
  3. Image reproducibility: Complete environment reproduction via Dockerfile
  4. Ecosystem: Image sharing via Docker Hub

Difference Between VMs and Containers

flowchart TB
    subgraph VM["Virtual Machine (VM)"]
        direction TB
        VA["App A"] --- VB["App B"]
        VB --- VGA["Guest OS"]
        VGA --- VGB["Guest OS"]
        VGB --- VH["Hypervisor"]
        VH --- VHO["Host OS"]
        VHO --- VHW["Hardware"]
    end

    subgraph Container["Container"]
        direction TB
        CA["App A"] --- CB["App B"]
        CB --- CR["Container Runtime<br/>(Docker)"]
        CR --- CHO["Host OS"]
        CHO --- CHW["Hardware"]
    end
PropertyVMContainer
Startup timeMinutesSeconds
Memory usageGB scaleMB scale
Isolation levelCompleteProcess level
OS independenceCompleteShared kernel

Official Documentation: Docker overview

Docker Basic Concepts

Images and Containers

  • Image: The “blueprint” of an application. Read-only
  • Container: A “running instance” created from an image
flowchart LR
    subgraph Static["Image (static)"]
        Dockerfile["Dockerfile"]
        Image["node:20"]
        Dockerfile -->|build| Image
    end

    subgraph Dynamic["Container (dynamic)"]
        Running["running"]
        Stdout["stdout"]
        Running -->|logs| Stdout
    end

    Image -->|run| Running

Image Layer Structure

Docker images are composed of layers, and only changed parts are rebuilt:

FROM node:20-alpine    # Base layer (~100MB)
WORKDIR /app           # Config layer (~0KB)
COPY package*.json ./  # Dependencies layer (~1KB)
RUN npm install        # node_modules layer (~50MB)
COPY . .               # App code layer (~10KB)

Best Practice: Place less frequently changed items at the top and more frequently changed items at the bottom for efficient cache utilization

Step 1: Creating the Project Structure

First, create the project directory structure.

mkdir docker-tutorial
cd docker-tutorial
mkdir src
touch Dockerfile docker-compose.yml src/index.js package.json

Step 2: Creating the Node.js Application

Create a simple Express server.

package.json

{
  "name": "docker-tutorial",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

src/index.js

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
    res.json({
        message: 'Hello from Docker!',
        timestamp: new Date().toISOString()
    });
});

app.get('/health', (req, res) => {
    res.json({ status: 'healthy' });
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

Step 3: Creating the Dockerfile

A Dockerfile is a blueprint for building images.

Dockerfile

# Specify base image
FROM node:20-alpine

# Set working directory
WORKDIR /app

# Copy dependency files (cache optimization)
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy application code
COPY . .

# Expose port (documentation purpose)
EXPOSE 3000

# Startup command
CMD ["npm", "start"]

Detailed Dockerfile Instruction Guide

InstructionDescriptionExample
FROMSpecify base imageFROM node:20-alpine
WORKDIRSet working directoryWORKDIR /app
COPYCopy filesCOPY . .
RUNExecute command at build timeRUN npm install
CMDDefault command at container startCMD ["npm", "start"]
ENTRYPOINTContainer entry pointENTRYPOINT ["node"]
ENVSet environment variableENV NODE_ENV=production
EXPOSEExpose port (documentation)EXPOSE 3000
ARGBuild-time argumentARG VERSION=1.0

Difference Between CMD and ENTRYPOINT

# CMD: Overridable default command
CMD ["npm", "start"]
# docker run myapp npm run dev  ← Gets overridden

# ENTRYPOINT: Always executed command
ENTRYPOINT ["node"]
CMD ["index.js"]
# docker run myapp app.js  ← Executes as node app.js

Official Documentation: Dockerfile reference

Step 4: Building and Running the Image

# Build image
docker build -t my-node-app .

# Check built image
docker images

# Start container
docker run -p 3000:3000 my-node-app

# Start in background
docker run -d -p 3000:3000 --name my-app my-node-app

# Access http://localhost:3000 in browser

Common docker run Options

docker run \
  -d                      # Detached mode (background)
  -p 3000:3000            # Port mapping (host:container)
  --name my-app           # Container name
  -e NODE_ENV=production  # Environment variable
  -v $(pwd):/app          # Volume mount
  --rm                    # Auto-remove on exit
  my-node-app             # Image name

Step 5: Building Environment with docker-compose

Manage multiple services (app + database) at once.

docker-compose.yml

version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgres://user:password@db:5432/mydb
    volumes:
      - ./src:/app/src  # For hot reload
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:

docker-compose.yml Structure

version: '3.8'          # Compose specification version

services:               # Service (container) definitions
  service_name:
    image: xxx          # or build: ./path
    ports:              # Port mapping
    environment:        # Environment variables
    volumes:            # Volumes
    depends_on:         # Dependencies
    restart:            # Restart policy

volumes:                # Named volumes
networks:               # Custom networks

docker-compose Commands

# Start all services
docker-compose up

# Start in background
docker-compose up -d

# Rebuild and start
docker-compose up --build

# Check logs
docker-compose logs -f app

# Start specific service only
docker-compose up app

# Stop services
docker-compose down

# Remove including volumes
docker-compose down -v

# Check service status
docker-compose ps

Hot Reload: Mounting source code with volumes reflects file changes immediately in the container.

Official Documentation: Docker Compose overview

Dockerfile Best Practices

1. Multi-Stage Build

Create lightweight images for production:

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]

2. Using .dockerignore

Exclude unnecessary files from build context:

.dockerignore

node_modules
npm-debug.log
.git
.gitignore
.env
*.md
.DS_Store
coverage
.nyc_output

3. Running as Non-Root User

Avoid root user for security:

FROM node:20-alpine

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001

WORKDIR /app
COPY --chown=nodejs:nodejs . .

# Switch to non-root user
USER nodejs

CMD ["node", "index.js"]

4. Adding Health Checks

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

5. The 12 Factor App Principles

The Twelve-Factor App are design principles for cloud-native applications:

  • Config: Manage configuration via environment variables
  • Dependencies: Declare explicitly (package.json)
  • Port: Expose services via port binding
  • Processes: Execute as stateless processes
  • Logs: Output as streams to stdout

Frequently Used Docker Commands

Container Operations

# Check running containers
docker ps

# Check all containers (including stopped)
docker ps -a

# Execute command in container
docker exec -it container_name sh

# Check container logs
docker logs container_name
docker logs -f container_name  # Follow

# Stop container
docker stop container_name

# Remove container
docker rm container_name

# Remove all stopped containers
docker container prune

Image Operations

# List images
docker images

# Remove image
docker rmi image_name

# Remove unused images
docker image prune

# Check image history
docker history image_name

Cleanup

# Remove unused resources collectively
docker system prune

# Remove including volumes (caution!)
docker system prune -a --volumes

# Check disk usage
docker system df

Troubleshooting

Container Won’t Start

# Check logs
docker logs container_name

# Start in interactive mode for debugging
docker run -it my-node-app sh

Port Already in Use

# Check port in use
lsof -i :3000

# Map to different port
docker run -p 3001:3000 my-node-app

Slow Build

  1. Check .dockerignore
  2. Optimize layer order
  3. Use multi-stage builds

Large Image Size

# Check image size
docker images

# Use lightweight base image
FROM node:20-alpine  # ~100MB (vs ~900MB for regular)

# Remove unnecessary files
RUN npm ci --only=production && npm cache clean --force

Next Steps

Once you’ve mastered Docker basics, learn about orchestration:

Official Documentation

Best Practices

Tools & Resources

Cheat Sheets

← Back to list