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.
- 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
.envfile 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.
-slimDebian 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.jsonand the lockfile, runnpm ci, and only then copy the rest of the code. That way, editing code doesn't trigger a full reinstall. - Add a
.dockerignorethat excludesnode_modules,.git, and build output. A smaller build context means faster builds.