Free through lesson 3

Intro to Shipping and Running a Service

From choosing where to host, through hardening your server, Docker Compose, TLS with Caddy, your first deploy, backup and restore, rollbacks, a maintenance page, and updates from CI. Across 50 lessons you'll get to the point where you can keep one of your own services running on a VPS for a few dollars a month. Docker and Caddy are run for real on your own machine, and every result shown was actually measured.

Curriculum

The 50 lessons are split into 7 chapters. We recommend working through them in order starting from Chapter 1, but feel free to skim just the parts you're curious about. * The commands can't run in the browser, so try them in your own environment with Docker and Caddy installed. Test server settings on a disposable VPS, never straight on a live production server. Domains, IPs, and usernames have all been replaced with example values like deploy.example.com.

Chapter 1 — Where to Host (lessons 1–6)

Decide between a VPS, PaaS, or a container service, estimate how much it'll cost per month, get a domain, and point DNS at your server. We finish by confirming how to bundle the Caddy → app → db three-tier setup with compose.

Chapter 2 — Standing Up a Server (lessons 7–14)

Set up SSH key login, a working user with minimal sudo, hardened sshd, a firewall, automatic updates, and fail2ban, one at a time. We finish by building a checklist that mechanically checks all of it for gaps.

7

Logging In with an SSH Key

Generate an ed25519 key pair and put the public key in the server's authorized_keys. Why the private key stays on your own machine, and how to name your server in ~/.ssh/config.

🔒 Basic
8

A Working User with Minimal sudo

Never work as root — grant a working user sudo for only the commands it needs. The steps for checking sudoers with visudo -cf before installing it.

🔒 Basic
9

Hardening sshd

Lock down your SSH entry point through sshd_config. Disabling root login, key-only auth, limiting attempts, and how to apply it without locking yourself out.

🔒 Basic
10

Turning Off Password Authentication

Confirm key login works before closing off password authentication. Why the order matters, and restricting which users can log in with AllowUsers.

🔒 Basic
11

A Firewall That Denies by Default

Block inbound traffic by default with ufw, opening only SSH, HTTP, and HTTPS. Why the DB port stays closed, and double-checking against your provider's own packet filter.

🔒 Basic
12

Automatic Security Updates

Apply security updates automatically with unattended-upgrades. What its three configuration lines mean, and how to handle updates that need a reboot.

🔒 Basic
13

Blocking Brute Force with fail2ban

Count repeated SSH failures and ban an IP for a fixed time once it crosses the threshold. How to set maxretry, findtime, and bantime in jail.local, and layering it with key-only auth.

🔒 Basic
14

A Hardening Checklist

Check sshd, automatic updates, fail2ban, and firewall settings all at once with a single function. Close out Chapter 2 by mechanically catching anything you thought you'd set up but didn't.

🔒 Basic

Chapter 3 — Running the App in a Container (lessons 15–22)

Package your Django app in a Dockerfile and bundle it with PostgreSQL using docker compose. Confirm the spots that most often cause production incidents — healthchecks, .env, persistent volumes, and generating secrets — with real, measured results.

15

Packaging the App with a Dockerfile

Package a Django app into a python:3.13-slim image. Build the Dockerfile so collectstatic runs at build time and migrate runs at startup.

🔒 Basic
16

Layer Caching and Running as Non-Root

Speed up builds by COPYing requirements.txt first, and run as non-root with useradd and USER app. The standard playbook for a Dockerfile that produces a fast, safe image.

🔒 Basic
17

Bundling app and db with docker compose

Combine your app and PostgreSQL into one compose file, declaring startup order with depends_on's service_healthy. How to prevent the app from crashing before the DB is ready to accept connections.

🔒 Basic
18

Deciding "Usable" with a healthcheck

Starting up and being usable are different things. Judge healthy with pg_isready for db and a DB-touching /api/health for app, and have the deploy wait for that before proceeding.

🔒 Basic
19

Collecting Secrets in Environment Variables and .env

Never hardcode keys and passwords — collect them in .env on the server and pass them in via env_file. Use .env.example as a template, and catch gaps with a consistency checker.

🔒 Basic
20

A .env Pitfall: Mismatched Passwords and DEBUG

The single most common incident is DATABASE_URL's password not matching POSTGRES_PASSWORD. Catch that, along with DJANGO_DEBUG=1 left on in production, with a checker before startup.

🔒 Basic
21

Keeping Data with a Persistent Volume

Containers are disposable — data lives in a named volume. Confirm data survives restarting db, and avoid the pitfall of down -v wiping out your data along with it.

🔒 Basic
22

Generating Secrets Safely

Generate DJANGO_SECRET_KEY and DB passwords with secrets.token_urlsafe. What makes a value unguessable and safe to embed in DATABASE_URL without breaking it.

🔒 Basic

Chapter 4 — Reverse Proxy and TLS (lessons 23–30)

Put Caddy in front, receiving traffic on 443 and passing it to your app. Build automatic certificate issuance and renewal, static files, HTTPS redirects, running multiple services side by side, and the edge network — all with a Caddyfile and compose.

23

Putting Caddy in Front

Let a front-facing Caddy handle TLS termination and reverse proxying. A Caddyfile that's just your domain and a few lines of reverse_proxy, verified with real caddy validate.

🔒 Basic
24

Inspecting reverse_proxy with caddy adapt

Convert a reverse_proxy that receives on 443 and forwards to app:8000 into JSON with caddy adapt to inspect it. How the service name app is enough to find the backend.

