aboutme
This website — containerized with Docker and shipped through a GitHub Actions CI/CD pipeline
Why I built the pipeline
I'm applying for junior cloud/DevOps roles, and the actual job is less about any single app and more about how code gets from a commit to a running server safely. Rather than just reading about Docker and CI/CD, I used this site itself as the sandbox — low stakes, but a real pipeline with real consequences if I got it wrong.
What I built
The app is packaged with a Dockerfile based on python:3.13-slim,
running under Gunicorn with two workers. requirements.txt is copied
and installed before the rest of the source code, so Docker's layer cache skips
the dependency install entirely when only application code changes.
Deployment is a single-service docker-compose.yml that pulls the
built image, binds it to a fixed address on the VPS, loads secrets from an env
file, and restarts automatically if the container ever crashes.
On top of that sits a three-stage GitHub Actions pipeline that runs on every push and pull request:
The pipeline, stage by stage
Lint and test: every push and PR runs ruff for
linting and pytest for the test suite. Nothing moves forward until
this passes.
Build and push: the image is built with Docker Buildx and pushed
to GitHub Container Registry, tagged both :latest and with the commit
SHA. Pull requests only verify that the Dockerfile still builds — the push to
GHCR only happens on a real push to master, so a PR can never publish
an image.
Deploy: on a successful push to master, GitHub
Actions SSHes into the VPS to trigger the deploy.
What I learned
The most useful lesson wasn't Docker syntax, it was thinking about blast radius.
The deploy step sends a command over SSH, but the server doesn't actually run
whatever text is sent, the deploy key's entry in authorized_keys
has a forced command= restriction, so that key can only ever run one
fixed deploy script, no matter what the workflow sends. If the key ever leaked,
it still couldn't be used to run arbitrary commands on the server.
I also learned to separate "verify" from "publish": every PR proves the code
lints, passes tests, and still builds into a working image, but only a merge to
master is trusted enough to actually push that image and deploy it.