Docker Compose Across Multiple Servers: Your Options Without Kubernetes

Nick Potts, who builds Ployz. Published , updated .

Docker Compose runs on one server. Docker's own docs say "Compose supports production deployments on single hosts" (Compose overview), and for anything bigger they point you to Swarm (Compose in production). So "docker compose multiple servers" really means choosing one of about seven setups, and they differ mostly in one thing: whether your containers on different servers can talk to each other privately.

Facts checked 2026-10-01, from Docker's, Kamal's, HashiCorp's, K3s's and Hetzner's own docs, linked next to each claim. I haven't run benchmarks for this piece; where I give an opinion, I say so.

Why I care: I moved from paying $500+ a month on Railway to one €80 dedicated server, and the next question was always "what happens when I need a second one?" I didn't want Kubernetes, a control plane or a three-server minimum. That's the lens here.

Which multi-server option fits you?

Here's the short version. "Private network" means a container on server A can reach a container on server B without you exposing ports to the internet.

OptionUses your Compose file?Private network between serversLoad balancing across serversServers you needExtra parts to run
Docker contextsYes, unchangedNoNo, add your own1+None
Docker SwarmYes, legacy v3 formatYes, overlay (unencrypted by default)Yes, routing mesh3 managers for fault toleranceRegistry, open ports 2377/7946/4789
WireGuard or Tailscale + Compose per serverYes, one per serverYes, encryptedNo, add your own1+The VPN, a load balancer
KamalNo, its own deploy.ymlNoNo, add your own1+Registry
NomadNo, its own job filesVia Consul or templatesAdd your own (e.g. Traefik)3 to 5 servers recommendedNomad servers, often Consul
K3sNo, Kubernetes manifestsYes, Flannel (WireGuard optional)Yes, via Kubernetes1, or 3 for HAKubernetes itself
PloyzNo, services set up separatelyYes, encrypted WireGuardYes, Caddy on every server1+None to rent

If you're still on one server and it isn't full, the cheapest option is a bigger server. I'd do that first. Everything below is for when one box really isn't enough, or when you want a second one so a single machine isn't your whole business.

Can Docker Compose run across multiple servers?

No. Compose talks to one Docker daemon at a time. It creates containers, networks and volumes on that daemon, and the network it makes is a bridge network that only exists on that machine. You can point Compose at a remote daemon, and you can repeat that for several servers, but each run is a separate, single-host deployment. Nothing joins them up.

That's not a missing feature so much as a design choice. Compose is a file format plus a client. The parts that make multi-server hard, like scheduling containers onto machines, a network that spans hosts, and moving traffic when a server dies, belong to an orchestrator. Docker's answer for that was Swarm mode. Kubernetes, Nomad and K3s are the other answers.

What Compose does give you for multiple servers is remote control. Docker says you can "deploy an app to a remote Docker host" by setting DOCKER_HOST, and then "all the normal docker compose commands work with no further configuration" (Compose in production). Docker contexts make that tidy.

How do you deploy the same Compose file to several servers?

Use Docker contexts. A context stores the connection details for one Docker daemon, including an SSH address, and lets you "switch the daemon your docker CLI connects to" (docker context create). Create one context per server, then run docker compose up once per context. Each server gets an identical copy of the stack. No agent, no cluster, no registry.

Here's a small worked example: a web app on two servers, with Postgres on a third. The SSH form of docker context create comes from Docker's daemon access docs, and the SSH user needs permission to use the Docker socket on each server.

# One context per server (SSH keys already set up)
docker context create web1 --docker "host=ssh://[email protected]"
docker context create web2 --docker "host=ssh://[email protected]"
docker context create db1  --docker "host=ssh://[email protected]"

# Postgres on its own server
DOCKER_CONTEXT=db1 docker compose -f db.compose.yaml up -d

# The same app stack on both web servers
for ctx in web1 web2; do
  DOCKER_CONTEXT=$ctx docker compose -f app.compose.yaml up -d --build
done

The DOCKER_CONTEXT variable "overrides the context set with docker context use" (Docker contexts), so the loop doesn't change your default. With --build, each server builds the image itself, so you don't need a registry for this.

And here's the catch, in the app file:

services:
  web:
    build: .
    ports:
      - "8080:3000"
    environment:
      # Not "db:5432". The db service lives on another server's network.
      DATABASE_URL: postgres://app:[email protected]:5432/app

