Why we choosed docker compose ?

The docker compose file is more powerfull than you think

Lucas SOVRE

Insight

Why we chose Docker Compose

The docker-compose file is more powerful than you think.

If you've ever migrated an application off a PaaS, you know the moment. The screen where you copy out the last bits of config, hoping you didn't forget anything. The realization that the railway.toml you spent a weekend writing is now a museum piece. The quiet acknowledgment that you didn't really build on infrastructure. You built on someone else's product, and you're paying for that decision now.

We've been there. So when we set out to build compose_run, we made a deliberate choice. No proprietary configuration format. Not one. The file you write to deploy on us is the same file Docker has been reading since 2014.

Here's why that matters more than it looks.

The standards trap

There's a famous xkcd comic about how standards proliferate. It's funny because it's true. Every time someone notices that the existing standards aren't quite right, they create a new one, and now there's one more standard to compete with. The result is a graveyard of half-adopted formats, each promising to be the one.

Software infrastructure is full of these graveyards. Procfile for Heroku. app.json for Heroku, again. railway.toml. fly.toml. vercel.json. render.yaml. netlify.toml. nixpacks.toml. Each one was, at some point, "the better way to describe your application". Each one ties you to a single vendor.

This is not an accident.

Why companies love proprietary standards

Creating a proprietary configuration format is one of the oldest moats in software. It looks technical, but it's a business decision. The advantages, for the company, are obvious.

Lock-in by inertia. Once your team has written hundreds of lines of railway.toml, leaving Railway means rewriting all of it for the next platform. Most teams don't.

No need to adapt. When customers conform to your format, you don't have to adapt to theirs. You set the rules. They follow.

Asymmetric power. The more developers learn your format, the more your format becomes a hiring criterion, the more your platform becomes the default. The flywheel spins in your favor.

For the customer, the deal is the inverse. You write code that only runs on one platform. You build expertise that only transfers within one ecosystem. You become a hostage of a format you adopted to save time.

We didn't want to do that to anyone.

Open source isn't just code

There's a common misconception that open source is about source code. It's not, or at least, it's not only that. Open source is about who controls the contract between the user and the tool.

Docker Compose is, first and foremost, an open specification. The format is publicly documented. Anyone can implement it. Anyone can contribute to it. It has survived multiple companies, multiple ownership changes, multiple competing tools, and it's still here, maintained, evolving, and supported by an entire ecosystem.

That's the difference between a standard and a format. A format belongs to a company. A standard belongs to the people who use it.

When you write a docker-compose.yml file, you're not writing for compose_run. You're writing for an open spec that runs on your laptop, on your CI, on your colleague's machine, on a Raspberry Pi, on a self-hosted server, and yes, on us. The same file, everywhere.

Our philosophy: adapt to the user, not the other way around

The decision to build compose_run on top of Docker Compose wasn't a technical shortcut. It was a philosophical one.

We believe a platform should adapt to its users, not force its users to adapt to it. You already know docker-compose.yml. You probably already have one in your repo. You've used it locally for years. We see no reason to ask you to learn yet another DSL just because we'd like to offer you a managed service.

So we made a rule for ourselves. If you're already running with Compose, you should be able to run with us without changing a thing.

What about platform-specific features?

Fair question. A managed PaaS does need to know things that pure Docker Compose doesn't natively cover, like which internal port to route public traffic to, what image registry to pull from, when to back up a volume. Pure Compose doesn't have an opinion on most of this.

So we extend the spec, but always with the same rule: prefer native Compose directives whenever they exist, fall back to labels only when they don't.

A service in compose_run might look like this:

services:
  web:
    build: .
    ports:
      - "3000:3000"
    deploy:
      replicas: 3
    labels:
      fr.composerun.image: "4429730a-c670-419f-83c3-83233e151b47/4429730a-c670-419f-83c3-83233e151b47:latest"
      fr.composerun.routedPort.80: 4343
      fr.composerun.routedPort.web_data.backup_cron: "5 4 * * *"
services:
  web:
    build: .
    ports:
      - "3000:3000"
    deploy:
      replicas: 3
    labels:
      fr.composerun.image: "4429730a-c670-419f-83c3-83233e151b47/4429730a-c670-419f-83c3-83233e151b47:latest"
      fr.composerun.routedPort.80: 4343
      fr.composerun.routedPort.web_data.backup_cron: "5 4 * * *"
