Containers Crash Course uses impress.js (CSS 3D transforms). Open it in a modern desktop browser.

Part 1 · The HooksWhy does my app work on my laptop but break on the server?
Part 1 · The HooksPackage your work in containers that run the same anywhere: your laptop, a friend's PC, a cloud server.
Part 1 · The HooksGet hands-on with the same Linux commands that cloud engineers use every day.
A hands-on crash course

Containers.
Dockerized.

Lead, AWS User Group Cebu|Co-Lead, PizzaPy|Admin, IoT Cebu
Who this is for

Builders who are new to all this.

By the end: you'll have built, run, and curl-tested a real containerized API, and you'll know why Podman is the safer sibling worth meeting next.

The problem we are solving

“It works on my laptop.”

Different OS. Different Python version. A dependency installed years ago that nobody wrote down. A missing env var. A port already in use.

Containers end that conversation. Your app ships with its whole little world around it.

Part 2 · How we got here

Bare metal → VMs → containers.

1960s–90s Bare metal one app = one server (expensive, idle, fragile) 2000s Virtual Machines many apps per server, but each with a whole OS copy 2013 → today Containers (Docker) share the host kernel, isolate everything else
A one-slide OS primer

What's an operating system?

Why Linux keeps showing up

Linux is the gateway.

Teasers for all of these at the end. Worth the self-study after this workshop.

The reveal

Containers are not really about packaging.

That's the surface win. The deeper reason they exist: isolation. Shrink the blast radius when something goes wrong.

WITHOUT ISOLATION app A app B app C one breach → whole server owned WITH CONTAINERS app A app B app C one breach → blast radius contained
A whole little world, in a box.
That is a container.
Part 3 · Setup

Get a Linux shell. Install Docker.

Yes, Docker runs on Windows and Mac too. We use Linux (via WSL2) because that's what real cloud servers run, and the muscle memory transfers.

Windows users · Install WSL2

Get Ubuntu running inside Windows.

If the Store fails: open PowerShell as Admin → wsl --install -d Ubuntu-24.04 → reboot if asked.

Shell quick tour

The commands you'll use every minute.

pwd              # where am I?
ls -la           # list files (long, show hidden)
cd ~/projects    # change directory
whoami           # which user am I?
cat file.txt     # print a file to the screen
mkdir myapp      # make a new folder
nano notes.md    # simple text editor (Ctrl-O save, Ctrl-X exit)
Install Docker Engine on Linux / WSL2

Not Docker Desktop. Just the engine.

Docker Desktop needs a paid license for larger companies. Engine-only is free, same CLI.

# 1. Add Docker's official apt repo
curl -fsSL https://get.docker.com | sudo sh

# 2. Let your user run docker without sudo
sudo usermod -aG docker $USER
newgrp docker

# 3. Verify
docker --version
docker run hello-world
Part 4 · Hands-on

Your first real container.

Run a real web server · one command

nginx in a container.

docker run -d -p 8080:80 --name web nginx

Now open http://localhost:8080 in your browser. You're running nginx, in Linux, in a container, and never typed apt install nginx.

Daily commands you'll type forever

Living with your containers.

docker ps                 # running containers
docker ps -a              # including stopped
docker logs web           # what's it saying?
docker logs -f web        # follow logs (Ctrl-C to stop)
docker exec -it web bash  # a shell INSIDE the container
docker stop web           # graceful stop
docker rm web             # remove it
docker images             # local images you've pulled/built
docker pull python:3.12-slim
docker rmi hello-world    # remove an image
docker system prune       # clean up unused stuff
The site we are about to containerize · cebu-eats

One file. Plain HTML. No JS, no CSS.

Eight dishes, six spots. Tags mean what they say, so the browser renders it readable even with zero styling.

<!-- cebu-eats/site/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Cebu Eats &amp; Spots</title>
</head>
<body>

<h1>Cebu Eats & Spots</h1>
<p>Eight dishes you must try. Six places you must see.</p>