On one server you'd write db:5432 and Compose's network would resolve it. Across servers there's no shared network, so the web servers reach Postgres by IP, which means Postgres has to listen on a port other machines can reach. On a public IP that's a database on the internet, guarded only by a firewall rule and a password. You also still need something in front of web1 and web2 to split traffic, plus a plan for deploys, since the loop updates one server and then the other.

Contexts are what I'd use for "same stack, several independent servers", like one copy per customer or per region. For one app spread across servers, they solve the easy half.

Is Docker Swarm still worth it for multiple servers?

Swarm is the closest thing to "Compose, but across servers". You turn a few servers into a swarm and run docker stack deploy --compose-file compose.yaml (stack deploy tutorial). It gives you replicas, an overlay network between hosts, and a routing mesh. It's built into Docker Engine and still supported, but it brings ports, quorum and a registry with it.

The details people find out late, all from Docker's docs:

  • Your Compose file may not work as-is. docker stack deploy uses "the legacy Compose file version 3 format", and "the latest format, defined by the Compose specification isn't compatible" with it. build is ignored, so you push images first (stack deploy).
  • You need a registry. "Because a swarm consists of multiple Docker Engines, a registry is required to distribute images to all of them" (same page).
  • Three ports between nodes. 2377/tcp for the control plane, 7946 tcp and udp between nodes, 4789/udp for overlay traffic (overlay networks).
  • Overlay traffic is unencrypted by default. You can create the network with --opt encrypted, which uses IPsec, but Docker warns it "imposes a non-negligible performance penalty" (same page).
  • Quorum. "Raft requires a majority of managers." Three managers survive one failure, five survive two. Lose quorum and running tasks keep going, but nothing "can be started, stopped, moved, or updated" (Swarm admin guide).

On status: Swarm mode is part of Docker Engine, and Docker separates it from "Docker Classic Swarm which is no longer actively developed" (Swarm mode). Mirantis said in July 2025 that "Swarm will be fully supported through at least 2030" (Mirantis). So it isn't dead. I think the real cost is that fewer people run it, so when it misbehaves you're reading old GitHub issues.

Can you connect Docker hosts with WireGuard or Tailscale instead?

Yes, and it fixes the worst part of the contexts approach. Put every server on a private WireGuard network, or on Tailscale, then run a normal Compose stack on each server and bind ports to the private address instead of the public one. Postgres listens on 10.0.0.3:5432, not on the internet. You still need a load balancer and a deploy routine.

What this looks like in practice:

  • Each server gets a private address on the VPN. Tailscale's free Personal plan allows up to 6 users and is for personal, non-commercial use (Tailscale pricing); for a business, plain WireGuard is free and a little more work to set up.
  • In Compose, publish database ports on the private IP only, for example "10.0.0.3:5432:5432".
  • Service names don't resolve across servers. You use private IPs, or you run your own DNS on the VPN.
  • For traffic, you run Caddy, nginx or HAProxy on one server, which becomes the single point of failure, or rent one. Hetzner's smallest load balancer, LB11, is €6.99 a month in Germany (Hetzner Load Balancer).

I like this setup more than Swarm for small teams. Every piece is boring and you can debug each one alone. The trade is that you're the orchestrator: if a server dies, nothing moves its containers, and you update each server's IPs by hand.

Is Kamal good for deploying to multiple servers?

Kamal, from 37signals, is multi-host by design: you list servers in config/deploy.yml, and one kamal deploy pushes your image to a registry and rolls it out over SSH, with zero-downtime switches through kamal-proxy. It doesn't read Compose files, it needs a registry, and it doesn't balance traffic between servers.

Kamal's install guide is blunt about the last part: "If you're running multiple servers, you need to put a load balancer in front of them" (Kamal installation). Its proxy docs add that Let's Encrypt may not be an option "if you are running from more than one host" (proxy config), so you usually end TLS at that load balancer.

Databases are "accessories", which you pin to a host. Kamal says they "are not updated when you deploy, and they do not have zero-downtime deployments" (accessories). There's no private network across servers, so the WireGuard advice above applies here too. I think Kamal is the best pick if you're on Rails and like config files.

Should you use Nomad or K3s instead?

Both are real orchestrators, which is more machinery than most small apps need. K3s is Kubernetes in one binary and runs on a 2-core, 2 GB server. Nomad is simpler than Kubernetes, but HashiCorp recommends 3 to 5 servers per region for production. Neither reads Compose files.

