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 services array (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 gets https://<app>.cynchro.cloud, additional ones get https://<app>-<service>.cynchro.cloud (override per service with domain).
  • 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 their name (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-level repo.
  • repo / branch / repoToken stay 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.
  • web reaches cache over the private network at redis://cache:6379.
  • cache is 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 a volumes path 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 your schema.sql.
  • Baked in: add a tiny db/Dockerfile to 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


This site uses Just the Docs, a documentation theme for Jekyll.