Guides

Compose & multi-service apps

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.

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.

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.

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.

Screenshot

Project → Services tab: the list of service rows, each with its Public/Internal badge and status. (screenshot pending)

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.

Openship never writes to your repo

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.

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.

Open the Add Service dialog

On the Services tab, press Add Service.

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.

Screenshot

The Add Service catalog: category rail on the left, a grid of service cards, and the Custom image tile. (screenshot pending)

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.

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.

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:

# 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

Sync needs Docker Compose v2

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.

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.

Screenshot

A single service's detail screen showing the Overview / Terminal / Logs / Environment / Settings / Backup tab strip. (screenshot pending)

Start, stop, restart

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

  • RunningStop and Restart.
  • StoppedStart.
  • 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).

Open the service → Settings tab → press Start, Stop, or Restart.

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.

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
nonothing — your values stand
yesnoapplies the repo's value automatically
yesyeskeeps your value and flags it as drift for you to review
new service in the repoadds it automatically
removed from the reponoremoves it
removed from the repoyeskeeps 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.

Screenshot

The Services tab drift banner: a service name, its from → to field diffs, and the Keep mine / Accept upstream buttons. (screenshot pending)

On the Services tab, use the drift banner's Keep mine or Accept upstream button for each service.

# 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

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.

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

I added a service but nothing is running

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).

`openship service sync` says docker wasn't found

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.

A service keeps re-flagging the same drift

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.

What next?

On this page