Module 7 · DevOps · Drills

Drills: Git & Docker

These are the commands and files you'll run a hundred times on the job. Type each one yourself in a real terminal before revealing the solution — fluency here is what makes you look like a professional.

How to use this page Each drill is a small task. Attempt it first in a terminal or editor, then click "Show solution" to compare. Run the Docker drills against the DocChat backend if you have it; otherwise any small FastAPI app works. Tick each box as you go; your progress is saved in this browser.

A · Warm-up reps Basic

Drill 1 git flow

From scratch: initialise a repo, commit everything, create a feature/login branch, then push main to a remote called origin.

Show solution
git init
git add .
git commit -m "Initial commit"

git checkout -b feature/login   # new branch + switch

git checkout main               # back to main
git remote add origin git@github.com:you/app.git
git push -u origin main

The -u sets origin/main as the upstream, so later you can just type git push.

Drill 2 .gitignore

Write a .gitignore for a Python project that excludes byte-code, the virtual environment, the secrets file, and editor noise.

Show solution
.gitignore
__pycache__/
*.pyc
.venv/
venv/
.env
.vscode/
.DS_Store

If .env was already committed, add it here and run git rm --cached .env to stop tracking it.

Drill 3 dockerfile

Write a Dockerfile for the FastAPI backend: Python 3.13 slim, install from requirements.txt, run on port 8000 reachable from outside the container.

Show solution
Dockerfile
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["fastapi", "run", "main.py", "--port", "8000", "--host", "0.0.0.0"]

Build and run: docker build -t app . then docker run -p 8000:8000 app. Drop --host 0.0.0.0 and you won't reach it.

B · Stretch Intermediate

Drill 4 compose

Write a compose.yaml running two services: your api (built locally, port 8000) and a db using Postgres 17, with a volume so data survives restarts.

Show solution
compose.yaml
services:
  api:
    build: .
    ports:
      - "8000:8000"
    depends_on:
      - db

  db:
    image: postgres:17
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Start it with docker compose up -d. The api reaches Postgres at host db — the service name is the hostname.

Drill 5 secrets

This code has a hardcoded secret. Move it into an environment variable read by pydantic-settings.

# Before — DON'T do this
OPENAI_API_KEY = "sk-live-abc123"
Show solution
.env  (git-ignored)
OPENAI_API_KEY=sk-live-abc123
config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    openai_api_key: str
    model_config = SettingsConfigDict(env_file=".env")

settings = Settings()
# use settings.openai_api_key — never the literal

Add .env to .gitignore first. In production you set OPENAI_API_KEY on the host, no file at all.

C · Build challenge Build

Mini-project Containerise DocChat. Produce the three files that let anyone run the full stack with one command: a Dockerfile for the FastAPI backend, a compose.yaml wiring it to Postgres, and a .env holding the config — kept out of Git. This is the exact deliverable that makes next lesson's deploy trivial.

Build · containerise DocChat

Write all three files, then bring the stack up and confirm the API is reachable.

Show solution
Dockerfile
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["fastapi", "run", "main.py", "--port", "8000", "--host", "0.0.0.0"]
compose.yaml
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: ${DATABASE_URL}
      OPENAI_API_KEY: ${OPENAI_API_KEY}
    depends_on:
      - db

  db:
    image: postgres:17
    environment:
      POSTGRES_USER: docchat
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: docchat
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
.env  (git-ignored)
DATABASE_URL=postgresql://docchat:secret@db:5432/docchat
OPENAI_API_KEY=sk-...
# bring it up; Compose auto-loads .env for ${...} substitution
docker compose up -d
curl http://localhost:8000/docs   # FastAPI is alive

The ${DATABASE_URL} syntax pulls values from .env at startup — so secrets live in one ignored file, not in compose.yaml. Confirm git status never lists .env.

D · Rapid recall Flashcards

Click a card to flip it. Say the answer out loud before you flip — that's the rep that builds storage strength.

Image vs container?
Image = frozen blueprint; container = a running instance of it. One image, many containers.
click to flip
What is .gitignore for?
Listing paths Git must never track — secrets, .venv/, caches — keeping them out of history.
click to flip
What does git checkout -b x do?
Creates branch x and switches to it in one step.
click to flip
Dockerfile CMD for FastAPI?
CMD ["fastapi","run","main.py","--port","8000","--host","0.0.0.0"]
click to flip
What does docker compose do?
Defines and runs a multi-service stack (app + db) from one compose.yaml with one command.
click to flip
Where do secrets live?
In environment variables — a git-ignored .env locally, host config in production. Never in code.
click to flip

E · Self-check before moving on

Tick each only if you can do it without looking:

Next All ticked? DocChat is version-controlled and containerised — now we put it on the internet. Next: Lesson 7.2 — Deploy & CI.