Multi-Service Apps (compose-style)
One app can run several containers — e.g. a web frontend plus an internal cache or database. It’s the platform’s take on a Compose stack: 1 app = N services, but it still counts as one unit against your quota.
Already have a docker-compose.yml? Import it
In the Multi-service tab of the deploy form, enter your Git repo (and a token for private repos) and click Import from docker-compose.yml. The platform reads the compose file and fills in the services for you — builds/images, ports, environment, named-volume mounts, and which service is public. Review the rows and deploy.
Generated credentials (secure by default)
Credential-looking variables — names with PASS / PASSWORD / SECRET / TOKEN / KEY — get a freshly generated strong value, even if your compose has a default (those defaults, like root or dev-token, are insecure). The same ${VAR} keeps a single value across services, so e.g. the DB password matches between your db and backend.
After importing, the form shows a 🔐 Generated credentials box with a Download .env button — they’re shown only once, so save them. (DB credentials are also always recoverable later via Manage database.) Non-secret vars keep their compose default.
What it can’t carry over (and warns about)
Host bind mounts (./code:/app), since the platform bakes code into the image, and depends_on / healthcheck. A DB init-script bind mount (schema.sql) is dropped too — load your schema once via Manage database. (GitHub repos.)
Rules
- Provide a
servicesarray (at least one). - At least one service must be
public: true. You can expose several (e.g. a SPA and its API): the first public service getshttps://<app>.cynchro.cloud, additional ones gethttps://<app>-<service>.cynchro.cloud(override per service withdomain). - Internal services (
public:false) are not exposed to the internet. They live on a private per-app network and are reachable by other services by theirname(e.g.http://cache:6379). Discovery vars (<SVC>_HOST,<SVC>_URL, …) are injected automatically — see the Full-Stack Walkthrough. - Each service can use its own
image, or build from a shared top-levelrepo. repo/branch/repoTokenstay at the app level (one repo for the stack);port/env/cpu/memory/ secrets are per service.
Example — public web + internal redis
curl -s -X POST $API/deploy \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{
"name": "shop",
"services": [
{ "name": "web", "image": "acme/storefront:1.0", "port": 8080, "public": true,
"env": { "REDIS_URL": "redis://cache:6379" } },
{ "name": "cache", "image": "redis:7-alpine", "port": 6379, "public": false }
]
}'
https://shop.cynchro.cloud→ the web container.webreachescacheover the private network atredis://cache:6379.cacheis invisible from the internet.
Service fields
| Field | Required | Notes |
|---|---|---|
name | ✅ | DNS-safe; also the internal hostname. |
image | ✅* | Prebuilt image, OR omit to build from the app-level repo. |
dockerfile / context | When building from repo (relative paths). | |
port | Default 80. Port the service listens on. | |
public | true to expose it (you can expose more than one). | |
domain | Override this public service’s hostname. | |
cpu / memory | Per-service limits. | |
env | Per-service env (encrypted at rest). | |
envFrom / secretFiles | Per-service Secrets references. | |
buildArgs | Env keys to bake at build time (--build-arg), e.g. ["VITE_API_URL"]. | |
envFilePath | Render this service’s env as a read-only .env at this path, e.g. /var/www/html/.env. | |
volumes | Persistent mount paths (absolute), e.g. ["/var/lib/mysql"] — for databases. |
* Each service needs either its own image or a top-level repo to build from.
Databases (persistent volumes)
The platform does not read your repo’s
docker-compose.yml— you declare services here. A database needs avolumespath so its data survives redeploys.
Add the database as an internal service with a volumes mount. Other services reach it by name (e.g. db). Data in a volume survives redeploys/restarts; it’s deleted only when you delete the app.
-d '{
"name": "myapp",
"services": [
{ "name":"web", "image":"myorg/web:1.0", "port":80, "public":true,
"env": { "DB_HOST":"db", "DB_USER":"root", "DB_PASS":"secret", "DB_NAME":"app" } },
{ "name":"db", "image":"mysql:8", "port":3306, "public":false,
"env": { "MYSQL_ROOT_PASSWORD":"secret", "MYSQL_DATABASE":"app" },
"volumes": ["/var/lib/mysql"] }
]
}'
Loading a schema: the official mysql/postgres images run scripts in /docker-entrypoint-initdb.d only on a fresh, empty volume. Two easy options:
- Once, by hand: open the database manager (the Database button), connect to
db, and run yourschema.sql. - Baked in: add a tiny
db/Dockerfileto your repo (FROM mysql:8+COPY schema.sql /docker-entrypoint-initdb.d/01-schema.sql) and build that service from the repo. The schema then loads automatically on first init.
Building all services from one repo
-d '{
"name": "stack",
"repo": "https://github.com/acme/monorepo",
"services": [
{ "name":"api", "dockerfile":"api/Dockerfile", "context":"api", "port":3000, "public":true },
{ "name":"worker", "dockerfile":"worker/Dockerfile", "context":"worker", "port":0, "public":false }
]
}'
The repo is cloned once and each service is built from its own Dockerfile.
Lifecycle
Restart / stop / delete act on the whole app (all its containers). Deleting also tears down the private network. Self-healing watches every container — if any dies, the app is redeployed.
→ Back to basics: Deploy from Git · Inject config: Secrets