Skip to content
AyoKoding

Overview

Prerequisites

  • Prior topics: Backend Essentials -- the small HTTP service this course hosts (run locally and hit with curl); Just Enough Bash -- the shell, ssh, and reading a script, all assumed fluent because this course's artifacts ARE shell scripts, systemd unit files, and proxy configs rather than application code.
  • Tools & environment: a macOS/Linux terminal; ssh; a single cheap Linux VM or a local VM; systemd; a reverse proxy (Caddy or Nginx) with automatic TLS; ufw; and, for the PaaS contrast, a git-push platform. No application framework is taught here.
  • Assumed knowledge: running a service locally and reaching it with curl; basic Linux file and permission concepts; using git.

Why this exists -- the big idea

A deployment is just a process running on a machine, reachable through a proxy, over TLS, restarted when it dies, reproducible from config. Every one of this topic's 78 worked examples is a small, complete illustration of one piece of that substrate -- from provisioning a box and logging in over SSH, through running a systemd-managed service behind a TLS-terminating reverse proxy on a real domain, to deploying the same app once via a git-push PaaS for contrast, and finally capturing the whole box as reproducible scripts and a tested backup.

Cross-cutting big ideas, taught here and carried through every tier: abstraction-and-its-cost -- a PaaS hides the box; self-hosting shows you what it was hiding and what you pay to peel that abstraction back; taming-state -- a service's lifecycle, config, and data must be managed deliberately, because on your own box nothing absorbs that responsibility for you.

How this topic's examples are organized

  • Beginner (Examples 1-16) -- the substrate, built by hand one primitive at a time: provisioning a Linux VM and recording its IP, key-only SSH, a non-root sudo user, a ufw firewall, installing the runtime, running the service in the foreground, a first systemd unit, enable-on-boot, reading status and journalctl logs, restart-on-crash, installing a reverse proxy, proxying public traffic to the app's local port, a DNS A record, automatic TLS via ACME, env-file configuration, and the hard rule that secrets never live in the repo.
  • Intermediate (Examples 17-32) -- operating and automating: a health-check endpoint, a timer- based uptime check, a reproducible setup.sh, making that script idempotent, secret rotation, proxy security headers, a least-privilege firewall audit, log rotation, installing a git-push PaaS, a git push deploy, buildpack and Dockerfile builds compared, PaaS env config and TLS, a rollback, and a zero-downtime restart.
  • Advanced (Examples 33-78) -- resilience, data, and the trade-off: a data-backed service with a persistent volume, a scripted tested backup and a restore drill, reboot resilience, multiple services on one box, systemd resource limits, a metrics endpoint, a git-driven deploy pipeline, a self-hosted-vs-managed writeup, a disaster rebuild, fail2ban, staging-vs-prod promotion, then a deep run of further systemd patterns (timer, socket activation, drop-in, graceful shutdown), the full proxy and TLS surface (Caddy-vs-Nginx, HTTPS redirect, HSTS, OCSP stapling, proxy rate limiting, multi-domain TLS), DNS CNAME/TTL/subdomain routing, env templating and the EnvironmentFile directive, secret rotation automation and a pre-commit secret scanner, structured logging and log shipping, health-failure alerting, a systemd watchdog and restart backoff, Procfile/release-phase/scaling PaaS patterns, blue-green and connection-drain deploys, restic and pg_dump backups with retention and restore verification, and a cloud-init first-boot reprovision -- closing with a capstone preview.
  • Capstone -- take a small service and fully self-host it on one box: SSH-hardened, firewalled, systemd-managed with restart-on-failure, behind a reverse proxy with automatic TLS on a real domain, configured via env with no committed secrets, with a tested backup and restore -- all captured as reproducible scripts -- then deploy the same app once more via a git-push PaaS for contrast.

Every example is a real shell script, systemd unit, Caddy/Nginx config, cron entry, or setup script -- minimal application code -- colocated under learning/code/ and followable on a single cheap VM or a local VM. Where an example shows an "Expected" block, it is the verification step's observable outcome (a systemctl status line, a curl response, a journalctl entry), framed as what you should see when you run the accompanying verify command on your own box.

