New to Docker? This guide breaks down images, containers, and Dockerfiles with hands-on examples, plus tips for using Docker in production and CI/CD pipelines.
Introduction
If you’ve ever spent hours debugging an issue that only appears on your colleague’s laptop or on a production server, you know how frustrating environment inconsistencies can be. Docker solves this by packaging applications and their dependencies into lightweight, portable containers.
In this article, we’ll explore Docker’s core concepts, build a real container step by step, and look at best practices for using Docker in real-world projects.
What Is Docker?
Docker is an open-source platform that automates the deployment of applications inside software containers. Unlike traditional virtual machines, containers share the host system’s OS kernel, making them faster to start and more resource efficient.
Images – Read-only templates with instructions for creating a container (e.g., ubuntu:22.04, node:20).
Containers – The running instances of an image – isolated, but lightweight.
Dockerfile – A script that defines how to build an image.
Registry – A place to store and share images (Docker Hub is the default).
Why Use Docker?
Consistency – Works the same on a developer’s laptop, a test server, and the cloud.
Isolation – No dependency conflicts between different projects.
Reproducibility – Entire environments described in version‑controlled Dockerfiles.
Efficiency – Lower overhead than VMs, faster startup times.
Your First Dockerized Application
Let’s containerize a simple Node.js app. Create a project folder with two files.
1. app.js
const express = require('express'); const app = express(); const PORT = 3000; app.get('/', (req, res) => { res.send('Hello from Docker! 🐳'); }); app.listen(PORT, () => { console.log(`App running on port ${PORT}`); });<svg class="_9bc997d _33882ae" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"></svg><svg class="_9bc997d _28d7e84" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"></svg>
2. Dockerfile
# Base image with Node.js FROM node:18-alpine # Set working directory WORKDIR /usr/src/app # Copy package files (if any) and install deps COPY package*.json ./ RUN npm install express # Copy app source COPY . . # Expose the app port EXPOSE 3000 # Start the app CMD ["node", "app.js"]<svg class="_9bc997d _33882ae" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"></svg><svg class="_9bc997d _28d7e84" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"></svg>
3. Build and Run
docker build -t my-node-app . docker run -p 3000:3000 my-node-app<svg class="_9bc997d _33882ae" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"></svg><svg class="_9bc997d _28d7e84" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"></svg>
Open http://localhost:3000 – you should see the welcome message.
Useful Docker Commands
| Command | Description |
|---|---|
docker ps | List running containers |
docker ps -a | List all containers (including stopped) |
docker images | Show local images |
docker stop | Stop a running container |
docker rm | Remove a stopped container |
docker rmi | |
docker exec -it | Open a shell inside a running container |
Docker in Real Projects
Using Docker Compose for multi‑service apps
A docker-compose.yml example for a Node.js app with Redis:
version: '3.8' services: app: build: . ports: - "3000:3000" depends_on: - redis redis: image: redis:alpine ports: - "6379:6379"<svg class="_9bc997d _33882ae" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"></svg><svg class="_9bc997d _28d7e84" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"></svg>
Start everything with docker compose up.
Best practices
Use small base images (Alpine variants).
Combine RUN commands to reduce layers.
Don’t run containers as root – create a non‑privileged user.
Use .dockerignore to exclude unnecessary files.
Tag images meaningfully (e.g., myapp:1.2.3).
Docker and CI/CD
Docker fits naturally into CI/CD pipelines. A typical flow:
Run tests inside a container.
Build a production image.
Push the image to a registry (Docker Hub, GitHub Container Registry, Amazon ECR).
Deploy by pulling the image on your server (or Kubernetes cluster).
Example GitHub Actions snippet:
- name: Build Docker image run: docker build -t myapp:${{ github.sha }} . - name: Push to registry run: | docker tag myapp:${{ github.sha }} myrepo/myapp:latest docker push myrepo/myapp:latest<svg class="_9bc997d _33882ae" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"></svg><svg class="_9bc997d _28d7e84" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"></svg>
Common Pitfalls and How to Avoid Them
| Problem | Solution |
|---|---|
| Huge image size | Use multi‑stage builds or Alpine images |
| Secrets hard‑coded in images | Use environment variables or Docker secrets |
| Containers losing data | Use volumes (docker run -v /host/path:/container/path) |
| Slow builds | Order Dockerfile layers from least to most frequently changing |
12345.png)