<h2>Eats</h2>
<dl>
  <dt><b>Lechon</b> <i>(Carcar / Talisay)</i></dt>
  <dd>Spit-roasted suckling pig. Crackling skin, lemongrass-stuffed belly, no sauce needed.</dd>

  <dt><b>Tuslob Buwa</b> <i>(Pasil, Cebu City)</i></dt>
  <dd>Pig brain and liver simmered into a frothy dip. Eaten by hand with puso, elbow-to-elbow at a street stall.</dd>

  <dt><b>Puso</b> <i>(all over Cebu)</i></dt>
  <dd>Hanging rice woven inside coconut leaves. The default pairing for lechon, bbq, and tuslob buwa.</dd>

  <dt><b>Humba</b> <i>(Cebuano household)</i></dt>
  <dd>Pork belly slow-braised in soy, vinegar, brown sugar, and tausi. Deep, sweet, savory.</dd>

  <dt><b>Ngohiong</b> <i>(Chinese-Cebuano)</i></dt>
  <dd>Five-spice spring roll, deep-fried, served with sweet-and-sour sauce. Street-food royalty.</dd>

  <dt><b>SuTuKil</b> <i>(Mactan seaside)</i></dt>
  <dd>Sugba, Tuwa, Kilaw: grilled, in soup, raw-cured. Pick your fish, pick all three preparations.</dd>

  <dt><b>Sinuglaw</b> <i>(Cebuano fusion)</i></dt>
  <dd>Grilled pork belly meets kinilaw. Smoky sugba on top of vinegar-cured raw fish. Two worlds in one bowl.</dd>

  <dt><b>Siomai sa Tisa</b> <i>(Tisa, Cebu City)</i></dt>
  <dd>Pork-and-shrimp dumplings steamed in bamboo, dunked in chili-vinegar-soy. An entire alley of stalls competes for your pesos.</dd>
</dl>

<h2>Spots</h2>
<dl>
  <dt><b>Magellan's Cross</b> <i>(Cebu City)</i></dt>
  <dd>Planted in 1521 by Magellan's crew. The symbolic start of Christianity in the Philippines.</dd>

  <dt><b>Basilica del Santo Ni&ntilde;o</b> <i>(Cebu City)</i></dt>
  <dd>The country's oldest Roman Catholic church, founded in 1565. Home of the revered Santo Niño image.</dd>

  <dt><b>Kawasan Falls</b> <i>(Badian)</i></dt>
  <dd>Turquoise three-tiered waterfall at the end of a short jungle hike. The headline canyoneering destination.</dd>

  <dt><b>Oslob Whale Sharks</b> <i>(Oslob)</i></dt>
  <dd>Snorkel beside wild butanding at dawn. Go early, go ethical.</dd>

  <dt><b>Moalboal Sardine Run</b> <i>(Moalboal)</i></dt>
  <dd>Millions of sardines moving as one silver cloud, five meters off the shore of Panagsama Beach.</dd>

  <dt><b>Tops Lookout</b> <i>(Busay hills)</i></dt>
  <dd>The city laid out below at sunset. Bring a jacket, the wind is honest.</dd>
</dl>

<hr>
<p><small>Static page, served by nginx in one container. Workshop sample for <b>Containers Crash Course</b>.</small></p>

</body>
</html>
Your first Dockerfile · cebu-eats

Three lines. A real website in a container.

Base image, copy your files in, declare the port. That's the whole recipe for a static site.

# cebu-eats/Dockerfile
FROM nginx:alpine
COPY site /usr/share/nginx/html
EXPOSE 80

That's it. Three real lines. The whole site lives in cebu-eats/site/ and nginx serves it.

Build it · run it · open it

Your city, in a container.

# in cebu-eats/
docker build -t cebu-eats .

docker run -d -p 127.0.0.1:8080:80 \
  --name eats cebu-eats

open http://localhost:8080

Image is small (around 90 MB, almost all nginx). You get one page, two sections: Eats (lechon, tuslob buwa, sinuglaw, siomai sa Tisa, and more) and Spots (Kawasan, Oslob, Tops, and more). No JS, no CSS, pure semantic HTML.

Next sample · pickle match PH

Same recipe, bigger payload.

A FastAPI CRUD service with a plain-JS frontend. Post a pickleball game, list open games, delete yours. One container, but now there's a backend talking.

EndpointDoes
POST /matchescreate a game listing
GET /matcheslist all games
GET /matches/{id}one game
PUT /matches/{id}update it
DELETE /matches/{id}remove it

Code lives in app/ next to this deck. Clone it and follow along.

The Dockerfile · line by line

Your app's recipe.

FROM python:3.12-slim          # small, pinned base

ENV PYTHONUNBUFFERED=1         # print logs immediately

WORKDIR /app                    # working dir inside the container

COPY requirements.txt ./        # deps first (cache-friendly)
RUN pip install -r requirements.txt

COPY main.py ./                 # then the app
COPY static ./static

RUN useradd app && chown -R app /app
USER app                        # NOT root. critical.

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Build it · then run it

From Dockerfile to running container.

