Deploy a Full-Stack App — step by step

An end-to-end walkthrough for a real app: a frontend (SPA) + backend (API) + database, from an empty dashboard to a running, HTTPS-served, migrated and seeded system. If your repo has a docker-compose.yml, most of this is auto-detected.

Contents
  1. What you need
  2. 1. Import your docker-compose.yml
  3. 2. Review the detected services
  4. 3. How services find each other (service discovery)
  5. 4. Apps that need a .env/config file on disk
  6. 5. Secrets
  7. 6. Deploy
  8. 7. Database migrations & seeders
  9. 8. Verify it works
  10. 9. Update your app
  11. Full example (API)
  12. Troubleshooting quick hits

What you need

  • An account on the dashboard (https://app.cynchro.cloud). Accounts are created by the platform admin.
  • A Git repo with a Dockerfile per service (e.g. backend/ and frontend/ each with their own Dockerfile). A docker-compose.yml at the repo root makes the whole setup one Import click.
  • If the repo is private, a read-only access token (GitHub PAT).

One app on the platform can run several containers (services) — a frontend, a backend and a database live together under a single app.


1. Import your docker-compose.yml

  1. + Deploy → Multi-service.
  2. Paste your Git repo URL (and token if private) and click Import.
  3. The platform reads your compose file and pre-fills the service rows: build vs image, ports, environment, build args, persistent volumes, and which services are public. Credentials it had to generate (DB passwords, ${VAR:?...} values) are shown once — download the .env and keep it safe.

What the importer maps for you:

Compose → Platform
build: { context, dockerfile } build-from-git service
build.args (e.g. VITE_API_URL) build args (baked into the image at build time)
image: pre-built image service
ports: with a web port published marks the service public (SPA and API can both be public)
named volumes: persistent container paths (e.g. a DB’s data dir)
environment: / ${VAR:-default} / ${VAR:?required} env values (secrets auto-generated & reused across services)
host bind mounts (./x:/y), env_file, depends_on dropped with a warning — see below

The importer looks for docker-compose.yml / compose.yml at the repo root. If your production file is named differently (e.g. docker-compose.prod.yml), copy its service rows into the form by hand, or rename it in a deploy branch.


2. Review the detected services

For each service confirm:

  • Public? A public service gets its own HTTPS domain + certificate. The first public service takes https://<app>.cynchro.cloud; additional public services get https://<app>-<service>.cynchro.cloud. Internal services (a database, a cache) are not exposed to the internet.
  • Port — the port the container listens on (not any host port from compose).
  • Volumes — a database needs its data dir persisted (e.g. /var/lib/mysql, /var/lib/postgresql/data), or you lose data on redeploy.

A typical stack ends up as: frontend (public), backend (public), db (internal).


3. How services find each other (service discovery)

Services in the same app share a private network and reach each other by service name — that name is the hostname. You don’t have to guess IPs. The platform also injects discovery variables into every container:

Variable Example
<SVC>_HOST DB_HOST=db
<SVC>_PORT DB_PORT=5432
<SVC>_URL BACKEND_URL=http://backend:3000
<SVC>_PUBLIC_URL (public services) BACKEND_PUBLIC_URL=https://myapp-backend.cynchro.cloud

Your own env always wins over these. So your backend can connect to the DB with DB_HOST=db, and — crucially — a browser-side SPA must call the backend at its public URL, not http://backend. For a SPA whose API URL is baked at build time (e.g. Vite’s VITE_API_URL, Next’s NEXT_PUBLIC_*), set it as a build arg to the backend’s public URL:

  • buildArgs: ["VITE_API_URL"] + env VITE_API_URL=https://myapp-backend.cynchro.cloud

The #1 gotcha: your app’s DB_HOST (or API URL) must match the service name. If your code expects moduxdb, name the database service moduxdb.


4. Apps that need a .env/config file on disk

Some frameworks require a config file to exist on disk (not just environment variables). Instead of changing your Dockerfile, set the service’s Env file path:

  • Env file path: /var/www/html/.env

The platform renders the service’s environment as a read-only .env file (in RAM) at that path — in both the running container and one-off command runs. No entrypoint hacks.


5. Secrets

Keep credentials out of the deploy form using Secrets (org-wide or scoped to one app). Inject them as env vars or mount them as files at /run/secrets/<KEY>. See Secrets.


6. Deploy

Click Deploy. The platform builds each service from your repo, provisions HTTPS domains + Let’s Encrypt certificates, and starts the containers. Watch progress in the app’s Inspect panel (build/deploy events, per-service logs).


7. Database migrations & seeders

The platform does not migrate or seed your database automatically — you tell it how:

  • Release command (app-level, runs automatically after every deploy): put your migration command here, e.g. php artisan migrate --force, npm run migrate, php modux migrate. It runs in a throwaway container with your app’s full env + network. Keep it idempotent (real migration tools only apply pending migrations).
  • Run a command (one-off, from the app detail view): for seeders and ad-hoc tasks, e.g. npm run seed, php seeders/AdminSeeder.php admin <pass> "My Studio". Output shows in the events feed. A failing one-off run does not take your app down.

Readiness is automatic. Database images (mysql/mariadb/postgres/mongo) get a healthcheck, and the platform waits for the DB to accept connections before running the release command — so migrations don’t race a still-initializing database. You don’t need a manual wait loop.


8. Verify it works

  • Open the public URL(s). The Connections panel on the app’s detail page lists every service’s internal address (http://backend:3000) and public URL.
  • Inspect shows container status/health, build & deploy events (where build failures appear), and live per-service logs.
  • Manage database opens a web DB manager (Adminer) auto-logged into your app’s DB.
  • Show env / credentials downloads the exact values the app is running with (owner-only).

9. Update your app

  • A repo deploy sets up a webhook: git push to the deployed branch → automatic rebuild + redeploy (migrations re-run via the release command).
  • Or click Redeploy in the dashboard to rebuild the current config.
  • Restart just bounces the containers (no rebuild).

Full example (API)

The same as the dashboard, via POST /deploy — a SPA + API + MySQL, migrating on deploy:

curl -X POST $API/deploy -H "authorization: bearer $TOKEN" -H 'content-type: application/json' -d '{
  "name": "shop",
  "repo": "https://github.com/acme/shop.git",
  "branch": "main",
  "repoToken": "ghp_...",                         // only for private repos
  "releaseCommand": "php artisan migrate --force",
  "services": [
    { "name": "backend",  "context": "backend",  "dockerfile": "backend/Dockerfile",
      "port": 80, "public": true,
      "envFilePath": "/var/www/html/.env",         // app requires a .env on disk
      "env": { "DB_HOST": "db", "DB_DATABASE": "shop", "DB_USERNAME": "shop", "DB_PASSWORD": "s3cret",
               "APP_URL": "https://shop-backend.cynchro.cloud" } },
    { "name": "frontend", "context": "frontend", "dockerfile": "frontend/Dockerfile",
      "port": 80, "public": true,
      "buildArgs": ["VITE_API_URL"],
      "env": { "VITE_API_URL": "https://shop-backend.cynchro.cloud" } },
    { "name": "db", "image": "mysql:8.0", "port": 3306, "public": false,
      "volumes": ["/var/lib/mysql"],
      "env": { "MYSQL_DATABASE": "shop", "MYSQL_USER": "shop", "MYSQL_PASSWORD": "s3cret",
               "MYSQL_ROOT_PASSWORD": "r00t" } }
  ]
}'

After deploy, run one-off seeders from the app detail view (Run a command), targeting the backend service.


Troubleshooting quick hits

Symptom Fix
getaddrinfo for X failed / can’t resolve the DB Your DB_HOST ≠ the DB service name. Make them match.
SPA loads but login/API calls fail The backend must be public, and the SPA’s API URL must be its public URL (build arg), not localhost or http://backend.
Connection refused to the DB during migrate The DB wasn’t ready — the platform now waits automatically; make sure the DB service uses a known image (mysql/postgres/…).
App returns 503 “config not found” Your app needs a file on disk — set Env file path (e.g. /var/www/html/.env).
Build failed, no container Open Inspect → Build & deploy events (the source of truth for build errors).

See also Multi-Service Apps, Managing Apps and Debugging & databases.


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