%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161, Gray #808080
%% Concept clusters in the order this topic teaches them (co-01 through co-22)
graph TD
    A["Provision + reach:<br/>VM, SSH keys, hardening,<br/>firewall<br/>co-02 to co-05"]:::blue
    B["Run the service:<br/>systemd unit, lifecycle,<br/>restart-on-crash<br/>co-06 to co-08"]:::orange
    C["Expose it:<br/>reverse proxy, TLS,<br/>DNS, env + secrets<br/>co-09 to co-13"]:::teal
    D["Operate it:<br/>logs, health, resilience,<br/>PaaS git-push deploy<br/>co-14 to co-18"]:::purple
    E["Protect the data:<br/>backups, reproducible<br/>setup scripts<br/>co-19, co-21"]:::brown
    F["Decide the altitude:<br/>self-host vs managed,<br/>when to stay managed<br/>co-20, co-22"]:::gray
 
    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
 
    classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
    classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
    classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
    classDef purple fill:#CC78BC,stroke:#000000,color:#FFFFFF,stroke-width:2px
    classDef brown fill:#CA9161,stroke:#000000,color:#FFFFFF,stroke-width:2px
    classDef gray fill:#808080,stroke:#000000,color:#FFFFFF,stroke-width:2px

Concepts

Every worked example in this topic's follow-up pages cites the co-NN concept it exercises -- this section is the 1:1 reference those citations point back to.

co-01 · What Self-Hosting Is

Self-hosting is running your own software on a machine you control, versus handing it to a fully managed platform that hides the box from you.

Why it matters: a managed platform is an abstraction over the exact primitives this course exposes by hand -- once you have run a process, a proxy, TLS, a firewall, and a restart policy yourself, you can reason about deployment anywhere, including inside the managed platform when its abstraction leaks (and it always leaks).

Verify it: Example 41 stands the same app up both self-hosted and on a managed PaaS and writes up the concrete trade-off; the capstone then proves a full self-host is reproducible end to end.

co-02 · Provision a Box

Creating a Linux VM or instance and getting initial access is the first, reproducible step of any self-host -- without a box, none of the later primitives have a place to run.

Why it matters: provisioning is where reproducibility either begins or never does -- recording the box's IP and the exact steps that produced it is what lets Example 42's disaster rebuild succeed later.

Verify it: Example 1 provisions a VM and records its IP; Example 78's cloud-init user-data codifies the whole first-boot so a fresh box converges to the same baseline unattended.

co-03 · SSH Access and Keys

Key-based SSH -- not passwords -- is the secure default for reaching a remote box: a private key on your laptop authenticates against a public key on the server, with no secret transmitted.

Why it matters: passwords are brute-forceable and reuseable across boxes; a key pair is unguessable and scoped to one machine, which is why disabling password login (Example 2) closes the single most common remote-compromise vector.

Verify it: Example 2 generates a key, logs in with it, and verifies password login is refused; Example 43 adds fail2ban to throttle the brute-force attempts that still hit the SSH port.

co-04 · Basic Server Hardening

A non-root user, key-only SSH, and a firewall together form the minimum safe baseline for any box exposed to the internet -- running services as root with password SSH and no firewall is a compromise waiting to happen.

Why it matters: hardening is defense in depth -- each layer (no root login, no passwords, least-privilege ports) independently blocks a different attack class, so a failure in one layer does not hand the attacker the whole box.

Verify it: Examples 3 and 4 add the non-root user and the firewall; Example 22 layers security headers at the proxy; Example 43 adds brute-force protection.

co-05 · Firewall Basics

ufw (or nftables) allows only the ports a service genuinely needs (SSH, HTTP, HTTPS) and denies the rest by default -- the single most effective way to shrink a box's attack surface.

Why it matters: a service bound to a port you forgot about is reachable by the whole internet without a firewall; the default-deny stance means "open port" becomes a deliberate, auditable decision rather than an accident.

Verify it: Example 4 opens SSH/HTTP/HTTPS and verifies a closed port is unreachable; Example 23 audits the open set down to the true minimum.

co-06 · Run a Service

Starting your application process on the box so it actually serves requests is the literal definition of hosting -- everything else exists to keep this one process reachable, safe, and alive.

Why it matters: running it in the foreground first (Example 6) separates "does the app work" from "does the supervision work," which is the cleanest way to debug a deployment that won't respond.

Verify it: Example 6 runs the service in the foreground and curls it locally; Example 33 wraps a data-backed version of the same idea.

co-07 · systemd Service Units

A systemd unit declares how to start a service, what to do when it fails, and that it should start on boot -- turning "a process I launched by hand" into "a managed service the OS owns."

Why it matters: a unit file is reproducible configuration (a taming-state move) replacing a fragile "I typed this command in a shell that one time" -- it is what makes restart-on-crash and boot-resilience expressible at all.

