Docker Compose in Homelab: organization, profiles and best practices

Last update: May 24th 2026
  • Organizing Docker Compose by profiles and roles simplifies the management of homelabs with dozens of services.
  • Centralizing configuration in .env, using overrides, and versioning in Git makes the environment portable and easy to migrate.
  • Dedicated networks, Traefik and healthchecks improve the safety, isolation and resilience of services.
  • Monitoring, controlled logs, and automated backups make the homelab a stable platform in the long term.

Docker Compose Homelab

Setting up a modern homelab with containers has become a favorite hobby for many techies. Docker Compose is almost always at the heart of this setup : define your services in YAML, version them with Git, and start your entire environment with a single command.

However, when you start to grow, things change: you go from having two or three containers to dozens of services, internal networks, reverse proxies, databases, and CI runners . That's when the big question arises: a single giant Docker Compose instance or many small files? How do I organize profiles, networks, backups, security, and on top of that, make it easy to migrate?

Real-world approaches to setting up Docker Compose in a homelab

Homelab setup with Docker Compose

In practice, people who have been using Homelabs for a while usually work with three different Compose organizational models, each with its own advantages and disadvantages. Choosing the right approach saves you a lot of trouble when scaling up or migrating to a new machine.

On one hand, there are those who started with standalone Docker Run commands, then moved to Portainer, and finally jumped to Docker Compose . It's a typical scenario: Portainer offers great visibility, a user-friendly interface, templates, etc., but ultimately, editing complex parameters or migrating configurations becomes a hassle if you don't have anything in files.

At the opposite extreme is the one who has consolidated everything into a single "mega" docker-compose.yml capable of running absolutely all the homelab services: reverse proxy, media, utilities, monitoring, LLMs, databases... All in a single stack.

In between, many users stick with a mixed approach: several small docker-compose.yml files grouped by context (e.g., media, infrastructure, productivity, monitoring), all under the same repository and usually sharing global environment variables.

A rather elegant solution blends both worlds: a "root" docker-compose that includes other files (each in a subfolder of apps or services). This way you maintain a global view of the homelab, but without suffering through a thousand-line YAML file that's impossible to read.

Profiles, grouping by function, and large homelabs

docker compose homelab profiles

When your homelab starts to approach 30, 40, or 50 services (including backup services like databases, caches, or indexers), it's vital to bring order to them. This is where both grouping by function and using Docker Compose profiles come into play.

A very common pattern is to group everything into a single Compose “project,” but logically divided by profiles. For example:

  • Core profile: homelab core, with Traefik as a reverse proxy and an identity provider (e.g., OAuth or Authentik) to authenticate all apps under the same domain with HTTPS.
  • Media profileServices like Plex, Sonarr, Radarr, Ombi, SABnzbd or qBittorrent, responsible for curating, downloading and serving multimedia content.
  • Utilities ProfileTools such as Portainer, Watchtower (if used), Diun, dockcheck or similar to manage and monitor containers and updates.
  • Infrastructure/monitoring profile: Traefik, cAdvisor, Prometheus, Grafana, Uptime Kuma, Dozzle and everything related to monitoring and logging.
  • Experimental profiles or LLM: specific stacks for LLMs or curious apps (ChatGPT Next Web local, LibreOffice Online, etc.) that are usually disabled by default.

The beauty of profiles is that you can deploy only a portion of the infrastructure as needed. For example, you can run only the core + infrastructure profile on a low-power mini PC, and only deploy the media profile on the large server with more disks and GPUs.

In well-designed repositories, there's usually a "master" docker-compose.yml file at the root that uses include to push individual files into an apps/ or services/ folder . Additionally, almost all services are configured via a single global .env file, and some secrets are stored in a secrets/ directory , which greatly simplifies the initial setup.

Following this pattern, managing the homelab basically boils down to editing the .env file and secrets, enabling or disabling profiles, and deciding which services to start on each host . This is ideal if you're going to deploy the same set of applications across multiple machines.

