Skip to content

Migrating from IBM Cloudant to managed Apache CouchDB: a zero-downtime guide

11 min readCouchDBCloudantMigrationNoSQL

Short version: replicate one database as a smoke test, replicate the rest, move design documents and rebuild any Cloudant Search indexes as Mango indexes, switch to continuous replication, then cut the application over and verify document counts. Because CouchDB replication is symmetric, rollback is the same call with the endpoints swapped, which is what makes the cutover genuinely zero-downtime rather than nominally so. The things that do not come across are Cloudant Search indexes, IAM keys, and anything Cloudant-specific in your access setup.

CouchDB's replication protocol makes leaving any host straightforward in a way that almost no other database can match. There is no pg_dump, no mongoexport, no extraction step at all. You point one CouchDB at another, hit POST /_replicator, and the data flows. The same protocol works between Cloudant and any Apache CouchDB instance, in either direction, which means a Cloudant migration can run as a zero-downtime cutover with rollback that is just as one-sided.

This guide walks the full migration from IBM Cloudant to a managed Apache CouchDB on Layerbase Cloud. It covers the replication setup, what transfers and what does not, how to verify, how to cut over with zero downtime, and how to roll back if something goes sideways.

If you are still deciding whether to leave, the related posts cover the why: Cloudant deprecations in 2025 and 2026 lists what has changed, and Cloudant vs Layerbase Cloud is the head-to-head.

Contents

Before you start

Gather the following:

  1. Cloudant credentials with read access to every database you want to migrate. Legacy username and password works, IAM API keys work, the only requirement is that the credentials can read the source databases and _all_dbs.
  2. An inventory of databases. GET /_all_dbs on Cloudant returns the full list. Save it, you will reuse it.
  3. An inventory of design documents. GET /{db}/_design_docs?include_docs=true per database. Design docs replicate normally, but it is worth knowing up front whether any of them use _show, _list, _update, or _rewrite handlers (deprecated and unsupported on Cloudant, but still functional on Apache CouchDB; see the deprecations post).
  4. A list of Cloudant Search indexes. These are Lucene-backed and Cloudant-only. They will not replicate to vanilla Apache CouchDB. See What does not transfer for the workaround.
  5. A list of IAM API keys your application uses. They will not work against the new instance, and you will need to rotate.

Document counts for each source database, captured before the migration starts, give you a clean before/after to verify against:

bash
for db in $(curl -s "https://$CLOUDANT_AUTH@$CLOUDANT_HOST/_all_dbs" | jq -r '.[]'); do
  count=$(curl -s "https://$CLOUDANT_AUTH@$CLOUDANT_HOST/$db" | jq '.doc_count')
  echo "$db,$count"
done > cloudant-doc-counts.csv

Step 1: Provision the target CouchDB

Create a CouchDB instance on Layerbase Cloud. CouchDB runs on the Pro plan at $15/mo, which covers up to 10 databases and 25 GB of storage, adds no database-count cap inside the instance itself, and runs the upstream Apache CouchDB binary (so design documents, Mango queries, MapReduce views, and the replicator all behave the same as on any other Apache CouchDB host). Pro includes a 7-day trial, which is usually enough to rehearse the whole migration before you commit.

Grab the connection URL from the instance page. It looks like:

text
https://admin:GENERATED_PASSWORD@your-host.cloud.layerbase.dev

Confirm it responds:

bash
curl https://admin:GENERATED_PASSWORD@your-host.cloud.layerbase.dev/
json
{"couchdb":"Welcome","version":"3.5.0","features":["access-ready","partitioned","reshard","scheduler"],"vendor":{"name":"The Apache Software Foundation"}}

Export it for the rest of the steps:

bash
export TARGET="https://admin:GENERATED_PASSWORD@your-host.cloud.layerbase.dev"
export SOURCE="https://APIKEY:PASSWORD@your-cloudant.cloudantnosqldb.appdomain.cloud"

Step 2: Replicate one database as a smoke test

Pick the smallest database first. The goal of this step is to flush out any auth or networking issues before you commit to a long-running batch.

bash
curl -X POST $TARGET/_replicator \
  -H "Content-Type: application/json" \
  -d "{
    \"_id\": \"migrate-smoketest\",
    \"source\": \"$SOURCE/smalldb\",
    \"target\": {
      \"url\": \"$TARGET/smalldb\",
      \"headers\": {}
    },
    \"create_target\": true,
    \"continuous\": false
  }"

Watch progress:

bash
curl $TARGET/_scheduler/docs/_replicator/migrate-smoketest

When state returns "completed", compare counts:

bash
curl -s $SOURCE/smalldb | jq '.doc_count'
curl -s $TARGET/smalldb | jq '.doc_count'

These should match. If they do not, the most common causes are documents with conflicts (replicated, but counted differently) or partial reads against Cloudant Search-backed views (which are not part of _all_docs and therefore not a discrepancy, just confusing).