Verify it: Example 7 writes the unit; Example 48 adds socket activation; Example 49 uses a drop-in override; Example 62 uses EnvironmentFile; Example 68 adds a watchdog; Example 38 sets resource limits.

co-08 · Process Lifecycle

Start, stop, status, enable, and reading logs (journalctl) are the operations that manage a running service day to day -- the vocabulary every later resilience example assumes.

Why it matters: systemctl status and journalctl -u are the first two commands reached for when "is it up and why did it fail" -- knowing them cold is what turns an outage into a five-minute diagnosis instead of a guess.

Verify it: Example 9 reads status and logs; Example 21 rotates a secret and reloads; Example 50 tunes graceful shutdown; Example 47 replaces a cron job with a timer.

co-09 · Reverse Proxy

A reverse proxy terminates public traffic on ports 80/443 and forwards it to the app on a local, unprivileged port -- letting one box serve many services behind one public face and adding TLS, headers, and rate limiting at a single chokepoint.

Why it matters: binding your app directly to port 443 means re-implementing TLS, virtual hosting, and graceful shutdown inside the app; a proxy centralizes all of that and lets the app stay simple.

Verify it: Examples 11 and 12 install and configure the proxy; Example 37 hosts two services behind one proxy; Example 51 contrasts Caddy and Nginx; Example 56 adds rate limiting at the proxy.

co-10 · TLS and HTTPS