One giant single docker-compose file vs. several small files

Docker Compose Homelab file structure

This is the eternal debate: a single docker-compose.yml file containing everything, or multiple files per service/stack? The real answer is usually "it depends on what you want to prioritize: simplicity of migration or clarity per service."

Those who advocate for a single master file typically highlight several advantages:

  • Migrating hosts is super easyYou clone the repository, copy the .env file and the secrets, mount the volumes, and run `docker compose up -d`. There's no need to go directory by directory.
  • Infrastructure as a code of truth: the entire topology of the homelab (services, networks, volumes, dependencies) is in one place.
  • Centralized updates: you change an image version, a reboot policy, or some logging, and you know exactly where to touch.
  Server setup tutorials: a complete and practical guide

But it also has clear drawbacks: a huge YAML file is harder to maintain, merge conflicts increase, and when debugging a specific problem, you find yourself navigating a monster of hundreds of lines. It's not uncommon to feel a little regretful when everything gets too big.

The other approach is to have a docker-compose.yml file per app or per logical stack , within a structure like this:

docker/
├── bookstack/
│   └── docker-compose.yml
├── dashy/
│   └── docker-compose.yml
└── traefik/
    └── docker-compose.yml

With this, each container is named something like bookstack-app-1 or traefik-reverse-proxy-1 , which helps you locate problems quickly: if the bookstack-app-1 container crashes, you know exactly which folder to look in.

Visually, it's much cleaner and allows you to manage each service independently (starting, stopping, or updating it without affecting the others). Furthermore, applications like Dozzle take advantage of having separate stacks to better organize logs.

The downside is that if you separate everything too much, coordination between common services (such as Traefik or shared networks) requires a bit more care : you have to declare external networks, specific Traefik labels, and remember the nomenclature of networks created by other docker-compose.

Best practices with .env, overrides, and version control

One of the most underrated tricks is centralizing configuration in .env files . Instead of flooding your docker-compose.yml with environment variables, you define something like this:

DB_USERNAME=myuser
DB_PASSWORD=secretpassword

And then in the YAML they are referenced as ${DB_USERNAME} or ${DB_PASSWORD} . This makes Compose readable at a glance, allows you to share variables between multiple services , and, most importantly, stores passwords in a separate file (which you can exclude from Git).

For different environments (production, testing, development), it's very useful to leverage docker-compose.override.yml . The idea is to have a base docker-compose.yml file and, in the override, only override what changes: ports, paths, debug flags, etc.

For example, in development you can load an override where you expose a different port, enable debugging, and mount the local source code . You don't touch the main YAML, but you adapt the stack to the environment where you're running it.

Obviously, versioning everything with Git is mandatory if you want your homelab to be even remotely professional . You'll usually have something like this:

homelab-docker/
├── docker-compose.yml
├── .env.example
├── services/
│   ├── media/
│   ├── infra/
│   └── ...
└── scripts/

From there, you initialize the repository, commit the infrastructure changes, and if something breaks, you can revert to a previous version of your Compose in seconds . For ambitious homelabs, this isn't just an option; it's the only way to avoid going crazy.

Networks, Traefik, and secure service exposure

In almost all moderately advanced homelabs, the same combination appears: Traefik as a reverse proxy and a centralized identity provider (Auth or Authentik) . This allows exposing many apps under subdomains with HTTPS and SSO.

A classic approach is to set up a dedicated Docker network, such as reverse_proxy or similar, where Traefik and all the web services you'll be serving externally are connected. The remaining containers (databases, caches, etc.) stay on isolated internal networks.

If you use Traefik and separate your services into different Docker Compose instances, you need to define a shared external network . Something like this:

services:
  bookstack:
    image: lscr.io/linuxserver/bookstack
    networks:
      - traefik-net
    labels:
      - "traefik.docker.network=traefik_default"

