Docker Compose recipes for homelab: storage-first stacks you can trust in a malware-heavy landscape

A practical deep dive into Docker Compose recipes for homelab — real examples, comparisons, and setup guides.

Docker Compose recipes for homelab: storage-first stacks you can trust in a malware-heavy landscape

Docker Compose recipes for homelab: storage-first stacks you can trust in a malware-heavy landscape

A few days ago I tripped over a news item that hits home for anyone running a homelab: a report claiming thousands of GitHub repos are distributing trojan malware. If your containers pull from the wrong source, your NAS, your media server, and your personal cloud can become a thorny attack surface in minutes. It’s not fear-mongering—this is a reminder that the container world is as much about careful image sourcing, signing, and build hygiene as it is about clever orchestration patterns. Related to that, a separate piece argued that .gitignore isn’t the only way to ignore files in Git; the broader lesson is clear: ignore mistakes upstream, but also assert guardrails downstream. In a homelab, guardrails are your Compose files, your image pins, and your network topology.

Combine that with the recent chatter about enterprise NAS solutions built on ZFS (think the Ubiquiti article about Enterprise NAS), and you’ve got a useful mental model: your storage layer matters as much as your containers. ZFS gives you snapshots, checksums, and rollback power; your Compose stacks sit on top of that. If you’re using a ZFS-backed pool to host your Docker volumes, you can snapshot a Nextcloud or Jellyfin dataset before big upgrades, then roll back if something goes sideways. In other words, this is a good moment to bake storage-awareness, security-conscious image habits, and pragmatic Compose recipes into your homelab playbook.

In this post I’ll share practical Docker Compose recipes you can reuse on a single-node homelab, with notes on security, storage, and day-to-day ops. I’ll also include a compact comparison table to help you decide between legacy docker-compose, the modern docker compose CLI, and a lightweight k8s alternative for future growth. Finally, you’ll find a concrete example you can deploy today, plus commands you can run to sanity-check and maintain your stack.

What makes Docker Compose relevant for homelabs today

  • Simplicity with scale: On a single host, Compose is still the fastest way to define a multi-service stack without the overhead of a full orchestrator.
  • Portability and backups: A single docker-compose.yml is easy to version-control, copy to another host, or snapshot via ZFS. You can keep data directories separate and snapshot them quickly.
  • Security-by-default habits: Compose encourages you to pin images, use explicit environment isolation, and avoid running containers as root when possible. It’s also where you can enforce a sane approach to pull policies, image digests, and restricted networks.
  • Storage integration: With ZFS-backed volumes, you can leverage snapshots, compression, and rollbacks for critical stacks (Nextcloud, Jellyfin, and Home Assistant dashboards) without losing data.
  • Network and TLS patterns: Traefik, Nginx Proxy Manager, and similar stacks pair nicely with Compose to give you TLS everywhere and consistent routing rules, without reconfiguring every service.

A cautionary note about images and supply-chain hygiene

If you’re not careful, your Compose file can unintentionally pull malicious images or stale builds. The “10k GitHub repos distributing trojan malware” story is a blunt reminder: pin your images to a known-good digest when possible, prefer official or highly trusted images, and consider pull policies and image scanning as part of your normal workflow. It’s not about paralyzing your workflow; it’s about making safe assumptions explicit in your docker-compose.yml and your CI/build steps.

The practical recipes below are designed to be drop-in templates. They assume you’re using a single host with Docker Engine 20.x+ and Docker Compose v2 (the docker compose CLI). If you’re coming from an older version or a different OS, you’ll adapt paths and environment specifics, but the concepts stay the same.

Recipe 1: Quick, testable reverse-proxy with Traefik and a whoami test service

Why this matters: You want TLS-ready access to multiple services with minimal ceremony. This tiny stack gives you a safe sandbox to test routing, TLS (in a future iteration), and the basic habit of keeping services isolated behind a gateway.

docker-compose.yml (example)

version: "3.8"

services:
traefik:
image: traefik:v2.12
command:
- --providers.docker
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --api.insecure=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
networks:
- web

whoami:
image: traefik/whoami
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(whoami.local)"
- "traefik.http.routers.whoami.entrypoints=web"
- "traefik.http.services.whoami.loadbalancer.server.port=80"

networks:
web:
driver: bridge

Notes and tips for this recipe
- This is deliberately minimal. It’s a safe stub you can expand with real services and TLS certificates later.
- If you want TLS, add a certificates resolver (like ACME) and a proper domain. You’ll likely want a volume to store ACME data and a real email address for Let’s Encrypt.
- For image safety, pin the Traefik image tag to a specific version (e.g., v2.12) and periodically verify the digest. Use docker inspect or a CI gate to confirm image provenance.

