IAnswerable

Zero-Downtime Deploys in Dokploy

28 Oct 2025

By default, a deploy stops the old container and starts the new one. There is a gap in between, and users see 502s. Here is how to close it.

The mechanism

Docker Swarm can start the new container, wait for it to pass a healthcheck, shift traffic, then stop the old one. That requires two things you probably have not configured: a healthcheck, and an update policy.

flowchart TD
    A[Deploy triggered] --> B[New container starts]
    B --> C{Healthcheck passes?}
    C -->|Yes| D[Traffic shifts to new container]
    D --> E[Old container stops]
    C -->|No| F[Old container keeps serving]
    F --> G["Rollback: build marked failed"]
  

1. Add a real healthcheck

Your app needs an endpoint that returns 200 only when it is genuinely ready to serve:

app.get('/health', async (req, res) => {
  try {
    await db.query('SELECT 1')
    res.status(200).send('ok')
  } catch {
    res.status(503).send('not ready')
  }
})

Then in your Dockerfile:

HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

--start-period is the grace window during which failures do not count against you. Set it longer than your slowest cold start. Get this wrong and Swarm kills the container before it finishes booting, forever.

2. Set the update policy

In Dokploy, open the application → Advanced → Swarm Settings. Set:

start-first is the important one. It means the new task starts and becomes healthy before the old one is stopped. The default is stop-first, which is exactly the gap you are trying to remove.

rollback on failure means a broken build reverts itself instead of taking the site down.

3. Run at least two replicas

With one replica and start-first, Swarm briefly runs two containers — which works, but only if your app tolerates it. Two replicas is cleaner and gives you a survivor if one node dies:

Set Replicas to 2 in the same settings panel.

This requires your app to be stateless. If it stores sessions in memory, two replicas will randomly log people out. Move sessions to Redis or a database first.

4. Handle shutdown gracefully

Swarm sends SIGTERM and waits before sending SIGKILL. Your app should stop accepting new connections, finish in-flight requests, then exit:

process.on('SIGTERM', () => {
  server.close(() => {
    db.end().then(() => process.exit(0))
  })
})

Without this, in-flight requests are cut off mid-response during every deploy. Users see it as a random failure that “only happens sometimes”.

5. Verify it

Run a request loop during a deploy:

while true; do
  curl -s -o /dev/null -w "%{http_code}\n" https://app.example.com/health
  sleep 0.2
done

Deploy while that runs. You should see nothing but 200. Any 502 means one of the four steps above is not set.

The database caveat

Zero-downtime deploys do not survive a breaking migration. If the new code drops a column the old code still reads, you will have errors during the overlap regardless of your Swarm settings. Use expand-contract migrations: add the new column, deploy code that writes both, backfill, deploy code that reads the new one, then drop the old column in a later release.