Skip to content

How I Manage Docker Across Three Servers

As my homelab has grown, I have ended up running Docker across three Ubuntu servers rather than putting everything on one machine.

This has happened gradually rather than as part of some grand plan. As I have added more applications and services, it has made sense to spread them across the available hardware.

The result is a small Docker environment consisting of three mini PCs, each with its own role.

This page explains how I organise those servers, where I keep my Docker Compose projects, and how I manage the containers across the three machines.

My Three-Server Docker Architecture

My current homelab consists of:

  • minipc
  • micropc
  • micro2pc

The three machines are not identical and do not have exactly the same purpose.

minipc

The minipc is primarily an application and sandbox host.

It is useful for applications that I want to keep separate from the core infrastructure running on my other servers.

It also gives me somewhere to experiment without having to disturb the services that I rely on every day.

micropc

The micropc is my main infrastructure server.

It runs a number of important services and is also where I run infrastructure such as Caddy, which acts as my reverse proxy.

I also run services such as Netdata and Dozzle here, allowing me to monitor this server as well as the other Docker hosts.

micro2pc

The micro2pc is another Docker host and has become an important part of the homelab.

It runs many of my applications and services, including The Homelab Site itself.

It also gives me additional capacity without having to move everything onto a single machine.

My /opt/docker Structure

One of the conventions I use across my servers is to keep Docker application data under:

/opt/docker

Rather than putting all of my containers into one large Docker Compose project, I normally give each application its own directory.

For example:

/opt/docker/
├── caddy/
├── dozzle/
├── netdata/
├── ntfy/
├── paperless/
├── bookstack/
├── docmost/
├── filebrowser/
└── ...

The exact contents obviously differ between the three servers.

The important thing is that the structure is consistent.

Each application has its own directory, making it relatively easy to find its Compose file, configuration and persistent data.

One Application, One Directory

For most of my Docker applications, I use a structure along these lines:

/opt/docker/application/
├── docker-compose.yml
├── configuration/
└── data/
The exact names depend on the application.

Some Docker images expect their persistent data in a particular directory, while others allow me to choose the location.

I therefore don't try to force every application into exactly the same internal structure.

What I do try to keep consistent is the idea that everything belonging to an application lives together.

This makes maintenance much easier.

If I need to work on an application, I know where to go.

For example:

cd /opt/docker/netdata

or:

cd /opt/docker/docmost

I don't have to search the whole server to discover where the application was installed.

Docker Compose Is My Standard

I use Docker Compose for deploying most of my applications.

A typical project directory contains a docker-compose.yml file:

/opt/docker/example/
└── docker-compose.yml

I can then manage that application from its directory:

cd /opt/docker/example
docker compose ps
docker compose logs
docker compose pull
docker compose up -d

This is one of the main reasons I prefer keeping applications in separate directories.

I can work on one application without having to think about the other containers running on the same server.

I Don't Have One Giant Compose File

I deliberately don't put every application on a server into one enormous Docker Compose file.

That might make sense in some environments, but for my homelab I find separate Compose projects much easier to understand.

For example, if I want to update Netdata, I can work in:

/opt/docker/netdata/

without having to interact with BookStack, ntfy, Dozzle or any of the other applications on the same machine.

Similarly, if something goes wrong with one application, it is much easier to isolate it.

Managing Containers Across Three Servers

The downside of having three Docker hosts is that there is no single Docker daemon controlling everything.

Each server has its own Docker installation.

For example:

minipc
  └── Docker
       ├── container
       ├── container
       └── container

micropc
  └── Docker
       ├── container
       ├── container
       └── container

micro2pc
  └── Docker
       ├── container
       ├── container
       └── container

This means that when I want to work on a container, I first need to know which server it is running on.

For a homelab, I don't find this particularly difficult.

In fact, I think the separation is useful.

A problem on one Docker host doesn't necessarily affect the applications running on the other two.

Dozzle Gives Me a Single View

One of the applications that makes this arrangement much easier is Dozzle.

I run the main Dozzle instance on one server and use Dozzle agents on the other Docker hosts.

This allows me to see containers running across my different servers from one interface.

Instead of logging into each server simply to look at container logs, I can use Dozzle to inspect them from one place.

This is particularly useful when troubleshooting an application.

Netdata Gives Me Another Shared View

I use Netdata in a similar way for monitoring.

I run Netdata on my three servers:

  • minipc
  • micropc
  • micro2pc

The Netdata interface allows me to see the three nodes together.

This means I can quickly compare CPU, memory, disk and other system activity across the machines.

It is useful when trying to answer a simple question:

Which server is actually having the problem?

Without centralised monitoring, I would have to log into each server separately.

The Homelab Site Is Also a Docker Application

The Homelab Site itself is another example of this approach.

The site is built using MkDocs and the generated static files are served by nginx.

The Docker project lives under:

/opt/docker/thehomelabsite/

The current deployment separates the process of building the site from serving the finished static files.

The generated site is then served by nginx.

Caddy runs on the micropc and acts as the reverse proxy in front of the site, while the actual website container runs on micro2pc.

This is a good example of how the three-server arrangement can allow different responsibilities to be separated.

Reverse Proxy and Docker Hosts

My reverse proxy is Caddy.

Caddy runs on the micropc.

Applications can therefore run on other machines while Caddy provides the public HTTPS entry point.

