# Migrate from Coolify, Dokploy, or Dokku
URL: https://synquic.tech/docs/guides/migrate-from-coolify-dokploy-dokku.md

Move an existing self-hosted server onto Synq without redeploying from scratch — Synq scans your running Docker containers and adopts them in place, with a zero-downtime path and server-to-server moves.

import { Step, Steps } from 'fumadocs-ui/components/steps';

Already running apps on **Coolify**, **Dokploy**, or **Dokku**? You don't rebuild them on Synq. Synq
**scans the Docker containers already running on your server and adopts them in place** — reusing the exact
image that's live right now, with no rebuild and, on the same server, no data move. It's a guided, one-click
import, and your workloads keep serving while it happens.

<Callout title="The one idea that makes this different">
Every other platform here only deploys **new** workloads. Synq can point at a server's **existing**
running containers, read their domains and volumes, and take them over — including taking over the
`:80/:443` reverse proxy from Coolify/Dokploy/Dokku, rollback-safe.
</Callout>

## How the import works

Synq models the move as a short, reversible pipeline:

1. **Scan** *(read-only — nothing is changed)* — over SSH, Synq enumerates **all** containers, volumes,
   and networks on the server (not just ones it owns), reads their compose config, and joins your existing
   proxy's vhosts to each container by published port to recover its **current domains + SSL**.
2. **Adopt** *(records only)* — it creates a `services` project mirroring the running containers. Each
   adopted service carries the container's **current image and no build context**, so Synq re-runs
   exactly what's live and **never rebuilds**. Services come in **unexposed**, so routing is untouched at
   import time.
3. **Take over the edge** *(later, opt-in, rollback-safe)* — when you add a domain, Synq journals your
   old proxy's state, disables it, installs its **OpenResty** edge, and re-registers your vhosts + certs. If
   anything fails — or the box reboots mid-way — it rolls back automatically.
4. **Cut over** *(opt-in, confirmed)* — only when you say so does it stop and remove the *old* containers.
   Your source volumes are never touched.

## Before you start

<Callout type="warn" title="Keep your old proxy running for the scan">
Don't stop Coolify/Dokploy/Dokku's proxy *before* scanning — Synq reads your live domains and
certificates from it. You'll retire it later, and Synq does that step for you (and can roll it back).
</Callout>

- Migration is **self-hosted only** and needs `server:write` on the source (and target, for server-to-server).
- **Connect the source server to Synq over SSH first** (Servers → Add server). Your apps and their proxy
  stay running throughout.
- You can safely stop the old platform's **control-plane app** at any time — your running app containers and
  their proxy don't depend on it. Leave the *proxy* up until the takeover step.

## Migrate a server (step by step)

<Steps>

<Step>
### Connect the server and scan