# inside the app/ folder
docker build -t pickle-api .

# run it, publish to localhost only (safer than 0.0.0.0!)
docker run -d \
  -p 127.0.0.1:8000:8000 \
  -v $(pwd)/data:/app/data \
  --name pickle pickle-api

# visit it
open http://localhost:8000

-v $(pwd)/data:/app/data is a volume. Data survives container restart because it lives on your laptop's disk.

curl tests · prove your API works

No Postman needed.

# CREATE
curl -X POST http://localhost:8000/matches \
  -H "Content-Type: application/json" \
  -d '{"venue":"Ayala Terraces Court 2","date":"2026-10-20 19:00",
       "skill_level":"beginner","players_needed":2,"organizer_name":"Ron"}'

# LIST
curl http://localhost:8000/matches

# GET ONE / UPDATE / DELETE  (ID from the create response)
curl http://localhost:8000/matches/<id>
curl -X PUT    http://localhost:8000/matches/<id> -H "Content-Type: application/json" -d '{...}'
curl -X DELETE http://localhost:8000/matches/<id>

Full script: curl-tests.sh in the deck repo.

Volumes · why your data survives

Containers are disposable. Data is not.

docker rm -f pickle
docker run -d -p 127.0.0.1:8000:8000 \
  -v $(pwd)/data:/app/data \
  --name pickle pickle-api
curl http://localhost:8000/matches   # your matches are still there
Part 5 · Doing it well

Best practices (and what to never do).

Dockerfile · DOs

Habits that scale.

Do

  • Pin versions: python:3.12-slim not python:latest
  • Pick a slim or alpine base when you can
  • Add a non-root USER at the end
  • Write a .dockerignore (skip .git, __pycache__, .env)
  • Copy dependency files BEFORE app code (layer cache)
  • Use multi-stage builds to ship only what runs

Don't

  • Tag with :latest. Silent breaking changes.
  • Run as root. Container escape then equals host root.
  • COPY . . at the top. Every code change busts the cache.
  • Bake secrets (API keys, passwords) into the image
  • Install build tools you don't actually need at runtime
Security pitfalls

Containers are not magic security.

Badly configured containers leak just like anything else. The big ones:

Isolation is the whole point. Don't disable it to make a tutorial easier.

Pick tools that are friendly to all three

Machine. Human. LLM.

In the AI era, your tool stack is read by three audiences. Keep all three happy.

AudienceLikesHates
The machinelow CPU, low RAM, small imagesbloat, slow startup
Humansreadable, simple, well-namedcleverness, deep inheritance
The LLMsmall codebases that fit in contextsprawling repos it can't hold

Starter Python/Node is fine. For a leaner next step: Go (tiny binaries, fast startup) and Rust (safer, no runtime). Both drop container image sizes dramatically and give Claude/Cursor less to chew on.

Part 6 · After today

Where to go next (self-study).

Five on-ramps. Each is one evening of study.

docker compose

When your app needs more than one container.

One YAML file describes all your services. One command brings the stack up.

# docker-compose.yml
services:
  api:
    build: ./backend
    ports: ["8000:8000"]
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: devpass
docker compose up -d
docker compose logs -f
docker compose down
Podman · the safer sibling

Rootless. Daemonless. Same CLI.

sudo apt install -y podman
alias docker=podman
podman run -d -p 127.0.0.1:8000:8000 pickle-api
nginx for load balancing

One nginx, many backends.

Lives on the host, not in a container. That's the point, and the reason we taught Linux.

sudo apt install -y nginx

# /etc/nginx/sites-available/pickle
upstream pickle_backend {
  server 127.0.0.1:8001;
  server 127.0.0.1:8002;
  server 127.0.0.1:8003;
}
server {
  listen 80;
  location / { proxy_pass http://pickle_backend; }
}

sudo nginx -t && sudo systemctl reload nginx
fail2ban · stop brute force at the host

Jail attackers automatically.

sudo apt install -y fail2ban

# /etc/fail2ban/jail.local
[sshd]
enabled  = true
maxretry = 5

[nginx-botsearch]
enabled  = true
logpath  = /var/log/nginx/access.log

sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

Add nginx rate-limiting with limit_req_zone for the belt-and-suspenders combo.

And the big words you'll keep hearing

A one-liner each.

Part 7 · Close

What you can do now.

Thank you · Q&A

Containers.
Dockerized.

Lead, AWS User Group Cebu|Co-Lead, PizzaPy|Admin, IoT Cebu
Containers Crash Course
1 / 00