networks:
  traefik-net:
    name: traefik_default
    external: true

Here, the traefik_default network is created by the Traefik stack, and the other services are added to it via an external network called traefik-net. Labels tell Traefik which network to use for routing traffic.

When a single stack includes backend services (for example, a web container and its database), you can connect them to a shared default network, and only grant the web container access to the Traefik network . The database will have a label set to `traefik.enable=false` so that Traefik ignores it.

This type of setup offers two key benefits: isolation between services and controlled exposure . Only the containers you label with Traefik labels and that are on the proxy network become accessible from outside.

Data persistence, volumes, and disk structure

A homelab without persistent data isn't very useful: databases, configurations, media, documents… everything has to survive a Docker Compose Down. Volumes and bind mounts are your lifeline.

  Complete Guide to the Linux Kernel 7.1: New Features and Recommendations

Many people organize their storage using a structure like this:

/mnt/storage/
├── downloads/
│   ├── movies/
│   └── tv/
├── media/
│   ├── movies/
│   ├── tv/
│   └── music/
└── srv/
    └── 

The idea is that downloaders (qBittorrent, SABnzbd, etc.) only see the downloads folder , managers like Radarr/Sonarr have access to both downloads and media (to move/create hard links), and servers like Plex or Jellyfin only see the media folder.

This way you apply the principle of least privilege : each container only accesses what it actually needs. And the clear separation also helps when deciding which volumes or paths to back up to the cloud or external drives.

The srv directory is typically used to store app configurations (for example, /srv/jellyfin/config, /srv/traefik, /srv/paperless, etc.). This is usually partially versioned (templates, Caddyfile, etc.), leaving out anything critical or resource-intensive.

In some cases, it's useful to use hard links in the download chain: services like Radarr or Sonarr can link downloaded files to maintain seeding without duplicating disk space. The directory structure proposed by guides like TRaSHGuides is based precisely on this principle.

Automating deployments with GitHub Actions and local runners

If you like to take things a step further, you can automate homelab updates with CI/CD . Several users have replaced Jenkins and similar tools with a workflow using GitHub Actions and a runner self-hosted within the homelab itself.

The mechanism is simple: every time you push to the main branch of your homelab repo, a GitHub Actions workflow is launched that runs tests, linters, and, if all goes well, deploys the changes to the server.

A typical workflow includes steps such as:

  • Gitleaks-type secret scanner: in case you have accidentally uploaded passwords or tokens to the repo.
  • lining of YAML or infrastructure code, to maintain a readable and consistent format.
  • Updating the repository within the homelab itself: git pull on the target server.
  • Controlled recreation of containers: stop the old ones, launch the new ones and check the status.

Advantages: added security (you control leaks of secrets), better code quality, and repeatable deployments with a single push . And since you use a local runner, the images and volumes don't leave your network; you simply leverage the GitHub interface to visualize the pipelines.

Why Docker Compose makes life so much easier in a homelab

Many people have spent years relying on Docker Run and Portainer until, following an incident or a migration, they've been forced to re-evaluate their approach. When you lose a host or have to move services to another machine, depending on isolated commands or configurations solely within Portainer is a trap.

The big difference when you switch to Compose is that the entire service definition becomes text : volumes, ports, networks, labels, variables… All in a YAML file that you can copy, share, version, and reuse.

Editing a service is no longer about "rebuilding a container by hand"; it's now about modifying a line in a file, saving, and running `docker compose up -d` . You don't have to remember the original command or click through multiple Portainer screens.

Furthermore, if you work with multiple servers (mini PCs, NAS, desktops), it's extremely convenient to be able to copy the same Compose file to another machine, adjust four paths, and run the same stack on different hardware . In fact, many people acknowledge that, after a scare involving data loss or chaotic migrations, Compose has saved them a lot of time in subsequent events.

