Infrastructure and deployment case study

Building and self-hosting this portfolio

I wanted this portfolio to be a project I could operate myself. I built the Django application, prepared a Fedora Server mini PC, installed Coolify, configured the network and security, and packaged the site in a multi-stage Docker image. The result is a small production environment that I understand from the HTML all the way down to the host and router.

Application

Django and Tailwind CSS

Platform

Fedora Server and Coolify

Delivery

GitHub to Docker

Why I built the full stack myself

A hosted portfolio would have been quicker, but I wanted practical experience with the parts that sit behind an application. Running it at home gave me responsibility for the operating system, deployment platform, DNS, firewall, TLS, persistent data, logs, health checks, and recovery after a failed release. Each layer became something I could configure, inspect, and explain.

I used a mini PC because it is quiet, power efficient, and capable of running several containers comfortably. It also turns an otherwise unused machine into a useful home server that I can extend with future projects.

Preparing Fedora Server

I installed Fedora Server as a minimal base and assigned the mini PC a stable address on my local network through a DHCP reservation. That keeps router rules and internal DNS predictable after reboots. I applied system updates, enabled automatic security updates, configured the time zone, and limited administration to a dedicated user with sudo access.

Remote access uses SSH keys. Password authentication and direct root login are disabled, and the host firewall only permits services that are actually required. This keeps the server simple to audit because every open port has a clear purpose.

Security and Fail2ban

I configured Fail2ban to monitor SSH authentication logs and temporarily ban addresses that repeatedly fail to authenticate. The ban rules complement key-only SSH and Fedora's firewall. I also keep Coolify and the host packages updated, avoid exposing container ports directly, and let Coolify's reverse proxy handle public web traffic.

Secrets such as Django's SECRET_KEY live in Coolify's environment variable store. They are absent from Git and the Docker image. Production settings restrict allowed hosts, trust only the site's HTTPS origins for CSRF, understand forwarded HTTPS headers, and serve versioned static assets through WhiteNoise.

Networking, DNS, and port forwarding

The router forwards public TCP ports 80 and 443 to the mini PC. Port 8000, which Gunicorn uses inside the application container, stays private. Coolify's proxy receives HTTP and HTTPS requests, selects the application from the requested hostname, terminates TLS, and forwards the request across Docker's internal network.

I pointed the domain's DNS records at the public IP and attached the domain to the application in Coolify. Coolify then requested and renews the TLS certificate. I verified the path in stages: local container, Docker network, reverse proxy, local network, public DNS, and finally an external connection. Testing one boundary at a time made routing issues much easier to locate.

Coding the website

The site is a Django application with reusable templates for navigation, pages, project cards, and blog content. Tailwind CSS and daisyUI handle the design system and responsive layout. Django also powers the blog reactions and the server-side GitHub contribution display, which is cached so normal page loads do not repeatedly request GitHub.

Development and production settings are separate. Local development keeps useful debugging tools available, while production enables WhiteNoise, proxy-aware HTTPS handling, secure host validation, console logging, and environment-based configuration. SQLite is stored on a persistent Coolify volume at /data/db.sqlite3, so deployments replace the container without replacing the database.

Designing the Docker image

The Dockerfile uses two stages. A Node 22 stage installs locked frontend dependencies with npm ci and compiles the Tailwind stylesheet. A Python 3.12 slim stage installs the Django dependencies and copies the completed application from the frontend stage. Node tooling stays out of the final runtime image.

The container entrypoint runs database migrations and collectstatic before starting Gunicorn. Gunicorn binds to the platform-provided PORT, which lets the same image run locally and in Coolify. I also installed curl in the runtime image because Coolify uses it for container health checks.

Connecting GitHub to Coolify

Coolify pulls the main branch from GitHub and builds the repository's Dockerfile. The application receives runtime settings from Coolify, mounts a persistent data volume, and joins the proxy network. A successful push can therefore move from source control to a fresh container without manually copying files onto the server.

The first deployment exposed two useful integration details. Coolify initially selected Railpack, which detected Python but skipped the Node build and entrypoint. Switching the build pack to Dockerfile restored the complete build. The next image started correctly, but its health check failed because the slim Python image did not include curl or wget. Adding curl allowed Coolify to verify the running application and complete the rolling update.

build sequence from metal to web

1. Host

Installed Fedora Server, applied updates, created a dedicated administrator, and assigned a stable LAN address.

2. Security

Enabled key-only SSH, disabled root login, configured the firewall, and added Fail2ban monitoring.

3. Platform

Installed Coolify and connected the GitHub repository for repeatable deployments.

4. Network

Configured DNS, forwarded ports 80 and 443, and issued TLS certificates through Coolify.

5. Application

Built Django templates, Tailwind styling, production settings, caching, and persistent storage.

6. Container

Created a multi-stage Docker build, startup entrypoint, Gunicorn process, and health-check support.

Technology choices

  • Fedora Server: familiar Linux administration, current packages, SELinux, and firewalld.
  • Coolify: Git deployments, reverse proxy routing, TLS, environment variables, volumes, and health checks in one interface.
  • Django: mature routing, templates, data models, security defaults, and a clear production path.
  • Docker: reproducible builds and the same application environment after every deployment.

What I learned

Self-hosting made the boundaries between application code and infrastructure much clearer. A healthy Django process still depends on DNS, proxy routing, forwarded headers, firewall rules, storage permissions, and a health-check command that exists inside the image.

The most valuable habit was checking each layer directly. Container logs confirmed migrations and Gunicorn startup. Health logs revealed the missing curl binary. That evidence led to small, targeted fixes and a deployment I can reproduce from the repository.

Current result

The mini PC now runs a secured Fedora host with Coolify managing the portfolio container. GitHub is the source of truth, production data persists between releases, public traffic is encrypted, and deployments are observable through build, application, and health-check logs.