AI Development Handbook preview

Chapter 2: Get running - tools, Docker, repository

Before you write application code, you need Docker running, git tracking, and a project directory. This chapter gets you there in under an hour. Everything downstream - the example apps, Stripe webhooks, Dokploy deploys - assumes the foundation you build here.

Learning objectives

After this chapter you will be able to:

  • Install Docker and verify it works
  • Run your first container and serve a static page
  • Initialize a git repository with proper ignore rules
  • Set up a Makefile for repeatable commands

Why this matters

Every example app in Chapters 3–6, every Stripe webhook handler you write, every Dokploy deploy in Chapter 7 - all of them assume Docker, git, and a Makefile. Skipping this chapter means debugging toolchain issues while you are trying to learn backend patterns. Spending the hour here saves days scattered across later chapters.

Tools you need

Before writing application code, confirm your environment matches what later chapters assume. This guide assumes a small set of tools that work the same way on macOS, Windows (via WSL2), and Linux.

Tool Purpose Required? Install guide
Docker All dev commands run in containers Yes Below + Appendix E
Terminal Run git, Docker, Makefile targets Yes Built-in
Git Version control Yes Below
Modern browser Test checkout and storefront pages Yes Built-in
Cursor AI-assisted editing Recommended cursor.com

You do not need Go or Node installed on your host - Docker provides toolchains. Pinning versions inside images means your laptop stays clean and every developer on the project shares the same Go, Node, and nginx versions.

Pinned versions

Every example app pins toolchain versions inside its Dockerfile. These are the versions used throughout the handbook as of May 2026:

Tool Version Where pinned
Go 1.26 FROM golang:1.26-alpine in each Go Dockerfile
Node 24 FROM node:24-alpine in each React Dockerfile
React 19 package.[json](https://www.json.org) in Portfolio and YouTube Dashboard
Vite 8 package.json in Portfolio and YouTube Dashboard
nginx stable FROM nginx:stable-alpine in each frontend Dockerfile
Ubuntu 24.04 LTS VPS instance (Chapters 7)
Stripe Go SDK v86 examples/stripe-saas/go.mod
Dokploy latest stable Verify current release at dokploy.com before installing (Chapter 7)

Git in two minutes

This guide assumes you can navigate a shell and use basic git. If you need a refresher, here are the commands you will use every session:

git status                         # what changed?
git add path/to/file               # stage changes
git commit -m "Describe the change" # save a checkpoint
git log --oneline -5               # recent history
git diff                           # unstaged changes
git restore .                      # undo all uncommitted changes

The git restore . command is your emergency reset when an agent produces bad edits - commit before starting agent work so you always have a clean rollback point.

Environment sanity check - run this after installing tools and save the output in project notes:

echo "=== Tool check ==="
git --version
docker --version
docker compose version
docker run --rm hello-world 2>&1 | tail -1

All commands should exit zero. On Windows, run inside WSL Ubuntu, not PowerShell.

Docker: what it is and why you need it

Every example app in this guide runs inside containers. That choice is deliberate - it solves recurring problems that derail first-time production deploys.

Why Docker for local development

Clean host machine. Without Docker, a typical developer laptop accumulates three Node versions via nvm, a system Python that macOS tools depend on, Go installed and upgraded once then mismatched with CI, and PostgreSQL or SQLite CLI tools installed globally. Each project README says “use version X.” Each project lies a little compared to what is actually on disk. Docker moves those toolchains inside images defined per repo. Your host only needs Docker and git.

Reproducible environments. When another developer - or you on a new laptop - clones the repo, docker compose up produces the same running state with the same toolchain versions. When something breaks, you debug the Dockerfile and compose file, not “works on my machine” folklore.

Parity with production. Dokploy (Chapter 7) deploys container images. It does not install Go via apt on the server and run go run. Local Docker uses the same abstraction: build an image, run a container, attach volumes, publish ports. If nginx serves your static site locally in Compose, the production change is mainly which host port and domain attach - not a different deployment religion.

Concern Without Docker With Docker
Toolchain version Per-developer drift Pinned in Dockerfile
Database file location Mystery path on host Named volume or bind mount in compose
“Works locally” Meaningless Same image runs on VPS
Onboarding time Hours of README tweaks make up

Docker concepts

Docker vocabulary is small but precise. Mixing terms causes confusion when reading logs or Dokploy docs.

Concept What it is You’ll use it for
Image Immutable filesystem snapshot + default command Built from Dockerfile in each example app
Container Running (or stopped) instance of an image Your app while make up is active
Volume Persistent storage managed by Docker SQLite database files, product uploads
Bind mount Host directory mapped into container path Live-reload source code during dev
Compose Multi-container orchestration from docker-compose.yml App + sidecars, env files, port maps
Registry Image storage (Docker Hub, GHCR) Pull nginx:alpine; push your app image in Chapter 7

Think of an image as a recipe and a container as the meal you just cooked. Delete the container; the image remains for the next run. Change the Dockerfile; rebuild the image before the next meal tastes different.

Images and layers

Images stack layers - each Dockerfile instruction creates one. Layers cache: if you change only app code, rebuilding reuses cached base layers.

FROM golang:1.26-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /server ./cmd/server

Rebuild after editing Go source; dependency download stays cached as long as go.mod is unchanged. This matters when example app images include full Node or Go toolchains - cold builds take minutes; incremental builds take seconds.

Volumes vs bind mounts

Volumes are managed by Docker - good for database files you do not need to edit by hand. Bind mounts map a host path - good for live code reload. Both live outside the container’s ephemeral filesystem.

  Container (ephemeral layer)
  ┌─────────────────────────┐
  │  App binary             │
  │  /data/app.db ──────────┼──► named volume (persists)
  └─────────────────────────┘

Warning: docker compose down -v deletes named volumes. Use intentionally - this wipes your SQLite database. Document backup steps before production (Chapter 9).

Compose networking

Compose services on the same project share a default network. The service name is the DNS hostname:

services:
  api:
    environment:
      DATABASE_URL: postgres://db:5432/app
  db:
    image: postgres:17-alpine

From inside api, host db resolves - not localhost. Publishing ports (ports:) is for your browser on the host, not container-to-container traffic. Chapter 5 (Fitness Tracker) uses this pattern with a Docker volume for SQLite.

  Host machine (your laptop)
  localhost:8081 ──► api container :8080
                         │
                         └──► db:5432 (internal DNS, not published)

Environment variables in Compose

Services read configuration from the environment - never hardcode secrets in images:

services:
  api:
    env_file:
      - .env
    environment:
      APP_ENV: development

.env stays on disk, never in image layers. Changing .env requires docker compose up -d again to recreate containers - a common surprise during Stripe key rotation in Chapter 6. The environment: block in compose overrides env_file keys with the same name; keep one source of truth to avoid chasing ghost values.

Installing Docker

Installation differs by OS. Detailed walkthroughs with troubleshooting are in Appendix E. For now, follow the quick reference:

OS Method Verify
macOS Docker Desktop from docker.com docker run hello-world
Windows Docker Desktop + WSL2 Ubuntu Same, inside WSL
Linux Engine + compose plugin from Docker’s apt/dnf repo Same, after docker group

On Linux, add your user to the docker group and re-login:

sudo usermod -aG docker "$USER"
newgrp docker
docker run hello-world

If docker run hello-world fails, go to Appendix E for platform-specific troubleshooting. You cannot proceed past this chapter without Docker working - every example app and every checkpoint exercise depends on it.

Your first container

Theory ends where typing begins. Run nginx and prove you can serve a page.

Run nginx

docker run --rm -d -p 8080:80 --name web nginx:alpine
curl -I http://localhost:8080
docker logs web
docker stop web

Flag breakdown:

  • --rm - remove container on stop (experiments only; do not use for databases)
  • -d - detached background
  • -p 8080:80 - host:container port map
  • --name web - friendly name for logs and stop
  • nginx:alpine - official image, small tag

curl -I should return HTTP/1.1 200 OK and Server: nginx.

Static site with Compose

Create a practice folder and serve a real HTML page with bind mounts:

mkdir -p ~/Projects/docker-checkpoint/public
cd ~/Projects/docker-checkpoint

public/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Docker checkpoint</title>
</head>
<body>
  <h1>Docker checkpoint</h1>
  <p>If you see this, nginx and Compose work.</p>
</body>
</html>

docker-compose.yml:

services:
  site:
    image: nginx:stable-alpine
    ports:
      - "8080:80"
    volumes:
      - ./public:/usr/share/nginx/html:ro
docker compose up -d
curl http://localhost:8080

Open http://localhost:8080 in a browser. Edit index.html on the host; refresh - nginx serves the updated file immediately because of the bind mount. This live-reload pattern runs through every example app in Chapters 3–6.

Stop the stack:

docker compose down

Inspecting and debugging

When something goes wrong, these commands answer “is it running?” and “what does it say?”:

docker compose ps                    # service status
docker compose logs site             # recent output
docker compose logs -f site          # follow live output
docker compose exec site sh          # interactive shell inside container
docker stats --no-stream             # CPU and memory usage

exec requires the container to be running. Use it to inspect the filesystem: ls /usr/share/nginx/html inside the container should show your index.html.

Port conflicts

If port 8080 is already in use, change the left side of the mapping:

ports:
  - "8081:80"

Use lsof -i :8080 on macOS/Linux to find what is using the port. Only one process can bind a host port at a time.

Docker security basics

These habits prevent painful incidents when your local dev workflow reaches a VPS serving paying customers:

  1. Never commit .env or mount secrets into images you push to a registry. Use env_file in Compose locally and Dokploy’s environment UI in production (Chapter 7). .env stays on disk, never in image layers.

  2. Run as non-root where possible. Production Dockerfiles should drop privileges when base images support it: USER nonroot:nonroot.

  3. Pin base images. Use nginx:stable-alpine, not :latest. Update pins deliberately after reading release notes - not on every random build.

  4. Limit exposed ports. Publish only what you need. On a VPS, firewall rules restrict inbound traffic to 80/443 on the reverse proxy - not every app port.

  5. Never mount /var/run/docker.sock. This pattern effectively grants host root to whoever compromises the container. Use dedicated CI runners or Dokploy builds instead.

  6. Prefer official and verified publisher images for nginx, postgres, and other infrastructure. Scan your own app images before deploy (Chapter 9).

Repository setup

Directory structure

Before you write application code in Chapters 3–6, your repository needs a predictable layout. Predictability matters because AI agents search by path, Makefiles assume service names, and Dokploy deploys whatever your Dockerfile copies.

your-product/
├── cmd/server/          ← main(): listen, routes, graceful shutdown
├── internal/            ← business logic (handlers, store)
│   ├── handlers/
│   └── store/
├── web/
│   ├── templates/
│   └── static/
├── content/             ← product copy, templates, or static assets (optional)
├── [openspec](https://github.com/Fission-AI/OpenSpec)/            ← spec-driven change proposals
├── scripts/             ← build, backup helpers
├── docker-compose.yml
├── Dockerfile
├── Makefile             ← single entry for humans and CI
├── .env.example         ← committed template - never commit .env
├── .gitignore
└── README.md

Key paths explained:

Path Purpose
cmd/server/ HTTP server entry point - wires router, middleware, ListenAndServe
internal/ Business logic - Go enforces that only code in this module can import it
web/ Presentation layer - templates and static files separated from Go packages
Makefile One entry point means you never memorize compose flags
.env.example Every required variable with placeholder values - committed template for collaborators

You do not need all of this on day one. The checkpoint exercise scaffolds the spine; Chapters 3–6 flesh out handlers and stores.

Git init and first commit

Initialize git before the first agent session on real code:

mkdir my-product && cd my-product
git init

.gitignore:

# Secrets and local env
.env
.env.local
.env.*.local

# Runtime data
data/
*.db
*.sqlite3

# Build artifacts
bin/
dist/
node_modules/
coverage/

# OS and editor
.DS_Store
Thumbs.db
.idea/
*.swp

# Backups
backups/

.env.example:

PORT=8080
SITE_URL=http://localhost:8080
DATABASE_PATH=./data/app.db
# STRIPE_SECRET_KEY=sk_test_REPLACE_ME

Only .env.example is tracked. Document in README: cp .env.example .env, then edit.

The template above matches the fitness-tracker example you will build in Chapter 5. If your project uses Stripe or external APIs, uncomment and add the relevant keys. Chapter 6 covers the Stripe variables you will need.

First meaningful commit

git add .
git commit -m "Initial commit: README and gitignore"

Configure identity if you have not already:

git config user.name "Your Name"
git config user.email "you@example.com"

Daily git workflow with AI agents

Agents edit multiple files per turn. Build this habit loop before every agent session:

git status                    # know your baseline
# ... agent work ...
git diff --stat               # review what changed
git diff                      # read the actual changes
git add internal/handlers/ cmd/server/
git commit -m "Add GET /health endpoint"

If an agent session goes wrong:

git restore .                 # undo all uncommitted changes

Warning: Never commit .env “because it’s just test keys.” Habits formed locally reach production. Stripe test keys still grant API access to your account. If you accidentally commit secrets, rotate them immediately.

Makefile for process management

Humans and CI should run the same commands. Makefiles wrap Docker Compose so you never memorize long docker compose invocations or wonder which override files apply.

Minimal Makefile

.PHONY: help up down test

.DEFAULT_GOAL := help

help:
    @echo "make up     Start stack"
    @echo "make down   Stop stack"
    @echo "make test   Run tests"

up:
    docker compose up -d --build

down:
    docker compose down

test:
    @echo "Tests pass - add real tests in Chapter 5"

One entry point means you never memorize compose flags. Adjust service names and test commands as your project grows. Make uses tabs for recipe lines - not spaces.

Daily Docker workflow

Internalize this loop until it is muscle memory:

  edit code on host
       ↓
  make up
       ↓
  curl / browser test
       ↓
  docker compose logs -f
       ↓
  fix → repeat
       ↓
  git commit (human reviewed)

When builds feel slow, distinguish cold builds (first go mod download) from incremental (only changed layers rebuild). Do not --no-cache unless debugging the Dockerfile itself - cache exists to save time.

Use the agent to generate Dockerfiles, Makefile targets, and .gitignore entries - read the output, run it, and commit the working version. If a generated Dockerfile fails to build, paste the error back into the agent and ask for the fix.

OpenSpec - a preview

OpenSpec is a spec-driven workflow for multi-file changes. It lives under openspec/ in your project and follows three phases: propose (write problem statement, design, and tasks), apply (implement tasks incrementally), and archive (move completed changes to history).

You will use OpenSpec in later chapters for features like Stripe webhooks and email flows. For now, know it exists and that its directory structure follows this pattern:

openspec/
├── config.yaml
└── changes/
    └── bootstrap/
        ├── proposal.md
        └── tasks.md

Agents read task checklists instead of guessing scope. Full details - including Agent mode, plan mode, skills, rules, MCP, and common failure modes - are in Appendix C. OpenSpec is optional for the checkpoint exercise below but becomes the default workflow for non-trivial changes starting in Chapter 5.

AI coding tools accelerate every phase in this handbook, but they fail in predictable ways - context drift, infinite loops, over-automation on sensitive code. Appendix C catalogs these failure modes and the verification habits that contain them. The habit starts here: after every agent session, run git diff and make up before you commit. If the agent produced garbage, git restore . and narrow the prompt. This loop - spec, apply, verify, commit or revert - is the foundation you will practice in every remaining chapter.

Checkpoint exercise

Complete these steps in order. They confirm your environment matches what every later chapter assumes.

1. Verify Docker

docker run hello-world
docker compose version

Both must succeed without sudo on Linux (configure group membership first).

2. Create repository scaffold

mkdir -p ~/Projects/my-product && cd ~/Projects/my-product
git init

Add .gitignore and .env.example using the content from the sections above, then:

git add .
git commit -m "Initial commit: README and gitignore"

3. Create Compose file and static page

mkdir -p public
# Write public/index.html and docker-compose.yml using the examples above

4. Add Makefile

Create a Makefile with up, down, and test targets using the minimal Makefile above.

5. Run and verify

make up
# Confirm http://localhost:8080 shows your page
make down

Success criteria

  • Docker works without sudo
  • make up serves a page at http://localhost:8080
  • .env is gitignored, .env.example is committed
  • At least one git commit exists
  • You can explain image vs container vs volume in one sentence each

Key takeaways

  • Docker containers isolate your entire toolchain - no Go, Node, or nginx installations needed on your host
  • Multi-stage builds keep production images small and attack surfaces minimal
  • A Makefile encodes your project’s commands as self-documenting targets
  • .env.example is committed; .env is gitignored - this habit prevents secret leaks

Further reading

  • Docker Get Started - official tutorial on images, containers, and Compose
  • Git Reference - official documentation for the commands you will use daily
  • GNU Make Manual - Makefile syntax and conventions
  • Appendix A: Environment reference (Available in full handbook) - every env var used across the example apps
  • Appendix E: OS setup (Available in full handbook) - platform-specific Docker installation walkthroughs

Common pitfalls

  • Installing Go or Node on the host while this guide uses Docker. If the Makefile runs Go inside a container, do not go test on the host and assume parity. Version skew breaks “works in container” guarantees.

  • Forgetting to start Docker Desktop on macOS/Windows. Every command fails with Cannot connect to the Docker daemon until the whale icon shows Running. Check docker info before starting work.

  • Committing .env “because it’s just test keys.” This habit reaches production. Stripe test keys grant API access to your account. Use .env.example only in git.