As an added bonus, building new services from old ones becomes trivial: for example, cloning the Plex configuration to set up Jellyfin by reusing the same media paths and transcoding devices takes only a few minutes if you do it by copying YAML blocks.

Optimization: build context, multi-stage builds and resources

Although many Homelab containers come from public images, in some cases you'll compile your own. In these instances, it's important to manage your build context : don't upload the entire repository unfiltered, but rather limit yourself to your project folder (using a strong `.dockerignore` directive) to ensure fast and lightweight builds.

Another very useful technique is to use multi-stage builds in your Dockerfiles: in the first stage you install dependencies and compile, and in the second stage you copy only the necessary artifacts to a small base image. The result: much smaller and safer final images , because they don't carry over unnecessary toolchains or libraries.

  Data backup strategies: a practical and comprehensive guide

On the Compose side, you have the option to define CPU and RAM limits (especially in Swarm environments or when Docker respects those parameters) to prevent resource-intensive apps from hogging resources. In Homelabs, this helps prevent a misconfigured service from crippling the rest of the system.

Don't forget the restart policies (restart: always, unless-stopped, on-failure): with them you ensure that critical services (reverse proxy, VPN, key databases) restart automatically after a reboot or a one-off failure.

Finally, it is advisable to schedule periodic cleanup tasks with commands such as docker image prune, docker container prune and docker volume prune to remove remnants of old builds, stopped containers or orphaned volumes and thus recover disk space.

Health services, logging and monitoring

To prevent your homelab from becoming a black box, it's important to work on three key aspects: healthchecks, controlled logging, and monitoring . Docker Compose allows you to declare healthchecks per service (using commands like `curl -f http://localhost` or specific scripts) that determine whether a container is healthy.

This allows you to ensure that only "healthy" containers receive traffic (for example, via Traefik) and that if they stop responding, they are restarted according to the configured policy. This significantly increases resilience with minimal effort.

Regarding logs, adjusting the json-file driver with max-size and max-file limits prevents the disk from filling up with gigabytes of forgotten logs. Web tools like Dozzle help you browse the logs of all containers from a browser, which is very convenient for debugging specific services.

For metrics and continuous monitoring, the classic combination is cAdvisor + Prometheus + Grafana . cAdvisor exposes CPU, memory, disk, and network usage statistics per container; Prometheus collects them periodically, and Grafana displays them in attractive dashboards, with alerts if anything spikes.

A well-set-up homelab typically includes Uptime Kuma for availability checks (HTTP, ICMP, TCP, etc.) and an automated backup system like Duplicati to copy critical data to other disks or the cloud. This way, you know what's happening, and if something goes wrong, you don't lose what's important.

Security and remote access to the homelab

However DIY the setup, security is not optional. Many people choose not to directly expose their NAS or its services to the outside world , limiting remote access through a VPN (WireGuard is a very popular option due to its performance and simplicity).

In this model, the router acts as a gateway: only a random port is opened to the VPN server, and once connected, all requests to internal services pass through an encrypted tunnel . Neither Traefik nor the apps are exposed to the internet without this prior filtering.

Those who prefer not to manage their own VPN sometimes turn to Cloudflare Tunnel or Tailscale to access their home lab without opening ports. These are convenient alternatives, although if privacy is your top priority, you'll need to consider what metadata these third parties might collect.

Another good practice is to encrypt the server and NAS disks , apply patches regularly, and limit automatic updates (many avoid Watchtower in favor of controlled manual updates). It's better to be a little behind but with control than to break half of Homelab because of an update you haven't checked.

As you can see, you don't need to reach an "enterprise" level, but it is advisable to establish a minimum level of security and discipline so that your homelab is not a sieve or a constant source of scares.

Ultimately, setting up a serious homelab with Docker Compose is a mix of organization, common sense, and a willingness to tinker: if you group services, define the networks well, document in Git, and automate a bit, you end up with an environment that you can start with a single command, migrate to another machine easily, and expand little by little without it becoming an uncontrollable jungle.