Step 3: Replicate every database

Once the smoke test passes, do the rest. For a small fleet, a loop over your saved _all_dbs list works:

bash
while read -r db; do
  curl -X POST $TARGET/_replicator \
    -H "Content-Type: application/json" \
    -d "{
      \"_id\": \"migrate-$db\",
      \"source\": \"$SOURCE/$db\",
      \"target\": {\"url\": \"$TARGET/$db\"},
      \"create_target\": true,
      \"continuous\": false
    }"
done < db-list.txt

For larger fleets (dozens or hundreds of databases), use IBM's couchreplicate CLI. It parallelises with a bounded concurrency, retries on failure, and renders a TUI progress bar. The same tool runs unmodified against any source-target pair, not just Cloudant-to-Cloudant.

bash
npm install -g couchreplicate
couchreplicate -s "$SOURCE" -t "$TARGET" -c 4

The -c 4 flag is concurrency. Four parallel replications is a safe starting point. Bump it if Cloudant tolerates it (watch for 429: too many requests from the source side; that is your signal to back off).

Step 4: Migrate design documents and indexes

Design documents replicate as regular documents, so they come across as part of step 3. There is no separate "index" migration step in CouchDB, the indexes are derived from the design documents and rebuild on first query against the new host.

Two things to be aware of:

  1. First-query latency is real. A freshly-replicated database has design docs but no built indexes. The first Mango query that hits a new index pays the build cost. For a large database this can be seconds to minutes. The fix is to warm the indexes after replication and before cutover:
bash
for view in mango-index-1 mango-index-2; do
  curl -X POST $TARGET/yourdb/_find \
    -H "Content-Type: application/json" \
    -d "{\"selector\": {\"_id\": {\"\$gt\": null}}, \"limit\": 1, \"use_index\": \"$view\"}"
done
  1. Cloudant Search indexes do not transfer. These are the Lucene-backed full-text indexes you create through Cloudant's _search endpoint. They live outside the CouchDB replication protocol and are a Cloudant-only feature. Migration options are:
  • Rebuild as Mango indexes if the queries are structured rather than full-text.
  • Rebuild as full-text in a separate engine (Meilisearch is a good fit and is also available on Layerbase Cloud). Stream document changes from CouchDB into the search engine through the _changes feed.
  • Self-host an Apache CouchDB build that ships with Lucene (some 3.x packagings include it; the standard Apache binary does not).

Step 5: Switch to continuous replication for zero downtime

The one-shot replications from step 3 leave you with two databases that are point-in-time identical. For a zero-downtime cutover, switch them to continuous mode so the target keeps catching up while the application is still writing to Cloudant.

For each database, post a new replicator doc with continuous: true:

bash
curl -X POST $TARGET/_replicator \
  -H "Content-Type: application/json" \
  -d "{
    \"_id\": \"sync-$db\",
    \"source\": \"$SOURCE/$db\",
    \"target\": {\"url\": \"$TARGET/$db\"},
    \"continuous\": true
  }"

Verify everything is in running state:

bash
curl -s $TARGET/_scheduler/docs/_replicator | jq '.docs[] | {id: .id, state: .state}'

You can leave continuous replication running for hours or days. It is idempotent, restartable, and survives target restarts.

Step 6: Cut over the application

When you are ready:

  1. Quiesce writes to Cloudant. Either deploy a maintenance-mode flag in your app, route writes to the new endpoint, or accept a short read-only window.
  2. Wait for the replicator to drain. Watch the _scheduler/docs output until info.changes_pending (or equivalent) is zero for every replication.
  3. Stop the continuous replications. DELETE /_replicator/sync-$db.
  4. Verify final document counts match the source one more time, including any documents written in the last minute.
  5. Flip the application's connection string to the new host, redeploy, and resume writes.

Total downtime if you do this carefully is whatever your deploy pipeline takes, often seconds.

Step 7: Verify and decommission Cloudant

Run the count script from the Before you start section again, against the Layerbase target. Diff against the pre-migration CSV:

bash
diff cloudant-doc-counts.csv layerbase-doc-counts.csv

For deeper verification, spot-check a few documents by _id:

bash
for id in $(shuf -n 20 sample-doc-ids.txt); do
  cloudant_doc=$(curl -s "$SOURCE/yourdb/$id")
  target_doc=$(curl -s "$TARGET/yourdb/$id")
  diff <(echo "$cloudant_doc") <(echo "$target_doc")
done

The _rev values should match exactly. If they do not, the source had writes the replicator did not pick up, which means you cut over before the continuous replication caught up.

Once you are satisfied, leave the Cloudant instance running for a week or two as a hot fallback. You are still paying for it during that window, but the cost of an unplanned rollback during the first week post-cutover usually exceeds the marginal Cloudant bill. After that, delete the Cloudant databases (carefully, ideally one at a time with at least a day between each in case the application surfaces a missed dependency) and finally the instance.

