
Docker Guide, Labs & Cheatsheet: From Zero to Container Hero
This guide serves two purposes: a comprehensive classroom walkthrough for Docker fundamentals, and a standalone reference you can revisit anytime. We start with the "why" before the "how"!
Table of Contents
Core Concepts - The fundamental building blocks
Lab 1: Build a Secure Multi-Container App - Hands-on practice
Lab 2: Security Audit with Trivy - Vulnerability scanning
Lab 3: Debugging Container Issues - Troubleshooting techniques
Docker Cheatsheet - Quick reference
What is Docker and Why Does It Exist?
Before Docker, deploying software was painful. Developers would write code on their machines, and when it moved to a test or production server, things would break. "But it works on my machine!" became the most frustrating phrase in software development.

The Problem Docker Solves:
Imagine you build a Python web application. It requires:
Python 3.11 (not 3.10, not 3.12 - exactly 3.11)
PostgreSQL client libraries
Specific versions of Flask, SQLAlchemy, and 15 other packages
Certain environment variables configured correctly
A specific folder structure
Now imagine deploying this to 50 servers. Or handing it to a colleague with a different operating system. Every difference in environment becomes a potential bug.
Docker's Solution: Containers
Docker packages your application with everything it needs to run - the code, runtime, libraries, and system tools - into a single, portable unit called a container. This container runs identically everywhere: your laptop, your colleague's Mac, a Linux server in the cloud, or a Kubernetes cluster.
Think of it like shipping. Before standardized shipping containers, loading a cargo ship was chaotic - different sized boxes, crates, and barrels. The invention of the standard shipping container revolutionized global trade. Docker does the same for software.
Containers vs Virtual Machines:
You might wonder: "Can't I just use a virtual machine?" You can, but containers are fundamentally different:
What it includes
Full OS (Windows, Linux)
Only app + dependencies
Size
Gigabytes
Megabytes
Startup time
Minutes
Seconds
Resource usage
Heavy (runs full OS)
Lightweight (shares host kernel)
Isolation
Complete (separate kernel)
Process-level (shared kernel)
Containers share the host operating system's kernel, making them incredibly efficient. You can run dozens of containers on a laptop that would struggle with 3 virtual machines.
When to Use Docker:
Consistent development environments across a team
Microservices architecture (multiple small services instead of one monolith)
CI/CD pipelines (test in the same environment as production)
Easy scaling (spin up more containers when traffic increases)
Dependency isolation (different apps can use different versions of the same library)
Part 1: Core Concepts
Before we start running commands, let's understand the key concepts. Docker has its own vocabulary, and confusing these terms leads to confusion later.
Image vs Container - The Most Important Distinction
If you confuse these two terms, everything else in Docker becomes confusing. Take time to internalize this:
An image is a read-only template containing everything needed to run an application: operating system files, application code, dependencies, and configuration.
Think of it like a class definition in programming, or a recipe in cooking. It doesn't "run" - it's a blueprint.
A container is a running instance of an image. When you "run" an image, Docker creates a container from it.
Think of it like an object instantiated from a class, or a dish made from a recipe. You can create many containers from one image.

The container adds a thin writable layer on top of the image layers. Any files you create or modify inside the container go into this writable layer. When you delete the container, this layer is gone - that's why containers are called "ephemeral."
Try these commands to see the difference:
Dockerfile - The Recipe for Building Images
A Dockerfile is a text file containing instructions that Docker executes step-by-step to build an image. You write the recipe once, and Docker follows it exactly every time.
Each instruction in a Dockerfile creates a new layer. Docker caches these layers, which speeds up subsequent builds - if a layer hasn't changed, Docker reuses the cached version instead of rebuilding it.
Here are the instructions you will use most frequently:
FROM
Base image (always first)
FROM python:3.11-slim
RUN
Execute command during build
RUN pip install flask
COPY
Copy files from host to image
COPY app.py /app/
WORKDIR
Set working directory
WORKDIR /app
ENV
Set environment variable
ENV DEBUG=false
EXPOSE
Document which port app uses
EXPOSE 8080
USER
Switch to non-root user
USER appuser
ENTRYPOINT
Main command (fixed)
ENTRYPOINT ["python"]
CMD
Default arguments (overridable)
CMD ["app.py"]
The relationship between ENTRYPOINT and CMD confuses many people. Think of it this way:
ENTRYPOINT sets the executable that always runs. CMD provides default arguments that users can override. Together, they form the complete command that runs when a container starts.
Volumes - Making Data Persistent
Here's a common mistake beginners make: they store important data inside a container, delete the container, and lose everything. This happens because containers are ephemeral by design.
The Problem - Data Loss:
Let's see this problem in action:
This is exactly what happens with databases, user uploads, configuration files - anything written inside a container. When the container is removed, everything inside disappears.
The Solution - Volumes:
Volumes store data outside the container, on your host machine. The container accesses this data through a mount point, but the data lives independently of the container's lifecycle.