Add the server over SSH, then start the import. Synq scans it read-only and lists what it found — each
container with its recovered domain(s), ports, and volumes. (On a large or slow server, use the streaming
scan so the client doesn't time out.)

</Step>

<Step>
### Pick workloads (3-step wizard)

The wizard walks **select → link source → domains**. Your reverse proxy (Traefik / nginx / Caddy / …) is
**automatically excluded** from the workload set and left running untouched — Synq never adopts or
blind-stops it during import.

Review the detected proxy and discovery notices before proceeding. Traefik routes are matched by the
container's private network address as well as published ports, so services do not need a host port to keep
their domains. Imported environment values are saved in each service's **Environment** tab. With reuse in
place, edits are saved for the next environment reapply or redeploy; the existing container keeps running
with its current values.
</Step>

<Step>
### Choose a volume strategy — this decides downtime

This is the important choice:

- **Reuse in place** *(same server, default)* → **attach-live**: the running container is adopted as-is,
  its records rebuilt from the live container, the current image reused. No move, no recreation.
  **→ zero downtime.**
- **Copy**, or **any server-to-server move** → **deploy path**: originals are stopped for a consistent
  volume copy, then a fresh deploy is verified to "ready." **→ a real downtime window** (polled up to ~20 min).

<Callout type="info" title="Trust the strategy, not the banner">
The preview may conservatively warn about "downtime" even for the zero-downtime reuse case. The **reuse
in-place** strategy is genuinely zero-downtime for the workload — the container is never stopped.
</Callout>
</Step>

<Step>
### Take over the domains + edge

When the migration includes domains, Synq prepares the target edge during migration. It shows which
existing sites belong to the selected project and services, then asks for confirmation before replacing
the proxy. Choosing **Migrate sites & take over** journals the old configuration, installs OpenResty on
`:80/:443`, and imports the existing sites and certificates, including sites outside the selected project.
If takeover fails, Synq attempts to restore the previous proxy and reports the recovery outcome.

Selected domains are attached to their migrated services and re-applied from the live containers. Existing
certificates are reused and pending verification is retried immediately. Routes that cannot be reproduced
are reported in discovery notices; review those before taking over. If routing or TLS still needs attention,
the migration keeps a warning and does not automatically remove the old deploy-set containers.

<Callout type="info" title="The only brief interruption">
With reuse in place, your containers keep running. Requests can be interrupted briefly while `:80/:443`
moves from the old proxy to OpenResty. Copy and cross-server migrations also have the downtime described above.
</Callout>

</Step>

<Step>
### Verify, then cut over and remove the old platform

Confirm everything serves through Synq. Then run **cutover** (explicit, token-confirmed): it stops and
removes only the *old* deploy-set containers — **never your source volumes**. Once you're happy, remove
Coolify/Dokploy/Dokku entirely; Synq now owns the box.

<Callout title="Verify before you cut over" type="warn">
Cutover **stops and removes the old workload**, so don't run it until the Synq-managed app is fully healthy:
it loads and serves traffic, your **data is intact** (sign in, open a record, check volumes/uploads and any
database), and every service is `RUNNING`. Cutover leaves your **source volumes untouched**, so the old
deploy-set stays as a fallback until you're certain. If anything's off — missing data, a service that won't
start, routing/TLS wrong — **don't cut over**; leave the old containers running and
[check the troubleshooting guide](/docs/troubleshooting).
</Callout>
</Step>

</Steps>

## What an adopted service keeps from its container

A later redeploy recreates each container under a Synq name (`synq-<project>-<service>`). So that the
containers still find each other and the host the same way, adoption reads the running container and
stores how it is wired. The next deploy recreates exactly that:

| Wiring | What is kept |
| --- | --- |
| **External networks** | A network created outside the compose project (`docker network create app_net`, declared `external: true`) is **joined by name**, not flattened. On it the container keeps its **old container name**, its **compose service name** and every alias it had, spelled exactly as before (underscores included). Synq never creates, changes or removes an external network; a deploy fails before touching the running container if the network is missing. |
| **Project networks** | The compose project's own networks (`<project>_default` and other networks it created) become the one Synq project network. There each service answers to its service name and, exactly as before, to its old container name (for example a compose `container_name`). |
| **Extra hosts** | `extra_hosts` entries, including `host.docker.internal:host-gateway`. |
| **Host ports** | A publish pinned to one interface, such as `127.0.0.1:6379:6379`, is kept exactly, so host processes keep reaching the app. Publishes on all interfaces (`5432:5432`, `0.0.0.0:5432:5432`) are dropped to the container port, as are 80/443 and ports in Synq's reserved ranges; the migration log names each dropped publish. |
| **Stop grace period** | The container's `StopTimeout` (compose `stop_grace_period`). A redeploy stops the old container with it and waits for it before starting the new one. |
| **Volumes** | Named volumes are reused by their exact names (not renamed per project) with reuse in place. |
| **Environment** | Values the operator set, saved in the service's **Environment** tab. |

Every service is one container; a compose profile such as `worker` becomes its own service with one replica.
Open a service's **Settings → Networking → Host networking** to check the external networks, aliases, extra
hosts, stop grace period and volume naming before the first redeploy. The migration run log lists the same
per service. To change them, send `advanced.externalNetworks`, `advanced.networkAliases`,
`advanced.extraHosts` or `advanced.stopGracePeriod` to the service update API.

## Server-to-server migration

Moving a whole stack from one host to another is the same flow, with the data streamed across:

- Named volumes and movable app-data mounts stream **A → B** directly (same daemon: a `tar → tar` copy; across
  daemons: a gzip stream). System paths (`/proc`, `/sys`, `/var/lib/docker`, sockets, …) are never copied.
- Synq **refuses to overwrite a non-empty same-named volume** on the target, so you can't clobber data.
- External networks exist only on the source, so on the target those services join the project network
  instead, keeping the names they had there, with a warning naming each network. Anything else on that
  network (a host process on its gateway, containers outside the migration) is not reachable that way:
  create the network on the target and add it to the service.
- Server-to-server always uses the **deploy path** (a downtime window), and it **can't move locally-built
  images** — those have no registry to pull from on B, so take them over **in place on their current server**
  instead.

## Why the architecture is different

One thing is worth understanding, because it's where Synq genuinely diverges from the others:

- **A programmable OpenResty edge.** Routing runs on OpenResty with a **database-driven, reload-free
  per-route rules engine** — per-route rate limiting, IP/CIDR + country + user-agent bans, method/CIDR/country
  allow-lists, and hotlink protection — pushed live into a shared dictionary with **no nginx reload and no
  Redis**, plus built-in request/bandwidth analytics and a live log stream. Coolify and Dokploy expose Traefik
  middlewares and Dokku exposes nginx templates that can approximate these rules; Synq's difference is that
  it's a managed, reload-free, per-route surface — **not** that it does more at the protocol level (see the
  honest caveats below).

