Docker

Get your docker compose app live.

run.zero does not read docker-compose.yml or build Dockerfiles. You translate the file into a run-zero.yaml: a buildpack builds your code, the database becomes an addon, ready-made images start directly.

Needs the Developer plan, the free plan only covers websites. Pricing · Check the limits first

  • One service per container
  • Managed Postgres
  • Ready-made images work
claude · run-zero-examples/docker-compose

> Deploy the docker-compose folder to run.zero.

runzero_create_appdoneexample-docker-compose

runzero_push_filesdoneone commit

runzero_deploydoneservices and Postgres created

runzero_wait_for_builddonelive

live at https://web-example-docker-compose-da4587ce.run-zero.de

The session that created the live demo, with the real MCP tools.
How it works

From compose to live in four steps.

Tested with the example that runs live.

  1. 01

    Sort your containers

    Containers with build: become services with path:. Containers with a ready-made image: stay that way. Your Postgres container becomes an addon.

  2. 02

    Set the port

    Each service listens on the port from the file, 8080 by default. For a ready-made image, set the port the image listens on.

  3. 03

    Add run-zero.yaml

    The file sits at the root of the repo. attachTo names the services that get DATABASE_URL.

    run-zero.yaml
    version: "1"services:  web:    path: ./web    port: 8080addons:  db:    type: postgres    attachTo: [web]
  4. 04

    Create an account and deploy

    Create an account, connect your agent with this command, then sign in from Claude Code with /mcp and Authenticate.

    • Then tell your agent: deploy this repo to run.zero.
    Without an agent

    Create the app in the dashboard and connect your GitHub repo. Every push rebuilds.

    Connect Claude Code
    claude mcp add --transport http \  -s user run-zero \  https://api.run-zero.de/mcp \  --client-id runzero-cli \  --callback-port 53761

After 1 to 3 minutes the app is live at https://web-<app>.run-zero.de. Check a route of your app, in the example /api/health. If the build is red, runzero_get_build_logs tells you why.

Files

One app, two descriptions.

docker compose stays for local work, the other file runs it on run.zero.

Your local setup, unchanged. run.zero ignores this file.

Example on GitHub(opens in a new tab)
docker-compose/docker-compose.yml
# Local only. On run.zero, run-zero.yaml applies.services:  web:    build: ./web    ports:      - "8080:8080"    environment:      PORT: "8080"      DATABASE_URL: postgres://app:app@db:5432/app    depends_on:      - db  db:    image: postgres:16-alpine    environment:      POSTGRES_USER: app      POSTGRES_PASSWORD: app      POSTGRES_DB: app    volumes:      - db-data:/var/lib/postgresql/datavolumes:  db-data:

web builds from ./web, whoami runs a ready-made image, db becomes the Postgres addon.

Example on GitHub(opens in a new tab)
docker-compose/run-zero.yaml
version: "1"services:  web:    path: ./web    port: 8080  whoami:    image: docker.io/traefik/whoami:v1.10    port: 8080    env:      WHOAMI_PORT_NUMBER: "8080"addons:  db:    type: postgres    attachTo: [web]

Reads DATABASE_URL and counts visits. Locally from compose, on run.zero from the addon.

Example on GitHub(opens in a new tab)
docker-compose/web/server.js
// Web service of the compose example. Locally DATABASE_URL comes from docker-compose.yml, on run.zero from the postgres addon.const express = require('express');const { Pool } = require('pg');const port = process.env.PORT || 8080;const url = process.env.DATABASE_URL;// run.zero: the addon URL carries sslmode=require against a self-signed cert, so drop it and pass ssl explicitly.const onPlatform = !!url && url.includes('sslmode=');const pool = url  ? new Pool({      connectionString: onPlatform ? url.replace(/[?&]sslmode=[^&]*/, '') : url,      ssl: onPlatform ? { rejectUnauthorized: false } : false,    })  : null;async function countVisit() {  await pool.query('CREATE TABLE IF NOT EXISTS visits (id serial PRIMARY KEY, at timestamptz DEFAULT now())');  await pool.query('INSERT INTO visits DEFAULT VALUES');  const { rows } = await pool.query('SELECT count(*)::int AS n FROM visits');  return rows[0].n;}const app = express();app.get('/', async (req, res) => {  const visits = pool ? await countVisit() : 'no database attached';  res.type('html').send(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>docker compose on run.zero</title></head><body style="font-family:system-ui,sans-serif;max-width:640px;margin:64px auto;padding:0 24px"><h1>docker compose on run.zero</h1><p>The web service came from ./web, the database from the postgres addon. Visits stored in Postgres: <strong id="visits">${visits}</strong></p><p><a href="/api/health">/api/health</a></p></body></html>`);});app.get('/api/health', (req, res) => res.json({ status: 'ok', database: !!pool, platform: 'run.zero' }));app.listen(port, () => console.log(`listening on ${port}`));