What does not transfer

A short list of things that will not come across automatically and need a per-case decision:

ItemReasonAction
Cloudant Search indexesLucene, Cloudant-only featureRebuild as Mango or as a separate full-text engine.
IAM API keysCloudant-account-scopedGenerate new credentials on the target, update app config.
Cloudant-specific monitoring and dashboardsTied to IBM CloudRe-create on your new host's observability stack.
Per-account capacity and rate-limit configurationCloudant Standard plan concept, does not exist on Apache CouchDBDrop. Apache CouchDB does not throttle the same way.
CouchApp _show, _list, _update, _rewrite handlers using QuickJS-incompatible JSThe design docs replicate, but the JS engine on Apache CouchDB may still be SpiderMonkeyTest on the target. Most CouchApps work unchanged. See the deprecations post for context.

Things that do transfer cleanly, in case you were wondering:

  • All JSON documents and their full revision history.
  • Design documents and the MapReduce views they define.
  • Mango indexes (the JSON-only ones, not Lucene-backed _search).
  • Document attachments (_attachments), via the standard replicator.
  • Conflicts, with both winning and losing revisions preserved.

Rollback plan

The same protocol that gets you onto Layerbase gets you back to Cloudant if something is wrong. Two options:

  1. Continuous replication in reverse. Before cutover, also set up continuous replication from Layerbase back to Cloudant. After cutover, Cloudant stays warm with the new writes. If you need to roll back, flip the connection string and the data is already there.
bash
curl -X POST $SOURCE/_replicator \
  -H "Content-Type: application/json" \
  -d "{
    \"_id\": \"reverse-$db\",
    \"source\": \"$TARGET/$db\",
    \"target\": \"$SOURCE/$db\",
    \"continuous\": true
  }"
  1. Catch-up replication on demand. If you skipped the reverse-replication and need to roll back, run a one-shot from Layerbase to Cloudant before flipping the connection string. This costs a few minutes of replication time, which is fine for an unplanned rollback.

Both options exist because CouchDB's replication is symmetric. The "source" and "target" are conventions, not roles.

Migration with couchbackup as a fallback

If your network policy does not allow direct CouchDB-to-CouchDB replication (for example, a corporate proxy that blocks one of the endpoints), you can fall back to IBM's couchbackup tool. It dumps a Cloudant database to a local JSON-Lines file, which you then restore against any CouchDB.

bash
npm install -g @cloudant/couchbackup

# Backup
couchbackup --url "$SOURCE" --db yourdb > yourdb-backup.txt

# Restore against the target
cat yourdb-backup.txt | couchrestore --url "$TARGET" --db yourdb

A few caveats:

  • couchbackup does not back up attachments. If your databases use attachments, replication is the only sensible path.
  • Document conflicts are preserved by default. The --mode shallow flag drops them for speed.
  • The output is large but compresses well. Pipe through gzip if you are storing the dump.

Direct replication is faster, simpler, and lossless. Use couchbackup only if direct replication is blocked.

FAQ

Is this really zero downtime?

It can be, and the mechanism is continuous replication rather than optimism. Once continuous replication is running, both sides stay current, so the cutover is a connection-string change in your application rather than a window where writes have nowhere to go. What you should still plan for is the moment after cutover, which is why the guide leaves Cloudant running as a hot fallback.

How do I roll back?

Run the replication in the other direction. That is the whole answer, and it is unusual: with almost any other database, rollback after a cutover means reversing a migration you built one-way. CouchDB's protocol is symmetric between any two instances, so the fallback path is the same call with source and target swapped.

Does document revision history survive?

Yes, along with attachments and conflicts, both winning and losing revisions included. Design documents and the MapReduce views they define come across too, as do JSON Mango indexes. What does not is anything Lucene-backed, which on Cloudant means _search.

What do I have to rebuild by hand?

Four things, and none of them are the data. Cloudant Search indexes get rebuilt as Mango indexes or handed to a dedicated full-text engine. IAM keys are account-scoped, so you generate new credentials and update your app config. Cloudant-specific monitoring and dashboards are re-created on your new observability stack. And per-account capacity and rate-limit configuration simply has no counterpart, because Apache CouchDB does not throttle that way.

Will my CouchApp design docs still work?

Usually, and it is worth testing rather than assuming. The design documents replicate fine; what varies is the JavaScript engine evaluating them, since Cloudant is moving to QuickJS while an Apache CouchDB host may still be on SpiderMonkey. Most CouchApps work unchanged. The deprecations post covers what actually differs between the two engines.

Where to go from here

Migrating off Cloudant is mostly a matter of running POST /_replicator once per database and verifying the doc counts. Continuous replication makes the cutover zero-downtime, and the same protocol works in reverse if you need to roll back. There is no other database where the migration story is this clean.

Start the migration here.