## Honest comparison

All four are open-source, Docker-based, self-hosted PaaS tools, and they share most of the baseline. We're not
going to pretend otherwise — **migrate for the differences, not the essentials.**

**Shared essentials (all four do these):** git-push / webhook auto-deploy · Dockerfile builds · automatic
Let's Encrypt TLS with auto-renew · one-click databases · scheduled S3 backups · multi-server over SSH · REST
API + scoped tokens · a CLI · rollbacks · per-event notifications · **and an official MCP server** (Coolify's
is read-only, Dokploy's covers the full API, Synq's is write-capable). "Has an MCP server" is **not** a
reason to switch.

**The full picture** — differentiators in **bold**; `✓` built-in · `~` partial / manual / paid · `✗` not
available. Where a competitor leads (Coolify's 280+ catalog, Dokploy's HTTP/3, Dokku's zero-downtime default,
buildpack breadth), it's marked honestly.

**Deploys & builds**

| | Coolify | Dokploy | Dokku | Synq |
|---|:---:|:---:|:---:|:---:|
| Git push / webhook auto-deploy | ✓ | ✓ | ✓ | ✓ |
| Dockerfile & prebuilt-image deploy | ✓ | ✓ | ✓ | ✓ |
| Docker Compose / multi-service | ✓ | ✓ | ✗ | ✓ |
| Buildpacks (Nixpacks / Heroku) | ✓ | ✓ | ✓ | ~ auto stack-detect |
| Rollback to a previous release | ✓ | ✓ | ✓ | ✓ |
| Per-PR preview environments | ✓ | ✓ | ~ | ~ |
| Zero-downtime *new* deploys | ~ health-gated | ~ health-gated | ✓ default | ~ health-gated |

**Servers & migration**

| | Coolify | Dokploy | Dokku | Synq |
|---|:---:|:---:|:---:|:---:|
| Multi-server over SSH | ✓ | ✓ | ~ K3s | ✓ |
| Server health / resource monitoring | ~ | ✓ | ~ plugin | ✓ |
| Fine-grained server access grants | ✗ | ~ ent. | ✗ | ✓ |
| **Adopt existing running containers** | ✗ | ✗ | ✗ | **✓** |
| **Server-to-server move (with data)** | ✗ | ✗ | ✗ | **✓** |
| **Leave management, keep apps + data running (re-importable)** | ✗ delete prunes managed containers | ✗ delete tears down | ✗ `apps:destroy` removes | **✓ record-only, re-adoptable** |
| **Take over an existing reverse proxy** | ✗ | ✗ | ✗ | **✓** |

**Networking & edge**

