Welcome to My Home Lab
This is Part 1 of an ongoing series where I document my self-hosted infrastructure journey—setting up, securing, and scaling an efficient and modular Docker ecosystem on my home server. I’ll take you through the choices I made, services I run, and challenges I solved.
For context: I'm running this system on a Beelink SER8 mini PC, equipped with:
- AMD Ryzen 7 8745HS
- 12GB DDR5 RAM
- 1TB NVMe SSD (boot + Docker)
- 3.6TB NVMe SSD (media & Immich storage)
- 932GB External SSD (backups & overflow)
- Ubuntu 24.04 LTS
This machine replaced my Mac mini for Docker workloads and now acts as the heart of my home cloud, developer platform, and AI lab.
Additionally, I maintain a backup VPS on Oracle Cloud Infrastructure (OCI) that serves as an off-site backup location and disaster recovery solution. I migrated from Digital Ocean to OCI for better performance and cost efficiency — see my migration post for details.
Why Docker?
I chose Docker for the following reasons:
- Isolation: Each service runs in its own container
- Reproducibility: No more “it works on my machine” issues
- Portability: Move the stack between devices
- Declarative Configuration: All setup lives in
docker-compose files
Design Principles
My goal was to keep things modular, composable, and resilient. Here’s what that looked like:
- Split Compose files into roles:
core.yml, media.yml, infra.yml, extras.yml, immich.yml
- Avoid monolithic configurations
- Use external networks defined in a central
networks.yml
- Enable restart policies and volume mapping for persistence
- Log and monitor every service via Netdata
Each service is deployed headlessly, accessed either via browser, REST APIs, or reverse proxy.
Overview of the Series
| Part | Title |
|------|-------|
| 1️⃣ | Intro & Architecture (you are here) |
| 2️⃣ | Core Services: Nginx Proxy Manager, Authentik, Tailscale |
| 3️⃣ | Infra Layer: Portainer, Netdata, Watchtower, Backups |
| 4️⃣ | Media Stack: Jellyfin, Sonarr, Radarr, Lidarr, Navidrome |
| 5️⃣ | Productivity & Photos: Nextcloud, Immich, Paperless-NGX, Vaultwarden |
Let’s begin with the big picture—how it’s all wired together.
Architecture
Here's a bird’s eye view of my setup:
+-----------------------+
| Beelink SER8 (Host) |
+-----------------------+
|
+------------------------+-------------------------+
| | |
Docker Compose External Access VPN Tunnel
(core.yml + others) (NPM, Authentik) (Tailscale)
|
+--------------+---------------------+------------------+
| | | |
Core Stack Media Stack Productivity AI & Tools
(NPM, Auth, (Jellyfin, Sonarr, (Nextcloud, Immich, (Open WebUI,
MariaDB) Radarr, Lidarr) Paperless-NGX) PodcastAI)
Every service belongs to a category and communicates through pre-defined Docker networks, with networks.yml acting as the shared config.
Backup & Disaster Recovery:
My setup includes an Oracle Cloud (OCI) VPS that serves as an off-site backup location, running the portfolio and critical services with data redundancy and quick recovery in case of hardware failure.
Modularity with Compose
File Structure
docker_services/
├── core.yml
├── infra.yml
├── media.yml
├── extras.yml
├── immich.yml
├── networks.yml
└── .env
Why Split Files?
This modular split helps in:
- Running specific layers independently
- Upgrading only what you need
- Easier debugging and configuration
- Clear separation of concern
I also prefix volumes and container names per file to avoid naming conflicts.
Networks & Naming
Defined in networks.yml:
networks:
proxy_net:
name: proxy_net
driver: bridge
attachable: true
ipam:
config:
- subnet: 172.28.0.0/24
gateway: 172.28.0.1
core_net:
name: core_net
driver: bridge
internal: true # Isolates DBs from direct internet access
media_net:
name: media_net
driver: bridge
infra_net:
name: infra_net
driver: bridge
extras_net:
name: extras_net
driver: bridge
Services declare these external networks in their own files. This ensures containers are interoperable across layers without duplicating definitions.
Secrets and Security
Security is critical. Even a home server should have strong hygiene:
.env files store secrets (never committed)
- Authentik SSO + Nginx Proxy Manager enable 2FA and domain routing
- Tailscale manages zero-config VPN with ACLs
All admin UIs are hidden behind TOTP authentication or VPN entry.
Reverse Proxy Flow
The reverse proxy stack routes requests like this:
User → https://service.jay739.dev
↳ Nginx Proxy Manager
↳ Authentik (SSO/2FA if protected)
↳ Internal Docker container
Each app is accessible via a subdomain thanks to NPM + wildcard DNS setup via Porkbun DDNS, with local DNS resolution handled by dnsmasq pointing to the LAN IP (10.0.0.101).
Monitoring & Maintenance
Every container’s health is tracked using Netdata, running on a dedicated infra container and exposed via:
https://metrics.jay739.dev
I also have a custom Telegram bot for real-time alerting on service health — covered in a separate post.
Core Tools Installed on Host
Even though most things run in Docker, I have some native tools:
tailscale — for remote access
docker, docker-compose — core engine
fail2ban — SSH brute force protection
rsync, cron — automated local backups
htop, btop — system resource monitoring
What’s Next?
In the next part of this series, I’ll walk you through the Core Services stack in detail: reverse proxy, domain mapping, VPN access, and 2FA gateways.
Stay tuned, and feel free to reach out if you want to replicate this setup or have questions on customizing your own server stack!
Part 2 → Core Services (Nginx Proxy Manager, Authentik, Tailscale)
— Jayakrishna