# Compose & multi-service apps
URL: https://openship.io/docs/guides/compose-multi-service.md

Run an app that's really several pieces at once — web, worker, database — from a docker-compose file or added by hand, and keep every service in sync.

import { Step, Steps } from 'fumadocs-ui/components/steps';

Some apps aren't one thing — they're a team. A website out front, a background **worker** doing slow jobs,
and a **database** holding the data. Think of a restaurant: the dining room, the kitchen, and the pantry are
three separate stations, but together they're one restaurant. Openship calls a set of services that run
together a **stack**, and it can run the whole stack for you.

Each service in the stack is its own little box (a **container**) with its own image, ports, storage, and
settings — but you manage them all from one place: the project's **Services** tab.

<Callout title="What you need first">
- A project in Openship. A stack is just a project that has more than one service.
- Your services either come from a **`docker-compose.yml`** file in your repo, or you **add them by hand** in
  the dashboard. Both are covered below.
- Nothing else — no Docker knowledge required to add a service from the catalog.
</Callout>

## Where your services live

Open your project and choose the **Services** tab. You'll see one row per service. Each row tells you, at a
glance:

- The service **name** (e.g. `web`, `worker`, `db`).
- A **Public** or **Internal** badge — Public means it has a web address people can visit; Internal means
  only the other services can reach it (a database is almost always Internal).
- A **status** dot: **Running**, **Stopped**, **Starting**, **Failed**, or **Disabled**.