🔒 Basic
25

Automatic TLS Certificate Issuance

Just write your domain, and Caddy fetches a certificate from Let's Encrypt and listens on 443. What automatic HTTPS assumes, and where to look when it fails.

🔒 Basic
26

Automatic Certificate Renewal

An expired certificate is a common incident for a personal service. How Caddy renews automatically before expiry, and confirming every listed hostname is covered.

🔒 Basic
27

Serving Static Files with whitenoise

Serve CSS and JS from Django with whitenoise instead of Caddy. The middleware, compressed hash-named storage, and how build-time collectstatic keeps it all inside one container.

🔒 Basic
28

HTTPS Redirects and Excluding health

Redirect http to https while excluding just the healthcheck's /api/health. SECURE_PROXY_SSL_HEADER, which tells Django it's on https behind a proxy, and what happens if you forget the exclusion.

🔒 Basic
29

Running Multiple Services on One Machine

Host multiple sites on one VPS by stacking blocks in the Caddyfile. The danger that validate still passes even after deleting one, and why you read the live file before editing it.

🔒 Basic
30

Separating edge and backend Networks

Separate edge, connecting the front Caddy to the app, from backend, connecting app to DB. Check the setup: app joins both, db joins only backend, and edge is a shared external network.

🔒 Basic

Chapter 5 — The First Deploy and Verification (lessons 31–38)

Combine check → transfer → build → verify into a deploy script and run your first deploy. Confirm migrations, the admin account, and SMTP connectivity, then finish with a pre-launch checklist that catches anything missing.

31

The Flow of a deploy Script

Combine local checks, rsync transfer, the maintenance page, build/restart, healthy-wait, and a connectivity check into one script. set -euo pipefail and shellcheck stop it from carrying on broken.

🔒 Basic
32

Local Checks: Never Transfer Broken Code

Run manage.py check with your local .venv's python before sending anything with rsync. Why you should catch it locally instead of on the server, and why the system's python won't do.

🔒 Basic
33

The First Deploy: Through to health 200

Run docker compose up -d --build to go from build to startup; success means migrate ran at startup and /api/health returns 200. What to place on the server, and the order for your very first deploy.

🔒 Basic
34

Apply Migrations at Startup

Apply schema changes automatically through the startup CMD so nothing is left unapplied. Checking with migrate --check and showmigrations, and why you should never run makemigrations on the server.

🔒 Basic
35

Creating an Admin Account Inside the Container

Create the admin account, your first task after launch, from inside the container with docker compose exec. The interactive way to do it in production, when to add -T, and the setup that follows.

🔒 Basic
36

Checking SMTP Connectivity

The email setup behind signup confirmation, password resets, and login codes. How EMAIL_HOST switches you over to SMTP, and how to map each kind of failure back to the environment variable to fix.

🔒 Basic
37

Protecting .env with rsync Exclusions

rsync --delete removes extra files on the receiving end. Exclude .env, var, staticfiles, and .venv so only the code gets updated, leaving the server's own secrets and build artifacts intact.

🔒 Basic
38

A Pre-Launch Checklist

List out 6 items — TLS, health, admin, SMTP, the signup path, and backups — and catch gaps with a function. Why you confirm backups before launch, and the bridge into Chapter 6.

🔒 Basic

Chapter 6 — Recovering When Something Breaks (lessons 39–45)

Take a backup with pg_dump and measure the full round-trip of deleting your data and restoring it. Build the safety net that keeps you from being done the moment you launch — code and image rollbacks, a maintenance page, and triaging from logs.

39

Taking Backups with pg_dump

Your data lives only inside PostgreSQL. backup.sh, which gzips the output of pg_dump, why --clean --if-exists makes restoring easier, and confirming the resulting file is real SQL.

🔒 Basic
40

Restoring: Delete, Restore, and Confirm

A backup only matters if it can be restored. Measure the full round-trip on a real stack: create a row, back it up, delete everything, and bring it back with restore.sh.

🔒 Basic
41

Taking Daily Backups with cron

Take backups automatically each morning with cron, deleting anything older than 14 days. How to read a crontab line, and why you also ship backups elsewhere to survive losing the whole server.

🔒 Basic
42

Rolling Back Code

Rolling back a broken release fast is the top priority. Steps for reverting to an earlier commit with git and redeploying, and why you restore the DB first when undoing a migration-included change.

🔒 Basic
43

Rolling Back an Image

Before deploying, tag the current myapp:latest with a date and stash it away. If something goes wrong, just swap in the stashed tag and run up -d to revert to the previous version without rebuilding.

🔒 Basic
44

A Maintenance Page: 503 from the Edge Caddy

The front Caddy returns a 503 and maintenance page based on one flag file. Why it needs no reload, why it works even if the app is down, and using Retry-After to signal it's temporary.

🔒 Basic
45

Reading Logs and Triaging by Symptom

Common symptoms have predictable causes. Map is restarting, unhealthy, DisallowedHost, and password authentication failed to their causes, and read the exception from docker compose logs' tail.

🔒 Basic

Chapter 7 — Keeping It Running (lessons 46–50)

Cover subsequent updates and automated deploys from CI, keeping cost and memory usage in check, and the bare minimum of monitoring. We close out all 50 lessons by summing them up as a pattern for operations: build it, ship it, keep it running.

Once you finish all 50 lessons, move on to GitHub Actions, which automates everything from push to deploy, or Terraform, which provisions the server itself as code. All courses unlock with a membership.