| | Coolify | Dokploy | Dokku | Synq |
|---|:---:|:---:|:---:|:---:|
| Automatic Let's Encrypt TLS | ✓ | ✓ | ✓ | ✓ |
| Unlimited custom domains + wildcards | ✓ | ✓ | ✓ | ✓ |
| HTTP/3 (QUIC) | ✗ | ✓ | ✗ | ✗ |
| CDN / response caching | ✗ | ✗ | ✗ | ✗ |
| **Per-route rate limiting** | ~ Traefik mw | ~ Traefik mw | ~ nginx | **✓ UI, reload-free** |
| **IP / CIDR ban + allow-lists** | ~ mw | ~ mw | ~ nginx | **✓ UI** |
| **Country (geo) blocking** | ✗ | ✗ | ~ GeoIP module | **✓ built-in ** |
| **Bad-user-agent + hotlink protection** | ~ mw | ~ mw | ~ nginx | **✓ UI** |
| **Built-in request analytics** (traffic, status codes, top paths, geo) | ✗ add-on | ✗ add-on | ✗ | **✓** |
| **Live per-route request-log stream** | ~ proxy logs | ~ proxy logs | ~ files | **✓** |
| Resource metrics (CPU / memory / bandwidth) | ~ basic | ✓ | ~ plugin | ✓ |

The edge is where the gap is widest: every route gets **per-route rate limiting, IP/CIDR + country + user-agent
bans, and hotlink protection** from a UI (pushed live, no nginx reload), plus **built-in traffic analytics**
— requests, status codes, top paths, and **geo/country breakdowns** — and a **live per-route request-log
stream**. Coolify and Dokploy give you Traefik middlewares and bolt-on Grafana/Plausible; native
country-level blocking and built-in request analytics aren't there. (Honestly: it's *not* a CDN or WAF — no
HTTP/3, caching, or OWASP rules.)

**Data, team & platform**

| | Coolify | Dokploy | Dokku | Synq |
|---|:---:|:---:|:---:|:---:|
| One-click databases | ✓ 8 | ✓ 6 | ✓ 16 | ✓ catalog |
| Scheduled S3 backups + restore | ✓ | ✓ | ~ per-DB | ✓ |
| Volume backups | ~ | ✓ | ~ | ✓ |
| One-click app catalog | ✓ **280+** | ✓ | ~ community | ✓ curated |
| Teams / organizations | ✓ | ✓ | ~ Pro | ✓ |
| Fine-grained RBAC + audit log | ~ coarse | ~ ent. | ✗ | ✓ |
| Scoped API tokens (PAT) + CLI | ✓ | ✓ | ~ Pro | ✓ |
| Scheduled jobs / cron | ✓ | ✓ | ~ plugin | ✓ |
| Web dashboard | ✓ | ✓ | ✗ Pro | ✓ |
| MCP for AI agents | ✓ read-only | ✓ full-API | ~ community | ✓ write + re-auth |
| **Built-in full mail server** | ✗ | ✗ | ~ community | **✓** |

The defensible reasons to move are the bold rows: **adopting what's already running**, **moving stacks between
servers with their data**, **leaving without lock-in** (a record-only removal drops just Synq's record and
leaves your apps + data running on your box, re-importable later), **taking over your existing proxy**, the
**programmable edge**, and the **built-in mail server**. None of the other three can adopt a running stack,
move one with its data, or hand it back to you still running — every "migration" is a manual rsync-and-redeploy
or a community script.

## Honest caveats

- **Zero-downtime is narrow, not blanket.** Only a **same-server "reuse in place"** import is truly
  zero-downtime. Same-server "copy" and **every** server-to-server move stop the originals for a consistent
  copy and verify a fresh deploy (up to ~20 min) — a real window. The edge takeover briefly interrupts
  `:80/:443` while OpenResty installs.
- **The edge is programmable access rules + analytics — not a CDN and not a WAF.** There's no HTTP/3, no
  brotli, and no response caching layer; the rules are a built-in ruleset (bans / allow-deny / hotlink /
  method / rate-limit), not ModSecurity/OWASP CRS.
- **The mail server is a real mail server.** It's a genuine Postfix + Dovecot stack — so DNS/DKIM/SPF setup,
  deliverability, and ongoing mail ops are yours to run. The others sidestep this by not offering mail at all.
- **Catalog breadth isn't the pitch.** Coolify's 280+ one-click services is the largest catalog here; Synq
  leads on the *migration and edge* story, not template count.

## Next steps

<Cards>
  <Card title="Custom servers" href="/docs/guides/custom-servers" description="Connect a VPS or dedicated box over SSH — the first step before any import." />
  <Card title="Custom domains" href="/docs/guides/custom-domains" description="Point a domain at a migrated service and let the OpenResty edge issue TLS." />
  <Card title="Backups & restore" href="/docs/guides/backups-restore" description="Set up scheduled backups for your newly-adopted databases and volumes." />
</Cards>
