Page ↑ / Page ↓ to move · scroll works too
Linux & DevOps — Lecture 5 of 8
Lecture 4 ended with a rollout one-liner you type over SSH by hand.
Today: enough Python to write a small program that does the typing for you, the moment you push.
Everything downstream of this lecture — git from Lecture 4, services and units from Lecture 3, bash from Lecture 2 — is already something you know. What’s missing is the thing that sits and waits — a small Python process that GitHub can talk to, that decides whether to trust what it hears, and that hands off to the same kind of deploy script you’d otherwise run by hand. We’ll start from nothing — variables, if/for, functions — build the listener, stand up the app it deploys, wire the two together, and only then go flip the switch on GitHub’s side.
If you've never written Python before, this is the whole language you need for today.
python3 alone drops you into the REPL — >>> is Python's prompt, answering back after each line, good for trying one thing at a time. A .py file is what you write once and run the same way every time, which is what today's listener will actually be.str, int, bool — text, numbers, true/false, no type declared up front[ ] — a list, an ordered, changeable sequence{ } — a dict, key-value pairs. This is exactly what a JSON object becomes once Python parses it.No braces, no then/fi like bash — the block that belongs to an if, for, or def is whatever’s indented one level further under its :. Get the indentation wrong and Python refuses to run the script at all, rather than just formatting it oddly.
payload["ref"] raises an error if that key is missing; payload.get("ref") returns None instead, and .get("branch", "unknown") supplies a fallback value. When you don't control what's inside a request, .get(...) is almost always the safer read.import brings a module's names into your script — you call hashlib.sha256(...), not just sha256(...)json, hashlib, os — all standard library, installed the moment python3 isfrom x import y pulls one name out directly, so you can write datetime.now() instead of datetime.datetime.now()json and hashlib came free with Python and are enough to parse a payload and check a signature. But something first has to hold a network port open, parse raw HTTP requests, and route them to the right function — that's a real, fiddly job, and rewriting it from scratch is a waste of a lecture. Flask is a library that already does it. It isn't part of the standard library, so — unlike json — it has to be installed.pip is Python's dnf — a package manager that fetches from PyPI (the Python Package Index) instead of a distro's repositories. Installing Flask pulls in Werkzeug alongside it, the lower-level piece Flask is actually built on.A venv is a private copy of Python’s package directory. Without it, pip install writes into the system Python that other tools on the box depend on — one version conflict and you’ve broken something unrelated. Every real deployment gets its own venv.
@app.route(...) is a decorator — it registers the function right below it to run whenever a POST request hits /webhook. Flask handles the socket, the HTTP parsing, and the routing; your job is just to write the function.Every script on the last few slides did something and exited — top to bottom, then done. app.run() is different: it blocks forever, listening. That’s not a bug, and it’s exactly why this can’t be something you leave attached to a terminal — it needs a service to hold it, the way myapp.service held a process open back in Lecture 4.
X-GitHub-Event — push, ping, and dozens of othersrequest.get_json() — the payload, already parsed into a dict for youSame three sections as Lecture 4’s myapp.service. ExecStart points at the venv’s own python, not the system one — that’s what actually pulls in Flask.
The route from the last part runs whatever code follows for any POST request, from anyone, no matter where it came from. Before this touches deploy, the listener has to prove the request was signed by GitHub with a secret only the two of you know — not just check that it looks like the right shape of JSON.
GitHub signs the raw request body with your shared secret and sends the result in X-Hub-Signature-256. You recompute the same HMAC on your end and compare. hmac.compare_digest instead of == — a plain string comparison exits early on the first mismatched character, and an attacker can time that to guess the digest one byte at a time. compare_digest always takes the same time either way.
Before the listener can deploy anything, the thing it deploys has to exist: an image to rebuild, and a proxy already pointed at the container it runs in.
-d detaches, --name gives it a handle docker compose can stop and replace by name next deploy127.0.0.1:8081, not 0.0.0.0:8081-p 127.0.0.1:8081:80 binds the container's port only to the host's loopback interface — reachable from the same machine, not from the internet. The app is deliberately not exposed directly. The only thing allowed to reach it from outside is whatever's listening on the public interface: Caddy.The chess app’s own Dockerfile actually builds on caddy:2-alpine too — a small, container-local Caddy that just serves the built static files on port 80, nothing more. That’s a different job from the host-level Caddy on the next slide, which handles TLS and routes several apps by domain. Two Caddys, two jobs.
That’s the whole config. reverse_proxy forwards every request for that hostname to the container’s port on loopback. Caddy also requests and renews a Let’s Encrypt certificate for chess.example.com automatically the first time it starts — no separate certbot step, no cron job to remember.
Two different public ports for two different callers: 443 is where browsers reach the app, through Caddy. 9000 is where GitHub’s servers will reach the webhook listener directly, once it’s wired up — it’s its own small service, not proxied through Caddy, though it could be. From outside the VM, that 9000 shows up as a different number entirely — more on that when we hook this up to GitHub for real, a couple of parts from now.
Popen starts deploy and returns immediately — the listener answers GitHub right away instead of making it wait for a build to finish. Status 202 Accepted means "received, working on it," which is exactly what happened.set -euo pipefail makes the script stop at the first failure instead of plowing on with a half-finished deploy. git reset --hard @{u} instead of git pull — a server checkout should never carry its own local commits, so this throws them away and matches upstream exactly, every time (@{u} is git's shorthand for "the upstream branch this one tracks"). docker compose up -d --build is exactly the docker build / docker run pair from last part, driven from the compose.yml that already names the image and the port mapping — no separate stop/rm/run needed.Python earned its place for the part that needed a real language and a library behind it: holding a socket open, parsing JSON, computing an HMAC. Rebuilding an image and restarting a container is just a fixed sequence of commands you’d type yourself — exactly what bash from Lecture 2 was already good at. Rewriting that sequence in Python would just be subprocess.run calls pretending not to be a shell script.
Every piece on this line already exists and works on its own — you can run deploy by hand right now and watch it work. All that’s left is the first box: getting GitHub to actually send that push.
Everything downstream of a push now works. Last step: telling GitHub where to send one.
Every VM listens on the same port, 9000 — that part of listener.py never changes. What’s shared is one public IP for the whole classroom, so the gateway in front of it forwards a per-VM public port straight to your VM’s 9000: VM 07’s webhook traffic arrives on public port 9007, VM 12’s on 9012. The payload URL you give GitHub uses that public port, not 9000 itself.
http://<public-ip>:90XX/webhook — your VM's forwarded port, not 9000 itself and not localhost.
application/json — otherwise the body arrives form-encoded and request.get_json() comes back empty.
The same string as WEBHOOK_SECRET on the server. Set once, never sent in the clear again — only its HMAC is.
Just push. "Send me everything" means handling event types your code never checks for.
The moment you save a webhook, GitHub fires a one-off ping event at it — no push required — so you find out immediately whether the URL and secret line up. After that, every delivery GitHub attempts is logged on the webhook’s page: request, response, status code, and a Redeliver button, which is the fastest way to replay one delivery while you’re debugging the listener instead of making a real commit every time.