Docker provides three types of storage:
Named Volume
-v mydata:/app/data
Databases, persistent data
Managed by Docker
Bind Mount
-v /host/path:/container/path
Development (live code reload)
Exact folder on your computer
tmpfs
--tmpfs /tmp
Temporary/sensitive data
Only in memory (RAM)
Named Volumes - Best for Production Data:
Named volumes are managed by Docker. You don't need to know where they're physically stored - Docker handles that for you. This is the recommended approach for production data like databases.
Let's prove it works:
Bind Mounts - Best for Development:
Bind mounts map a specific folder on your computer directly into the container. This is perfect for development because any changes you make to files on your host immediately appear inside the container (and vice versa).
The -v flag explained in detail:
Read-Only Mounts for Security:
Sometimes you want a container to read files but not modify them. Use the :ro option:
Volume Commands Reference:
Real-World Example - PostgreSQL with Persistent Data:
Docker Networks - How Containers Communicate
By default, each container is isolated. It cannot see other containers, and other containers cannot see it. To enable communication, you put containers on the same Docker network.
Why Networks Matter:
Imagine you have a web application container and a database container. Without networking:
The web app cannot connect to the database
You cannot use nice hostnames like
db- you'd need IP addressesIP addresses change every time containers restart
No security isolation between different applications

Docker's Built-in DNS:
Docker provides automatic DNS resolution. When containers are on the same network, they can reach each other by container name instead of IP address. This is powerful because container IP addresses can change, but names stay constant.
Network Types:
bridge
Default. Containers can communicate if on same bridge network
Most applications
host
Container shares host's network stack
High-performance, no isolation
none
No networking
Maximum isolation
overlay
Connect containers across multiple Docker hosts
Docker Swarm, distributed apps
Practical Example - Connecting Containers:
Let's create a network and connect two containers that can talk to each other:
Network Commands Explained:
The --network Flag Explained:
Real-World Example - Web App + Database + Redis:
Port Mapping vs Network Communication:
This confuses many beginners. Here's the difference:
Port mapping
-p 8080:80
Your host machine (localhost:8080)
Network connection
--network mynet
Other containers on same network
Security Best Practice - Network Isolation:
Use separate networks to isolate different parts of your application:
Clean up after network examples:
Docker Compose - Orchestrating Multiple Containers
Real applications rarely consist of a single container. A typical web application might need a web server, an application server, and a database. Managing these manually with individual docker run commands becomes tedious and error-prone.
Docker Compose solves this by defining your entire application stack in a single YAML file. One command starts everything, with the correct configuration, networks, and volumes.
Key commands:
Container Security Essentials
The Golden Rules:
Use minimal base images (
-slim,-alpine) - fewer packages = fewer vulnerabilitiesNever run as root - use
USERinstruction in DockerfileScan images before deployment - use Trivy, Docker Scout, etc.
Keep images updated - rebuild regularly to get security patches
Don't embed secrets - use environment variables or secret managers
Lab 1: Build a Secure Multi-Container App
Now that you understand the concepts, let's apply them by building a real multi-container application. This lab demonstrates how all the pieces fit together in a realistic scenario.
Goal: Create a complete web application with frontend (NGINX), backend (Python API), and database (PostgreSQL) using Docker Compose. Along the way, you'll apply every security best practice we've discussed.
Why This Lab Matters:
This is not just an exercise - this is exactly how professional applications are built and deployed in production. You'll learn:
How to structure a multi-service application
How containers communicate through Docker networks
How to persist data that survives container restarts
How to apply security best practices from the start
How to debug issues when things go wrong
What We're Building
We're creating a simple but realistic three-tier web application:

How the pieces work together:
NGINX
Reverse proxy, serves frontend
Handles HTTP, SSL, load balancing
Python API
Business logic, data processing
Processes requests, talks to database
PostgreSQL
Data storage
Stores persistent data safely
The Request Flow:
User opens
http://localhost:8080in their browserBrowser connects to NGINX container (port 8080 on your machine → port 80 in container)
NGINX sees the request path and decides what to do:
/api/*requests → forwards to Python API container/health→ forwards to Python API for health checkEverything else → returns a simple frontend response
Python API processes the request, potentially querying PostgreSQL
Response travels back through NGINX to the user's browser
Step 1: Create Project Structure
What you're doing: Creating the folder structure for our application. Each service has its own folder with its configuration files.
Your structure will be:
Why this structure?
Each service is isolated in its own folder
Docker Compose can build each service from its folder
Easy to manage, test, and deploy independently
Step 2: Create the Python API
What you're doing: Creating a simple Flask API that will serve as our backend. This API exposes endpoints that NGINX will forward requests to.
File: api/requirements.txt
api/requirements.txtWhy these packages?
flask==3.0.0- A lightweight Python web framework for building APIspsycopg2-binary==2.9.9- PostgreSQL driver for Python (so our API can talk to the database)
flask is a lightweight Python web framework for building APIs. psycopg2-binary is the PostgreSQL database driver for Python.
File: api/app.py
api/app.pyKey points explained:
host='0.0.0.0'
Listen on all network interfaces, not just localhost. Required in containers because requests come from outside.
os.getenv("DB_HOST")
Read environment variable. Docker Compose will set this.
/health endpoint
Standard practice - allows Docker/Kubernetes to check if service is alive.
File: api/Dockerfile
api/DockerfileStep 3: Create NGINX Configuration
File: nginx/nginx.conf
nginx/nginx.confKey concepts explained:
Reverse Proxy
NGINX sits between users and your app, handling routing, SSL, caching
upstream api_backend
Defines where to forward requests. api is the Docker service name.
proxy_pass
Forwards the incoming request to the backend server
Docker DNS
Docker Compose creates a network where services can find each other by name
Step 4: Create Docker Compose File
File: docker-compose.yml
docker-compose.ymlStep 5: Build and Run
What this command does:
up
Create and start containers
-d
Detached mode (run in background)
--build
Rebuild images before starting
Watch the startup:
Verify everything is running:
Expected output:
Step 6: Test the Application
Expected responses:
Step 7: Cleanup
Lab 2: Security Audit with Trivy
Goal: Learn to scan Docker images for vulnerabilities, understand what the results mean, and know how to fix security issues in your containers.
Why This Matters:
Every Docker image you pull from the internet contains hundreds of software packages - the base operating system, libraries, utilities, and your application dependencies. Each of these packages might have security vulnerabilities that attackers can exploit.
In 2023, over 26,000 new CVEs (Common Vulnerabilities and Exposures) were published. Running old, unpatched images in production is like leaving your front door open - you're inviting attackers in.
What is Trivy?
Trivy is a comprehensive security scanner created by Aqua Security. It's free, open-source, and widely used in the industry. Trivy finds vulnerabilities in:
OS packages - apt, apk, yum packages in the base image (e.g., openssl, curl, libc)
Application dependencies - pip, npm, gem, cargo packages you install (e.g., flask, requests)
Misconfigurations - Security issues in Dockerfiles, Kubernetes manifests
Secrets - Accidentally committed passwords, API keys, private keys
How Trivy Works:

Step 1: Install Trivy
macOS:
Linux (Debian/Ubuntu/Kali):
Using Docker (works everywhere - no installation needed):
Verify installation:
Step 2: Scan a Vulnerable Image - See the Problem
Let's scan an intentionally old image to understand what vulnerabilities look like and why they matter:
What you're doing: You're asking Trivy to analyze every package installed in this Docker image and check each one against a database of known vulnerabilities.
Understanding the Output:
The scan produces a lot of output. Let's break down what you'll see:
1. Summary Line (at the top):
CRITICAL
Actively exploited, easy to attack, gives attacker full control
HIGH
Serious issue, could lead to data breach or system compromise
MEDIUM
Moderate risk, requires specific conditions to exploit
LOW
Minor issue, difficult to exploit
UNKNOWN
Severity not yet determined by security researchers
2. Detailed Table:
How to read this:
CVE-2023-0286 in OpenSSL is a real vulnerability that allows attackers to read sensitive memory
Installed Version shows what's in your image (outdated)
Fixed Version shows what version patches the vulnerability
Filter to see only critical issues:
Why This Matters - Real Attack Scenarios:
CVE-2023-4911
Gain root access to the container (Looney Tunables glibc bug)
CVE-2023-0286
Read sensitive data from memory (OpenSSL vulnerability)
CVE-2023-38545
Execute arbitrary code via crafted URL (curl SOCKS5 heap overflow)
An attacker who exploits even ONE of these could gain full control of your container, access databases, steal secrets, or use your infrastructure to attack others.
Step 3: Compare with a Secure Image
Now scan a modern, minimal image to see how proper image selection dramatically reduces risk:
Expected result:
The difference is dramatic:
Total vulnerabilities
~1247
~68
95%
CRITICAL
~39
0
100%
HIGH
~256
~2
99%
Image size
~900 MB
~150 MB
83%
Why such a big difference?
Base OS
Debian 10 (oldoldstable, limited updates)
Debian 12 (stable, actively patched)
Python version
3.6 (end-of-life Dec 2021)
3.11 (actively maintained)
Included packages
Full OS with compilers, docs, etc.
Minimal - only what's needed to run Python
Security patches
None for 3+ years
Monthly updates
Step 4: Scan Your Own Image
Now let's scan the image we built in Lab 1 to verify our security practices work:
What you should see:
Because we used python:3.11-slim as our base image, you should see a relatively clean report with zero or very few CRITICAL vulnerabilities.
If you find vulnerabilities, here's how to fix them:
Old base image
Update FROM python:3.11-slim to python:3.12-slim
Vulnerable Python package
Update version in requirements.txt
Vulnerable OS package
Run apt-get upgrade in Dockerfile
No fix available
Wait for patch, or use alternative package
Step 5: Scan More Than Just Images
Trivy can scan many things beyond just Docker images:
Scan a filesystem (your project folder):
Scan a Dockerfile for misconfigurations:
Common misconfigurations Trivy finds:
Running as root
If container is compromised, attacker has root
ADD instead of COPY
ADD can download from URLs, security risk
latest tag
Unpredictable - could suddenly have vulnerabilities
No HEALTHCHECK
Can't detect if application is actually working
Secrets in Dockerfile
Credentials exposed in image history
Scan for secrets accidentally committed:
Step 6: Automated Security Gate
In real projects, you want to automatically block deployments that have critical vulnerabilities. This is how professional teams prevent vulnerable code from reaching production.
Using exit codes in scripts:
How this works in CI/CD:
Why this matters:
Developers get immediate feedback when they introduce vulnerabilities
Vulnerable images never reach production
Security becomes part of the development process, not an afterthought
Step 7: Generate Reports
Generate reports for documentation, compliance, or sharing with your team:
Security Best Practices Summary
Use slim/alpine images
FROM python:3.11-slim instead of FROM python:3.11
Use specific version tags
FROM python:3.11.7-slim not FROM python:latest
Run as non-root
USER appuser in Dockerfile
Scan in CI/CD
trivy image --exit-code 1 --severity CRITICAL
Update base images monthly
Rebuild images regularly to get patches
Scan before pushing
trivy image myapp:latest before docker push
Use multi-stage builds
Separate build dependencies from runtime
Lab 2 Key Takeaways
CVE database
Trivy checks against NVD and vendor databases
Image age matters
Old images accumulate hundreds of vulnerabilities over time
Slim images
Fewer packages = smaller attack surface = fewer vulnerabilities
Defense in depth
Scan images, Dockerfiles, filesystems, and secrets
CI/CD integration
Use --exit-code 1 to automatically block vulnerable deployments
Regular updates
Rebuild images monthly to get security patches
Lab 3: Debugging Container Issues
Goal: Learn how to troubleshoot the most common Docker problems. When containers don't work as expected, you need to know how to investigate and fix issues quickly.
Why This Matters:
Containers can fail for many reasons: missing ports, wrong configuration, application crashes, resource limits, network issues. Knowing how to debug these problems is essential for any DevOps or development work.
Scenario 1: Container Exits Immediately
The Problem:
You run a container but it exits right away:
Why This Happens:
A container runs only as long as its main process runs. Here's the key concept:

The Ubuntu image's default command is bash. When there's no terminal attached (no -it flags), bash has nothing to do and exits immediately. When bash exits, the container stops.
The Solution - Keep It Running:
Flag Reference:
-i
Interactive - keep STDIN open so you can type
-t
TTY - allocate a terminal so you see formatted output
-d
Detached - run in background, return control to your terminal
-it (combined)
Interactive terminal - the way you normally "enter" a container
sleep infinity
A command that never exits - keeps container alive
Clean up:
Scenario 2: Cannot Connect to Container
The Problem:
Your app is running but you can't access it from your browser or curl:
Why This Happens:

NGINX is running on port 80 inside the container, but containers are isolated. Your computer cannot reach into the container without explicit port mapping.
The Solution - Map Ports:
Port Mapping Syntax:
Debugging Port Issues:
Scenario 3: Check What's Inside a Running Container
The Problem:
Your container is running but something isn't working. You need to look inside to debug.

The Solution - Docker Exec:
What Docker Exec Does:
docker exec app ls
Run ls inside container, show output, exit
docker exec -it app bash
Start interactive bash shell inside container
docker exec -u root app bash
Run as root user (even if container runs as non-root)
docker exec -w /tmp app ls
Run command in specific directory
Common Debugging Commands Inside Container:
Scenario 4: View Container Logs
The Problem:
Your container is crashing or behaving strangely. You need to see what it's outputting.
The Solution - Docker Logs:
Understanding Docker Logs:
Docker captures everything your application writes to STDOUT (standard output) and STDERR (standard error). This is why in Docker:
Applications should log to console, not files
Use
print()in Python,console.log()in Node.jsConfigure your app's logging framework to output to stdout

Scenario 5: Inspect Container Details
The Problem:
You need detailed information about a container: its IP address, environment variables, mounts, configuration.
The Solution - Docker Inspect:
Scenario 6: Container Uses Too Many Resources
The Problem:
A container is consuming too much CPU or memory, affecting other containers or your system.
The Solution - Monitor and Limit:
Understanding the Stats:
CPU %
CPU usage as percentage of host CPU
MEM USAGE / LIMIT
Current memory / Maximum allowed
MEM %
Memory as percentage of limit
NET I/O
Network bytes received / sent
BLOCK I/O
Disk bytes read / written
PIDS
Number of processes in container
Set Resource Limits:
Scenario 7: Clean Up Disk Space
The Problem:
Docker is using a lot of disk space. You need to reclaim it.
The Solution - Docker System Commands:
Before Using Prune Commands:
Always check what will be removed:
Debugging Cheatsheet
Container exits immediately
docker logs <container>
docker inspect <container> --format '{{.State}}'
Can't connect to container
docker port <container>
docker inspect <container> (check ports)
Need to look inside
docker exec -it <container> bash
docker exec <container> cat /some/file
Check if healthy
docker ps (look for health status)
docker inspect <container> --format '{{.State.Health}}'
Out of disk space
docker system df
docker system prune -a
High CPU/memory
docker stats
Add --memory and --cpus limits
Network issues
docker network inspect <network>
docker exec <container> ping <other-container>
Clean Up After This Lab
Docker Cheatsheet
Container Lifecycle
Image Management
Volumes
Networks
Docker Compose
Cleanup
Debugging
Dockerfile Instructions
Docker Compose YAML
Security Scanning (Trivy)
Quick Reference Card
Most Used Commands
Run container
docker run -d -p 8080:80 --name web nginx
Stop container
docker stop web
View logs
docker logs -f web
Shell into container
docker exec -it web bash
Build image
docker build -t myapp .
List containers
docker ps -a
List images
docker images
Start Compose
docker compose up -d
Stop Compose
docker compose down
View Compose logs
docker compose logs -f
Clean up
docker system prune -a
Scan image
trivy image myapp
Port Mapping
Volume Mounting
Last updated