Skip to content

Migrating from Coolify ​

Let your AI agent do it: the yoho-migrate-coolify skill (installed by yoho skill install, or read it on GitHub) walks through inventory, secrets, cutover with rollback, and the tunnel switch.

  1. Find the app. The uuid is in the Coolify UI URL and in the com.docker.compose.project label of its containers. On the Server, /data/coolify/applications/<uuid>/ (and /data/coolify/services/<uuid>/) hold docker-compose.yaml and .env. Inspect keys and lengths, not values.
  2. Clean the compose file. Remove Coolify labels, the coolify network, and SERVICE_FQDN_* / SERVICE_URL_*. Keep volume names to carry data over.
  3. Routing. Domains become x-yoho.proxy.hosts; the container port becomes proxy.port.
  4. Secrets. SERVICE_PASSWORD_* / SERVICE_BASE64_* map to x-yoho.generate for new values; for already-initialized databases, copy the existing value. Put all sensitive env values in 1Password or Bitwarden, reference them from .yoho/secrets, and declare per Service in x-yoho.secrets. Coolify injects every variable into every container; Yoho gives each Service only what it declares.
  5. Data. Mark databases stateful: true with x-yoho.backup; dump and restore or reuse the volume.
  6. Cut over. yoho init, yoho config check, yoho setup --plan, yoho plan. Dump the databases and have the user turn off Coolify auto-deploy. Add a migrate block to the production Destination: stop.compose_project: <uuid> (or stop.labels: ["coolify.applicationId=<id>"]), the reused volumes and bind paths under claim, and the tunnel hostnames under tunnel (with a Cloudflare API token in .yoho/secrets). Run yoho migrate plan, then yoho deploy: after the image is built and shipped, Yoho stops the Coolify containers (never removes them), refuses to continue while any other container still mounts the claimed data, deploys, repoints the tunnel ingress to http://yoho-proxy:80, and records it all in cutover.json. yoho rollback --cutover stops the Yoho containers, starts the Coolify ones and restores the saved ingress; if the new version already migrated the database, restore the dump first. A one-time pre-deploy Hook still works for cutovers the block cannot express.
  7. Shared Server. If Coolify keeps running other apps, its Traefik owns 80/443: set proxy.http_port: 0 and https_port: 0 behind a Cloudflare Tunnel, setup.firewall: false, and with root setup.user: root plus authorized_keys: [].
  8. Ingress and cleanup. Point tunnel hostnames (or DNS) at the Yoho Proxy (http://yoho-proxy:80). After a few days, delete the Coolify app with volume deletion off (Yoho uses those volumes).

Rehearse first ​

For an App that matters, deploy to a second Destination before the real cutover: its own servers (a LAN box works), an overlay that points data at a restored copy, side-effect Services (schedulers, off-Server Backups, the tunnel) replaced with sleep, and test-mode payment keys. Stop only the rehearsal App's containers (by the yoho.app and yoho.destination labels; leave other Apps and yoho-proxy running) and run yoho deploy -d rehearsal to reproduce the ordering of the cutover. The rehearsal Destination has no migrate block, so it never touches the Coolify stack. Set proxy: {tunnel: null} in the rehearsal overlay (yoho.rehearsal.yml) or give it its own tunnel: the Destination inherits proxy.tunnel.token_secret, and apply would register the rehearsal Server as a connector of the live tunnel and send production traffic to test data. The production cutover itself is a migrate block on the production Destination (ADR 0012).