Docker for Developers: A Practical Guide from Scratch
Everything you need to know about Docker as a developer: from basic concepts to Docker Compose and best practices.
What Is Docker and Why Should You Use It?
Docker is a containerization platform that allows you to package an application with all its dependencies (runtime, libraries, environment variables, configuration files) into a standard unit called a container. Unlike virtual machines, containers share the host operating system's kernel, making them extremely lightweight and fast to start.
The problem Docker solves is universal: "it works on my machine." When you package your application in a Docker container, you guarantee it will run exactly the same in any environment: your laptop, the staging server, your teammate's machine, or the cloud. The container includes everything needed to run, eliminating the environment inconsistencies that cause so many bugs and wasted time.
For a developer, Docker has become an essential daily tool, comparable to Git or your code editor. You don't need to be a DevOps expert to benefit from Docker; just understanding the fundamental concepts and knowing how to write a decent Dockerfile is enough.
Fundamental Concepts
Images vs Containers
An image is a read-only template that contains the file system, dependencies, and instructions needed to run an application. Think of it as a "snapshot" or a "mold" of your application at a given point in time. Images are immutable: once built, they don't change.
A container is an executable instance of an image. It's like a lightweight virtual machine that runs your application in isolation. You can create multiple containers from the same image, each with its own state and lifecycle. Containers are ephemeral: they're easily created, run, and destroyed.
The classic analogy is: the image is the class (in object-oriented programming) and the container is the instance of that class. Or the image is the recipe and the container is the prepared dish.
Registries and Docker Hub
A registry is a Docker image store. Docker Hub is the default public registry, where you can find official images for virtually any software: Node.js, Python, PostgreSQL, Redis, Nginx, etc. Private registries like GitHub Container Registry, AWS ECR, and Google Artifact Registry also exist for your organization's own images.
Volumes and Bind Mounts
Containers are ephemeral: when they're destroyed, all data inside is lost. Volumes are Docker's solution for data persistence: they're directories managed by Docker that survive the container lifecycle. Bind mounts map a host directory directly into the container, ideal for development where you want code changes to be reflected instantly.
Your First Dockerfile
A Dockerfile is a text file with instructions that Docker uses to build an image. Each instruction creates a layer in the image, and Docker caches these layers to speed up subsequent builds.
Let's look at a Dockerfile for a Node.js application:
FROM node:20-alpine - Uses the official Node.js 20 image based on Alpine Linux, a minimalist distribution that produces smaller images with a smaller attack surface.
WORKDIR /app - Sets the working directory inside the container. All subsequent instructions will be executed from this directory.
COPY package*.json ./ - Copies the dependency files first. This takes advantage of Docker's cache: if dependencies haven't changed, this layer is reused and you don't need to reinstall them.
RUN npm ci --only=production - Installs production dependencies. npm ci is faster and more deterministic than npm install, ideal for CI/CD.
COPY . . - Copies the rest of the source code. This layer is invalidated with every code change, but since dependencies are already installed in a previous layer, rebuilding is fast.
EXPOSE 3000 - Documents the port the application uses. Note: this is just documentation; actual port mapping is done when running the container.
CMD ["node", "server.js"] - The command that runs when the container starts. Uses the exec format (JSON array) so the process receives system signals correctly.
Docker Compose: Orchestrating Multiple Services
Most modern applications aren't a single service. Your typical stack includes an API, a database, a Redis cache, perhaps a queue service, and a web server. Docker Compose allows you to define and manage all these services in a single YAML file.
A docker-compose.yml file for a Node.js + PostgreSQL + Redis stack would include three services, each with its image, ports, environment variables, and volumes. With a single docker compose up command, all services start, connect to each other via an internal Docker network, and are ready for development.
Docker Compose is especially valuable for onboarding new developers. Instead of following a 20-step setup guide to install PostgreSQL, Redis, configure environment variables, and seed data, the new developer only needs Docker installed and to run docker compose up. In minutes they have the entire stack running locally.
The key Docker Compose concepts are: services (the containers that make up your application), networks (virtual networks connecting services), volumes (persistent storage), and depends_on (startup order between services).
Best Practices for Dockerfiles
Use Small Base Images
Alpine images (based on Alpine Linux, ~5MB) are significantly smaller than full Debian/Ubuntu images. Smaller images mean faster downloads, less disk space, and a smaller attack surface. For Node.js, Python, and Go, Alpine variants are the recommended default choice.
For Go, you can go even further with scratch (empty) images or Google's distroless images, which contain only the compiled binary without an operating system. The result is 10-20MB images containing only your application.
Leverage Layer Caching
Docker caches each layer of the Dockerfile. When rebuilding, only the layers that have changed (and subsequent ones) are rebuilt. Order instructions from least to most frequently changing: system dependencies first, then application dependencies (package.json), and finally the source code.
The most important pattern is copying package.json and installing dependencies before copying the source code. This way, code changes don't invalidate the dependency layer, reducing build times from minutes to seconds.
Use Multi-Stage Builds
Multi-stage builds allow you to use an image with all build tools in one stage and copy only the necessary artifacts to a minimal final image. For a React application, the first stage compiles with Node.js and the second stage serves the static files with Nginx, resulting in a final image of ~25MB instead of ~500MB.
For Go, multi-stage builds are essential: you compile in an image with the Go SDK and copy the static binary to a scratch image. The result is an image containing only your executable, without a compiler, without an operating system, without anything superfluous.
Don't Run as Root
By default, processes inside a container run as root. This is a security risk: if an attacker compromises your application, they have root access inside the container and potentially the host. Create a non-privileged user in your Dockerfile and use it to run the application with the RUN adduser and USER instructions.
Use .dockerignore
The .dockerignore file works like .gitignore: it specifies which files should not be copied into the image. Exclude node_modules, .git, .env, logs, test files, and anything not needed in production. This reduces image size and prevents leaking sensitive information.
Common Use Cases for Developers
Local Development Environment
Instead of installing PostgreSQL, Redis, Elasticsearch, and other services directly on your machine, run them as Docker containers. This keeps your operating system clean, allows running multiple versions simultaneously, and makes it easy to completely reset state by removing and recreating containers.
Testing with Dependencies
Integration tests that require databases, external services, or message queues are perfect for Docker. In your CI pipeline, spin up the necessary services as containers, run tests against them, and destroy everything when done. This guarantees a clean, reproducible environment for every test run.
Local Microservices
In microservices architectures, developing a service that depends on five other services can be a nightmare. Docker Compose allows you to spin up the entire microservices ecosystem locally, making it easy to develop and debug service interactions.
Production Deployment
Although Kubernetes is used for large-scale production, Docker is the foundation of the entire container ecosystem. For small and medium projects, Docker Swarm or simply docker compose on a server can be sufficient. The image you tested locally is exactly the same one deployed to production.
Essential Commands Every Developer Should Know
docker build -t my-app . - Builds an image from the Dockerfile in the current directory and tags it as "my-app".
docker run -p 3000:3000 -d my-app - Runs a container in the background mapping port 3000 from the host to port 3000 in the container.
docker compose up -d - Starts all services defined in docker-compose.yml in the background.
docker logs -f container - Shows a container's logs in real time (follow mode).
docker exec -it container sh - Opens an interactive shell inside a running container, useful for debugging.
docker system prune -a - Cleans up unused images, containers, and volumes. Use it periodically to free disk space.
Conclusion
Docker isn't just for DevOps teams. As a developer, Docker lets you create consistent development environments, run tests with complex dependencies, and ensure your application works the same in any environment. The learning curve is short: in a couple of hours you can have your first functional Dockerfile, and in a day you can master Docker Compose for your entire stack.
Start by containerizing your development dependencies (databases, Redis, etc.), then containerize your own application, and finally set up Docker Compose to orchestrate the entire stack. This incremental approach lets you adopt Docker gradually without feeling overwhelmed.