🏠 Introduction

Over the past few weeks, I went on a home-lab adventure to consolidate my self-hosted services:
As most of you self-hosters would know, if you tweek one thing, something else is bound to break. 😅

  • OpenMediaVault (OMV) for storage & Docker management
  • Immich for private photo backup
  • Jellyfin for private home video backup
  • AdGuard Home for DNS-level ad blocking
  • Hugo Blog to document my journey
  • Tailscale for private remote access
  • Cloudflare Tunnel for secure public access without port forwarding
  • Uptime-Kuma for monitoring all the services that I am hosting

After a few networking headaches and some Docker/NAT surprises, I now have a stable hybrid setup where:

  • I can upload photos privately via Tailscale to Immich
  • I can upload home videos privately via Tailscale and Syncthing to Jellyfin
  • I can access OMV and AdGuard securely from anywhere via Cloudflare
  • My blog is public on a custom domain with HTTPS
  • No ports are exposed to the internet directly
  • I can monitor all service using uptime-kuma

This post documents the final working configuration.


πŸ–₯️ Hardware & Base Setup

  • Raspberry Pi 5 (8GB)
  • NVMe SSD mounted for OMV and Docker storage
  • OpenMediaVault (OMV) as the host OS
  • Docker & Portainer for container management

I installed these services via Docker Compose:

  • Hugo Blog ✏
  • Portainer

Then I installed the following using Portainer:

  • AdGuard Home πŸ›‘οΈ
  • Immich (Server, DB, Redis, ML) and Jellyfin as a single stack πŸ“Έ
  • Uptime-Kuma ⏱️
  • Syncthing πŸ”„

🌐 Networking Architecture

I wanted public access for some services but private uploads for Immich and Jellyfin.

Here’s the architecture:

    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚   Internet    β”‚
    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
    Cloudflare Tunnel
           β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Raspberry Pi 5 (OMV) 	               β”‚
β”‚                                      β”‚
β”‚ - Hugo (Public Blog) 	               β”‚ 
β”‚   β†’ mydomain.com                     β”‚
β”‚ - AdGuard (DNS)                      β”‚ 
β”‚   β†’ adguard.mydomain.com             β”‚
β”‚ - OMV (Web UI)                       β”‚ 
β”‚   β†’ via Tailscale or optional tunnel β”‚
β”‚ - Immich (Uploads)                   β”‚ 
β”‚   β†’ via Tailscale MagicDNS           β”‚
β”‚                                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
LAN + Tailscale  
  • Tailscale handles private admin, Immich and Jellyfin uploads
  • Cloudflare Tunnel handles public blog and optional subdomains
  • No local SSL needed; Cloudflare provides HTTPS

πŸ”§ Fixing Immich & Tailscale Access

Initially, Immich logs showed: Immich Server is listening on http://[::1]:2283

This meant the container was only listening on IPv6 localhost.
LAN, Tailscale, and Cloudflare could not reach it.

Solution: Bind to all interfaces.

immich-server:
  image: ghcr.io/immich-app/immich-server:release
  container_name: immich_server
  ports:
    - "2283:2283"
  environment:
    - IMMICH_SERVER_HOST=0.0.0.0

After restarting the container:

Immich Server is listening on http://0.0.0.0:2283

βœ… Immich uploads via Tailscale MagicDNS now work again.


☁️ Cloudflare Tunnel Config

I used subdomains for apps that don’t support subpaths:

tunnel: hugo-blog
credentials-file: /home/user/.cloudflared/UUID.json

ingress:
  - hostname: adguard.mydomain.com
    service: http://127.0.0.1:8081
  - hostname: omv.mydomain.com
    service: http://127.0.0.1:81
  - hostname: im.mydomain.com
    service: http://127.0.0.1:2283
  - hostname: mydomain.com
    service: http://127.0.0.1:8090
  - service: http_status:404

Key Points:

  • mydomain.com β†’ Hugo Blog
  • adguard.mydomain.com β†’ AdGuard Home
  • Added im.mydomain.com to access Immich publicly.

βœ… Current Status

If you’re following my journey and setting up something similar, I highly recommend starting private with Tailscale, then adding Cloudflare Tunnel for selective public access.

Overall Architecture

Architecture