How to run
- Up: docker compose up -d
- Check: docker compose ps
- Validate: docker compose logs traefik

Recipe 2: Nextcloud behind Traefik with a proper database

Why this matters: A personal cloud is one of the higher-value services in a homelab. It needs persistence, sane backups, and a routing layer that doesn’t leak secrets. This recipe demonstrates a two-tier approach: Nextcloud as the app container and MariaDB as the data store, with Traefik as the front door.

docker-compose.yml (example)

version: "3.8"

services:
traefik:
image: traefik:v2.12
command:
- --providers.docker
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.myresolver.acme.httpchallenge=true
- --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
- --certificatesresolvers.myresolver.acme.email=me@example.com
- --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
ports:
- "80:80"
- "443:443"
volumes:
- ./letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
networks:
- web

db:
image: mariadb:10.11
environment:
- MARIADB_ROOT_PASSWORD=change_me_root
- MARIADB_DATABASE=nextcloud
- MARIADB_USER=nextcloud
- MARIADB_PASSWORD=change_me_user
volumes:
- nextcloud_db:/var/lib/mysql
restart: unless-stopped
networks:
- web

app:
image: nextcloud:28-fpm-alpine
environment:
- NEXTCLOUD_TRUSTED_DOMAINS=cloud.local
- MYSQL_HOST=db
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_PASSWORD=change_me_user
- NEXTCLOUD_ADMIN_USER=admin
- NEXTCLOUD_ADMIN_PASSWORD=admin_password
depends_on:
- db
volumes:
- nextcloud_data:/var/www/html
restart: unless-stopped
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.nextcloud.rule=Host(cloud.local)"
- "traefik.http.routers.nextcloud.entrypoints=websecure"
- "traefik.http.routers.nextcloud.tls=true"
- "traefik.http.services.nextcloud.loadbalancer.server.port=80"

web:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- web

volumes:
nextcloud_db:
nextcloud_data:

networks:
web:

Notes and tips for this recipe
- Security: avoid leaking credentials. Use docker secrets or a .env file for sensitive values; consider upgrading to a more secure storage for passwords and use the Nextcloud admin panel to set strong credentials.
- Backups: Turn on ZFS snapshots for the nextcloud_data volume if you can, and add a separate backup job in your homelab (rclone to cloud storage, or an offsite backup to another dataset).
- Database tuning: With Nextcloud, pay attention to MySQL/MariaDB settings, like innodb_buffer_pool_size on a single-node homelab with plenty of RAM.

How to run
- Up: docker compose up -d
- Setup: Browse to cloud.local and finish Nextcloud web installer if you’re starting fresh
- Backup: Regularly snapshot the nextcloud_db and nextcloud_data volumes

Recipe 3: Jellyfin (or any media server) behind Traefik

Why this matters: Media stacks are exactly the kind of thing you want to be able to access from the inside and outside, with TLS, but without fuss. Jellyfin is a great fit for a homelab, and with Traefik you get convenient routing and TLS termination without custom nginx config.

docker-compose.yml (example)

version: "3.8"

services:
traefik:
image: traefik:v2.12
command:
- --providers.docker
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.myresolver.acme.httpchallenge=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
restart: unless-stopped
networks:
- web

jellyfin:
image: jellyfin/jellyfin:10.8.0
environment:
- TZ=UTC
volumes:
- /path/to/movies:/movies
- jellyfin_cache:/config
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyfin.rule=Host(media.local)"
- "traefik.http.routers.jellyfin.entrypoints=websecure"
- "traefik.http.routers.jellyfin.tls=true"
- "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
networks:
- web

volumes:
jellyfin_cache:

networks:
web:

Notes and tips for this recipe
- Jellyfin will store libraries in /movies (or wherever you mount). Keep large media on a separate pool/dataset with strong snapshot options.
- If you’re feeling fancy, you can add a separate Plex or Jellyfin client network to keep streaming traffic isolated from admin interfaces.

A quick note on credentials, image pinning, and safe assembly

  • Pin images by digest where possible. For example, instead of image: nginx:1.23.4, you can pin to image: nginx@sha256:abc123... This is more reproducible and less prone to supply-chain drift.
  • Use a .dockerignore file (the analog of Git’s .gitignore) to prevent sensitive files from entering the build context. This is the same habit you’d want to keep in mind as you craft a docker build that you might reuse in a CI pipeline or local development. If you don’t have a build step here, at least keep your local Docker context clean.
  • Consider a basic image scanning step in your CI or a nightly local scan of your Compose-derived stacks. It’s not a heavy load to add a scan against your base images and the final tags before you run docker compose up.
  • If you’re building images locally for these stacks, consider using a minimal, well-audited base image and avoid installing extra software you don’t strictly need.