Started with npm start. The Dockerfile next to it is for compose only.

Example on GitHub(opens in a new tab)
docker-compose/web/package.json
{  "name": "run-zero-example-compose-web",  "version": "1.0.0",  "private": true,  "engines": { "node": "22.x" },  "scripts": { "start": "node server.js" },  "dependencies": { "express": "^4.21.2", "pg": "^8.13.1" }}
Translation

What your compose file turns into.

Line by line, as in the example.

services.web.build: ./webpath: ./web, built by a buildpack
services.x.image: name:tagimage: name:tag, the whoami service in the example
ports: "8080:8080"port: 8080, public at https://<service>-<app>.run-zero.de
environment:env: on the service, secret values as secrets in the dashboard
services.db.image: postgres:16becomes an addon, currently version 18, not selectable
volumes:- path: /pb_data, optional size: 20 (default 10 GiB, cannot change later), one volume per service
depends_on:dropped, every service starts on its own
Limits

What does not work today.

Developer allows 2 services per app.

  • Needs the Developer plan

    Free has no server apps and no Postgres, a deploy stops with an error.

    See pricing
  • No Dockerfile builds

    The Dockerfile is silently ignored: no RUN apt-get, no multi-stage builds. With an index.html and no manifest the repo becomes a static page.

  • Volumes: one instance

    Without volumes:, local data is gone after a restart. With a volume it runs exactly one instance, with a short gap on deploy.

  • Only Postgres and Keycloak

    Postgres runs version 18, not selectable. No Redis, no MongoDB, no dump import.

See all limits
Operations

What you stop doing yourself.

Builds, HTTPS, the database and backups run on the platform.

  • Data stays in Germany

    Servers and backups at Hetzner in Nuremberg. With a data processing agreement.

  • HTTPS by default

    Every service gets an address on run-zero.de, custom domains a Let’s Encrypt certificate.

  • Postgres as an addon

    One entry in run-zero.yaml, DATABASE_URL arrives as an environment variable.

  • No lock-in

    Your code stays in your repo, the database is a dump you can download.

Questions.

About docker compose.

My repo only has a Dockerfile. Now what?

Build the image yourself, for example in your CI, push it to a registry and reference it with image:. Code in Node, Python, Go, PHP or Java also works without a Dockerfile through the buildpack.

Does run.zero read my docker-compose.yml?

No, the platform only reads run-zero.yaml. The compose file can stay in the repo for local development.

What happens to my Postgres container?

It becomes the managed Postgres addon. The platform sets DATABASE_URL plus PGHOST, PGPORT, PGUSER, PGPASSWORD and PGDATABASE in every service listed in attachTo.

How does existing data get from my volume into the addon?

There is no dump import. Single SQL statements go through the SQL console or runzero_db_query. We have not tested a full move or extensions.

Can I run SQLite or PocketBase as an image?

Yes, with volumes: on the service, from the Developer plan. In our PocketBase example a record survived a redeploy and scaling to 0. Without volumes:, local data is gone on restart.

What applies to a service with a volume?

It runs exactly one instance and cannot scale higher, and scaling to 0 keeps the data. A deploy has a short gap, preview and manual publishing do not work, and a volume cannot be resized yet.

Why does my image crash on a variable like PB_PORT?

Kubernetes sets variables like <NAME>_PORT=tcp://… for every service. A service named pb collides with PocketBase’s own PB_PORT. Name the service pocketbase, for example.

What about Redis or MongoDB?

Neither can be declared as an addon in run-zero.yaml today. For caches and queues, use Postgres or an external provider for now.

More guides

All guides

Your code. Live on run.zero.

One website is free, no credit card. Apps with a server, a database and a custom domain come with the Developer plan.