"It works on my machine" is one of the oldest problems in software. Docker solves it by packaging your application with everything it needs (runtime, libraries, configuration) into a container that runs the same way on a laptop, a test server or the cloud.
Image vs container
- An image is a read-only template: your app plus its environment.
- A container is a running instance of an image. You can run many containers from one image.
Containers vs virtual machines
| Container | Virtual machine | |
|---|---|---|
| Includes | App + libraries; shares the host kernel | Full guest operating system |
| Start time | Seconds | Minutes |
| Size | Megabytes | Gigabytes |
| Isolation | Process level | Hardware level (stronger) |
Install and check
Install Docker Desktop (Windows/macOS) or Docker Engine (Linux), then:
docker --version
docker run hello-worldStep 1: A small app
app.py:
from flask import Flask
app = Flask(__name__)
@app.get("/")
def home():
return {"status": "ok", "message": "Hello from a container"}requirements.txt:
flask==3.0.3
gunicorn==23.0.0Step 2: Write a Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]Copying requirements.txt before the rest of the code lets Docker cache the dependency layer. Changing your code then does not reinstall every package on each build.
Step 3: Build and run
docker build -t course-api:1.0 .
docker run -d -p 8080:8000 --name course-api course-api:1.0
curl http://localhost:8080-p 8080:8000 maps port 8080 on your machine to port 8000 inside the container.
Everyday commands
| Command | Purpose |
|---|---|
docker ps | Running containers (-a for all) |
docker logs -f course-api | Follow container logs |
docker exec -it course-api sh | Open a shell inside the container |
docker stop course-api | Stop it |
docker rm course-api | Remove the container |
docker images | List images |
docker system prune | Clean unused data (careful) |
Volumes: keep data after a container is removed
Containers are disposable. Anything written inside is lost when the container is removed. Use a volume for databases and uploads:
docker volume create pgdata
docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data postgres:16Docker Compose: app + database together
compose.yaml:
services:
api:
build: .
ports:
- "8080:8000"
environment:
DATABASE_URL: postgres://postgres:secret@db:5432/postgres
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:docker compose up -d --build
docker compose logs -f api
docker compose downServices reach each other by service name: the API connects to the host db.
Best practices
- Use small official base images (
-slimor-alpine) and pin versions. - Add a
.dockerignorefile (.git,node_modules,.env) to keep images small and secret-free. - Never bake passwords into images; pass them as environment variables or secrets at runtime.
- Run as a non-root user in production images.
- One main process per container.
Interview questions
- `CMD` vs `ENTRYPOINT`?
ENTRYPOINTsets the fixed executable;CMDprovides default arguments that can be overridden atdocker run. - `COPY` vs `ADD`? Both copy files;
ADDcan also extract archives and fetch URLs. PreferCOPYfor clarity. - What is a multi-stage build? Using one stage to compile or build and a smaller final stage that contains only the runtime output.
Next steps
Containerize one of your own projects, then deploy it with a CI/CD pipeline. Learn Kubernetes, Terraform and AIOps in the DevOps + AI course.
