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
- What you need
- 1. Import your
docker-compose.yml - 2. Review the detected services
- 3. How services find each other (service discovery)
- 4. Apps that need a
.env/config file on disk - 5. Secrets
- 6. Deploy
- 7. Database migrations & seeders
- 8. Verify it works
- 9. Update your app
- Full example (API)
- 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/andfrontend/each with their own Dockerfile). Adocker-compose.ymlat 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
- + Deploy → Multi-service.
- Paste your Git repo URL (and token if private) and click Import.
- 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.envand 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.ymlat 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 gethttps://<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 expectsmoduxdb, name the database servicemoduxdb.
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 pushto 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.