The general pattern is:

Internet
   |
   v
Cloudflare
   |
   v
Caddy on micropc
   |
   +--------------------+
   |                    |
   v                    v
Services on micropc   Services on micro2pc
                       |
                       v
                    Docker

The important point is that the reverse proxy does not have to run on the same server as every application.

Caddy can forward requests across the local network to the appropriate Docker host.

Why I Like This Arrangement

There are several advantages to running Docker across three relatively small machines.

1. Separation

A problem with one server does not necessarily bring down everything.

If I am experimenting with something on minipc, I am not necessarily risking the infrastructure running on micropc.

2. More Available Resources

Three small machines together provide considerably more CPU, memory and storage resources than relying on one machine.

I can move applications between servers if the workload changes.

3. Easier Troubleshooting

When applications are separated between hosts, it can sometimes be easier to identify the source of a problem.

For example, if several services on micro2pc suddenly become unavailable, I immediately have a much smaller area to investigate.

4. Hardware Failure Is Less Catastrophic

This isn't a highly available production cluster.

If one of the machines dies, some services will still be unavailable.

However, not everything necessarily disappears at once.

That is a useful distinction for a homelab.

It Is Not a Kubernetes Cluster

I sometimes think about whether I should be using something more sophisticated.

I could, for example, start looking at Kubernetes, Docker Swarm or another orchestration system.

For my current homelab, I don't think I need that complexity.

My requirements are relatively simple.

I want to be able to:

  • deploy applications easily
  • update them
  • see their logs
  • back them up
  • restart them
  • understand where they are running
  • recover them when something goes wrong

Docker Compose does all of this adequately for me.

There is a certain appeal to being able to SSH into a server, change into an application directory and run:

docker compose ps

and immediately understand what is happening.

Backups Are Separate From the Docker Installation

I also try not to confuse the Docker Compose project with the backup of the application.

The Compose file describes how to recreate the application.

The persistent application data is what I really need to protect.

For example, an application might have:

docker-compose.yml

alongside a data directory containing its database or configuration.

If the server fails, having the Compose file is useful because I can recreate the container.

But without the persistent data, recreating the container may simply give me an empty application.

This is why my Docker backup strategy concentrates on the persistent application data as well as the Compose files.

Keeping the Structure Understandable

One of my main goals with this setup is not technical sophistication.

It is understandability.

I want to be able to come back to the homelab months later and still understand how I built it.

That is why I favour:

/opt/docker/
    application/
        docker-compose.yml
        data/
        configuration/

over complicated deployment systems that might technically be more powerful but are harder for me to remember.

How I Decide Where an Application Goes

I don't have a rigid rule that says a particular type of application must always run on a particular server.

Instead, I consider things such as:

  • available CPU and memory
  • storage requirements
  • whether the application needs access to network storage
  • whether it is experimental or important
  • whether it needs to be close to another service
  • whether it would be useful to keep it isolated

Sometimes the decision is simply:

' This server has plenty of spare capacity, so I'll put it here. '

That is one of the benefits of having three Docker hosts.

I have somewhere else to put an application if one machine starts becoming crowded.

Managing Updates

For most applications, updating is straightforward.

I normally work from the application's directory and pull the latest image:

docker compose pull

Then recreate the container:

docker compose up -d

I can check the result with:

docker compose ps

and inspect the logs if necessary:

docker compose logs

For applications where I want to be more cautious, I can take a backup before updating.

This simple workflow works well for my homelab.

The Three Servers Are Still One Homelab

Although Docker is distributed across three physical machines, I don't really think of them as three separate environments.

They are three parts of the same homelab.

The network connects them, Caddy connects the public services to the appropriate hosts, Dozzle gives me a shared view of container logs, and Netdata gives me a shared view of system performance.

The /opt/docker convention gives the individual machines a similar internal structure.

This makes the whole environment feel much more coherent.

What I Have Learned

Running Docker across three servers has taught me that I don't necessarily need complicated orchestration to build a useful homelab.

The important things for me have been consistency and simplicity.

I know where applications live.

I know which server they run on.

I know where their persistent data is stored.

I know how to start and stop them.

I can monitor them centrally.

And I have a backup strategy for the important data.

That is enough for my current needs.

My Current Architecture

The overall arrangement can be thought of as:

flowchart TB
    H[My Homelab]

    H --> MIN[minipc]
    H --> MICRO[micropc]
    H --> MICRO2[micro2pc]

    MIN --> MD[Docker]
    MICRO --> CD[Docker]
    MICRO2 --> M2D[Docker]

    MD --> MA[Applications and experiments]
    CD --> CA[Core infrastructure<br/>Caddy, Dozzle, Netdata]
    M2D --> M2A[Applications and services<br/>The Homelab Site and others]

The individual servers have different responsibilities, but they all follow the same basic philosophy:

Docker Compose projects live under /opt/docker, persistent application data is kept with the application, and monitoring and management tools provide a view across the whole homelab.

Final Thoughts

My three-server Docker setup is not something I designed from scratch.

It evolved.

I started with applications running wherever I had space, gradually added another mini PC, moved services around, and eventually developed conventions that made the environment easier to manage.

The most important convention has probably been the simplest one:

Give each application its own Docker Compose project and keep it organised under /opt/docker.

That small amount of structure has made a surprisingly large homelab much easier for me to understand and maintain.