Adding up the memory each application needs is the part everyone does. The parts that decide whether the arrangement survives a year are the routing, where the databases live, and whether one leaking container can take the other nine with it. This covers those, including the port-binding mistake that leaves an app you believed firewalled answering the open internet.
Ten applications on one machine is a different problem from ten machines running one each. The resource arithmetic is the easy half. What actually decides whether the arrangement holds up is how you route traffic, where the databases live, and what happens when one badly behaved container decides to consume everything.
One Proxy in Front of Everything
The first decision, and the one that shapes the rest: a single reverse proxy owning ports 80 and 443, with every application behind it on a private port.
The alternative, each application exposing its own port, means non-standard URLs, a certificate story per app, and a firewall rule list that grows. One proxy gives you one place for TLS, one place for access control, and a hostname per application instead of a port to remember.
# each app binds to loopback only
ports:
- "127.0.0.1:3001:3000"
Bind application ports to 127.0.0.1, not to 0.0.0.0. This is the single most common self-hosting mistake: Docker's default port publishing inserts rules that bypass a UFW configuration, so an app you believed was firewalled is answering the internet directly. Check it from outside the machine rather than assuming.
Keep a plain text file mapping hostnames to internal ports, and allocate from a range you decided in advance. Six months on, working out which application owns port 3007 by reading compose files is a waste of an evening.
Shared Postgres, or One Per App
Almost every application in this category wants Postgres, and running ten copies of it is the default because every compose file ships one.
| Approach | Argument for | Argument against |
|---|---|---|
| One Postgres per app | Isolated versions, upstream compose works unmodified | Ten idle instances, ten sets of memory overhead |
| One shared Postgres | Tune once, back up once, far less memory | Version conflicts; one restart affects everything |
| Shared for small apps, dedicated for heavy ones | Most of the saving, isolation where it matters | Two patterns to remember |
The third row is what experience converges on. Run one well-tuned Postgres with a database and role per application for the small services, and give a genuinely demanding application its own instance so its tuning and its restarts are independent.
Two cautions on the shared approach. Applications occasionally require a specific major version or an extension, so check before consolidating. And a shared instance is a shared failure: its restart during an upgrade takes every application with it, which is an argument for scheduling rather than against the approach.
Redis consolidates more easily, since it supports numbered databases and most applications let you pick one. One Redis with a database index per app is usually fine.
Cap Every Container
This is the difference between a machine that degrades and a machine that falls over. One application with a memory leak, or a transcoding job, or a runaway import, will take everything else down unless limits exist.
deploy:
resources:
limits:
memory: 1g
cpus: "1.5"
reservations:
memory: 256m
Set a memory limit on every container, without exception. An OOM-killed container restarts and its neighbours keep serving; an unbounded one triggers the kernel's OOM killer, which chooses its victim by its own arithmetic and frequently picks your database.
CPU limits are less critical because CPU is compressible: a starved container is slow rather than dead. Set them on the things that burst, such as anything doing media processing, so an upload does not make every other application unresponsive.
Add swap, modestly. It converts some out-of-memory kills into slowness, which is the better failure on a machine hosting things people use casually. Our guide to sizing self-hosted applications covers how much each application actually needs, which is what these numbers should be derived from.
One Backup Job, Not Ten
Ten applications backed up ten different ways means ten things to verify and, realistically, several that quietly stopped working.
Impose a convention instead. Every application's persistent data lives under one predictable path, one script dumps every database from the shared instance, and one job archives the lot. Then there is a single thing to monitor and a single thing to restore from.
/srv/apps/<name>/data # volumes
/srv/apps/<name>/compose # compose file and .env
/srv/backup/db # dumps from the shared Postgres
Two rules that make restores work. Compose files and .env files are part of the backup, not just the data, because a volume without the configuration that mounted it is a puzzle. And each application's secrets, especially any key that encrypts its stored credentials, belong in the backup as credentials. Our guides to database backup strategy and encryption keys and recovery cover both halves.
Upgrades Become the Recurring Cost
Ten applications is ten release cadences, and this is the part that quietly makes multi-app hosting expensive in time rather than money.
Three habits keep it manageable. Pin every image to an exact version, so nothing upgrades because a container was recreated. Batch the work into one monthly window rather than reacting to each release. And upgrade one application at a time within that window, so a failure has one obvious cause.
Automatic update tools are tempting here and they are the wrong trade for anything holding data you would miss. An unattended major version bump with a schema migration is not a change you can undo by reverting the tag. Our guide to upgrade strategy covers why, and what rollback actually costs once a migration has run.
When to Split the Machine
Consolidation has a limit, and four signals say you have reached it.
Blast radius. If some of these applications matter to other people and some are experiments, the experiments should not share a kernel with the things that matter.
Upgrade coupling. When you avoid upgrading the shared database because too many applications depend on it, the arrangement has started constraining you.
One resource-hungry tenant. Anything doing media transcoding or model inference wants its own machine, because its bursts are everyone else's outage.
Different exposure. A public service and an internal admin tool on one host means one compromise reaches both. That is an argument for separation regardless of resource use.
The practical shape most people land on is two or three machines rather than one or ten: a stable host for the services people rely on, a second for the noisy or experimental ones, and a separate one for anything genuinely public.
Buying the Right Shape
Multi-app hosting rewards memory over cores, since ten small services are mostly idle processes holding memory rather than competing for CPU.
MassiveGRID's per-resource pricing makes that shape purchasable directly at $2.87 per CPU core, $0.80 per GB of RAM and $0.01 per GB of SSD per month, so a memory-heavy, core-light instance is priced as what it is: 4 vCPU with 16 GB and 200 GB comes to $26.28 a month, where a fixed tier would have made you buy cores you do not need. Splitting into two hosts costs the resources twice rather than two instance minimums, which is what makes the split above affordable when the signals appear. Underneath, Proxmox high-availability clustering with automatic failover over Ceph storage replicating every block three times across independent NVMe drives means a host failure is a restart rather than ten services down until you rebuild.
Instances can be ordered across a partner footprint of more than 700 datacenters in 85 metros, 30 countries and six continents, with auto-provisioning in New York, London, Frankfurt and Singapore.