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 changesThe 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 -1All 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/serverRebuild 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 -vdeletes 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-alpineFrom 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-worldIf 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 webFlag 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 stopnginx: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-checkpointpublic/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:rodocker compose up -d
curl http://localhost:8080Open 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 downInspecting 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 usageexec 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:
Never commit
.envor mount secrets into images you push to a registry. Useenv_filein Compose locally and Dokploy’s environment UI in production (Chapter 7)..envstays on disk, never in image layers.Run as non-root where possible. Production Dockerfiles should drop privileges when base images support it:
USER nonroot:nonroot.Pin base images. Use
nginx:stable-alpine, not:latest. Update pins deliberately after reading release notes - not on every random build.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.
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.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 changesWarning: 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 versionBoth must succeed without sudo on Linux (configure group
membership first).
2. Create repository scaffold
mkdir -p ~/Projects/my-product && cd ~/Projects/my-product
git initAdd .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 above4. 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 downSuccess criteria
- Docker works without sudo
-
make upserves a page athttp://localhost:8080 -
.envis gitignored,.env.exampleis 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.exampleis committed;.envis 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 teston 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 infobefore 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.exampleonly in git.