K3s. Server nodes need 2 cores and 2 GB of RAM, agents 1 core and 512 MB (K3s requirements). Nodes talk on 6443/tcp for the API, 8472/udp for Flannel VXLAN and 10250/tcp, or 51820/udp if you pick Flannel's WireGuard backend. You get everything Kubernetes has: replicas, services, ingress, and a cross-node network. You also get everything Kubernetes asks of you: manifests or Helm charts, and a control plane to keep healthy. High availability with embedded etcd means three server nodes.

Nomad. HashiCorp says "usually running 3-5 servers in a region is recommended", and its production guide sizes those servers at "4-8+ cores, 16-32 GB+ of memory" (Nomad requirements). That's sized for big clusters, and you can run smaller, but it tells you who it's built for. Native service discovery works through templates in the job file. For DNS-style discovery and health-filtered routing, the docs point to Consul (service discovery), which is one more system to run.

I think both make sense if you already know them, or if you're heading to dozens of servers. For two or three servers running a few apps, they're the kind of thing I built Ployz to avoid.

How do you load balance Docker containers across multiple servers?

You need something that receives traffic and forwards it to healthy containers on each server. Swarm's routing mesh and K3s's ingress do this inside the cluster. With contexts, WireGuard or Kamal, you add it yourself: a rented load balancer, or Caddy or nginx on one server. Either way, it should check health and stop sending traffic to a server that's down.

The three common shapes:

ShapeCostWeak point
Rented load balancer (e.g. Hetzner LB11)€6.99/month in Germany (Hetzner)One more bill per project; tied to one provider's network
Caddy or nginx on one of your serversFreeThat server is now a single point of failure
DNS with several A recordsFreeVisitors can still be sent to a dead server until you change DNS

Ployz's approach is to run a load balancer (Caddy) on every server, so any server can take traffic and route it to a replica on another server over the private network. There's no load balancer to rent. Generated addresses and custom domains on a CNAME stop sending visitors to a dead server within the hour; a root domain on A records needs the dead server's IP taken out by hand.

What happens to databases and volumes on multiple servers?

They stay on one server. A Docker volume lives on the disk of the machine that created it. Swarm, Kamal and Ployz all leave volumes where they are, and moving one means stopping the database, copying the data and starting it elsewhere. Plan for one database server, reached over a private network, with your own backups.

That's true for every option in this article except a Kubernetes setup with networked storage, which is its own project. In practice, the pattern I'd use: app replicas on two or more servers, the database on one, a private network between them, and backups to object storage. If your database is the reason you're scaling, my Railway Postgres piece covers what a busy database costs on a platform, which is the bill that pushed me off Railway.

How does Ployz run apps across multiple servers?

Ployz joins servers into one private network and deploys services onto them, without Kubernetes, Swarm or Compose. You add a server with one command, it joins an encrypted WireGuard network, and services reach each other by private DNS name across servers. A service can run up to 50 replicas, and every server runs its own Caddy load balancer. It's free on your own servers.

What it does, against the problems above:

  • Private network: servers join an encrypted WireGuard network, so Postgres doesn't need a public port. Services find each other by name, like Compose's db, but across servers.
  • Load balancing: Caddy runs on every server, so there's nothing to rent.
  • Deploys: zero-downtime deploys once you set a health check: the new version must pass it before the old one stops.

And the honest gaps:

  • No Compose import. You set each service up separately in Ployz (an image, a Dockerfile, or a GitHub repo). If you have a 15-service Compose file you want to run as-is, Swarm or a dashboard that reads Compose fits better.
  • Volumes live on one server, same as everywhere else.
  • Moving a database to another server isn't shipped yet. It's coming.
  • No backups, no automatic failover. If a server dies, its containers don't move on their own; the docs cover what to do.

Which option should you pick?

Pick Docker contexts if you want the same Compose stack on several independent servers and the copies don't need to talk to each other. It's free, uses your file unchanged, and takes ten minutes.

Pick WireGuard plus Compose per server if you want one app across servers using tools you already know, and you're fine being the orchestrator yourself.

Pick Docker Swarm if you want replicas and a cross-host network from Docker itself, can stay on the v3 Compose format, and will run a registry and three managers.

Pick Kamal if you're on Rails or happy with a config file, and will put a load balancer in front.

Pick K3s or Nomad if you already know them or expect to grow to many servers.

Pick your own servers with Ployz if you want several servers on one encrypted network, private DNS between services and a load balancer on every server, and you don't need your Compose file imported.

If you're weighing this against a platform, my PaaS pricing comparison and Railway alternatives show what the same apps cost on hosted platforms.

I build Ployz, which runs this multi-server setup on servers you rent, and it's free on your own servers.

Run the same app on a server you rent. Ployz is free on your own servers.

Deploy your app