# Docker Guide, Labs & Cheatsheet: From Zero to Container Hero

## Table of Contents <a href="#table-of-contents" id="table-of-contents"></a>

1. [Core Concepts](file:///Users/przemo/Library/CloudStorage/Dropbox/Cybersteps/Module%203/Week%204%3A%20Docker/Weekly-Review/Docker-Complete-Lab-Walkthrough.md#part-1-core-concepts) - The fundamental building blocks
2. [Lab 1: Build a Secure Multi-Container App](file:///Users/przemo/Library/CloudStorage/Dropbox/Cybersteps/Module%203/Week%204%3A%20Docker/Weekly-Review/Docker-Complete-Lab-Walkthrough.md#lab-1-build-a-secure-multi-container-app) - Hands-on practice
3. [Lab 2: Security Audit with Trivy](file:///Users/przemo/Library/CloudStorage/Dropbox/Cybersteps/Module%203/Week%204%3A%20Docker/Weekly-Review/Docker-Complete-Lab-Walkthrough.md#lab-2-security-audit-with-trivy) - Vulnerability scanning
4. [Lab 3: Debugging Container Issues](file:///Users/przemo/Library/CloudStorage/Dropbox/Cybersteps/Module%203/Week%204%3A%20Docker/Weekly-Review/Docker-Complete-Lab-Walkthrough.md#lab-3-debugging-container-issues) - Troubleshooting techniques
5. [Docker Cheatsheet](file:///Users/przemo/Library/CloudStorage/Dropbox/Cybersteps/Module%203/Week%204%3A%20Docker/Weekly-Review/Docker-Complete-Lab-Walkthrough.md#docker-cheatsheet) - Quick reference

## What is Docker and Why Does It Exist? <a href="#what-is-docker-and-why-does-it-exist" id="what-is-docker-and-why-does-it-exist"></a>

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.

<figure><img src="https://1736120781-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlvAAyThlvtKMDypgmoOh%2Fuploads%2FKs7MUuDqMIvLo6TQR5Mw%2Fimage.png?alt=media&#x26;token=28fb5d23-5616-4a4f-8c98-7ea686c9cb50" alt=""><figcaption></figcaption></figure>

**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:

| Aspect           | Virtual Machine            | Container                        |
| ---------------- | -------------------------- | -------------------------------- |
| 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 <a href="#part-1-core-concepts" id="part-1-core-concepts"></a>

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 <a href="#image-vs-container---the-most-important-distinction" id="image-vs-container---the-most-important-distinction"></a>

If you confuse these two terms, everything else in Docker becomes confusing. Take time to internalize this:

{% hint style="info" %}
An **image** is a read-only template containing everything needed to run an application: operating system files, application code, dependencies, and configuration.&#x20;

Think of it like a class definition in programming, or a **recipe in cooking**. It doesn't "run" - it's a blueprint.
{% endhint %}

{% hint style="info" %}
A **container** is a running instance of an image. When you "run" an image, Docker creates a container from it.&#x20;

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.
{% endhint %}

<figure><img src="https://1736120781-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlvAAyThlvtKMDypgmoOh%2Fuploads%2FjFKUf2NCFyAMedw5Zqhy%2Fimage.png?alt=media&#x26;token=0425271c-2f48-4642-bd9e-aa82a344b308" alt=""><figcaption></figcaption></figure>

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:

```bash
# List all images on your system
docker images

# List running containers
docker ps

# List ALL containers (including stopped)
docker ps -a
```

### Dockerfile - The Recipe for Building Images <a href="#dockerfile---the-recipe-for-building-images" id="dockerfile---the-recipe-for-building-images"></a>

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:

| Instruction  | Purpose                         | Example                 |
| ------------ | ------------------------------- | ----------------------- |
| `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:

```bash
ENTRYPOINT ["python"]      # The "driver" - always runs
CMD ["app.py"]             # Default "destination" - can be overridden

# docker run myimage           → python app.py
# docker run myimage other.py  → python other.py
```

`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 <a href="#volumes---making-data-persistent" id="volumes---making-data-persistent"></a>

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:

```bash
# Start a container and create a file inside it
docker run -d --name test-container ubuntu sleep infinity
docker exec test-container bash -c "echo 'Important data!' > /mydata.txt"
docker exec test-container cat /mydata.txt
# Output: Important data!

# Stop and remove the container
docker stop test-container
docker rm test-container

# Start a new container with the same image
docker run -d --name test-container ubuntu sleep infinity
docker exec test-container cat /mydata.txt
# Error: No such file or directory - DATA IS GONE!

# Clean up
docker rm -f test-container
```

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.

<figure><img src="https://1736120781-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlvAAyThlvtKMDypgmoOh%2Fuploads%2FUDWeSaKQKh9Bg1j8ztvi%2Fimage.png?alt=media&#x26;token=a546b269-4bda-4f67-994a-1c0ebab4a3aa" alt=""><figcaption></figcaption></figure>

**Docker provides three types of storage:**

| Type         | Syntax                          | Use Case                       | Data Location                 |
| ------------ | ------------------------------- | ------------------------------ | ----------------------------- |
| 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.

```bash
# Create a named volume
docker volume create myappdata

# List all volumes
docker volume ls

# Run container with named volume mounted at /data inside container
docker run -d --name db-container \
  -v myappdata:/var/lib/postgresql/data \
  postgres:15-alpine

# The data in /var/lib/postgresql/data is now stored in 'myappdata' volume
# Even if you delete db-container, the data remains in the volume
```

**Let's prove it works:**

```bash
# Create container with named volume and save data
docker run -d --name vol-test -v testdata:/data ubuntu sleep infinity
docker exec vol-test bash -c "echo 'Persistent data!' > /data/myfile.txt"
docker exec vol-test cat /data/myfile.txt
# Output: Persistent data!

# Remove the container completely
docker rm -f vol-test

# Create a NEW container using the SAME volume
docker run -d --name vol-test-new -v testdata:/data ubuntu sleep infinity
docker exec vol-test-new cat /data/myfile.txt
# Output: Persistent data!  <-- The data survived!

# Clean up
docker rm -f vol-test-new
docker volume rm testdata
```

#### **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).

```bash
# Create a project folder with a file
mkdir -p ~/myproject
echo "print('Hello from Python!')" > ~/myproject/app.py

# Run container with bind mount
# Format: -v /absolute/path/on/host:/path/in/container
docker run --rm -v ~/myproject:/app python:3.11-slim python /app/app.py
# Output: Hello from Python!

# Now edit the file on your host machine
echo "print('Updated code!')" > ~/myproject/app.py

# Run again - the container sees the updated file immediately
docker run --rm -v ~/myproject:/app python:3.11-slim python /app/app.py
# Output: Updated code!
```

**The `-v` flag explained in detail:**

```bash
-v SOURCE:DESTINATION[:OPTIONS]

SOURCE:
  - Named volume:  myvolume (just a name, Docker manages location)
  - Bind mount:    /absolute/path or ./relative/path (your folder)

DESTINATION:
  - Path inside the container where data appears

OPTIONS (optional):
  - ro = read-only (container cannot modify files)
  - rw = read-write (default)

Examples:
  -v pgdata:/var/lib/postgresql/data         # Named volume
  -v /home/user/code:/app                    # Bind mount (absolute path)
  -v ./src:/app/src                          # Bind mount (relative path)
  -v ./config:/app/config:ro                 # Read-only bind mount
```

**Read-Only Mounts for Security:**

Sometimes you want a container to read files but not modify them. Use the `:ro` option:

```shellscript
# Container can read config but not modify it
docker run -d --name secure-app \
  -v ./config:/app/config:ro \
  -v appdata:/app/data \
  myapp

# The container CAN write to /app/data (normal volume)
# The container CANNOT write to /app/config (read-only mount)
```

#### **Volume Commands Reference:**

```shellscript
# Create a volume
docker volume create myvolume

# List all volumes
docker volume ls

# Inspect volume details (see where it's stored)
docker volume inspect myvolume

# Remove a volume (only works if no container is using it)
docker volume rm myvolume

# Remove all unused volumes (WARNING: data loss!)
docker volume prune

# Remove specific container AND its volumes
docker rm -v container-name
```

#### **Real-World Example - PostgreSQL with Persistent Data:**

```bash
# Start PostgreSQL with data stored in a named volume
docker run -d --name postgres-db \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -v postgres_data:/var/lib/postgresql/data \
  -p 5432:5432 \
  postgres:15-alpine

# Create some data (connect and create a table)
docker exec -it postgres-db psql -U postgres -c \
  "CREATE TABLE users (id SERIAL, name VARCHAR(100));"
docker exec -it postgres-db psql -U postgres -c \
  "INSERT INTO users (name) VALUES ('Alice'), ('Bob');"
docker exec -it postgres-db psql -U postgres -c \
  "SELECT * FROM users;"

# Stop and remove the container
docker stop postgres-db
docker rm postgres-db

# Start a FRESH PostgreSQL container with the same volume
docker run -d --name postgres-db-new \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -v postgres_data:/var/lib/postgresql/data \
  -p 5432:5432 \
  postgres:15-alpine

# Verify data still exists
docker exec -it postgres-db-new psql -U postgres -c "SELECT * FROM users;"
# Output shows Alice and Bob - data persisted!

# Clean up
docker rm -f postgres-db-new
# docker volume rm postgres_data  # Only if you want to delete the data
```

## Docker Networks - How Containers Communicate <a href="#docker-networks---how-containers-communicate" id="docker-networks---how-containers-communicate"></a>

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 addresses
* IP addresses change every time containers restart
* No security isolation between different applications

<figure><img src="https://1736120781-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlvAAyThlvtKMDypgmoOh%2Fuploads%2FQMIsDo8oCE4PSjaGVawb%2Fimage.png?alt=media&#x26;token=38c64fed-fb1d-49c5-9ab7-83c85b005691" alt=""><figcaption></figcaption></figure>

**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:**

| Type    | Description                                                   | Use Case                       |
| ------- | ------------------------------------------------------------- | ------------------------------ |
| 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:

```bash
# Step 1: Create a custom network
docker network create myapp-network

# Step 2: Start a database container on this network
docker run -d \
  --name mydb \
  --network myapp-network \
  -e POSTGRES_PASSWORD=secret \
  postgres:15-alpine

# Step 3: Start a web container on the same network
docker run -d \
  --name myweb \
  --network myapp-network \
  nginx:alpine

# Step 4: Test connectivity - from myweb, can we reach mydb?
# Install ping tool and test
docker exec myweb sh -c "apk add --no-cache curl > /dev/null && ping -c 3 mydb"
# Output: 3 packets transmitted, 3 received - SUCCESS!

# The container 'myweb' can reach 'mydb' by NAME, not IP address!
```

#### **Network Commands Explained:**

```bash
# CREATE a new network
docker network create mynetwork

# CREATE with specific driver (bridge is default)
docker network create --driver bridge mynetwork

# LIST all networks
docker network ls

# INSPECT network details (see connected containers, IP ranges)
docker network inspect mynetwork

# CONNECT existing container to a network
docker network connect mynetwork existing-container

# DISCONNECT container from network
docker network disconnect mynetwork container-name

# REMOVE a network (must disconnect all containers first)
docker network rm mynetwork

# REMOVE all unused networks
docker network prune
```

**The `--network` Flag Explained:**

```shellscript
docker run --network NETWORK_NAME --name CONTAINER_NAME IMAGE

--network mynetwork     # Connect to specific network
--network host          # Use host's network (no isolation)
--network none          # No network access
--network container:X   # Share network namespace with container X
```

#### **Real-World Example - Web App + Database + Redis:**

```bash
# Create network
docker network create webapp-net

# Start database
docker run -d --name postgres \
  --network webapp-net \
  -e POSTGRES_PASSWORD=dbpass \
  postgres:15-alpine

# Start Redis cache
docker run -d --name redis \
  --network webapp-net \
  redis:alpine

# Start web application
docker run -d --name webapp \
  --network webapp-net \
  -e DATABASE_URL=postgres://postgres:dbpass@postgres:5432/postgres \
  -e REDIS_URL=redis://redis:6379 \
  -p 8080:80 \
  my-webapp-image

# Notice the connection strings use container NAMES:
# - postgres:5432 (not 172.18.0.2:5432)
# - redis:6379 (not 172.18.0.3:6379)
```

#### **Port Mapping vs Network Communication:**

This confuses many beginners. Here's the difference:

| Concept            | Syntax            | Who Can Connect?                   |
| ------------------ | ----------------- | ---------------------------------- |
| Port mapping       | `-p 8080:80`      | Your host machine (localhost:8080) |
| Network connection | `--network mynet` | Other containers on same network   |

```bash
# This container is accessible from:
# - Host machine at localhost:8080 (port mapping)
# - Other containers at webapp:80 (network)
docker run -d \
  --name webapp \
  --network mynet \
  -p 8080:80 \
  nginx

# This container is only accessible from other containers on mynet:
# (No port mapping - host machine cannot reach it directly)
docker run -d \
  --name internal-api \
  --network mynet \
  my-internal-api
```

#### **Security Best Practice - Network Isolation:**

Use separate networks to isolate different parts of your application:

```bash
# Create separate networks
docker network create frontend-net
docker network create backend-net

# NGINX only on frontend (faces the internet)
docker run -d --name nginx \
  --network frontend-net \
  -p 80:80 \
  nginx

# API on both networks (bridge between frontend and backend)
docker run -d --name api \
  --network frontend-net \
  my-api
docker network connect backend-net api

# Database only on backend (hidden from internet)
docker run -d --name db \
  --network backend-net \
  postgres:15-alpine

# Result:
# - NGINX can reach API (same frontend-net)
# - API can reach Database (same backend-net)
# - NGINX CANNOT reach Database (different networks)
# - Internet CANNOT reach Database directly (no port mapping)
```

**Clean up after network examples:**

```
docker rm -f mydb myweb postgres redis webapp internal-api nginx api db 2>/dev/null
docker network rm myapp-network webapp-net frontend-net backend-net 2>/dev/null
```

## Docker Compose - Orchestrating Multiple Containers <a href="#docker-compose---orchestrating-multiple-containers" id="docker-compose---orchestrating-multiple-containers"></a>

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.

```bash
# docker-compose.yml
services:
  web:                          # Service name (becomes container name)
    build: .                    # Build from Dockerfile in current dir
    ports:
      - "8080:80"               # Map host:container ports
    environment:
      - DEBUG=false             # Environment variables
    depends_on:
      - db                      # Start 'db' first
    volumes:
      - ./code:/app             # Bind mount for development

  db:
    image: postgres:15          # Use existing image
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data  # Named volume

volumes:
  pgdata:                       # Declare named volume
```

**Key commands:**

```
docker compose up -d      # Start all services (background)
docker compose ps         # List running services
docker compose logs -f    # Follow logs
docker compose down       # Stop and remove containers
docker compose down -v    # Also remove volumes
```

## Container Security Essentials <a href="#container-security-essentials" id="container-security-essentials"></a>

**The Golden Rules:**

1. **Use minimal base images** (`-slim`, `-alpine`) - fewer packages = fewer vulnerabilities
2. **Never run as root** - use `USER` instruction in Dockerfile
3. **Scan images before deployment** - use Trivy, Docker Scout, etc.
4. **Keep images updated** - rebuild regularly to get security patches
5. **Don't embed secrets** - use environment variables or secret managers

```
# BAD - Running as root, fat image
FROM python:3.11
COPY . /app
CMD ["python", "app.py"]

# GOOD - Non-root user, slim image
FROM python:3.11-slim
RUN useradd -m appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["python", "app.py"]
```

## Lab 1: Build a Secure Multi-Container App <a href="#lab-1-build-a-secure-multi-container-app" id="lab-1-build-a-secure-multi-container-app"></a>

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 <a href="#what-were-building" id="what-were-building"></a>

We're creating a simple but realistic three-tier web application:

<figure><img src="https://1736120781-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlvAAyThlvtKMDypgmoOh%2Fuploads%2Fo5TABCruvpZKhBGQqjGK%2Fimage.png?alt=media&#x26;token=3f872b66-9a44-4ff0-8df6-3518ab071eae" alt=""><figcaption></figcaption></figure>

**How the pieces work together:**

| Component  | Role                            | Why We Use It                         |
| ---------- | ------------------------------- | ------------------------------------- |
| 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:**

1. User opens `http://localhost:8080` in their browser
2. Browser connects to NGINX container (port 8080 on your machine → port 80 in container)
3. NGINX sees the request path and decides what to do:
   * `/api/*` requests → forwards to Python API container
   * `/health` → forwards to Python API for health check
   * Everything else → returns a simple frontend response
4. Python API processes the request, potentially querying PostgreSQL
5. Response travels back through NGINX to the user's browser

### Step 1: Create Project Structure <a href="#step-1-create-project-structure" id="step-1-create-project-structure"></a>

**What you're doing:** Creating the folder structure for our application. Each service has its own folder with its configuration files.

```bash
mkdir secure-app && cd secure-app
mkdir api nginx
```

**Your structure will be:**

```bash
secure-app/
├── docker-compose.yml     # Orchestrates all services
├── api/
│   ├── Dockerfile         # How to build the Python API image
│   ├── app.py             # Python application code
│   └── requirements.txt   # Python dependencies
└── nginx/
    └── nginx.conf         # NGINX configuration
```

**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 <a href="#step-2-create-the-python-api" id="step-2-create-the-python-api"></a>

**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`**

```bash
flask==3.0.0
psycopg2-binary==2.9.9
```

**Why these packages?**

* `flask==3.0.0` - A lightweight Python web framework for building APIs
* `psycopg2-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`**

```python
"""
Simple Flask API that connects to PostgreSQL.
Demonstrates a typical backend service in a containerized application.
"""

import os
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/health')
def health():
    """
    Health check endpoint.
    Used by Docker healthcheck to verify the service is running.
    Returns a simple JSON response with status "ok".
    """
    return jsonify({"status": "ok"})

@app.route('/api/info')
def info():
    """
    Information endpoint.
    Returns details about the running environment.
    Demonstrates how containers can access environment variables.
    """
    return jsonify({
        "service": "Python API",
        "database_host": os.getenv("DB_HOST", "not configured"),
        "environment": os.getenv("ENVIRONMENT", "development")
    })

if __name__ == '__main__':
    # 0.0.0.0 means listen on all interfaces (required in containers)
    # Without this, the app would only be accessible from inside the container
    app.run(host='0.0.0.0', port=5000)
```

**Key points explained:**

| Code                   | Explanation                                                                                                      |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `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`**

```docker
# =============================================================================
# SECURE PYTHON API DOCKERFILE
# =============================================================================
# This Dockerfile follows security best practices:
# - Uses slim base image (smaller attack surface)
# - Creates non-root user (principle of least privilege)
# - Separates dependency installation from code copy (better caching)
# =============================================================================

# -----------------------------------------------------------------------------
# BASE IMAGE
# -----------------------------------------------------------------------------
# python:3.11-slim is based on Debian but with unnecessary packages removed.
# Size: ~150MB vs ~900MB for the full python:3.11 image.
# Fewer packages = fewer potential vulnerabilities.
FROM python:3.11-slim

# -----------------------------------------------------------------------------
# CREATE NON-ROOT USER
# -----------------------------------------------------------------------------
# Running as root inside containers is dangerous because:
# - If an attacker escapes the container, they have root access to the host
# - Processes inside can modify system files
#
# useradd flags:
#   -m : Create home directory
#   -r : Create system user (no login shell)
#   -s /bin/false : No shell access (extra security)
RUN useradd -m -r -s /bin/false apiuser

# -----------------------------------------------------------------------------
# SET WORKING DIRECTORY
# -----------------------------------------------------------------------------
# All subsequent commands will run from /app.
# Similar to 'cd /app' but persists for the entire image.
WORKDIR /app

# -----------------------------------------------------------------------------
# INSTALL DEPENDENCIES (CACHED LAYER)
# -----------------------------------------------------------------------------
# Copy requirements.txt FIRST, before the rest of the code.
# Why? Docker caches layers. If requirements.txt hasn't changed,
# Docker reuses the cached layer and skips pip install.
# This speeds up rebuilds when you only changed code, not dependencies.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# --no-cache-dir: Don't store pip's cache. Reduces image size.

# -----------------------------------------------------------------------------
# COPY APPLICATION CODE
# -----------------------------------------------------------------------------
# --chown=apiuser:apiuser : Set ownership to our non-root user.
# Without this, files would be owned by root and apiuser couldn't read them.
COPY --chown=apiuser:apiuser app.py .

# -----------------------------------------------------------------------------
# SWITCH TO NON-ROOT USER
# -----------------------------------------------------------------------------
# From this point on, all commands run as 'apiuser', not root.
# This is the last line before CMD because we want root for installation
# but non-root for running the application.
USER apiuser

# -----------------------------------------------------------------------------
# EXPOSE PORT
# -----------------------------------------------------------------------------
# EXPOSE doesn't actually open the port - it's documentation.
# It tells other developers and tools that this container listens on port 5000.
# The actual port mapping happens in docker-compose.yml or 'docker run -p'.
EXPOSE 5000

# -----------------------------------------------------------------------------
# DEFAULT COMMAND
# -----------------------------------------------------------------------------
# What runs when the container starts.
# Using exec form ["cmd", "arg"] instead of shell form for proper signal handling.
CMD ["python", "app.py"]
```

### Step 3: Create NGINX Configuration <a href="#step-3-create-nginx-configuration" id="step-3-create-nginx-configuration"></a>

#### **File: `nginx/nginx.conf`**

```nginx
# =============================================================================
# NGINX CONFIGURATION
# =============================================================================
# NGINX acts as a reverse proxy:
# - User connects to NGINX on port 80
# - NGINX forwards API requests to the Python backend
# - NGINX serves static files directly
# =============================================================================

# 'events' block is required but we use defaults
events {
    worker_connections 1024;
}

http {
    # 'upstream' defines a group of backend servers
    # 'api' is the Docker Compose service name - Docker DNS resolves this
    upstream api_backend {
        # 'api' is resolved to the container's IP by Docker's internal DNS
        # Port 5000 is where our Flask app listens
        server api:5000;
    }

    server {
        # Listen on port 80 (HTTP)
        listen 80;

        # Requests to /api/* go to Python backend
        location /api/ {
            # proxy_pass forwards the request to our upstream
            proxy_pass http://api_backend;

            # These headers pass client information to the backend
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        # Health check endpoint for NGINX itself
        location /health {
            proxy_pass http://api_backend;
        }

        # All other requests get a simple response
        # In production, this would serve your frontend files
        location / {
            return 200 'Secure App Frontend\n';
            add_header Content-Type text/plain;
        }
    }
}
```

**Key concepts explained:**

| Concept                | Explanation                                                                 |
| ---------------------- | --------------------------------------------------------------------------- |
| 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 <a href="#step-4-create-docker-compose-file" id="step-4-create-docker-compose-file"></a>

#### **File: `docker-compose.yml`**

```yml
# =============================================================================
# DOCKER COMPOSE CONFIGURATION
# =============================================================================
# This file defines our entire application stack:
# - What containers to run
# - How they connect to each other
# - What data to persist
# =============================================================================

services:
  # ---------------------------------------------------------------------------
  # DATABASE SERVICE
  # ---------------------------------------------------------------------------
  db:
    # Official PostgreSQL image, version 15 on Alpine Linux (smaller)
    image: postgres:15-alpine

    # Container name makes it easier to reference in commands
    container_name: secure-db

    # Environment variables configure PostgreSQL
    # In production, use secrets management instead of plain text!
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secretpassword
      POSTGRES_DB: appdb

    # Named volume persists database files
    # Without this, all data would be lost when container stops
    volumes:
      - postgres_data:/var/lib/postgresql/data

    # Health check - Docker periodically runs this to verify DB is ready
    # pg_isready is a PostgreSQL utility that checks if DB accepts connections
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s    # Check every 10 seconds
      timeout: 5s      # Wait max 5 seconds for response
      retries: 5       # Mark unhealthy after 5 failures

  # ---------------------------------------------------------------------------
  # PYTHON API SERVICE
  # ---------------------------------------------------------------------------
  api:
    # Build from Dockerfile in ./api directory
    build: ./api

    container_name: secure-api

    # Environment variables available inside the container
    # Our Flask app reads these with os.getenv()
    environment:
      DB_HOST: db              # 'db' is the service name above
      DB_USER: appuser
      DB_PASSWORD: secretpassword
      DB_NAME: appdb
      ENVIRONMENT: production

    # Wait for database to be healthy before starting API
    # This prevents "connection refused" errors on startup
    depends_on:
      db:
        condition: service_healthy

    # Health check for the API
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s  # Give app time to start before checking

  # ---------------------------------------------------------------------------
  # NGINX SERVICE (FRONTEND/PROXY)
  # ---------------------------------------------------------------------------
  nginx:
    image: nginx:alpine

    container_name: secure-nginx

    # Map host port 8080 to container port 80
    # Access the app at http://localhost:8080
    ports:
      - "8080:80"

    # Mount our custom nginx config
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro  # :ro = read-only

    # Wait for API to be healthy
    depends_on:
      api:
        condition: service_healthy

# -----------------------------------------------------------------------------
# VOLUMES
# -----------------------------------------------------------------------------
# Named volumes are declared here. Docker manages the storage location.
volumes:
  postgres_data:
```

### Step 5: Build and Run <a href="#step-5-build-and-run" id="step-5-build-and-run"></a>

```bash
# Build and start all services
docker compose up -d --build
```

**What this command does:**

| Flag      | Meaning                           |
| --------- | --------------------------------- |
| `up`      | Create and start containers       |
| `-d`      | Detached mode (run in background) |
| `--build` | Rebuild images before starting    |

**Watch the startup:**

```bash
# View logs from all services
docker compose logs -f
```

**Verify everything is running:**

```shellscript
# Check service status
docker compose ps
```

**Expected output:**

```bash
NAME            IMAGE              STATUS                   PORTS
secure-db       postgres:15-alpine Up (healthy)             5432/tcp
secure-api      secure-app-api     Up (healthy)             5000/tcp
secure-nginx    nginx:alpine       Up                       0.0.0.0:8080->80/tcp
```

### Step 6: Test the Application <a href="#step-6-test-the-application" id="step-6-test-the-application"></a>

```bash
# Test the frontend
curl http://localhost:8080/

# Test the health endpoint
curl http://localhost:8080/health

# Test the API info endpoint
curl http://localhost:8080/api/info
```

**Expected responses:**

```bash
# Frontend
Secure App Frontend

# Health
{"status":"ok"}

# API Info
{"database_host":"db","environment":"production","service":"Python API"}
```

### Step 7: Cleanup <a href="#step-7-cleanup" id="step-7-cleanup"></a>

```shellscript
# Stop and remove containers
docker compose down

# Also remove the volume (deletes database data!)
docker compose down -v
```

## Lab 2: Security Audit with Trivy <a href="#lab-2-security-audit-with-trivy" id="lab-2-security-audit-with-trivy"></a>

**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? <a href="#what-is-trivy" id="what-is-trivy"></a>

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:**

<figure><img src="https://1736120781-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlvAAyThlvtKMDypgmoOh%2Fuploads%2FVFWcf5Sl63d31VJeulfN%2Fimage.png?alt=media&#x26;token=e5402a14-88b3-42a5-a834-901aa119e6f3" alt=""><figcaption></figcaption></figure>

### Step 1: Install Trivy <a href="#step-1-install-trivy" id="step-1-install-trivy"></a>

**macOS:**

```bash
brew install trivy
```

**Linux (Debian/Ubuntu/Kali):**

```bash
# Add the Trivy repository
sudo apt-get install -y wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy
```

**Using Docker (works everywhere - no installation needed):**

```bash
# Run Trivy as a container - scan another image
docker run --rm aquasec/trivy image python:3.11-slim

# To scan local images, mount Docker socket
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image my-local-image:latest
```

**Verify installation:**

```shellscript
trivy --version
```

### Step 2: Scan a Vulnerable Image - See the Problem <a href="#step-2-scan-a-vulnerable-image---see-the-problem" id="step-2-scan-a-vulnerable-image---see-the-problem"></a>

Let's scan an intentionally old image to understand what vulnerabilities look like and why they matter:

```bash
# Pull an old Python image (Python 3.6 reached end-of-life in December 2021)
# This means no more security patches since then - over 3 years of unfixed vulnerabilities!
docker pull python:3.6

# Scan it with Trivy
trivy image python:3.6
```

**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):**

```
Total: 1247 (UNKNOWN: 3, LOW: 432, MEDIUM: 517, HIGH: 256, CRITICAL: 39)
```

| Category | Meaning                                                         |
| -------- | --------------------------------------------------------------- |
| 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:**

```
┌──────────────┬────────────────┬──────────┬───────────────────┬──────────────────┐
│   Library    │ Vulnerability  │ Severity │ Installed Version │  Fixed Version   │
├──────────────┼────────────────┼──────────┼───────────────────┼──────────────────┤
│ openssl      │ CVE-2023-0286  │ CRITICAL │ 1.1.1d-0          │ 1.1.1t-0         │
│ glibc        │ CVE-2023-4911  │ CRITICAL │ 2.28-10           │ 2.28-10+deb10u2  │
│ curl         │ CVE-2023-38545 │ HIGH     │ 7.64.0-4          │ 7.64.0-4+deb10u7 │
└──────────────┴────────────────┴──────────┴───────────────────┴──────────────────┘
```

**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:**

```bash
# Too much output? Focus on critical issues first
trivy image --severity CRITICAL python:3.6

# Or HIGH and CRITICAL together
trivy image --severity HIGH,CRITICAL python:3.6
```

**Why This Matters - Real Attack Scenarios:**

| CVE Example    | What An Attacker Could Do                                          |
| -------------- | ------------------------------------------------------------------ |
| 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  <a href="#step-3-compare-with-a-secure-image---see-the-solution" id="step-3-compare-with-a-secure-image---see-the-solution"></a>

Now scan a modern, minimal image to see how proper image selection dramatically reduces risk:

```bash
trivy image python:3.11-slim
```

**Expected result:**

```
Total: 68 (UNKNOWN: 0, LOW: 52, MEDIUM: 14, HIGH: 2, CRITICAL: 0)
```

**The difference is dramatic:**

| Metric                | python:3.6 | python:3.11-slim | Reduction |
| --------------------- | ---------- | ---------------- | --------- |
| 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?**

| Factor            | Old Image (python:3.6)                    | Modern Image (python:3.11-slim)            |
| ----------------- | ----------------------------------------- | ------------------------------------------ |
| 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 <a href="#step-4-scan-your-own-image" id="step-4-scan-your-own-image"></a>

Now let's scan the image we built in Lab 1 to verify our security practices work:

```bash
cd secure-app

# Rebuild to ensure we have the latest
docker compose build

# Scan our API image
trivy image secure-app-api
```

**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:**

| Issue                     | Solution                                             |
| ------------------------- | ---------------------------------------------------- |
| 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 <a href="#step-5-scan-more-than-just-images" id="step-5-scan-more-than-just-images"></a>

Trivy can scan many things beyond just Docker images:

#### **Scan a filesystem (your project folder):**

```bash
# Scan current directory for vulnerabilities in package files
# (package.json, requirements.txt, go.mod, etc.)
trivy fs .

# Scan with specific severity
trivy fs --severity HIGH,CRITICAL .
```

#### **Scan a Dockerfile for misconfigurations:**

```bash
# Check if your Dockerfile follows security best practices
trivy config ./Dockerfile

# Scan entire directory for config files
trivy config .
```

**Common misconfigurations Trivy finds:**

| Issue                   | Why It's Bad                                        |
| ----------------------- | --------------------------------------------------- |
| 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:**

```bash
# Scan for passwords, API keys, private keys in your code
trivy fs --scanners secret .
```

### Step 6: Automated Security Gate <a href="#step-6-automated-security-gate" id="step-6-automated-security-gate"></a>

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:**

```bash
# Exit with error code 1 if CRITICAL vulnerabilities found
trivy image --exit-code 1 --severity CRITICAL python:3.6
echo "Exit code: $?"  # Will show 1 (failure)

# Same for secure image
trivy image --exit-code 1 --severity CRITICAL python:3.11-slim
echo "Exit code: $?"  # Will show 0 (success)
```

**How this works in CI/CD:**

```bash
# Example: GitHub Actions workflow
name: Security Scan
on: [push]
jobs:
  trivy-scan:
    runs-on: ubuntu-latest
    steps:
      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'  # Fail the build if vulnerabilities found
```

**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 <a href="#step-7-generate-reports" id="step-7-generate-reports"></a>

Generate reports for documentation, compliance, or sharing with your team:

```bash
# Table format (human-readable)
trivy image python:3.6 > vulnerability_report.txt

# JSON format (for processing with scripts, SIEM integration)
trivy image --format json python:3.6 > report.json

# HTML format (for sharing via web)
trivy image --format template --template "@contrib/html.tpl" python:3.6 > report.html

# SARIF format (for GitHub Code Scanning)
trivy image --format sarif python:3.6 > report.sarif
```

#### Security Best Practices Summary <a href="#security-best-practices-summary" id="security-best-practices-summary"></a>

| Practice                   | How To Implement                                      |
| -------------------------- | ----------------------------------------------------- |
| 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 <a href="#lab-2-key-takeaways" id="lab-2-key-takeaways"></a>

| Concept           | Key Takeaway                                                      |
| ----------------- | ----------------------------------------------------------------- |
| 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 <a href="#lab-3-debugging-container-issues" id="lab-3-debugging-container-issues"></a>

**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 <a href="#scenario-1-container-exits-immediately" id="scenario-1-container-exits-immediately"></a>

**The Problem:**

You run a container but it exits right away:

```bash
# Clean up any existing test containers first
docker rm -f test test1 test2 test3 2>/dev/null

# Run a container
docker run --name test ubuntu
docker ps    # Nothing shows up!
docker ps -a # Shows container with "Exited (0)" status
```

**Why This Happens:**

A container runs only as long as its main process runs. Here's the key concept:

<figure><img src="https://1736120781-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlvAAyThlvtKMDypgmoOh%2Fuploads%2FJOXaTqdTY2iD3jxE7Xz1%2Fimage.png?alt=media&#x26;token=5f464a63-a564-4ed2-b57f-c2fb84549d61" alt=""><figcaption></figcaption></figure>

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:**

```bash
# Clean up first
docker rm -f test 2>/dev/null

# Option 1: Interactive mode - attach a terminal
# -i = keep STDIN open (interactive)
# -t = allocate a pseudo-TTY (terminal)
docker run -it --name test ubuntu bash
# You're now inside the container! Type 'exit' to leave.

# Option 2: Run a command that never exits
docker run -d --name test2 ubuntu sleep infinity
# Container runs forever in background

# Option 3: For debugging - run and then exec into it
docker run -d --name test3 ubuntu tail -f /dev/null
docker exec -it test3 bash
# Now you can investigate inside the container
```

**Flag Reference:**

| Flag             | What It Does                                                    |
| ---------------- | --------------------------------------------------------------- |
| `-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:**

```shellscript
docker rm -f test test2 test3 2>/dev/null
```

### Scenario 2: Cannot Connect to Container <a href="#scenario-2-cannot-connect-to-container" id="scenario-2-cannot-connect-to-container"></a>

**The Problem:**

Your app is running but you can't access it from your browser or curl:

```bash
# Start nginx (a web server)
docker run -d --name web nginx

# Try to access - fails!
curl http://localhost:80
# curl: (7) Failed to connect to localhost port 80: Connection refused
```

**Why This Happens:**

<figure><img src="https://1736120781-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlvAAyThlvtKMDypgmoOh%2Fuploads%2F8BeXXjlfJsRCtxzumlkr%2Fimage.png?alt=media&#x26;token=994bc743-c59e-4c56-a12f-6260263b3e43" alt=""><figcaption></figcaption></figure>

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:**

```bash
# Remove the broken container
docker rm -f web

# Start with port mapping: -p HOST_PORT:CONTAINER_PORT
docker run -d --name web -p 8080:80 nginx

# Now test it
curl http://localhost:8080
# Success! You see the nginx welcome page HTML

# Check what ports are mapped
docker port web
# Output: 80/tcp -> 0.0.0.0:8080
```

**Port Mapping Syntax:**

```bash
-p HOST_PORT:CONTAINER_PORT

Examples:
-p 8080:80        Your machine:8080 → Container:80
-p 3000:3000      Same port on both sides
-p 127.0.0.1:8080:80   Only localhost can connect (more secure)
-p 8080-8090:80-90     Range of ports
```

**Debugging Port Issues:**

```shellscript
# Check if container is running
docker ps

# Check mapped ports
docker port web

# Check container's exposed ports
docker inspect web --format '{{.Config.ExposedPorts}}'

# Check if something else is using the port on your host
lsof -i :8080   # macOS/Linux
netstat -an | grep 8080   # Windows
```

### Scenario 3: Check What's Inside a Running Container <a href="#scenario-3-check-whats-inside-a-running-container" id="scenario-3-check-whats-inside-a-running-container"></a>

**The Problem:**

Your container is running but something isn't working. You need to look inside to debug.

<figure><img src="https://1736120781-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlvAAyThlvtKMDypgmoOh%2Fuploads%2Fu4UAtrgJHcUyxfLD5zEb%2Fimage.png?alt=media&#x26;token=d77cc2c7-06cf-4ecb-92f1-01f9ae1502c5" alt=""><figcaption></figcaption></figure>

**The Solution - Docker Exec:**

```bash
# Make sure we have a running container
docker run -d --name app nginx

# Execute a single command inside the container
docker exec app ls /etc/nginx/
# Shows files in the nginx config directory

# Get an interactive shell inside the container
docker exec -it app bash

# Now you're INSIDE the container! Try these commands:
pwd                          # See current directory
ls -la                       # List files
cat /etc/nginx/nginx.conf    # Read nginx config
ps aux                       # See running processes
env                          # See environment variables
whoami                       # See current user
exit                         # Leave the container
```

**What Docker Exec Does:**

| Command                        | Effect                                                |
| ------------------------------ | ----------------------------------------------------- |
| `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:**

```bash
# Check running processes
docker exec app ps aux

# Check network configuration
docker exec app cat /etc/hosts
docker exec app cat /etc/resolv.conf

# Check environment variables
docker exec app env

# Check disk usage
docker exec app df -h

# Check if you can reach another container
docker exec app ping -c 3 other-container

# Check application logs inside container
docker exec app cat /var/log/nginx/error.log

# Check if a port is listening
docker exec app netstat -tlnp
```

### Scenario 4: View Container Logs <a href="#scenario-4-view-container-logs" id="scenario-4-view-container-logs"></a>

**The Problem:**

Your container is crashing or behaving strangely. You need to see what it's outputting.

**The Solution - Docker Logs:**

```bash
# View all logs (may be long!)
docker logs app

# Follow logs in real-time (like tail -f)
# Press Ctrl+C to stop following
docker logs -f app

# Show timestamps with each log line
docker logs -t app

# Show only the last 50 lines
docker logs --tail 50 app

# Show logs from the last 5 minutes
docker logs --since 5m app

# Show logs from the last 2 hours
docker logs --since 2h app

# Combine options: last 100 lines with timestamps, follow
docker logs --tail 100 -t -f app
```

**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.js
* Configure your app's logging framework to output to stdout

<figure><img src="https://1736120781-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlvAAyThlvtKMDypgmoOh%2Fuploads%2FGeoBkDqhMPGOeLHvdJ6F%2Fimage.png?alt=media&#x26;token=857e79cd-3542-42ef-968b-fd7fb1c05916" alt=""><figcaption></figcaption></figure>

### Scenario 5: Inspect Container Details <a href="#scenario-5-inspect-container-details" id="scenario-5-inspect-container-details"></a>

**The Problem:**

You need detailed information about a container: its IP address, environment variables, mounts, configuration.

**The Solution - Docker Inspect:**

```bash
# Full JSON dump of everything about the container
docker inspect app
# This outputs A LOT of information!

# Get specific information using Go template syntax
# Container status
docker inspect app --format '{{.State.Status}}'

# Container IP address
docker inspect app --format '{{.NetworkSettings.IPAddress}}'

# For containers on custom networks
docker inspect app --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'

# When the container was started
docker inspect app --format '{{.State.StartedAt}}'

# What command the container is running
docker inspect app --format '{{.Config.Cmd}}'

# Environment variables
docker inspect app --format '{{.Config.Env}}'

# Mounted volumes
docker inspect app --format '{{.Mounts}}'
```

### Scenario 6: Container Uses Too Many Resources <a href="#scenario-6-container-uses-too-many-resources" id="scenario-6-container-uses-too-many-resources"></a>

**The Problem:**

A container is consuming too much CPU or memory, affecting other containers or your system.

**The Solution - Monitor and Limit:**

```bash
# Live resource usage (like 'top' for containers)
# Press Ctrl+C to exit
docker stats

# Monitor specific container
docker stats app

# Output shows:
# CONTAINER  CPU %  MEM USAGE / LIMIT  MEM %  NET I/O  BLOCK I/O  PIDS
```

**Understanding the Stats:**

| Column            | What It Means                       |
| ----------------- | ----------------------------------- |
| 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:**

```bash
# Limit memory to 256MB and CPU to 50%
docker run -d --name limited \
  --memory="256m" \
  --cpus="0.5" \
  nginx

# Check limits are applied
docker inspect limited --format '{{.HostConfig.Memory}}'
docker inspect limited --format '{{.HostConfig.NanoCpus}}'
```

### Scenario 7: Clean Up Disk Space <a href="#scenario-7-clean-up-disk-space" id="scenario-7-clean-up-disk-space"></a>

**The Problem:**

Docker is using a lot of disk space. You need to reclaim it.

**The Solution - Docker System Commands:**

```bash
# Show Docker disk usage summary
docker system df

# Detailed breakdown
docker system df -v

# Remove stopped containers
docker container prune

# Remove unused images (not used by any container)
docker image prune

# Remove dangling images only (untagged)
docker image prune

# Remove ALL unused images (more aggressive)
docker image prune -a

# Remove unused volumes (WARNING: may delete data!)
docker volume prune

# Remove unused networks
docker network prune

# Nuclear option - remove EVERYTHING unused
# WARNING: This removes all stopped containers, unused networks,
# dangling images, and build cache. Use with caution!
docker system prune

# Even more aggressive - also remove unused images
docker system prune -a
```

**Before Using Prune Commands:**

Always check what will be removed:

```bash
# See what containers would be removed
docker container ls -a --filter status=exited

# See what images would be removed
docker images --filter dangling=true

# See what volumes would be removed (volumes with data!)
docker volume ls --filter dangling=true
```

#### Debugging Cheatsheet <a href="#debugging-cheatsheet" id="debugging-cheatsheet"></a>

| Problem                     | Commands to Investigate                                   |
| --------------------------- | --------------------------------------------------------- |
| 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 <a href="#clean-up-after-this-lab" id="clean-up-after-this-lab"></a>

```
docker rm -f web app limited 2>/dev/null
```

## Docker Cheatsheet <a href="#docker-cheatsheet" id="docker-cheatsheet"></a>

### Container Lifecycle <a href="#container-lifecycle" id="container-lifecycle"></a>

```bash
# CREATE AND RUN
docker run nginx                         # Run container from image
docker run -d nginx                      # Run in background (detached)
docker run -it ubuntu bash               # Interactive with terminal
docker run --name myapp nginx            # Give container a name
docker run -p 8080:80 nginx              # Map ports (host:container)
docker run -v mydata:/data nginx         # Mount volume
docker run -e DEBUG=true nginx           # Set environment variable
docker run --rm nginx                    # Remove when stopped

# MANAGE RUNNING CONTAINERS
docker ps                                # List running containers
docker ps -a                             # List ALL containers
docker stop <container>                  # Graceful stop (SIGTERM)
docker kill <container>                  # Force stop (SIGKILL)
docker start <container>                 # Start stopped container
docker restart <container>               # Stop + Start
docker rm <container>                    # Remove container
docker rm -f <container>                 # Force remove (even if running)

# INTERACT WITH CONTAINERS
docker exec -it <container> bash         # Shell into container
docker exec <container> <command>        # Run command in container
docker logs <container>                  # View logs
docker logs -f <container>               # Follow logs (live)
docker logs --tail 100 <container>       # Last 100 lines
docker attach <container>                # Attach to running container
docker cp file.txt container:/path       # Copy file into container
docker cp container:/path file.txt       # Copy file from container
```

### Image Management <a href="#image-management" id="image-management"></a>

```bash
# BASICS
docker images                            # List images
docker pull nginx                        # Download image
docker push myrepo/myimage               # Upload image
docker rmi <image>                       # Remove image
docker tag image:v1 image:latest         # Add tag to image

# BUILD
docker build -t myimage .                # Build from Dockerfile
docker build -t myimage:v1 .             # Build with tag
docker build -f Dockerfile.prod .        # Use specific Dockerfile
docker build --no-cache -t myimage .     # Build without cache
```

### Volumes <a href="#volumes" id="volumes"></a>

```bash
docker volume create mydata              # Create volume
docker volume ls                         # List volumes
docker volume inspect mydata             # Volume details
docker volume rm mydata                  # Remove volume
docker volume prune                      # Remove unused volumes

# MOUNTING
-v mydata:/data                          # Named volume
-v /host/path:/container/path            # Bind mount
-v /host/path:/container/path:ro         # Read-only bind mount
--tmpfs /path                            # tmpfs (RAM)
```

### Networks <a href="#networks" id="networks"></a>

```bash
docker network create mynet              # Create network
docker network ls                        # List networks
docker network inspect mynet             # Network details
docker network connect mynet container   # Add container to network
docker network disconnect mynet container # Remove from network
docker network rm mynet                  # Remove network

# RUN ON SPECIFIC NETWORK
docker run --network mynet nginx
```

### Docker Compose <a href="#docker-compose" id="docker-compose"></a>

```bash
docker compose up                        # Start services
docker compose up -d                     # Start in background
docker compose up --build                # Rebuild before starting
docker compose down                      # Stop and remove
docker compose down -v                   # Also remove volumes
docker compose ps                        # List services
docker compose logs                      # View logs
docker compose logs -f                   # Follow logs
docker compose logs api                  # Logs for specific service
docker compose exec api bash             # Shell into service
docker compose build                     # Build images
docker compose pull                      # Pull latest images
docker compose restart                   # Restart services
```

### Cleanup <a href="#cleanup" id="cleanup"></a>

```bash
docker system df                         # Show disk usage
docker system prune                      # Remove unused data
docker system prune -a                   # Remove ALL unused data
docker container prune                   # Remove stopped containers
docker image prune                       # Remove dangling images
docker volume prune                      # Remove unused volumes
docker network prune                     # Remove unused networks
```

#### Debugging <a href="#debugging" id="debugging"></a>

```bash
docker logs <container>                  # View logs
docker logs -f <container>               # Follow logs
docker inspect <container>               # Full details (JSON)
docker stats                             # Live resource usage
docker top <container>                   # Processes in container
docker port <container>                  # Port mappings
docker diff <container>                  # Filesystem changes
docker events                            # Real-time events
```

### Dockerfile Instructions <a href="#dockerfile-instructions" id="dockerfile-instructions"></a>

```docker
FROM image:tag              # Base image
RUN command                 # Execute during build
COPY src dest               # Copy files into image
ADD src dest                # Copy + extract/download
WORKDIR /path               # Set working directory
ENV KEY=value               # Environment variable
ARG KEY=value               # Build-time variable
EXPOSE 80                   # Document port
USER username               # Switch user
VOLUME /data                # Declare mount point
ENTRYPOINT ["cmd"]          # Fixed command
CMD ["args"]                # Default arguments
HEALTHCHECK CMD curl ...    # Health check
```

### Docker Compose YAML <a href="#docker-compose-yaml" id="docker-compose-yaml"></a>

```yml
services:
  web:
    image: nginx                    # Use existing image
    build: ./dir                    # OR build from Dockerfile
    build:
      context: ./dir
      dockerfile: Dockerfile.prod
    container_name: mycontainer     # Fixed name
    ports:
      - "8080:80"                   # Port mapping
    volumes:
      - ./code:/app                 # Bind mount
      - data:/var/lib/data          # Named volume
    environment:
      - DEBUG=true                  # Env variable
    env_file:
      - .env                        # Load from file
    depends_on:
      - db                          # Start after db
    networks:
      - frontend                    # Connect to network
    restart: unless-stopped         # Restart policy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost"]
      interval: 30s
      timeout: 10s
      retries: 3

volumes:
  data:                             # Named volume

networks:
  frontend:                         # Custom network
```

### Security Scanning (Trivy) <a href="#security-scanning-trivy" id="security-scanning-trivy"></a>

```bash
trivy image nginx                        # Scan image
trivy image --severity HIGH,CRITICAL img # Only high/critical
trivy image --exit-code 1 nginx          # Exit 1 if vulns found
trivy image --format json nginx          # JSON output
trivy fs /path/to/project                # Scan filesystem
trivy config /path                       # Scan Dockerfiles
```

## Quick Reference Card <a href="#quick-reference-card" id="quick-reference-card"></a>

### Most Used Commands <a href="#most-used-commands" id="most-used-commands"></a>

| Task                 | Command                                     |
| -------------------- | ------------------------------------------- |
| 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 <a href="#port-mapping" id="port-mapping"></a>

```bash
-p HOST:CONTAINER

-p 8080:80     Your machine:8080 → Container:80
-p 3000:3000   Same port on both
-p 127.0.0.1:8080:80   Only localhost (not network)
```

#### Volume Mounting <a href="#volume-mounting" id="volume-mounting"></a>

```bash
-v NAME:/path           Named volume (Docker manages)
-v /host/path:/path     Bind mount (your folder)
-v /host/path:/path:ro  Read-only bind mount
```
