# Part 1 - Building my homelab foundation (Docker, DNS, reverse proxy)

# Building the foundation of my homelab

I originally built my homelab just to experiment.

I wanted to try things out, run a few services, and see what I could do with a small machine at home. Nothing serious.

That didn’t last long.

Over time, it turned into something I actually rely on. I now run multiple services and have to think about networking, structure, and reliability in a more intentional way.

Before getting into media servers, VPN tunnels, or anything exposed to the internet, I needed a solid foundation.

This post is about that foundation.

---

## Where everything started

Before making any architectural decisions, I needed a machine to run everything.

I’m using a mini PC — a Beelink Mini S12 Pro with an Intel N100 (this will be important later), 16GB of RAM and a 500GB SSD. This thing is a beast while only drawing around 6W. It’s small, quiet, and more than capable for this setup.

I installed Debian and kept the system minimal.

One of the first things I did was configure a static lease on my router so the machine always has the same IP. Without this, everything else becomes harder than it needs to be.

---

## Bootstrapping the environment

Once the base system was ready, I installed Docker.

From there, I added Portainer as a management layer. I still define everything using docker-compose, but Portainer makes it much easier to operate — especially when working with stacks.

From this point on, everything in my homelab runs in containers.

That decision simplified everything that came after.

---

## From “just Plex” to needing structure

Like a lot of people, I started with a simple idea:

“I just want to run Plex at home.”

Very quickly I had multiple services running, ports everywhere, and no real structure. I was constantly trying to remember which IP and port belonged to which app.

That’s when it became clear I needed to organize things properly.

---

## Core principle: everything runs in containers

Docker became the backbone of the setup.

Running everything in containers gives isolation between services and makes updates straightforward. If something breaks, I recreate the container instead of fixing the system.

It also makes backups and migrations predictable.

---

## Managing containers with Portainer

Portainer sits on top of Docker as a control panel.

I mainly use it for:

* Viewing logs
* Restarting containers
* Checking container health
* Managing stacks

It’s not required, but it makes day-to-day operations much easier.

---

## Internal DNS: more than just naming

One of the first real problems I hit was access.

Everything looked like this:

```text
http://192.168.12.200:32400
http://192.168.12.200:8989
http://192.168.12.200:7878
```

It works, but it doesn’t scale — and it’s annoying.

So I introduced internal DNS.

Instead of using `.lan`, I moved to a proper domain structure.

Now everything looks like this:

```text
plex.home.mydomain.com
sonarr.home.mydomain.com
radarr.home.mydomain.com
```

Much easier to remember, and much nicer to use.

At the same time, I wanted network-wide ad blocking. I had previously used Pi-hole on a Raspberry Pi, and it worked really well. In this setup, I consolidated everything using Technitium, which handles both DNS resolution and ad blocking for the entire network.

Here are my current blocking lists:

```text
https://small.oisd.nl/
https://raw.githubusercontent.com/hagezi/dns-blocklists/main/wildcard/multi-onlydomains.txt
https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling/hosts
```

---

## DNS stack

This is the stack responsible for internal name resolution and ad blocking.

Here’s the actual stack I’m running:

```yaml
version: "3"
services:
  dns-server:
    container_name: dns-server
    hostname: dns-server
    image: technitium/dns-server:latest
    # For DHCP deployments, use "host" network mode and remove all the port mappings
    # network_mode: "host"
    ports:
      - "5380:5380/tcp" # DNS web console (HTTP)
      # - "53443:53443/tcp" # DNS web console (HTTPS)
      - "192.168.1.200:53:53/udp" # DNS service
      - "192.168.1.200:53:53/tcp" # DNS service
      - "192.168.1.200:853:853/udp" # DNS-over-QUIC service
    volumes:
      - config:/etc/dns
    restart: always
    sysctls:
      - net.ipv4.ip_local_port_range=1024 65000
 
volumes:
    config:
```

I bind DNS directly to port 53 on the host so it can serve the entire network.

---

## Internal reverse proxy

DNS solves naming, but not routing.

I still didn’t like having services exposed on different ports, so I added an internal reverse proxy.

Now everything goes through a single entry point, which routes requests to the correct container.

I’m using a dedicated subdomain:

```text
home.mydomain.com
```

And each service lives under it:

```text
plex.home.mydomain.com
sonarr.home.mydomain.com
radarr.home.mydomain.com
```

This also allows me to use HTTPS internally.

The proxy is handled by Nginx Proxy Manager, with Let’s Encrypt certificates issued using a Cloudflare DNS challenge. I use a wildcard certificate for `*.home.mydomain.com`, so every service is automatically covered.

This means any new service I add is immediately available over HTTPS without needing to issue new certificates.

I also needed to add a zone in my DNS pointing `home.mydomain.com` to my homelab IP.

This gives me:

* A consistent access pattern
* No need to remember ports
* Proper domain names instead of local-only hostnames
* HTTPS everywhere, even inside the network
* A clean path for exposing services externally later

---

## Proxy stack

This stack is responsible for routing traffic internally and handling HTTPS.

And here’s the proxy setup:

```yaml
version: '3.8'
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      - '80:80'
      - '81:81'
      - '443:443'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
```

---

## Why this structure matters

This setup might look like extra work at first, but it removes a lot of friction.

I don’t need to remember ports or IPs anymore, and everything has a clear place.

If something breaks, I rebuild from configuration instead of debugging a fragile system.

It also prepares the setup for exposing services safely later, without redesigning everything.

---

## What’s next

With this foundation in place, everything else becomes much easier.

In the next post, I’ll walk through my Plex stack and show how I expose it to the internet without opening ports at home.

That’s where things start to get interesting.

---

If you’re building your own homelab, starting with a structure like this will save you a lot of time and headaches later.

