Services API
Manage the services inside a project — compose services and monorepo sub-apps — including CRUD, sync from compose, container actions, drift resolution, and env vars.
A service is one runnable unit inside a project: a container in a docker-compose stack, or a sub-app in
a monorepo. Both live in the same table (discriminated by kind), so this API covers listing and editing
services, syncing them from a docker-compose file, starting/stopping/restarting their containers,
resolving compose drift, and setting per-service environment variables. In the dashboard this is a project's
Services tab; from the terminal it's openship service.
Base path & auth
Every path is nested under a project — relative to your instance, under /api/projects/:id/services,
where :id is the project id (e.g. https://your-host/api/projects/proj_123/services).
Send a personal access token as a bearer header (Authorization: Bearer <token>), created with
openship token create. The dashboard uses your session cookie instead. See the
API overview for the full auth model and security/auth for permissions.
Self-hosted and cloud
This module is available on both self-hosted and Openship Cloud. When :id is a cloud project the request
is transparently proxied to Openship Cloud; a local project is served by your own instance.
Endpoints
| Method & path | Permission | What it does |
|---|---|---|
GET /api/projects/:id/services | project:service:list | List a project's services (compose services / monorepo sub-apps). |
POST /api/projects/:id/services | project:service:write | Add a service to a project. |
GET /api/projects/:id/services/containers | project:read | List the running containers for a project's services. |
POST /api/projects/:id/services/sync | project:service:write | Sync services from the project's docker-compose file into the service table. |
GET /api/projects/:id/services/:serviceId | project:service:read | Get one service by id. |
GET /api/projects/:id/services/:serviceId/logs | project:service:read | Fetch a service's runtime logs (non-streaming). |
GET /api/projects/:id/services/:serviceId/logs/stream | project:service:read | Stream a service's runtime logs. |
PATCH /api/projects/:id/services/:serviceId | project:service:write | Update a service's configuration. |
DELETE /api/projects/:id/services/:serviceId | project:service:admin | Delete a service (tears down its container). |
POST /api/projects/:id/services/:serviceId/drift/accept | project:service:write | Accept upstream docker-compose changes for this service. |
POST /api/projects/:id/services/:serviceId/drift/keep | project:service:write | Keep local edits over upstream docker-compose changes for this service. |
POST /api/projects/:id/services/:serviceId/start | project:service:write | Start this service's container. |
POST /api/projects/:id/services/:serviceId/stop | project:service:write | Stop this service's container. |
POST /api/projects/:id/services/:serviceId/restart | project:service:write | Restart this service's container. |
GET /api/projects/:id/services/:serviceId/env | project:service:read | List a service's environment variables. |
PUT /api/projects/:id/services/:serviceId/env | project:service:write | Replace a service's environment variables. |
Add a service
POST /api/projects/:id/servicesA service row is a union: kind: "compose" uses the container fields (image, build, ports…), while
kind: "monorepo" uses the sub-app build fields (rootDirectory, framework…). Both field sets are accepted
and optional; the kind decides which subset is the source of truth. Unknown keys are rejected.
Prop
Type
Compose fields (used when kind is compose):
Prop
Type
Monorepo sub-app fields (used when kind is monorepo):
Prop
Type
curl -X POST https://your-host/api/projects/proj_123/services \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"db","kind":"compose","image":"postgres:16","ports":["5432:5432"]}'# Same thing from the CLI:
openship service create -p my-stack db --image postgres:16 --port 5432:5432Update a service
PATCH /api/projects/:id/services/:serviceIdEvery field is optional — send only what changes. Accepts the same compose and monorepo fields as create,
except kind: switching a row's kind is a destructive operation, so delete the service and re-create it
under the new kind instead.
curl -X PATCH https://your-host/api/projects/proj_123/services/svc_456 \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"restart":"unless-stopped","enabled":true}'Sync from docker-compose
Reads the project's docker-compose file and reconciles the service table to match it — adding, updating,
and flagging services. Where your edits diverge from the compose file, the affected services are marked as
drifted so you can resolve them explicitly (see below).
POST /api/projects/:id/services/syncopenship service sync -p my-stack docker-compose.ymlResolve compose drift
When a service's stored config diverges from the upstream docker-compose file, it's flagged as drifted.
Choose one resolution per service:
POST /api/projects/:id/services/:serviceId/drift/accept # take upstream, discard your edits
POST /api/projects/:id/services/:serviceId/drift/keep # keep your edits, stop flaggingopenship service drift accept -p my-stack web
openship service drift keep -p my-stack webContainer actions
Start, stop, or restart the running container for a single service. These act on the service's active-deployment container.
POST /api/projects/:id/services/:serviceId/start
POST /api/projects/:id/services/:serviceId/stop
POST /api/projects/:id/services/:serviceId/restartopenship service restart -p my-stack webUse GET /api/projects/:id/services/containers (openship service containers -p my-stack) to list the
running containers across the whole stack.
Environment variables
GET .../env lists a service's env vars (secret values masked). PUT .../env replaces the full set for
one environment — send every var you want to keep.
GET /api/projects/:id/services/:serviceId/env
PUT /api/projects/:id/services/:serviceId/envProp
Type
curl -X PUT https://your-host/api/projects/proj_123/services/svc_456/env \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"environment":"production","vars":[{"key":"DATABASE_URL","value":"postgres://…","isSecret":true}]}'# Same thing from the CLI:
openship service env set -p my-stack web DATABASE_URL=postgres://… --env productionPUT replaces, it doesn't merge
PUT .../env overwrites the environment's variable set with exactly what you send. Fetch the current vars
with GET .../env first if you only mean to change one — anything you omit is removed.
Errors you might see
400 — invalid body
The payload failed validation (unknown key, wrong type, or a value over its length limit). Unknown keys are
rejected outright — check field names against the tables above. On create, a kind: "monorepo" service must
carry a rootDirectory.
404 on a per-service route
The :serviceId doesn't belong to this project (or was deleted), or the project :id isn't in your
organization. List the project's services with GET /api/projects/:id/services to get valid ids.