A short comparison table: docker-compose v1 vs docker compose v2 vs Kubernetes (k3s)

Approach Pros Cons Best for
docker-compose (v1) Very simple; universal; easy to learn; great for single-host homelabs Slower to adopt new features; limited integration with the newer Docker CLI; some features deprecated Quick starts, single-node stacks, straightforward migrations
docker compose (v2) Native CLI workflow; supports multiple compose files; better integration with Docker Engine; profiles and extension features Some users see occasional CLI changes; requires newer Docker engine Typical homelabs that want a modern, integrated workflow
Kubernetes (k3s) Rich multi-node orchestration; strong ecosystem; resilient and scalable across multiple hosts High learning curve; more complex ops; more resources Advanced homelabs with multiple nodes, complex deployment patterns, and high resilience needs

What changed, and what to do next

  • Change: Image provenance matters more than ever in a world where supply-chain risk is real. Your Compose files should codify your guardrails: pins, digests, and explicit images from trusted sources. Action: audit your docker-compose.yml files. Replace any floating tags (like latest) with explicit version tags and, where feasible, digests.
  • Change: Storage-backed stacks benefit from a disciplined approach to backups and snapshots. Action: align your docker volumes with ZFS datasets so you can snapshot critical paths (e.g., nextcloud_data, jellyfin_cache) before updates or upgrades.
  • Change: TLS and edge security are easier to manage with a gateway pattern. Action: deploy Traefik or nginx-proxy-manager as a separate service and route your homelab apps through it. Start with a small stack (like the Traefik + whoami recipe) to validate TLS routing on your network.

What you should do next (step-by-step)

  • Step 1: Pick a gateway approach. If you’re starting from scratch, spin up the Traefik + whoami stack (Recipe 1) to validate your network and hostnames. Test on an internal hostname like whoami.local.
  • Step 2: Add an application stack. Start with Nextcloud or Jellyfin behind Traefik as in Recipe 2 or Recipe 3. Use a separate dataset for data (and consider a replication or backup plan).
  • Step 3: Harden your images and context. Add a .dockerignore, pin image tags, and consider a daily or weekly image scan. If you want to level up security, add a small CI step that checks the integrity of your Compose files and the digests of your images.
  • Step 4: Integrate storage snapshots. If you’re using ZFS, map Nextcloud and Jellyfin data directories to datasets that you can snapshot before upgrades. This will help you recover quickly if an update goes sideways.
  • Step 5: Backups and restore tests. Regularly test restore of critical data directories. A quick restore drill (snapshots on a test VM) is as valuable as a long-term backup plan.

A practical command you can run today

  • If you already have a Compose file, you can validate and apply it like this:

Validate the YAML and the config

docker compose config

Pull the latest images (efficient for multi-service stacks)

docker compose pull

Bring up the stack as a background service

docker compose up -d

See what’s running

docker compose ps

Cleanly stop and remove containers, networks (but keep volumes)

docker compose down

Caveat or two I’ll own up to

  • A single-host homelab is great, but real-world reliability starts to demand a bit more: multi-node setups with a lightweight orchestrator (like k3s) become compelling once you have two or more physical hosts. If you’re primarily interested in “low friction” reliability, you’ll probably stay with docker compose on a single node for a while.
  • The trailing edge of TLS and Let’s Encrypt automation can be fiddly on a DIY network. I’ve found that starting with a simple “http only” local domain (e.g., cloud.local) and then migrating to TLS once you’ve ironed out DNS, firewall rules, and cert management is the least painful path.

Final, short, actionable conclusion

Use Docker Compose as your repeatable, auditable pattern for homelab stacks, but treat it as part of a broader, security-aware lifecycle. Start with a small, tested gateway (Traefik + whoami) to get routing and TLS basics right. Build storage-conscious stacks (Nextcloud, Jellyfin) behind that gateway, with volumes on ZFS datasets you can snapshot and rollback. Pin images, avoid latest, and scan inbound images. If you’re growing beyond a single host, consider lightweight orchestration (k3s) to gain resilience while keeping your day-to-day management comfortable.

If you take one action this week, pick a single Compose file, pin the images with digests, and deploy a two-service stack behind Traefik. Then snapshot your data pool, and test a restore. You’ll be surprised how quickly a well-designed Compose recipe becomes the backbone of a reliable, safe, and enjoyable homelab.


Jellyfin

Product Notes Link
Jellyfin Link
Emby Link

Backup

Product Notes Link
Backblaze B2 Affordable offsite object storage Link
Wasabi Affordable offsite object storage Link