Most applications that outgrow one server are already described in a Compose file, and the developers who wrote it do not want to learn Kubernetes to get a second server. Docker Swarm is the orchestrator built for exactly that gap: the Compose file is the deployment format, and a cluster is a few nodes that have joined. This post follows a developer moving a Compose stack from a VPS to the Docker Swarm Cluster package on MassiveGRID PaaS, which is built on Virtuozzo Application Platform, formerly Jelastic.
The application is a customer support platform for a company we will call Bramble Support: twelve services in one Compose file, an API, a web front end, workers, a PostgreSQL database, Redis, a search service, a mail relay and a few sidecars, all on one virtual server that is now the company's single point of failure. The Docker Swarm Cluster package deploys Docker Engine CE nodes already joined in swarm mode, with managers and workers that scale.
What the package gives you before you type anything
Installing the package produces an environment with one or three manager nodes (three is the high-availability option; managers hold the cluster state through Raft and need a majority) and a layer of worker nodes, all running Docker Engine CE and already joined into one swarm. Overlay networking works between them out of the box, and new workers added later, by hand or by the platform's horizontal scaling triggers, join the swarm automatically. Each node is a platform container on its own physical host with vertical scaling, so a node grows and shrinks in cloudlets by the hour like any other.
The developer connects with the platform's Web SSH to a manager and runs docker node ls. Three managers, two workers, all ready. Nothing to install.
Turning the Compose file into a stack
Swarm deploys Compose files with docker stack deploy, reading the same service definitions plus a deploy section per service. The developer's changes to Bramble's file are additive.
services:
api:
image: registry.bramble.example/support-api:2026.07.3
networks: [backend, frontend]
secrets: [db_password, session_key]
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 20s
order: start-first
restart_policy:
condition: on-failure
placement:
constraints: [node.role == worker]
postgres:
image: postgres:16
volumes: [pgdata:/var/lib/postgresql/data]
networks: [backend]
deploy:
replicas: 1
placement:
constraints: [node.labels.db == true]
networks:
backend: { driver: overlay }
frontend: { driver: overlay }
secrets:
db_password: { external: true }
session_key: { external: true }
Three things changed. Replicas and update policy for the stateless services, so the API runs three copies across workers and updates one at a time, starting the new copy before stopping the old. Placement constraints, so stateless services run on workers and the database is pinned to the one node labelled for it. Secrets instead of environment variables from a file: the database password and session key are created once with docker secret create and mounted into containers as files, never visible in docker inspect.
Networking: overlay and the routing mesh
On the VPS every service talked to every other over the default bridge. In the swarm the developer defines two overlay networks: backend for the API, workers, database, Redis and search, and frontend for the web tier and API. Overlay networks span all nodes and are encrypted between them if you ask; services resolve each other by service name through Swarm's built-in DNS, so the API reaches PostgreSQL at postgres:5432 exactly as it did in Compose.
Published ports use Swarm's routing mesh: the web service publishes 8080, and that port is reachable on every node in the swarm, which forwards to a healthy replica wherever it runs. The developer exposes it to the internet through the platform: an endpoint on the manager maps the port through the shared load balancer without a public IP, or a public IP on one node with the container firewall opened for 443, with an NGINX load balancer node in front terminating TLS via the Let's Encrypt add-on. Bramble uses the latter. The platform's endpoint templates already include the Swarm ports (2377, 4789, 7946) if a node outside the environment ever needs to join.
State: the database and the volumes
The honest limitation of Swarm is stateful services. A named volume lives on the node where the container runs, so if PostgreSQL were rescheduled to another node it would find an empty volume. The developer's answer for Bramble is the conventional one: pin the database to a labelled node with a placement constraint, back it up nightly with pg_dump to a Backup Storage node, and rely on the platform's own resilience for that node, which runs on its own host with live migration off a failing host. The alternative he notes for later is to take PostgreSQL out of the swarm entirely and use the marketplace's PostgreSQL Primary-Secondary Cluster in the same environment group, reachable from the swarm over the internal network, which gives replication without Swarm having to understand it. For shared files across replicas, a platform shared storage container mounted into the worker nodes over NFS does what a local volume cannot.
The cut-over
- Push the images to the company's registry (the GitLab server package's registry, in Bramble's case).
- Create the secrets on a manager. Label the database node.
docker stack deploy -c stack.yml support. Watchdocker service lsuntil every service shows its replicas running.- Restore the VPS's PostgreSQL dump into the swarm's database service. Stop the VPS's workers so no job runs twice.
- Test through the swarm's URL. Switch DNS. Keep the VPS for a week, then delete it.
| Concern | One VPS with Compose | Swarm cluster on PaaS |
|---|---|---|
| A node fails | Everything is down | Replicas rescheduled to other workers; database node restored by the platform |
| Deploy a new API version | Restart, brief outage | Rolling update, start-first, no outage |
| Traffic doubles | Resize the VPS, reboot | docker service scale, or a horizontal trigger adds a worker that joins automatically |
| Secrets | .env file on disk | Swarm secrets, mounted in memory |
| Cost | Fixed monthly | Per cloudlet-hour on what the nodes use |
What the developer did not have to learn
No ingress controllers, no manifests in a new language, no cluster upgrade procedure. The Compose file gained a deploy section, the team kept its mental model, and the platform supplied the parts around the swarm: node placement on separate hosts, vertical scaling per node, automatic joining of new workers, TLS, backups and an environment that can be cloned for a staging swarm in minutes. For a twelve-service application with one database, that was the right amount of orchestration.
Frequently Asked Questions
How many manager nodes do I need?
One for development or a small internal tool; three for production. Managers use Raft and need a majority to accept changes, so three tolerate one manager's loss. Five tolerate two but add coordination overhead; more is rarely useful. Workers can be any number.
Can I use docker compose up on a swarm?
docker compose targets a single engine. For the swarm use docker stack deploy with the same file plus deploy sections; keys that only apply to single-engine Compose (such as build) are ignored with a warning. Build your images in CI and reference them by tag.
How do I add a node that is not part of the package?
Install the Docker Engine CE package and choose the option to connect to an existing swarm, providing the manager's join token and address. The platform's endpoint templates for ports 2377, 4789 and 7946 make the swarm reachable if the new node is in another environment.
Your Compose file, on three nodes
The Docker Swarm Cluster package deploys managers and workers already joined in swarm mode on MassiveGRID PaaS, with new workers joining automatically and per-cloudlet-hour billing. Free 14-day trial, no credit card.
Docker Swarm on MassiveGRID PaaS