Click any row to open that service's full screen. If the repo changed a value you'd edited, the row also
shows an **Upstream change** badge — more on that in [Drift](#when-the-repo-and-your-edits-disagree-drift).

<Callout title="Screenshot">Project → **Services** tab: the list of service rows, each with its Public/Internal badge and status. *(screenshot pending)*</Callout>

## How services get into a stack

There are two ways, and you can mix them.

### 1. From a `docker-compose.yml` file

If your repo has a compose file, Openship reads it and turns **each service in the file into a service row** —
its image (or a build context and Dockerfile), ports, volumes, environment, command, restart policy,
`depends_on` links, and healthcheck. From then on those rows are what Openship deploys.

Openship tracks the compose fields it models: `image`, `build`, `dockerfile`, `ports`, `depends_on`,
`environment`, `volumes`, `command`, `restart`, and `healthcheck`. Everything else in your file — `networks`,
build args, labels, `profiles`, comments — is left untouched: Openship neither reads it nor rewrites it.

<Callout title="Openship never writes to your repo" type="info">
Edits you make in the dashboard are stored by Openship and used on every deploy. Your `docker-compose.yml` is
never changed. If you want the file itself to reflect a change (so a teammate or a fresh clone sees it), edit
the file in your repo and push.
</Callout>

### 2. Added by hand

Any project can get extra services from a built-in catalog of common images — databases, caches, search,
queues, storage, and more — or from any Docker image you name.

<Steps>

<Step>

### Open the Add Service dialog

On the **Services** tab, press **Add Service**.

</Step>

<Step>

### Pick a service

You land on a catalog. Browse a category on the left (Databases, Caches, Search, Vector & AI, Queues &
Streams, Object Storage, Auth & Identity, Mail & SMTP, Runtimes), search by name, or choose **Custom image**
to type any image tag yourself. Click the one you want.

<Callout title="Screenshot">The Add Service catalog: category rail on the left, a grid of service cards, and the **Custom image** tile. *(screenshot pending)*</Callout>

</Step>

<Step>

### Fill in the details

Openship pre-fills what it can. Check these:

- **Service name** — short and unique within the stack (e.g. `db`).
- **Ports** — the port(s) the container listens on, comma-separated (e.g. `5432`).
- **Image** — the Docker image and tag (e.g. `postgres:16-alpine`).
- **Persistent volumes** — optional `name → path` pairs so data survives restarts.
- **Environment variables** — settings the service needs.
- **Routing** — leave it **Internal only** (the right choice for databases and background workers), or switch
  it to **Publicly exposed** to give the service its own web address — a free subdomain or your own custom
  domain.

Press **Add service**.

</Step>

<Step>

### It deploys automatically

If the project already has a live deployment, adding a service **redeploys** the project so the new container
actually starts — you'll see a "deploying…" message. If the project has never been deployed yet, the service
is just **saved**, and you deploy the project yourself to bring it up.

</Step>

</Steps>

### Re-syncing after the compose file changes

You don't normally sync by hand — on every deploy (and on every push, for GitHub-backed projects) Openship
**re-reads the repo's current `docker-compose.yml`** and reconciles it against your service rows
automatically. From the terminal you can also force a sync from a specific file:

```bash
# Rebuild the stack's service rows from a compose file.
# Services in the stack but NOT in the file are removed — you'll be asked to confirm.
openship service sync --project my-app ./docker-compose.yml
```

<Callout title="Sync needs Docker Compose v2" type="warn">
`openship service sync` shells out to `docker compose config` to parse and validate the file, so Docker
Compose v2 must be installed where you run the command. If `docker` isn't found, the sync stops with a clear
message.
</Callout>

## Opening a service

Click a service row to open its screen. Across the top is a row of tabs:

- **Overview** — read-only facts, each shown only when it applies: **Network** (the internal address other
  services use to reach this one, the container ports, the published host port, the current IP, and the
  container ID — shown as a **Workspace ID** on Openship Cloud), **Configuration** (restart policy, command,
  depends-on), and **Volumes**.
- **Terminal** — a live shell inside the container. It only opens while the service is **Running** ("Start the
  service to open a shell." until then).
- **Logs** — the service's output, streamed as it happens.
- **Environment** — the service's environment variables. Edit them and press **Save environment**.
- **Settings** — the build and runtime fields you can change: name, image (or a build context and Dockerfile),
  ports, depends-on, volumes, command, restart policy, and an optional healthcheck — plus the enable toggle,
  the lifecycle buttons, and **Delete**. (Environment variables have their own **Environment** tab.)
- **Backup** — schedule and run backups. This tab shows only for compose services (the stateful containers),
  not for source-built sub-apps.

<Callout title="Screenshot">A single service's detail screen showing the Overview / Terminal / Logs / Environment / Settings / Backup tab strip. *(screenshot pending)*</Callout>

## Start, stop, restart

The lifecycle buttons live on the service's **Settings** tab. Which ones appear depends on the state:

- **Running** → **Stop** and **Restart**.
- **Stopped** → **Start**.
- **Disabled** (never started) → **Enable & start**, which turns the service on and deploys it.
- **Enabled** with a live deployment → **Redeploy**, which rebuilds **only this service** and leaves the rest
  of the stack running.

There's also **Disable Service** / **Enable Service** (a disabled service is skipped on deploy) and
**Delete** (tears the service down).

<Tabs items={['Dashboard', 'CLI']}>

<Tab value="Dashboard">
Open the service → **Settings** tab → press **Start**, **Stop**, or **Restart**.
</Tab>

<Tab value="CLI">
```bash
openship service start   --project my-app db
openship service stop    --project my-app db
openship service restart --project my-app db

# See what's actually running
openship service containers --project my-app
```
Target a service by its name or its `svc_…` id.
</Tab>

</Tabs>

## When the repo and your edits disagree: drift

Here's the tricky bit, made simple. You can edit a service in the dashboard **and** keep a
`docker-compose.yml` in your repo. What happens when both change the same value?

On each deploy Openship does a **three-way compare** for every service:

- **base** — the compose values it last read in,
- **yours** — the service's current values (including your dashboard edits),
- **theirs** — what the repo file says right now.

| Repo changed? | You edited it? | What Openship does |
| --- | --- | --- |
| no | — | nothing — your values stand |
| yes | no | **applies the repo's value** automatically |
| yes | yes | **keeps your value** and flags it as **drift** for you to review |
| new service in the repo | — | adds it automatically |
| removed from the repo | no | removes it |
| removed from the repo | yes | keeps it — your work is never silently deleted |

So services you never touched track the repo on their own, and your edits are never overwritten without
asking. When a value you'd edited changes in the repo, the Services tab shows an amber **drift banner** with a
per-field `from → to` diff and two choices per service:

- **Accept upstream** — take the repo's value and move your baseline to it.
- **Keep mine** — keep your value; the flag clears so it stops nagging, but your edit stays in effect.

<Callout title="Screenshot">The Services tab drift banner: a service name, its `from → to` field diffs, and the **Keep mine** / **Accept upstream** buttons. *(screenshot pending)*</Callout>

<Tabs items={['Dashboard', 'CLI']}>

<Tab value="Dashboard">
On the **Services** tab, use the drift banner's **Keep mine** or **Accept upstream** button for each service.
</Tab>

<Tab value="CLI">
```bash
# Take the repo's values, discarding your edits
openship service drift accept --project my-app web

# Keep your edits and stop flagging the change
openship service drift keep --project my-app web
```
</Tab>

</Tabs>

## Ports & volumes: cloud vs self-hosted

How ports and volumes behave depends on **where** the stack runs:

- **Self-hosted / your own server (Docker):** full compose semantics. `host:container` port mappings are
  published on the host, and volumes (named or bind mounts) are mounted.
- **Openship Cloud:** only the **container** port matters — it's routed to your domain through the edge
  proxy, and a published *host* port is ignored. **Docker volume mounts don't apply.** A cloud service's data
  lives on its workspace disk, which survives restarts but is **not** carried across a rebuild unless you turn
  on backups.

You'll see this in the interface too. When you set the stack up in the deploy wizard, a cloud project **greys
out the published (host) port and the volume fields** and notes that only the container port is routed.
Stateful images — databases like Postgres, MySQL, MariaDB, MongoDB, Redis or Elasticsearch, and any service
that declares a volume — get an amber reminder that their data isn't kept across a rebuild unless you turn on
backups. Later, on the **Services** tab, a cloud service's **Overview** shows a **Workspace ID** in place of
the Container ID.

## Prefer the terminal?

Every service action has a CLI command. All of them target a stack with `-p / --project` (id, slug, or name)
and a service by name or `svc_…` id.

```bash
openship service list    --project my-app                 # list the services
openship service get     --project my-app db              # show one service's config
openship service create  --project my-app cache \
  --image redis:7 --port 6379                             # add a service by hand
openship service sync    --project my-app ./docker-compose.yml
openship service start   --project my-app web             # start / stop / restart
openship service logs    --project my-app web --follow    # stream logs
openship service env set --project my-app web KEY=value   # set env vars (--secret, -e <env>)
openship service drift accept --project my-app web        # resolve drift
openship service delete  --project my-app cache           # remove a service
```

## If something goes wrong

<Callout title="I added a service but nothing is running" type="warn">
Adding a service saves it, but a container only starts on a deploy. If the project already had a live
deployment, Openship redeploys for you; if it didn't, open the project and deploy it (or use the service's
**Enable & start** button on its Settings tab).
</Callout>

<Callout title="`openship service sync` says docker wasn't found" type="error">
The sync command uses Docker Compose v2 to read your file. Install Docker (which includes `docker compose`)
on the machine where you run the command, then try again. You can always add or edit services in the
dashboard instead.
</Callout>

<Callout title="A service keeps re-flagging the same drift" type="warn">
That means the repo file and your dashboard edit still disagree. Pick one source of truth: press **Accept
upstream** to take the repo's value, or **Keep mine** to lock in your edit. To make the change permanent for
everyone, edit `docker-compose.yml` in your repo and push — once your value matches the repo, the drift
clears on its own.
</Callout>

## What next?

<Cards>
  <Card title="Deploy from GitHub" href="/docs/guides/deploy-from-github" description="Get your repo — compose file and all — online in the first place." />
  <Card title="Core concepts" href="/docs/getting-started/core-concepts" description="The plain-language vocabulary: projects, services, deployments, and runtimes." />
  <Card title="Runtime model" href="/docs/architecture/runtime-model" description="The three places a stack can run — this machine, your server, or Openship Cloud." />
  <Card title="Data ownership" href="/docs/architecture/data-ownership" description="Where your service data lives when you self-host, and how backups protect it." />
</Cards>
