Back to all posts
6 min read

Self-Hosting Hobby Projects on a Home Server

Self-HostingDockerDevOps

I have a habit of spinning up little side projects — a dashboard here, a small API there — and for years I paid a cloud provider to host every one of them. At some point I realized I had an always-on machine sitting at home that could do the same job for free. So I turned it into a home server, and it's become one of my favorite playgrounds. Here's the setup I'd recommend if you want to do the same.

My home office setup where the server lives

Why self-host at all

Three reasons, in order of how much they matter to me:

  • Cost. A box you already own beats a monthly bill, especially for hobby apps that get a handful of visitors. Once the hardware is paid for, every new project is effectively free to host.
  • Learning. You touch real networking, DNS, TLS, and Linux administration — the stuff that's usually hidden behind a managed platform. It's made me a better engineer, and it pays off directly in my day job.
  • Control. Your data stays on your hardware, and you can run whatever you like without worrying about a platform's limits, pricing changes, or a service being sunset out from under you.

It's a low-stakes way to learn high-stakes skills. Break something at 11pm and the only person you're paging is yourself.

The one prerequisite: reaching your server

The trickiest part of self-hosting is letting the outside world find your machine. The cleanest fix is a static IP from your ISP — a fixed address you can point your domain's DNS at and forget about. Most ISPs offer one for a small fee, and it's well worth it to avoid chasing a changing address.

Once you have it, forward ports 80 and 443 on your router to the server so web traffic actually reaches it. One caution: exposing anything to the public internet means you're now responsible for it. Lock the box down before you open the door — key-based SSH only, and a firewall allowing just the ports you actually use.

The base: Ubuntu and Docker

I run Ubuntu LTS on the server. It's stable, widely documented, and supported for years, which is exactly what you want for a machine you'd rather not babysit. The Ubuntu Server documentation covers the initial install cleanly.

On top of that, everything I host runs in Docker containers. Containers keep each project isolated and reproducible — no more "it worked until I installed something for another app." Spinning a project up or tearing it down is a single command, and I never pollute the host system. The official Docker Engine install guide and the Docker Compose docs are all you need to get going.

Shared services on a common network

Here's the idea that makes a home server genuinely pleasant to live with: I don't give every project its own database. Instead I run a small set of shared backing services that every app can connect to — a PostgreSQL for relational data, MongoDB for the document-shaped projects, Redis for caching and background jobs, and MinIO for S3-compatible object storage when something needs to handle file uploads.

Each of these runs as its own small Docker Compose stack, and they all join a shared Docker network I call infra. Any app I deploy joins the same network and reaches a service simply by its name. Creating that network once is all the wiring it takes:

docker network create infra

The payoff is real:

  • Run once, use everywhere. A new project needing a database means creating one inside the existing Postgres, not standing up a whole new server.
  • Sane resource usage. One instance of each engine instead of a dozen half-idle copies eating memory.
  • Centralised backups. The data I care about lives in a known set of volumes, so backing up is one routine instead of many.
  • Disposable apps, stable data. I can rebuild or throw away an app freely, because the data layer it depends on lives independently.

I'll break down each of these services — how they're configured, secured, and connected — one at a time in the follow-up posts, since each deserves more than a passing mention.

Routing with Traefik

The piece that ties it all together is Traefik, a reverse proxy that sits in front of every container. It routes incoming requests to the right app based on the hostname, and — my favorite part — it automatically provisions and renews HTTPS certificates from Let's Encrypt. No manual cert wrangling, ever.

A minimal Traefik service in a docker-compose.yml looks like this:

services:
  traefik:
    image: traefik:v3.1
    command:
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.le.acme.tlschallenge=true"
      - "--certificatesresolvers.le.acme.email=you@example.com"
      - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "./letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

From there, any app I deploy just adds a few labels to declare its hostname, and Traefik handles the routing and TLS automatically:

  myapp:
    image: myapp:latest
    labels:
      - "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
      - "traefik.http.routers.myapp.entrypoints=websecure"
      - "traefik.http.routers.myapp.tls.certresolver=le"

The Traefik documentation goes much deeper if you want middlewares for auth and rate limiting, dashboards, or per-service TLS.

Dev locally, ship automatically

Two more pieces make the day-to-day smooth. For development, each project carries a docker-compose.dev.yml that spins up just what I need locally — the app plus throwaway copies of whatever backing services it uses — so my laptop mirrors production without touching the real data on the server.

Deployment itself is a GitHub Actions workflow, gated behind a release branch. Day-to-day changes land on main, but nothing ships from there. When a release is actually due, I merge main into a release branch — and that merge is what triggers the workflow: it builds the image, pushes it, and rolls the container over on the server. No SSH-ing in to run commands by hand, and main stays free for work-in-progress without every commit going live. I'll share the exact workflow and the dev compose file in the follow-ups.

Keeping it healthy

A home server is only fun if it stays up while you're not looking. A few habits have saved me more than once:

  • Unattended upgrades so security patches land without me thinking about it.
  • Automatic restarts (restart: unless-stopped) so a reboot brings everything back — apps and shared services alike.
  • Backups of the parts that matter — mostly the data volumes and that acme.json file holding your certificates.

None of this is heavy. It's an evening of setup that then quietly runs for months.

More coming soon

This is the high-level map; the hands-on details are where it gets interesting. I'm putting together follow-up posts that walk through the actual setup and configuration one service at a time — the infra network, each shared backing service, hardening the box, the local docker-compose.dev.yml, and the GitHub Actions deploy workflow — so you can copy-paste your way to a working server. I'll share those here as I write them, so keep an eye on this space.

A playground worth keeping

Once this is in place, deploying a new project is almost boring in the best way: write the app, point it at the shared database, drop in a compose file with a couple of labels, and it's live on its own subdomain with HTTPS. The whole setup costs me nothing beyond electricity, and I've learned more from running it than from any tutorial. If you've got a spare machine and a bit of curiosity, a home server is one of the most rewarding weekend projects you can take on.


Back to all posts