Docker basics: how it works and the commands I actually use

This is the short Docker guide I share with engineers joining a project. It covers why containers are fast, the commands you'll use every day, how containers talk to each other, and a few habits that save trouble later.

1. Containers vs virtual machines

The key difference is the kernel.

Docker vs virtual machine architecture

  • A virtual machine runs a full guest operating system, with its own kernel, on top of a hypervisor.
  • A container shares the host's kernel and only isolates its own processes, files, and network.
  • With no extra kernel to boot for each workload, containers start in seconds and use less memory.

One note: on macOS and Windows, Docker Desktop runs your containers inside a small Linux VM, because containers need a Linux kernel.

2. Everyday commands

Images

Build an image:

docker build -t <name>:<tag> .

Remove dangling images (untagged leftovers from old builds) to free disk space:

docker image prune

See which layers make an image big:

docker history <image>

Containers

Run an app in the background and map a port:

docker run -d -p 8080:3000 --name my-app <image>

-d runs it in the background, and -p 8080:3000 sends traffic from port 8080 on your machine to port 3000 in the container.

Follow the logs, which is usually the first thing to check when something breaks:

docker logs -f <container>

Open a shell inside a running container:

docker exec -it <container> sh

Cleanup

Remove stopped containers, unused networks, and images that no container is using:

docker system prune -a

Be careful with --volumes. Adding it also deletes any volume not attached to a running container, and that can include your database data if the database happens to be stopped. Only use it when you're sure nothing in those volumes matters.

3. Networking

In a stack like NestJS, Kafka, and PostgreSQL, the containers need to reach each other.

Create a network:

docker network create my-system-net

Start each container with --network my-system-net. On a network you create yourself, Docker gives you built-in DNS, so containers find each other by name. For example, NestJS connects to Postgres at postgres:5432 instead of an IP address. This doesn't work on the default bridge network, which is one reason to always create your own.

4. Docker Compose

Instead of running each container by hand, describe the whole stack in one docker-compose.yml and start it with docker compose up -d.

services:
  api-gateway:
    build: .
    ports:
      - "3000:3000"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - backend-net

  db:
    image: postgres:16-alpine
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10
    networks:
      - backend-net

  redis:
    image: redis:7-alpine
    networks:
      - backend-net

networks:
  backend-net:

volumes:
  db-data:

A few notes on this file. The top-level version field is no longer needed, and current Compose ignores it. On its own, depends_on only waits for a container to start, not for it to be ready, so the health check is what makes the API wait until Postgres can accept connections.

5. Checklist

Keep data in volumes. Always store database data in a volume, so it survives when the container is removed or recreated.

Keep secrets out of images.

  • Never put secrets in a Dockerfile or bake them into an image. Anyone who can pull the image can read them.
  • For local work, pass them in at runtime from a .env file that's never committed. In production, use a secrets manager or Docker secrets.

Keep images small.

  • Use slim base images. Alpine is the smallest, but it uses musl instead of glibc, which can break native modules, so test it before switching. -slim Debian images are a safer default.
  • Fewer packages in the image means less to patch and less to attack.

Make builds fast.

  • Order your Dockerfile so the slow, rarely changing steps come first. Copy package.json and the lockfile, run npm ci, and only then copy the rest of the code. That way, editing code doesn't trigger a full reinstall.
  • Add a .dockerignore that excludes node_modules, .git, and build output. A smaller build context means faster builds.