๐ข Welcome to Docker!
Let's find out what Docker is, the massive problem it solves, and why every software developer uses it.
Imagine you build a beautiful Lego castle on your bedroom floor. You want to show your friend, so you try to move it to their house. But along the way, pieces fall off, their table is bumpy, and their dog steals a piece. The castle is ruined!
In the software world, this happens all the time. A developer writes code on their laptop, and it works perfectly. But when they send it to a server or another teammate, it crashes. Why? Because the environments are different โ different software versions, different settings, different operating systems.
This is such a notorious problem in software development that it has its own meme. When a developer shrugs and says "well, it works on my machine", their teammates want to scream. Docker was built to eliminate this excuse forever.
Let's be specific. Here's a real scenario that breaks applications:
- Developer A has Python 3.10 installed. She writes code using features only available in 3.10.
- Developer B has Python 3.7 installed. When he runs the same code โ crash. Syntax error.
- The Production Server has Python 3.8 and a different set of libraries. The app behaves in a third, completely different way.
This happens with Node.js versions, database drivers, operating system libraries, environment variables, and a hundred other invisible settings. It's a nightmare โ and it was the normal state of software development before Docker.
Before the 1950s, putting cargo on a ship was chaos. Workers threw sacks of flour, barrels of oil, and boxes of shoes directly into the ship's hold. It was messy, slow, and things were constantly lost or damaged. Then, the standard shipping container was invented. Suddenly, everything went into identical steel boxes. Cranes, trucks, and ships didn't need to care what was inside the box; they just knew how to move the box. Docker does this for software!
Your app goes in a container. The container is identical whether it's running on your laptop in Bossier City or a cloud server in Tokyo. Same behavior, every time, guaranteed.
Docker is an open-source containerization platform. It takes your application and packages it with every single thing it needs to run โ the code itself, the correct version of Python (or Node, or Java), all the libraries, the configuration files, and the environment settings โ into one single, self-contained unit called a container.
This container can then be run on any machine that has Docker installed, and it will behave identically every single time. The host machine (the computer running Docker) could be your MacBook, a Windows PC, a Linux server, or a cloud VM. It does not matter. The container carries its own world inside it.
Docker was first released in 2013 by Solomon Hykes and quickly became one of the most transformative tools in modern software development. Today, nearly every major tech company โ Google, Netflix, Spotify, Amazon โ runs their services inside containers.
Reproducibility
It runs identically on a Mac, Windows, or Linux machine. "Works on my machine" becomes "works on every machine."
Efficiency
Starts up in milliseconds because it's super lightweight. You can run dozens of containers on a single laptop.
Portability
Move it to Amazon, Google Cloud, or your laptop instantly. One container, infinite destinations.
Scalability
Need more power? Spin up 100 more containers in seconds. Tools like Kubernetes automate this completely.
At a high level, working with Docker follows a simple three-step loop that you'll repeat throughout your career:
- Write a Dockerfile โ A plain-text recipe describing your application and everything it needs.
- Build an Image โ Docker reads the Dockerfile and bakes it into a portable, read-only snapshot called an Image.
- Run a Container โ Docker brings the Image to life as a running Container. You can run as many containers from one image as you want.
We'll go deep on each of these steps in the lessons ahead. For now, just know that everything starts with a Dockerfile and ends with a running container.
Before you can run any Docker commands, you need Docker Desktop installed on your machine. Docker Desktop is free for personal use and includes the Docker Engine, the Docker CLI, and a helpful GUI to manage your containers.
- Mac: Download Docker Desktop from
docker.com/products/docker-desktopand install it like any other app. - Windows: Same download page. Note: Windows requires WSL 2 (Windows Subsystem for Linux) to be enabled, which Docker Desktop will help you set up.
- Linux: Docker Engine can be installed via your package manager (e.g.,
sudo apt install docker.ioon Ubuntu).
Once installed, open your terminal and type:
# Check that Docker is installed and see the version docker --version # Output (your version may differ, that's fine!): # Docker version 24.0.5, build ced0996 # Run the classic "Hello World" to confirm everything works docker run hello-world # Docker will download the hello-world image (if not cached) # and run it, printing a success message to your terminal.
๐๏ธ Images and Containers
Two of the most important words in Docker! People mix them up all the time, but the difference is actually very simple.
Think about a Blueprint and a House. A blueprint is a piece of paper that tells you exactly how to build a house, but you can't live inside a piece of paper. The house is the real, physical building you create using the blueprint.
In Docker, the Image is the blueprint. The Container is the house!
A Docker Image is a read-only, layered snapshot of your entire application environment. "Read-only" means once you build an image, it never changes. It's frozen in time. It contains:
- Your application's source code or compiled binary
- The exact runtime it needs (e.g., Python 3.10, Node.js 18, Java 17)
- All third-party libraries and dependencies
- Configuration files and environment variables
- A starting command (what the container should do when it wakes up)
Images live on disk. They're also portable โ you can push them to a registry like Docker Hub and anyone in the world can pull and run them. This is exactly how open-source databases, web servers, and tools are shared.
This is one of Docker's most important and clever design choices. An image isn't one giant file โ it's a stack of layers. Each instruction in a Dockerfile (we'll cover these in Lesson 4) creates a new layer on top of the previous ones.
Think of it like a stack of transparent slides:
- Layer 1: A base Linux operating system
- Layer 2: Python 3.10 installed on top of that
- Layer 3: Your
requirements.txtlibraries installed - Layer 4: Your application code copied in
Why does this matter? Caching! If you rebuild your image after changing only your app code, Docker reuses Layers 1-3 from cache and only rebuilds Layer 4. Builds that would take minutes are done in seconds.
A Container is the live, running instance of an image. When you run a Docker Image, Docker reads the image layers and adds one more thin, writable layer on top. That's the container. All changes made while the container is running (files created, logs written, data saved) happen in that writable layer.
Containers are:
- Isolated โ they can't see each other's file systems or processes (by default)
- Ephemeral โ when you delete a container, its writable layer is destroyed (this is why Volumes exist โ Lesson 6!)
- Fast โ starting a container is nearly instantaneous because there's no OS to boot
- Cheap โ you can run dozens or hundreds of containers on one machine
# Run a container from an image (downloads image if not cached) docker run hello-world # Run a container in DETACHED mode (runs in background) # -d = detached, -p = port mapping (host:container) docker run -d -p 8080:80 nginx # See all currently RUNNING containers docker ps # See ALL containers, including stopped ones docker ps -a # Stop a running container (graceful shutdown) docker stop <container_id> # Force-remove a stopped container docker rm <container_id> # List all images stored on your machine docker images # Delete an image from your machine docker rmi <image_name>
You're not locked out of a container once it's running. You can open an interactive terminal session inside it using docker exec. This is incredibly useful for debugging โ you can poke around the container's file system as if you had SSH'd into a remote server.
# Open an interactive bash shell inside a running container # -i = interactive (keep stdin open) # -t = allocate a pseudo-TTY (gives you a real terminal) docker exec -it <container_id> bash # You are now INSIDE the container! Try listing files: ls /app # Or check what processes are running inside: ps aux # Type "exit" to leave the container shell exit # You can also inspect a container's logs at any time docker logs <container_id>
docker run -it ubuntu bash in your terminal. Docker will download the official Ubuntu image and drop you into a bash shell running inside it! You're literally inside a Linux container right now. Type cat /etc/os-release to see it confirm it's Ubuntu. Type exit to get out.
๐๏ธ Docker vs Virtual Machines
You might be wondering: "Don't Virtual Machines (VMs) already put things in isolated boxes?" Yes! But Docker does it in a much smarter, faster way.
A Virtual Machine is like building a massive House. You have to build the walls, install your own plumbing, run your own electricity lines, and hire your own security guard. It's safe, but it takes up a lot of space and takes forever to build.
A Docker Container is like renting an Apartment. The apartment building (your computer) already has plumbing, electricity, and security. You just bring your furniture (your app). It's incredibly fast to move in, and you can fit hundreds of apartments in the space of a few houses!
A Virtual Machine (VM) is created by a piece of software called a Hypervisor (like VMware, VirtualBox, or Hyper-V). The Hypervisor sits on top of your real hardware and tricks each VM into thinking it's a complete, physical computer.
Each VM contains:
- A full Guest Operating System (a complete copy of Linux, Windows, etc.)
- Virtual hardware (simulated CPU, RAM, disk, network card)
- The application itself
That Guest OS alone can be 5-20 GB. Booting it is like turning on a whole new computer โ it takes minutes. And because the OS is consuming RAM and CPU just to exist, you waste significant resources before your app even starts.
Docker containers take a radically different approach. Instead of virtualizing hardware, they use features built directly into the Linux kernel โ specifically namespaces and cgroups โ to create isolated environments.
- Namespaces give each container its own isolated view of the system โ its own process list, its own network interfaces, its own file system tree โ even though they're all running on the same host OS.
- cgroups (control groups) limit how much CPU, memory, and I/O each container can use, preventing one container from starving others.
The critical result: containers share the host's OS kernel. There's no Guest OS to boot up. The container starts in milliseconds because it's just a process (or a group of processes) with special isolation rules applied.
| Feature | Virtual Machine (VM) | Docker Container |
|---|---|---|
| Size on Disk | Gigabytes (Heavy) | Megabytes (Lightweight) |
| Startup Time | Minutes โณ | Milliseconds โก |
| OS Included? | Yes, a full Guest OS | No, shares host kernel |
| Isolation Level | Hardware-level (very strong) | Process-level (strong, slightly less) |
| Resource Usage | Heavy (OS overhead) | Minimal (only app + deps) |
| Portability | Harder (full OS image) | Easy (lightweight image) |
| Best For | Running different OSes, max isolation | Microservices, DevOps, CI/CD |
Not always! In large-scale production environments, you'll often see both used together. A cloud provider like AWS or Google Cloud creates a set of VMs (for strong isolation and OS-level guarantees), and then runs Docker containers inside those VMs. You get the security benefits of VMs combined with the speed and density of containers.
For most developers and DevOps engineers, though, Docker is the primary day-to-day tool. VMs are reserved for situations where you need to run a completely different OS, or when regulations require hardware-level isolation.
๐ The Dockerfile โ The Secret Recipe
We know that Images are blueprints. But how do we actually make an Image? We write a simple text file called a Dockerfile โ and Docker reads it like a recipe, line by line.
Think of a Dockerfile like a recipe to bake a cake. You write down the steps from top to bottom: "Start with a pre-made crust", "Add 2 cups of sugar", "Mix the batter for 3 minutes", "Bake at 350 degrees for 45 minutes". Docker reads this recipe line-by-line and bakes your perfect, unchanging Image. If anyone else follows the exact same recipe, they get the exact same cake โ no matter their kitchen.
A Dockerfile is just a plain text file โ no special extension needed beyond naming it literally Dockerfile. It contains a sequence of instructions, each one creating a new layer in the final image.
FROMโ The foundation. Specifies the base image to start from. Every Dockerfile MUST begin with this. Think of it as "start with a computer that already has X installed." e.g.,FROM python:3.10,FROM node:18,FROM ubuntu:22.04.WORKDIRโ Sets your working directory inside the container's file system. All following commands run from this folder. If it doesn't exist, Docker creates it. e.g.,WORKDIR /app.COPYโ Copies files from your local machine into the container image. Syntax isCOPY <source> <destination>. e.g.,COPY . .copies everything in your current folder into/appin the container.RUNโ Executes a command during the image build process. Used for installing packages, compiling code, creating directories. e.g.,RUN pip install -r requirements.txt.ENVโ Sets environment variables inside the container. e.g.,ENV NODE_ENV=production.EXPOSEโ Documents which port the container listens on. Note: this doesn't actually open the port โ it's documentation for whoever runs the container. e.g.,EXPOSE 3000.CMDโ The default startup command. Runs when a container is started from this image. UnlikeRUN, this doesn't execute during the build โ it's the instruction the container follows when it wakes up. e.g.,CMD ["python", "app.py"].
RUN executes during the image build (it's baking the cake). CMD executes when a container is started (it's serving the cake). You can have many RUN instructions. You should have only one CMD. If you have multiple CMD instructions, only the last one takes effect.
# 1. Start from the official Python 3.10 base image # (This gives us a Linux OS with Python already installed) FROM python:3.10-slim # 2. Set an environment variable to prevent Python # from writing .pyc files (just good practice) ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 # 3. Set the working directory inside the container WORKDIR /app # 4. Copy ONLY the requirements file first. # This is a performance trick! Docker caches this layer. # If requirements.txt hasn't changed, pip install won't re-run. COPY requirements.txt . # 5. Install all Python libraries listed in requirements.txt RUN pip install --no-cache-dir -r requirements.txt # 6. Now copy the rest of the application code COPY . . # 7. Tell Docker this container will listen on port 5000 EXPOSE 5000 # 8. The command that runs when the container starts CMD ["python", "app.py"]
Once you've written your Dockerfile, you build it into an image using docker build. The -t flag lets you give the image a human-readable name (called a tag).
# Build an image from the Dockerfile in the current directory (.) # -t gives it the name "my-python-app" and the tag "latest" docker build -t my-python-app:latest . # Watch the output โ Docker executes each line of your Dockerfile # and you'll see each step (layer) being created. # Now run a container from your newly built image! # -p 5000:5000 maps port 5000 on your laptop to port 5000 in the container docker run -p 5000:5000 my-python-app:latest # Your Python app is now running in a container. # Open http://localhost:5000 in your browser!
Just like .gitignore tells Git which files to ignore, .dockerignore tells Docker which files to not copy into your image when you use the COPY . . instruction.
This is important for two reasons:
- Security: You don't want your
.envfile (which contains secrets like API keys) accidentally baked into the image. - Size: Excluding
node_modules,.git, test files, and documentation keeps your image lean and fast to transfer.
# Git data (not needed in the container) .git .gitignore # Environment variables file (never put secrets in an image!) .env # Node.js dependencies (will be reinstalled inside the container) node_modules # Python cache files __pycache__ *.pyc # Documentation, tests, and local dev files README.md tests/ docs/
python:3.10-slim instead of just python:3.10. The "slim" variant strips out a lot of OS tools you don't need, cutting the image from ~900MB down to ~130MB. Even smaller: Alpine Linux variants (python:3.10-alpine) can bring it under 50MB. Smaller images = faster builds, faster deployments, smaller attack surface.
๐ง Docker Architecture
How does Docker actually work behind the scenes? Docker uses a Client-Server architecture โ several distinct components talk to each other every time you type a command.
Imagine a busy Restaurant:
- The Client (You): You look at the menu and give your order to the waiter. You don't go into the kitchen yourself.
- The Daemon (The Chef): The chef receives the order, grabs ingredients from the fridge, cooks the food, and plates it. They do all the heavy lifting โ you never see it happen.
- The Registry (The Grocery Store / Supplier): If the chef doesn't have an ingredient in stock, they call a supplier and have it delivered. The supplier's warehouse is Docker Hub.
Docker Client
The terminal/command line where you type commands like docker run or docker build. It translates your commands into API calls sent to the Daemon.
Docker Daemon (dockerd)
The invisible engine running in the background. It receives commands from the Client and does the actual work: building images, creating containers, managing networks and volumes.
Docker Registry
A remote storage system for Docker images. Docker Hub (hub.docker.com) is the public default registry, with millions of pre-built images. Companies often run their own private registries.
Let's trace exactly what happens when you type docker run nginx:
- You type
docker run nginxin your terminal (Docker Client). - The Docker Client converts this into an API request and sends it to the Docker Daemon running on your machine.
- The Docker Daemon checks if the
nginximage already exists locally (in its local image cache). - If not, the Daemon contacts the Docker Registry (Docker Hub) and pulls the
nginximage down to your machine. - The Daemon builds a container from the image โ creating the writable layer, setting up isolated namespaces, configuring networking.
- The container starts running. The nginx web server is live!
# Log in to Docker Hub (creates an account at hub.docker.com first) docker login # Pull an image from Docker Hub to your local machine docker pull postgres:16 # Tag your own image so you can push it to your Docker Hub account # Format: docker tag <local-name> <dockerhub-username>/<repo-name>:<tag> docker tag my-python-app:latest yourusername/my-python-app:v1.0 # Push your image to Docker Hub (now anyone can pull it!) docker push yourusername/my-python-app:v1.0 # Search for images on Docker Hub from the CLI docker search redis
Docker Hub (hub.docker.com) is the world's largest repository of container images. It hosts Official Images โ curated, security-scanned images maintained by Docker and the software's own teams. These are the ones you should use as your base images.
Popular official images you'll use all the time as a developer:
nodeโ for Node.js applicationspythonโ for Python applicationspostgresโ the PostgreSQL databaseredisโ the Redis in-memory data storenginxโ the Nginx web server / reverse proxymongoโ the MongoDB databaseubuntu/alpineโ bare-bones Linux base images
Instead of setting up a PostgreSQL database on your laptop, you just run docker run postgres and you have a fully working database in seconds, no installation needed.
๐พ Volumes โ Don't Lose Your Data!
By default, containers have short memories. If a container is deleted, everything inside it is gone forever. Volumes are how you make data survive.
Imagine a container is like a School Desk. When class is over, the janitor wipes the desk completely clean. If you left your homework on the desk, it's gone! But a Volume is like a Backpack. You put your homework in your backpack, leave the classroom, and bring that exact same backpack to a completely different classroom the next day. The desk changes; the backpack always stays with you.
Containers are intentionally designed to be temporary and disposable. This is actually a feature, not a bug! It means:
- You can destroy and recreate containers instantly without any setup
- Every container starts from a clean, known-good state
- Containers can be replaced, updated, or scaled without side effects
But this creates an obvious problem: what about your database? Your user account records? Your game save files? You can't lose all that data every time a container restarts. That's where Volumes and Bind Mounts come in.
A Docker Volume is a special, Docker-managed directory that lives on the host machine's file system โ completely outside the container. You "mount" it into the container at a specific path. When the container writes to that path, the data actually gets stored in the volume on the host. When the container is destroyed, the volume survives.
Volumes are the preferred way to persist data because:
- They're managed by Docker โ Docker handles where on disk they live
- They work identically on Mac, Windows, and Linux
- They can be shared between multiple containers simultaneously
- They can be backed up and restored easily
# Create a named volume (an empty, persistent "backpack") docker volume create my-db-data # List all volumes on your machine docker volume ls # Run a PostgreSQL container and attach the volume # -v my-db-data:/var/lib/postgresql/data # ^ volume name ^ where postgres stores its data inside the container docker run -d \ -e POSTGRES_PASSWORD=mysecretpassword \ -v my-db-data:/var/lib/postgresql/data \ --name my-postgres \ postgres:16 # Now if you delete and recreate the postgres container, # all your database tables and records are still there! # Inspect a volume to see its details and mount point docker volume inspect my-db-data # Remove a volume (WARNING: permanent data deletion!) docker volume rm my-db-data
A Bind Mount links a specific folder on your host machine directly into the container. Instead of a Docker-managed volume, you're pointing at an exact path like /Users/jude/projects/myapp.
Bind mounts are extremely useful during development because any changes you make to files on your laptop are instantly reflected inside the container โ no rebuild required. This lets you edit code with your normal text editor while the app runs in a Docker container.
# Mount your current local directory ($(pwd)) into /app in the container # Now edits you make to files on your laptop appear instantly inside the container! docker run -p 3000:3000 \ -v $(pwd):/app \ my-node-app # On Windows PowerShell, use ${PWD} instead of $(pwd): docker run -p 3000:3000 -v ${PWD}:/app my-node-app
| Situation | Use This |
|---|---|
| Storing a production database | Volume |
| Live code editing during development | Bind Mount |
| Sharing data between two containers | Volume |
| Accessing config files from your host | Bind Mount |
| Storing generated files (uploads, logs) | Volume |
๐ Networking โ Getting Containers to Talk
By default, containers are isolated. They can't see each other, can't see your computer, and can't reach the internet without permission. Docker Networking is how you open the right doors.
Think of Docker Networks like Walkie-Talkies. If two kids (containers) tune their walkie-talkies to the exact same channel (network), they can instantly hear each other and talk securely, without anyone else listening in. Anyone on a different channel is completely silent to them. And a "port mapping" is like setting up a relay station that lets someone with a different radio (someone on the internet) transmit a message in on one frequency and have it automatically re-broadcast on your private channel.
| Driver | What it does | When to use it |
|---|---|---|
| Bridge | The default. Creates a private internal network. Containers on the same bridge can communicate using each other's container names as hostnames. | Any multi-container app on a single host (development, staging). |
| Host | Removes all network isolation. The container uses your machine's actual network interface directly. | High-performance scenarios where network overhead must be eliminated. Rarely needed. |
| None | Completely disables networking. The container cannot send or receive any network traffic at all. | Security-sensitive batch jobs that should never touch a network. |
When you run an application inside a container (say, a web server on port 80), the outside world โ your browser, your teammates, the internet โ can't reach it yet. The container is on a private network. You need to create a mapping from a port on your host machine to a port inside the container.
The syntax is -p <host_port>:<container_port>.
-p 8080:80โ Traffic arriving at port 8080 on your laptop is forwarded into the container on port 80.-p 5432:5432โ Exposes a PostgreSQL database (port 5432) to your host.-p 3000:3000โ Common for Node.js dev servers.
# Create a custom bridge network called "app-network" docker network create app-network # Start a database container and attach it to the network docker run -d \ --name my-database \ --network app-network \ -e POSTGRES_PASSWORD=secret \ postgres:16 # Start your web app container on the SAME network docker run -d \ --name my-web-app \ --network app-network \ -p 8080:80 \ my-app-image # Now, inside my-web-app, you can connect to the database # using the hostname "my-database" โ Docker handles the DNS! # e.g., DATABASE_URL=postgresql://user:secret@my-database:5432/mydb # List all networks on your machine docker network ls # Inspect a network to see which containers are attached docker network inspect app-network
--name as a hostname. This is huge โ instead of hardcoding IP addresses (which change), you hardcode names (which don't). Docker Compose (next lesson!) leverages this heavily.
When containers can't talk to each other, it's almost always one of these causes:
- They're not on the same network โ Check
docker network inspect <network_name>to confirm both containers appear in the "Containers" section. - Wrong hostname โ Make sure you're using the container's
--nameas the hostname, not "localhost" (localhost inside a container refers to itself, not the host machine!). - Port not exposed inside container โ The application inside the container must actually be listening on the port you expect. Check its documentation.
-p 9090:80. What URL would you visit in your browser?๐ผ Docker Compose โ The Orchestra Conductor
What happens when your app needs a Web Server, a Database, a Cache layer, and a Background Worker all running at the same time? Typing individual docker run commands for each one is a nightmare. Docker Compose solves this.
Imagine an Orchestra. You have violins, trumpets, and drums (containers). You could walk up to every single musician one by one and tell them exactly when and how to start playing. Or, you could use a Conductor (Docker Compose) who reads a single piece of sheet music (the docker-compose.yml file) and commands everyone to start together perfectly in sync, in exactly the right order.
Docker Compose is a tool for defining and running multi-container Docker applications using a single YAML configuration file. With one command, Compose:
- Creates all the named networks your services need
- Creates all the named volumes for persistence
- Pulls or builds all the images required
- Starts all the containers in the correct dependency order
- Wires the containers together with DNS so they can communicate by service name
The result: a single docker compose up command spins up your entire application stack โ your full development environment โ from scratch in seconds.
The Compose file is written in YAML โ a human-readable format that uses indentation to show hierarchy (similar to Python). The key sections are:
services:โ The list of containers you want to run. Each service is a container.image:โ Which Docker image to use for this service (from Docker Hub or your own build).build:โ Build from a Dockerfile instead of a pre-existing image.ports:โ Port mappings (host:container).environment:โ Environment variables to pass into the container.volumes:โ Mount volumes or bind mounts.depends_on:โ Declare dependencies between services (start the database before the web server).networks:โ Put services on specific networks.
# A real-world Compose file: a web app + database + cache services: # === SERVICE 1: The web application === web: build: . # Build from the Dockerfile in this directory ports: - "8080:80" # localhost:8080 โ container port 80 environment: - DATABASE_URL=postgresql://admin:secret@database:5432/appdb - REDIS_URL=redis://cache:6379 depends_on: - database # Wait for the database to start first - cache volumes: - ./app:/app # Bind mount for live code reloading during dev # === SERVICE 2: The PostgreSQL database === database: image: postgres:16 environment: - POSTGRES_USER=admin - POSTGRES_PASSWORD=secret - POSTGRES_DB=appdb volumes: - db-data:/var/lib/postgresql/data # Persist DB data! # === SERVICE 3: The Redis cache === cache: image: redis:7-alpine # Declare the named volume used by the database service volumes: db-data:
With your docker-compose.yml file written and saved in your project folder, these commands control your entire application stack:
# Start ALL services defined in docker-compose.yml # --build forces a rebuild of images before starting docker compose up --build # Start in DETACHED mode (runs in background, frees your terminal) docker compose up -d # Stop all running services (gracefully) docker compose stop # Stop AND remove all containers + networks created by Compose # (volumes are preserved unless you add -v) docker compose down # Stop + remove containers, networks, AND volumes (clean slate) docker compose down -v # View the live logs of all running services combined docker compose logs -f # View logs for just one service docker compose logs -f web # Run a one-off command in a service container (e.g., run migrations) docker compose exec web python manage.py migrate # See the status of all your Compose services docker compose ps
docker-compose.yml file into Git alongside your code. Now any new developer who clones your repo can run docker compose up and have the entire application โ web server, database, cache, everything โ running on their laptop in under a minute. No "setup guide" with 47 steps. No "install this specific version of that". One command. This is transformative for onboarding.
depends_on key is used to:docker compose down -v do that docker compose down alone does not?๐ You're a Container Captain!
You've mastered the foundational concepts that keep the modern tech world running. Let's consolidate everything and give you a real project to try.
- ๐ Docker solves the "Works on my machine" problem by packaging your app with all its dependencies into a portable container.
- ๐ผ๏ธ Images are read-only, layered blueprints. Layers enable fast rebuilds via caching.
- ๐ฆ Containers are live, running instances of images. Ephemeral by design โ writable layer disappears when deleted.
- ๐๏ธ Docker vs VMs โ Containers share the host OS kernel (fast, lightweight). VMs include a full Guest OS (heavy, isolated).
- ๐ Dockerfiles are the step-by-step recipes that build images.
FROM,RUN,COPY,CMDare the core instructions. - ๐ง Architecture โ Docker Client talks to the Docker Daemon, which pulls images from a Registry like Docker Hub.
- ๐พ Volumes give containers permanent memory. Bind Mounts are great for development hot-reloading.
- ๐ Networking โ Bridge networks let containers communicate. Port mapping opens doors to the outside world.
- ๐ผ Docker Compose orchestrates multi-container apps with a single YAML file and a single command.
You've climbed the first mountain. Here's what experienced Docker practitioners learn next:
- Multi-Stage Builds โ Use multiple
FROMstatements in one Dockerfile to create tiny production images. You build in a "builder" stage (with all your compilers and dev tools), then copy only the compiled output into a minimal final image. - Docker Security โ Running containers as non-root users, scanning images for vulnerabilities (
docker scout), using secrets management instead of hardcoding passwords in Compose files. - Container Orchestration โ When you need to run containers across multiple servers and auto-heal when things crash: Kubernetes (K8s). Think of it as Docker Compose's powerful big sibling for production clusters.
- CI/CD Integration โ Automating Docker builds and pushes on every Git commit using GitHub Actions, GitLab CI, or Jenkins. Your pipeline builds the image, runs tests inside a container, then pushes to a registry if tests pass.
Deploy a Web App + Database with Docker Compose
Here's a real project to cement everything you've learned. You'll containerize a simple app and hook it up to a database:
- Create a new folder:
mkdir my-docker-project && cd my-docker-project - Create a simple
app.py(Python/Flask or Node.js โ your choice) that connects to a PostgreSQL database and displays data. - Write a
Dockerfilefor your app using the skills from Lesson 4. Remember:FROMโWORKDIRโCOPY requirementsโRUN installโCOPY .โCMD. - Write a
docker-compose.ymlthat defines two services:web(your app, built from your Dockerfile) anddatabase(using the officialpostgres:16image). - Add a named Volume to persist your database data.
- Use
depends_onto ensure the database starts before your web app. - Run
docker compose up --buildand visithttp://localhost:8080. - Stop everything with
docker compose down, thendocker compose upagain. Notice your database data is still there because of the Volume!
docker build -t name . โ Build an imagedocker run -d -p host:container image โ Run a containerdocker ps โ List running containersdocker exec -it id bash โ Shell into a containerdocker logs -f id โ Follow container logsdocker compose up --build -d โ Launch your full stackdocker compose down -v โ Tear everything down cleanly