Automatic TLS via ACME (Let's Encrypt) gives the service a real HTTPS certificate with no manual renewal chore -- the proxy obtains and rotates the cert on its own.

Why it matters: HTTPS is now table stakes for any public service -- browsers warn on plain HTTP, and many APIs refuse it -- and ACME is the automation that makes "real TLS on a personal box" practical rather than a weekend of openssl suffering.

Verify it: Example 14 enables ACME TLS; Example 53 forces the HTTP-to-HTTPS redirect; Example 54 adds HSTS; Example 55 enables OCSP stapling; Example 57 serves multiple domains.

co-11 · DNS and Domains

An A (or AAAA) record points a domain name at the box's IP so the service is reachable by name -- without DNS, ACME cannot prove domain control and no browser can find you.

Why it matters: DNS is the mapping that turns "an IP" into "a name a certificate can be issued for" -- it is the precondition for both TLS (co-10) and human-usable URLs, and getting the record type and TTL right avoids subtle propagation bugs.

Verify it: Example 13 sets the A record; Example 58 contrasts CNAME vs A; Example 59 covers TTL and propagation; Example 60 routes subdomains to multiple services.

co-12 · Environment Config

Service configuration and non-secret values live in the environment (an env file on the box), never hardcoded in the repo -- the same artifact runs everywhere, differing only by its environment.

Why it matters: separating config from code is what lets the identical image run in staging and prod -- the difference between two environments becomes a set of env variables, not a second copy of the application.

Verify it: Example 15 configures via an env file; Example 29 does the same on a PaaS; Example 61 templates the env file at deploy time; Example 62 uses systemd's EnvironmentFile directive.

co-13 · Secrets on a Server

Secrets are set out of band (an env file with locked-down permissions on the box), never committed to the repo -- the hard-iron "no secrets in git" rule applies to servers exactly as it applies to source.

Why it matters: a committed secret is permanent (git history is forever) and a single accidental push away from public -- the server-side discipline is to keep every secret in a mode-0600 file the repo never sees, and to scan for slips before they land.

Verify it: Example 16 proves the repo is clean; Example 21 rotates a secret; Example 63 automates rotation; Example 64 adds a pre-commit hook that blocks secrets.

co-14 · Logs and Basic Monitoring

Reading logs (journalctl) and exposing a simple health check tell you whether the service is alive and why it failed -- the floor of observability, without which you are flying blind.

Why it matters: "is it working" is unanswerable without logs and a health probe -- the moment a service has users, the gap between "it crashed" and "someone told us it crashed" is exactly the gap monitoring closes.

Verify it: Example 17 exposes a health endpoint; Example 18 runs an uptime check; Example 24 adds log rotation; Example 39 adds a metrics endpoint; Examples 65 and 66 cover structured logging and shipping; Example 67 alerts on health failure.

co-15 · Restart and Resilience

A service must come back after a crash or a reboot without manual intervention -- Restart= for crashes and enable + WantedBy for boot make "alive" the default state rather than a coincidence.

Why it matters: the difference between a 3am phone call and an unbroken night is whether the box recovers on its own -- resilience is the property that turns a single crash from an incident into a non-event.

Verify it: Example 10 restarts on crash; Example 36 verifies the whole stack survives a reboot; Example 68 adds a watchdog for hung (not crashed) processes; Example 69 adds a restart backoff.

co-16 · PaaS git-push Deploy

A PaaS builds and deploys your app from a git push, hiding the box while keeping it yours -- the contrast case that shows what the manual substrate was automating.

Why it matters: feeling the PaaS absorb the build, the release, the process supervision, and the TLS in one push is what makes the trade-off in co-20 concrete -- you can finally name what you pay a managed platform for and what you give up when you self-host.

Verify it: Example 25 installs a PaaS; Example 26 deploys via git push; Example 40 wires a full pipeline; Example 44 runs staging and prod with promotion; Examples 70-72 cover Procfile, release phase, and scaling.

co-17 · Buildpacks vs Dockerfile

A PaaS builds from either a buildpack (it detects your language and supplies the runtime) or a Dockerfile (you specify the image exactly) -- each is a reproducible recipe for the runtime, trading convenience for control.

Why it matters: a buildpack gets you deployed with zero image authoring but constrains you to what it detects; a Dockerfile is fully explicit and portable but you own every layer -- knowing which you reached for is knowing where your build's reproducibility actually comes from.

Verify it: Example 27 deploys via a buildpack; Example 28 deploys the same app via a Dockerfile and checks parity.

co-18 · Zero-Downtime Basics

A health-checked rolling restart (or a blue-green swap) avoids dropping requests during a deploy -- the light version of what a full orchestrator automates at fleet scale.

Why it matters: a naive "stop then start" deploy drops every in-flight request at the moment of the stop; draining connections first (or swapping to a warmed-up instance) is what keeps a deploy invisible to a caller.

Verify it: Example 32 does a health-checked zero-downtime restart; Example 73 does a blue-green swap via the proxy; Example 74 drains in-flight requests before stop.

co-19 · Backups Basics

A service with data needs a simple, tested backup of that data -- "tested" meaning you have actually restored from it, not just that the backup file exists.

Why it matters: an untested backup is a wish, not a safeguard -- the restore drill (Example 35) is the only proof a backup is worth anything, and it is the step most often skipped until the first real data loss makes it tragically obvious.

Verify it: Example 34 takes a scripted backup; Example 35 restores from it; Example 75 uses restic for files; Example 76 uses pg_dump for a database; Example 77 adds retention and restore verification.

co-20 · Self-Hosting vs Managed Trade-off

Self-hosting buys control and cost savings at the price of the operational responsibility (patching, backups, 3am uptime) a managed platform absorbs -- a genuine trade-off, not a strict upgrade.

Why it matters: the right answer depends on which side of the trade-off your situation is on -- a solo developer learning the substrate and a small team shipping a product with no ops appetite are on opposite sides, and confusing the two is how teams inherit pager duty they never wanted.

Verify it: Example 41 writes up the trade-off concretely; Example 45 turns it into a scoped recommendation; the capstone's final step names when each wins.

co-21 · Reproducible Server Config

The box's setup is captured as scripts and config so it can be rebuilt, not remembered -- a setup.sh (or a cloud-init user-data) is what turns "the box works" into "the box works again, on fresh hardware, without me."

Why it matters: reproducibility is the difference between a box you can lose and recover and one whose loss is a rebuild-from-memory marathon -- Example 42's disaster rebuild succeeds only because Example 19 captured the setup first.

Verify it: Example 19 writes the setup.sh; Example 20 makes it idempotent; Example 42 rebuilds from it after a total loss; Example 78 codifies the same setup as cloud-init.

co-22 · When to Stay Managed

Recognising when a managed platform is the right call -- small team, no ops appetite, stateful or high-availability or compliance-bound workload -- is as important a skill as knowing how to self-host.

Why it matters: the discipline of self-hosting is worthless if applied indiscriminately -- a stateful, HA, compliance-bound system is a poor first self-host, and "we should self-host this" without naming the specific force driving it is a decision waiting to become an incident.

Verify it: Example 45 gives a concrete recommendation; Example 41's writeup names the conditions under which managed is the correct call.

Examples by Level

Beginner (Examples 1–16)

Intermediate (Examples 17–32)

Advanced (Examples 33–78)


← Previous: Overview · Next: Beginner Examples

Last updated July 29, 2026

Command Palette

Search for a command to run...