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.ymlfile 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. Repo updates preserve your explicit edits and apply other changes automatically — see how Compose changes merge.
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.
Compose file in a subfolder
Openship looks for the compose file at the project root. If yours lives somewhere else — say
deploy/docker-compose/docker-compose.yml — open Compose file in the deploy wizard and enter
the path, then hit Scan. Openship re-reads the repo and picks up the services it declares. You
can give either the file or the folder that holds it, and the filename doesn't have to be the
standard one (stack.yml works).
Relative build: contexts are resolved the way compose resolves them — against the compose file's
own folder — so a file in deploy/docker-compose/ can use build: ../../api to build the api
directory at the repo root.
What build: means
build: is the docker build context: the directory docker builds from. COPY and ADD paths in
that service's Dockerfile are relative to it, dockerfile: is looked up inside it, and the
.dockerignore that applies is the one in that directory — exactly as docker compose build behaves.
Use build: . when the service really does build from the repo root.
Two consequences worth knowing:
- A service whose
build:directory has no Dockerfile fails with a clear error. It does not fall back to a Dockerfile elsewhere in the repo — that would build a different image under this service's name. - Compose
build.argsandbuild.targetare not modelled. Build arguments come from the service's environment variables instead (each is passed as a--build-arg).
BuildKit
Modern Dockerfile syntax — RUN --mount=type=cache, # syntax= directives, heredocs — needs BuildKit.
Openship uses it automatically for a Dockerfile that requires it when it builds against a Docker socket,
and on a remote server whenever that host's Docker CLI has the buildx plugin. If a build fails with
the --mount option requires BuildKit, install the plugin on the build host (the docker-buildx-plugin
package, or Docker's official packages) and redeploy. The build log names which builder it used.
To keep it in the repo instead of setting it per project, declare it in
openship.json:
{ "composePath": "deploy/docker-compose/docker-compose.yml" }The wizard field wins if you later change it there — the config file seeds the value, your edit is what deploys.
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.
Database access during a Next.js build
If next build reports getaddrinfo ENOTFOUND db, followed by an error such as
Failed to collect page data for /api/hours, code evaluated during the build tried to resolve
the db hostname. Check the named route and its imported helpers for database initialization.
Use "dependsOn": ["db"] in openship.json (or depends_on: [db] in Compose). This orders
container startup; it does not start Postgres before image builds or attach the builder to the
services' network. Even an already-running database is not automatically reachable by its
service name from the builder.
For a route that needs data only when handling a request, move Payload initialization into the
existing GET or POST handler, or a helper called by that handler. Next.js can import route
modules during the build, so export const dynamic = 'force-dynamic' alone does not prevent
module-level await getPayload(...) or an eagerly created initialization promise from running.
Check imported helpers and configuration for the same side effects.
For example, this Next.js 15 / Payload 3 handler initializes Payload when a request arrives.
It assumes an hours collection; when adapting your own route, keep its existing authentication,
query options, access checks, and response shape:
import config from "@payload-config";
import { getPayload } from "payload";
export const dynamic = "force-dynamic";
export async function GET() {
const payload = await getPayload({ config });
const result = await payload.find({
collection: "hours",
overrideAccess: false,
});
return Response.json(result);
}overrideAccess: false applies the collection's access rules; retain any existing authenticated
user context in your own queries. Keep the runtime connection string pointing at db:5432
when both running services share the project network.
If a page intentionally queries data during static generation, deferring an API handler will not remove that requirement. Supply a data source reachable from the build environment, or change that page to load its data at request time if that matches the application.
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.
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 → pathpairs 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 Openship re-reads the current Compose source (the
repository for GitHub projects or localPath for local projects) and reconciles it against your service rows
automatically. GitHub pushes use the same path and skip the scan only when the changed-file list proves the
Compose file was untouched. 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.ymlSync 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.
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).
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-appTarget a service by its name or its svc_… id.
When the repo and your edits disagree: drift
On each deploy Openship merges the current Compose source with your saved service configuration, comparing each field against the previously imported version:
- 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 | — | keeps your values |
| yes | no | applies the repo's value automatically |
| yes | yes | keeps the edited field and applies the other repo changes |
| new service in the repo | — | adds it automatically |
| removed from the repo | no | removes it only if it has no saved environment |
| removed from the repo | yes | keeps it until you explicitly delete it |
Routine Compose changes do not require Accept upstream or Keep mine before redeploying. Saved project and service environment values remain in place. A key omitted from the repo is retained as a local override, and an unresolved expression cannot replace the only saved value for that key. Remove a saved variable explicitly from the environment editor when you no longer need it.
If an older version left a pending review, the next deploy reconciles it automatically. The legacy resolution actions remain available: Accept upstream applies source configuration while preserving saved environment values; Keep mine retains the current configuration.
Redeploy the project to apply current source changes. You can also resolve a review left by an older version from its existing banner.
# Accept a legacy source review; saved environment values are preserved
openship service drift accept --project my-app web
# Keep your edits and stop flagging the change
openship service drift keep --project my-app webPorts & 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:containerport 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.
Compose port publishing and Openship-managed domain routing are separate
ownership boundaries. A domain is changed only when you explicitly configure a
project/service endpoint in Openship; an unrelated Caddy, nginx, or Traefik
vhost is never inferred from ports and is never rewritten. For an
Openship-managed domain on the same server, redeploy keeps its tracked host port
pinned so the route cannot silently move to a different upstream.
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.
CLI
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 serviceTroubleshooting
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.
An old Compose review banner is still visible
Redeploy to reconcile it with the current source. Your explicit overrides remain in effect and other repo changes apply automatically. A genuinely missing required variable still needs a value in the environment editor; the deployment error identifies its service and key.
Related guides
Deploy from GitHub
Get your repo — compose file and all — online in the first place.
Core concepts
The plain-language vocabulary: projects, services, deployments, and runtimes.
Runtime model
The three places a stack can run — this machine, your server, or Openship Cloud.
Data ownership
Where your service data lives when you self-host, and how backups protect it.
Deploy from a template or Git URL
Two quick ways to start a project without connecting your own GitHub — paste a public repo link, or pick a ready-made starter from the template grid.
Preview environments
Make a separate, safe copy of your project from a git branch — so you can try changes before they reach your real site.