Self-hosted Google Photos + Google Drive + Plex on a single laptop. $10.46/year all-in.
Built by @irrssue — replacing paid cloud services with open-source software running on a laptop on a shelf.
Live deployment:
- 📷 photos.irrssue.com — Immich (photos & video library)
- 📁 drive.irrssue.com — Nextcloud (file sync & sharing)
- 🎬 tv.irrssue.com — Jellyfin (media server)
A working, documented blueprint for replacing:
| Paid service | Replaced by | Public URL |
|---|---|---|
| Google Photos | Immich | photos.yourdomain.com |
| Google Drive | Nextcloud | drive.yourdomain.com |
| Netflix / Plex | Jellyfin | tv.yourdomain.com |
Total recurring cost: $10.46/year for the domain. Everything else — the OS, the services, the tunnel, the VPN — is free and open-source.
This repo contains the sanitized configs, scripts, and step-by-step docs from the build. It's the thing I wish existed when I started.
Internet → Cloudflare DNS → Cloudflare Tunnel ──┐
▼
Tailscale ──────────────────► Ubuntu server (Docker)
├── Immich :2283
├── Nextcloud :8080
└── Jellyfin :8096
│
▼
/mnt/storage (2TB exFAT, ro)
Two paths in: Cloudflare Tunnel for public URLs (no port-forwarding, no static IP), and Tailscale for private device-to-device access (no edge timeouts, full speed for bulk uploads). See ARCHITECTURE.md for the detailed diagram and reasoning.
- Acer Nitro AN715-51 — i7-9750H (6c/12t), 16GB RAM, GTX 1660 Ti, Ubuntu Server (Originally built on an older laptop on WiFi; migrated to this box on Ethernet after WiFi instability turned out to be the root cause of every "502 Bad Gateway" outage. See docs/08-troubleshooting.md.)
- 2TB external SSD — exFAT, mounted read-only into containers
- Ethernet — non-negotiable for an always-on server. WiFi will drop and will break your tunnel
If you already have Docker, a domain on Cloudflare, and SSH into a Linux box, the 5-minute path:
# 1. Clone
git clone https://github.com/irrssue/homelab.git ~/homelab
cd ~/homelab
# 2. Mount your external drive (replace UUID — find with `lsblk -f`)
sudo cp system/fstab.example /tmp/ && sudo nano /tmp/fstab.example # edit UUID
sudo bash -c 'cat /tmp/fstab.example >> /etc/fstab'
sudo mkdir -p /mnt/storage && sudo mount -a
# 3. Bring up Immich
mkdir -p ~/docker/immich && cp immich/docker-compose.yml immich/.env.example ~/docker/immich/
mv ~/docker/immich/.env.example ~/docker/immich/.env
nano ~/docker/immich/.env # set DB_PASSWORD
cd ~/docker/immich && docker compose up -d
# 4. Bring up Nextcloud and Jellyfin (same pattern)
# 5. Set up Cloudflare Tunnel (see docs/06-cloudflare-tunnel.md)Visit http://<your-server-ip>:2283 — Immich onboarding page. Done.
For the full walkthrough, start at docs/01-planning.md.
| Step | What it covers |
|---|---|
| 01 — Planning & domain | Picking services, buying a domain, deciding Tailscale vs. Tunnel |
| 02 — Mounting the drive | exFAT, fstab, the read-only library pattern |
| 03 — Immich setup | Docker compose, external library, ML container tradeoffs |
| 04 — Nextcloud setup | Compose, MariaDB, exposing /mnt/storage as External Storage |
| 05 — Jellyfin setup | Compose, published URL, loopback-only binding |
| 06 — Cloudflare Tunnel | cloudflared install, ingress rules, DNS routes, the two-config-files trap |
| 07 — Tailscale | Joining the tailnet, private access, mobile clients |
| 08 — Troubleshooting | The 502/524 saga, WiFi instability, Immich ML CPU spikes |
| 09 — Lessons learned | What I'd tell past-me |
- exFAT can't do Linux permissions. Mount external drives read-only and let containers write to ext4 volumes. Trying to
chownexFAT fails with "operation not permitted" and there is no fix — only a workaround. - Cloudflare's free tier has a hard 100-second edge timeout. Browsing photos works fine. Uploading a 4GB video over the tunnel does not. Route bulk transfers through Tailscale instead.
/etc/cloudflared/config.ymlis the file the service reads.~/.cloudflared/config.ymlis not. Edit the one in/etc. (Or symlink them — see docs/06.)- Immich's ML container will eat your CPU. 300%+ on a 4-core box during indexing. Disable it unless you have GPU acceleration.
- WiFi is unreliable for always-on servers. Every 502 in the early build came from
wlp2s0dropping for 30–45 seconds and killing all 4 QUIC tunnel connections at once. Get Ethernet. nofailin fstab is mandatory for any non-essential mount on a headless box. Without it, an unplugged drive drops the system into emergency mode and you'll need a monitor to recover.
homelab/
├── README.md
├── ARCHITECTURE.md
├── LICENSE # MIT
├── .gitignore # blocks all secrets, .env, *.json, *.pem
├── immich/
│ ├── docker-compose.yml # sanitized
│ └── .env.example
├── nextcloud/
│ └── docker-compose.yml # sanitized
├── jellyfin/
│ └── docker-compose.yml # sanitized
├── cloudflared/
│ └── config.yml.example # sanitized
├── system/
│ ├── fstab.example # exFAT mount line
│ └── disable-sleep.sh # keep a headless laptop awake
└── docs/
└── 01–09 walkthrough
Active. Currently on the Acer Nitro on Ethernet. Indexing complete. ML container disabled (CPU-bound on this hardware) — re-enabling once GTX 1660 Ti CUDA passthrough is configured. See issues for the live roadmap.
Standing on the shoulders of:
- Immich — photo/video management
- Nextcloud — file sync & collaboration
- Jellyfin — media server
- Cloudflare Tunnel — public access without port-forwarding
- Tailscale — private overlay network
MIT.