A team proposes running their production deployment as a Docker Compose stack on a single VM, updated by pulling new images and re-running `docker compose up -d`. What are the tradeoffs, and when would you argue for a different approach?
answer
- one VM = one failure domain
- up -d recreates = deploy downtime window
- restart policy ≠ health-based healing
- no scheduler, no rolling update, no rollout history
- move when: HA target, 2nd node, independent deploys
basics
~20 sIt is defensible for low-stakes, single-node workloads: simple, cheap, one reviewable file. But it gives no multi-node scheduling, no self-healing beyond restart policies, and up -d recreates containers, so updates mean downtime. Push back when uptime, scale or blast radius matter.
solid answer
~60 sI'd frame it as a deliberate, revisitable choice rather than right or wrong. **What it buys:** one artefact developers already understand, near-zero operational surface, no control plane to run or patch, fast recovery (`git pull && docker compose up -d`), and genuine dev/prod parity for a small team. **What it costs:** a single VM is a single failure domain — host down means service down, and there is no scheduler to move anything. `up -d` recreates changed containers in place, so a deploy is a short outage unless you front it with a proxy and do the blue/green dance manually. `restart: unless-stopped` restarts crashed containers but does not act on unhealthy ones. No rolling updates, no rollout history, no bin-packing, no secret management beyond files and environment, and horizontal scaling stops at the size of the box. **When I'd change:** an availability target that a single host cannot meet, more than one node, teams that need independent deploys, or compliance requiring audited rollouts. The step up is a small managed orchestrator — not usually a self-run cluster for a small team.
code
yaml · 25 linesservices:
api:
image: registry.example.com/myapp/api@sha256:9f2c...
restart: unless-stopped
read_only: true
tmpfs: ["/tmp"]
user: "10001:10001"
cap_drop: ["ALL"]
healthcheck:
test: ["CMD", "/app/healthcheck"]
interval: 10s
retries: 3
start_period: 30s
deploy:
resources:
limits:
cpus: "1.5"
memory: 1g
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
ports:
- "127.0.0.1:8080:8080" # only the proxy reaches itgo deeper
Recognise that Compose runs on one host and that recreating containers on deploy means a brief outage.
List the concrete gaps — no scheduler, no rolling update, restart policies are not health-aware — and the mitigations available on a single host.
Harden the existing setup properly (digests, limits, backups, off-host monitoring, proxy-fronted swaps) and quantify the availability ceiling it implies.
Turn it into a decision with named tradeoffs, explicit migration triggers tied to measurable thresholds, and an honest statement that adopting a cluster is itself operational risk if none of those triggers hold.
## Take the proposal seriously first The reflex answer ("Compose is not for production") is weak. Plenty of real systems — internal tools, staging environments, low-traffic B2B products, single-tenant appliances — run perfectly well as a Compose stack on one VM for years. The interviewer is testing whether you can weigh cost of operation against availability requirements rather than reach for the biggest tool. ## The honest case in favour - **One artefact, already understood.** The same file the developers run locally describes production. No second deployment language, no chart, no manifest drift. - **No control plane.** A cluster is itself a system that must be upgraded, secured, monitored and paid for. On a small team that operational cost can exceed the cost of the workload. - **Trivial recovery story.** Rebuild the VM from an image, clone the repo, `docker compose up -d`, restore volumes from backup. Anyone on the team can execute it at 3am. - **Cheap.** One instance, no managed-cluster fee, no per-node overhead. ## The costs to name explicitly **Availability.** One VM is one failure domain: kernel panic, disk full, noisy neighbour, or a host maintenance event takes the whole product down. Compose has no scheduler, so nothing relocates. The realistic ceiling here is "a few nines minus whatever your cloud provider's single-instance SLA is", plus your restore time. **Deploys are recreations.** `docker compose up -d` with a new image tag stops and recreates the affected containers. That is a downtime window per deploy — usually seconds, but during it requests fail. Mitigations exist and should be described concretely: put a reverse proxy in front, run two instances of the app service under different names, drain and swap them, or use a proxy that reads container labels and reloads on change. All of it is manual machinery that an orchestrator gives you as a primitive. **Health is weakly enforced.** `restart: unless-stopped` restarts a container whose process *exited*. It does not restart a container that is running but wedged — a healthcheck marks it `unhealthy` and, on a plain Docker host, nothing acts on that. You either add a supervisor that watches health, or you accept detect-by-alert-and-restart-by-hand. **No capacity management.** No bin-packing, no requests/limits enforcement across services beyond per-container `cpus`/`mem_limit`, no eviction policy. Scaling is vertical until the instance type runs out, and `docker compose up --scale api=3` still lands every replica on the same box, competing for the same page cache and the same NIC. **Configuration and secrets.** Secrets end up in `.env` files or the shell on the box. There is no rotation, no per-service scoping, no audit. Compose `secrets:` help by mounting files instead of environment, but the source is still a file on the host. **Change management.** No rollout history, no `rollback`, no record of what version was running when. You rebuild that from git tags plus discipline. State also lives on that box: named volumes on local disk mean backups are your responsibility and a host loss is a data-loss event unless volumes are on network storage or replicated. ## Make the current choice safer before you replace it A principal-level answer improves the option before abandoning it: - Pin images **by digest**, not `latest`, so `up -d` is deterministic and reproducible. - Put the compose file in git and deploy only from a tagged commit; treat the VM as replaceable. - Named volumes for state, off-host backups verified by restore drills, ideally network-attached storage. - `restart: unless-stopped` plus healthchecks plus external synthetic monitoring, because in-host monitoring dies with the host. - Resource limits per service so one leak cannot take the box down. - Read-only root filesystems, non-root users, dropped capabilities, no published ports except the edge, and the edge bound behind a proxy with TLS. - Log shipping off the host; the local json-file driver with rotation configured, so logs cannot fill the disk. ## The triggers to move Name the conditions rather than a preference: 1. **An availability target a single host cannot meet** — anything requiring survival of a host failure without human action. 2. **More than one node** — the moment you need two machines, you need something that places workloads on them. 3. **Independent deploys by multiple teams** — a shared file on a shared box becomes a coordination bottleneck and a shared blast radius. 4. **Zero-downtime as a requirement, not a nicety** — rolling updates, readiness gating and automatic rollback are primitives you should not hand-roll. 5. **Regulatory or audit needs** — who deployed what, when, and can it be rolled back. And the corollary: if none of those hold, moving to a cluster is *added* risk, not removed risk. The intermediate steps also deserve mention — a managed container runtime that takes an image and handles rollout, or a small managed cluster — before running a control plane yourself. The strongest version of this answer ends with a review trigger: revisit the decision when metric X (traffic, team size, downtime cost) crosses a stated threshold.
- How would you get close to zero-downtime deploys while staying on Compose?Put a reverse proxy in front and never let clients hit the app container directly. Run the new version as a second service, wait for its healthcheck to pass, switch the proxy upstream, then remove the old one — either by hand with two named services or with a proxy that watches container labels and reloads. It works, but you are hand-building rolling updates, which is itself the argument for an orchestrator once deploys get frequent.
- The team says `restart: always` gives them self-healing. What is missing?Restart policies react to the container process exiting, not to the application being broken. A wedged JVM, a deadlocked thread pool or a service that returns 500s keeps its process alive, so the policy never fires; the healthcheck will report `unhealthy` but plain Docker takes no action on that state. Real self-healing needs something that consumes health status and replaces the workload.
- What would you insist on before signing off on the single-VM plan at all?State on named volumes with verified off-host backups and a rehearsed restore, images pinned by digest and deployed only from tagged commits, per-service resource limits and log rotation so one service cannot take the host down, monitoring and alerting from outside the host, and a written recovery runbook. Plus an agreed trigger — a traffic, revenue or downtime-cost threshold — at which the decision is revisited.
It is a single well-run kitchen: fast, cheap, everyone knows where things are — but if the oven dies, dinner stops, and there is no second kitchen to route orders to.
saying these in an interview costs you the question
- Dismissing the plan outright with 'Compose is not for production' and no cost/benefit reasoning.
- Claiming `docker compose up -d` performs a rolling, zero-downtime update.
- Treating `restart: always` as equivalent to health-based self-healing.
- Assuming `--scale` on one host provides meaningful redundancy.
- Ignoring that state on local named volumes makes host loss a data-loss event.