Docker for Local Development

Docker lets you run applications in isolated containers on your local machine. Each container is a self-contained environment with its own runtime, dependencies, and configuration — completely isolated from your host system and from other containers. The core problem it solves: different projects often need different versions of PHP, Node.js, Python, or databases. Without Docker, you're juggling global installations and hoping nothing conflicts. With Docker, each project declares its own environment and it always works, regardless of what else is installed.

A container maps a port inside the container to a port on your localhost. A web app running on port 80 inside a container might be mapped to localhost:8080 on your machine. You hit the local port and the traffic routes into the container.

Common Container URLs: localhost:8080

Core Concepts

Before commands make sense, three concepts matter:

  • Image — a snapshot of an environment (e.g., "nginx:alpine" or "node:20"). Images are pulled from Docker Hub and used as templates.
  • Container — a running instance of an image. You can run ten containers from the same image simultaneously.
  • Volume — a persistent storage mount. Without a volume, data inside a container disappears when the container stops. Volumes keep data (like a database) alive across restarts.

Essential Commands

# Pull and run an nginx container, mapping port 8080 locally to port 80 in container
docker run -d -p 8080:80 --name my-nginx nginx

# Run a Node.js app, mounting current directory into container
docker run -d -p 3000:3000 -v $(pwd):/app node:20

# List running containers
docker ps

# Stop a container
docker stop my-nginx

# Remove a container
docker rm my-nginx

# View container logs (live)
docker logs -f my-nginx

# Run a command inside running container
docker exec -it my-nginx bash

# Pull an image without running it
docker pull postgres:16

Docker Compose — Multi-Container Apps

Most real applications need more than one service: a web server, a database, maybe a cache. Docker Compose defines all of them in a single compose.yml file and starts them together with docker compose up.

# compose.yml — PHP app with MySQL and phpMyAdmin
services:
  app:
    image: php:8.3-apache
    ports:
      - "8080:80"
    volumes:
      - ./src:/var/www/html

  db:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: myapp
    volumes:
      - db_data:/var/lib/mysql

  phpmyadmin:
    image: phpmyadmin
    ports:
      - "8081:80"
    environment:
      PMA_HOST: db

volumes:
  db_data:
# Start all services
docker compose up -d

# Stop all services
docker compose down

# Stop and delete volumes (wipes database)
docker compose down -v

# View logs for all services
docker compose logs -f

Common Development Stacks

StackServicesDefault Ports
LEMP (Linux, Nginx, MySQL, PHP)nginx, php-fpm, mysql8080 (web), 3306 (db)
Node + MongoDBnode, mongo3000 (app), 27017 (db)
Django + PostgreSQLpython, postgres8000 (app), 5432 (db)
Laravel + Redis + MySQLphp, redis, mysql8080, 6379, 3306
WordPresswordpress, mysql8080 (wp), 3306 (db)

WordPress in Docker (2 minutes)

services:
  wordpress:
    image: wordpress
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_PASSWORD: secret
    volumes:
      - wp_data:/var/www/html

  db:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: wordpress
    volumes:
      - db_data:/var/lib/mysql

volumes:
  wp_data:
  db_data:

Save as compose.yml, run docker compose up -d, then open localhost:8080.

Volume Mounts for Live Editing

The key to development workflows is mounting your local code into the container so edits appear immediately without rebuilding the image:

# Mount ./src into the container's web root
docker run -d \
  -p 8080:80 \
  -v $(pwd)/src:/var/www/html \
  php:8.3-apache

Now edit files in ./src on your machine — the container sees the changes instantly.

Persistent Databases

Always use a named volume for database containers. Without it, every docker stop wipes your data:

# Named volume — data survives container restarts and removals
docker run -d \
  -p 5432:5432 \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

Troubleshooting

ProblemFix
Port already in useAnother service is on that port — change the host port: -p 8081:80 instead of 8080:80
Container exits immediatelyRun docker logs [container-name] to see the error
Changes not showing upConfirm the volume mount path is correct — a typo silently fails
Database connection refusedIn Compose, use the service name (db, not localhost) as the database host
Out of disk spacedocker system prune -a removes unused images, containers, and volumes

Related Tools

Docker pairs well with Portainer for a visual container management UI. The MySQL port 3306, PostgreSQL port 5432, and Redis port 6379 pages cover connecting to containerized databases from your host machine or another tool.