DevOps · Software Engineering
MiniLink
Containerized URL Shortener + Analytics

Problem
Every public URL shortener sends your links (and your users' clicks) through someone else's servers. More importantly for portfolio purposes: public shorteners don't require writing a Dockerfile, wiring up Docker Compose, or deploying to a cloud platform. MiniLink exists to demonstrate those DevOps fundamentals in a scope small enough to reason about completely.
Solution
A self-hosted URL shortener where POST /shorten returns a short code and GET /{code} issues a 302 redirect. Every redirect increments a hit counter in Postgres. A live HTMX dashboard shows the top 20 links by hits. Short-code lookups are served from Redis on the hot path — Postgres is only touched on a cache miss or a write.
Tech Stack
| API | FastAPI · SQLModel |
| Database | PostgreSQL (Railway managed) |
| Cache | Redis — read-through cache, 24-hour TTL (Railway managed) |
| Container | Docker — multi-stage build (builder → runtime, ≈150 MB image) |
| Local Dev | Docker Compose — one command brings up API + Postgres + Redis |
| CI/CD | GitHub Actions: lint (ruff) → test (pytest) → build image → push to GHCR → Railway auto-deploy |
| Dashboard UI | HTMX + Tailwind CSS (no React, no build step) |
| Deploy | Railway — managed services, GitHub App auto-deploy on push to main |
Architecture
Browser │ ▼ FastAPI (Railway — Dockerfile, port 8080) │ ├─ short-code lookup │ │ │ ▼ │ Redis ──── miss ────► PostgreSQL │ (cache) ◄──── fill ─── (source of truth) │ └─ hit counter write ──────► PostgreSQL
Reads hit Redis first with a 24-hour TTL. A cache miss falls through to Postgres, returns the URL, and warms the Redis entry. Writes (new short code, hit counter increment) go straight to Postgres.
Selected Lessons
- ›Multi-stage Docker builds matter for image size. A naive single-stage image exceeds 1 GB — the pip install layer balloons. Separating the builder stage from the runtime stage shrinks the final image to ≈150 MB. Smaller images mean faster Railway deploys and less storage cost.
- ›Docker Compose healthchecks prevent startup races. The API's depends_on with a pg_isready healthcheck on the Postgres service means uvicorn never starts before the DB is ready. Without this, create_db_and_tables() fails on the first boot and the container exits silently.
- ›Cloud proxy auto-detection can quietly pick the wrong port. Railway's edge proxy detected port 8000 (from the old EXPOSE 8000 in the Dockerfile) while uvicorn was actually binding to 8080 (via Railway's injected $PORT). Every request returned 502. The fix: align EXPOSE to 8080 and set the Target Port in Railway's dashboard.
- ›Redis TTL is a product decision, not a technical detail. A 24-hour TTL is fine for a personal shortener where links rarely change. But if you add a delete/edit feature, the cache entry must be invalidated on write — otherwise visitors hit the stale cached redirect for up to 24 hours after deletion.
Local Setup
git clone https://github.com/neuralxjam/minilink cd minilink python -m venv .venv && .venv\Scripts\pip install -r requirements-dev.txt docker compose up # starts API + Postgres + Redis # Visit http://localhost:8000