services:
  web:
    build: .
    ports:
      - "3000:3000"
    deploy:
      replicas: 3
    labels:
      fr.composerun.image: "4429730a-c670-419f-83c3-83233e151b47/4429730a-c670-419f-83c3-83233e151b47:latest"
      fr.composerun.routedPort.80: 4343
      fr.composerun.routedPort.web_data.backup_cron: "5 4 * * *"

Notice the split. Replicas use deploy.replicas, the standard Compose directive. We don't reinvent scaling under a composerun.scale label, because the spec already has a perfectly good word for it. If you point this file at any Swarm-compatible tool, it understands what you mean.

Image references, port routing, and backup schedules use labels, because the Compose spec doesn't define those concepts. They're platform concerns. Labels are exactly the mechanism Compose offers for this purpose, and they're namespaced (fr.composerun.*) so they never collide with Docker's own labels or with another platform's extensions.

This is what extensibility without lock-in looks like. We use the spec the way it was meant to be used. We never invent something the spec already provides.

We don't just run Swarm

Here's where some readers will raise an eyebrow. "If you respect the Compose spec and use deploy.replicas, are you just a managed Docker Swarm?"

No. And this is worth explaining, because it's where most of our actual engineering work lives.

Docker Swarm is a fine orchestrator for small fleets. It's also, frankly, not where the industry has gone for serious scale. Swarm is in maintenance mode. It doesn't handle multi-region failover the way modern infrastructure expects. Its scheduling, networking, and storage primitives weren't designed for the workloads our customers want to run.

So we made a choice that took us a year of engineering to pull off cleanly: we read your docker-compose.yml, we honor every directive in the spec, and then we translate it into a completely different runtime under the hood.

Concretely, when you push a Compose file to compose_run:

  • We parse and validate your file against the official Compose specification, the same way Docker does locally.

  • We interpret the semantics, not the syntax. deploy.replicas: 3 doesn't get passed to Swarm. It gets translated into a deployment plan in our own scheduler, which knows about our actual infrastructure: bare-metal nodes across multiple EU datacenters, our own networking layer, our own storage backends with snapshotting, our own load balancers with TLS termination at the edge.

  • We execute that plan on our custom infrastructure. Your three replicas run as three workloads on our orchestrator, scheduled across hardware we operate, with health checks, autoscaling, and zero-downtime deploys handled by code we wrote.

The Compose file is the contract, not the engine. It's the language we agree to speak with you. What happens behind that contract is our job, and it's where the real product is.

This matters for a few reasons:

  • Scale. We're not bound by Swarm's known limits on cluster size, scheduling latency, or networking complexity. We can run workloads at sizes Swarm was never designed for.

  • Reliability. When we want to ship a feature like cross-region failover, automated backups with point-in-time recovery, or live migration of containers between nodes, we don't have to wait for Swarm to add it. We build it, against our own runtime.

That's the trade we're proposing. Open contract, custom engine.

100% portable, by design

Every project deployed on compose_run can be exported at any time. Not as a translation, not as a "best-effort migration script", but as the exact same file you've been editing all along. There's no proprietary intermediate representation. There's no compose_run-specific build artifact you'd need to reverse-engineer.

If you decide tomorrow that you want to run your stack on AWS ECS, on a self-hosted Hetzner box, on Fly Machines, on your colleague's spare laptop in a closet, your docker-compose.yml goes with you. It's yours. It always was.

The labels we add will be ignored by anything that doesn't recognize them. The services, volumes, networks, secrets, and configs sections, the parts that actually describe your application, are pure, unmodified Compose.

We think that's the contract a platform should sign with its users.

The price we pay

We're not pretending this approach is free for us. By committing to Compose compatibility, we've given up a lot of leverage.

We can't invent fancy proprietary primitives that would tie you to us forever. We can't ship a "compose_run-only" feature that breaks portability. We can't make the migration out of compose_run intentionally painful.

These are real costs. They mean we have to win, and keep winning, on the actual quality of our service, our pricing, our support, our infrastructure choices. Not on switching costs.

That's a higher bar. It's also the only bar that respects the people who trust us with their production workloads.

A small thing that says a lot

This decision, to build on Compose instead of inventing another compose_run.yml, is a small one in the grand scheme of a PaaS. It's also the most honest signal we can send about what kind of company we want to be.

If we ever stop being a good fit for you, we'd rather you leave with your config intact than stay because leaving is too painful. We think that's how trust gets built in infrastructure. Not by promising you'll never want to go, but by making sure you always could.

Welcome to compose_run. Bring your docker-compose.yml. Take it back whenever you want.

You might also like