Set up each platform
Choose the artifact and topology that fit your infrastructure. Core and Enterprise use the same configuration and replication workflow; Enterprise adds the API, metrics, backup, and download surfaces.
Install the edition you need
| Delivery | PostgreSQL and MySQL products | Redis product |
|---|---|---|
| Native package | Debian/Ubuntu, RHEL/Rocky, openSUSE Leap, Amazon Linux 2023, and Alpine. Core uses the normal package name; Enterprise uses the -ee package from the licensed channel. |
Not currently documented as a published package. Build from source. |
| Portable tarball | Debian/Ubuntu, EL9/EL8, SUSE 15, Amazon Linux 2023/2, Azure Linux 3, and Alpine. No root or package manager required. | Build from source. |
| Docker | Images are not published by CI. Build them locally from the corresponding project checkout. | |
| Source |
make builds Core; make ENTERPRISE=1 builds Enterprise from an entitled checkout. |
|
<host>, <version>, and <platform-suffix> with the values from your release portal. Verify the detached signature and checksum before installation.Native packages
echo "deb [trusted=yes] https://<host>/api/packages/wgodden/debian jammy main" | \
sudo tee /etc/apt/sources.list.d/patroni-anonymizer.list
sudo apt-get update
sudo apt-get install patroni-anonymizer
# MySQL/MariaDB/Percona:
sudo apt-get install mysql-anonymizer
# Enterprise uses the licensed channel and -ee suffix:
sudo apt-get install patroni-anonymizer-ee
| Linux family | Package command | Important limit |
|---|---|---|
| Debian / Ubuntu |
apt-get install <product>
|
Core and Enterprise release legs. |
| RHEL / Rocky EL9 |
dnf install <product>
|
MySQL streaming is absent where Connector/C is older than 3.3. |
| openSUSE Leap 15 |
zypper install <product>
|
MySQL streaming has the same Connector/C limit. |
| Amazon Linux 2023 |
dnf install <product>
|
Use the release registry definition supplied with the artifact. |
| Alpine |
apk add <product>
|
Uses the Alpine release repository. |
| EL8 | Portable tarball | Core only: available OpenSSL is too old for Enterprise. |
| Amazon Linux 2 / Azure Linux 3 | Portable tarball | No useful native registry delivery. |
Portable tarball
curl -fsSL -o product.tar.gz \
https://<host>/wgodden/<product>/releases/download/v<version>/\
<product>-<version>-<platform-suffix>.tar.gz
tar -xzf product.tar.gz
./<product>-<version>-<platform-suffix>/<product> --help
Docker or source
third_party/drcommon/tooling/packaging/docker/build-images.sh
docker run --rm \
-v "$PWD/config.db:/etc/devreplicate/config.db:ro" \
<product>:<version> replicate --config /etc/devreplicate/config.db
For source builds, install the listed development packages, then run make. The MySQL product needs MariaDB Connector/C, SQLite, OpenSSL, and Jansson. Redis needs SQLite, OpenSSL, Jansson, pkg-config, and a C11 compiler. PostgreSQL additionally needs libpq and the PostgreSQL client tools used by initial sync.
The five newer products
Build MongoDB, Kafka, SQL Server, Oracle and CouchDB products inside their development container with scripts/dev.sh make. A default make builds the core; make ENTERPRISE=1 builds Enterprise.
| Product | Build dependencies beyond SQLite, OpenSSL, Jansson and pkg-config
|
|---|---|
mongodb-anonymizer
|
libmongoc and libbson 1.23 or later. |
kafka-anonymizer
|
librdkafka (2.0 in the container and in CI). |
mssql-anonymizer
|
FreeTDS (freetds-dev / libsybdb5) at build and run time. |
oracle-anonymizer
|
Nothing Oracle-specific to build — ODPI-C is vendored — but Oracle Instant Client (Basic or Basic Lite, 19c or later) must be present at run time, since libclntsh.so is loaded then. |
couchdb-anonymizer
|
libcurl (libcurl4-openssl-dev). |
PostgreSQL / Patroni
| Requirement | What to configure |
|---|---|
| Version | PostgreSQL 13 or later on source and replica. |
| Source |
wal_level=logical; the source role needs REPLICATION plus SELECT on synchronized tables. |
| Replica | Create the database first. The role needs object-management rights and permission to set session_replication_role. |
| Tools |
pg_dump and pg_restore must be in PATH. |
| Discovery | Use multiple Patroni REST URLs. For standalone PostgreSQL, set primary_dsn_override. |
patroni-anonymizer init-config --config /etc/patroni-anonymizer/config.db
sqlite3 /etc/patroni-anonymizer/config.db <<'SQL'
INSERT INTO settings (key,value) VALUES
('patroni_urls','http://10.0.0.1:8008,http://10.0.0.2:8008'),
('replica_dsn','host=10.0.0.9 port=5432 dbname=appdb user=pa_repl'),
('slot_name','pa_slot'),
('hash_key_file','/run/secrets/devreplicate-hmac');
SQL
patroni-anonymizer init-sync --config /etc/patroni-anonymizer/config.db
patroni-anonymizer replicate --config /etc/patroni-anonymizer/config.db
primary_dsn_override is the exception and carries its own dbname.MySQL / MariaDB / Percona Server
| Variant | Minimum and required settings |
|---|---|
| MariaDB | 10.11+. Binary log enabled, binlog_format=ROW, binlog_row_metadata=FULL. |
| MySQL | 8.0+. The same binlog settings plus gtid_mode=ON. The source role also needs RELOAD for the brief snapshot lock. |
| Percona Server | 8.0+. Uses the MySQL binlog, GTID, and client behavior. |
| HA source | No built-in discovery. Point primary_dsn at ProxySQL or HAProxy that always fronts the current primary. |
mysql-anonymizer init-config --config /etc/mysql-anonymizer/config.db
sqlite3 /etc/mysql-anonymizer/config.db <<'SQL'
INSERT INTO clusters (name,primary_dsn,replica_dsn,server_id,hash_key_file)
VALUES ('default',
'host=10.0.0.1 port=3306 user=anon dbname=appdb ssl_ca=/etc/ssl/ca.pem',
'host=10.0.0.9 port=3306 user=anon dbname=appdb_anon ssl_ca=/etc/ssl/ca.pem',
4200,'/run/secrets/devreplicate-hmac');
SQL
mysql-anonymizer init-sync --config /etc/mysql-anonymizer/config.db
mysql-anonymizer replicate --config /etc/mysql-anonymizer/config.db
The replica database must already exist. Each cluster maps one source database to one replica database; the names may differ. Use a separate cluster row for each additional database. Production DSNs should set ssl_ca, or at minimum require_tls=1.
Redis / Valkey
Redis source topologies can be standalone, Sentinel, or Cluster. Targets can be standalone or Cluster. Redis Cluster uses database 0 only. Valkey 8.1 and later is the same product and the same binary: the handshake announces a 9.0.0 client so a Valkey 9 primary sends its own RDB format, and the parser picks its dialect from the format’s magic. Redis answers that announcement with an error and nothing changes.
| Source | Target | Configuration |
|---|---|---|
| Standalone | Standalone |
source_type=standalone, target_type=standalone. |
| Sentinel | Standalone or Cluster | List Sentinel endpoints and set sentinel_master_name. The master is resolved again after reconnects. |
| Cluster | Standalone or Cluster | List reachable nodes; CLUSTER SLOTS discovers shard masters. One stream is maintained per source shard. |
| Standalone / Sentinel | Cluster | Related multi-key writes must share a Redis hash tag or can fail with CROSSSLOT. |
sudo apt-get install libsqlite3-dev libssl-dev libjansson-dev pkg-config
make
./bin/redis-anonymizer init-config /etc/redis-anonymizer/config.db
sqlite3 /etc/redis-anonymizer/config.db <<'SQL'
INSERT INTO settings(cluster_name,key,value) VALUES
('default','source_type','standalone'),
('default','source_urls','10.0.0.1:6379'),
('default','target_type','standalone'),
('default','target_urls','10.0.0.9:6379'),
('default','hash_key_file','/run/secrets/devreplicate-hmac');
SQL
./bin/redis-anonymizer init-sync /etc/redis-anonymizer/config.db --cluster default
./bin/redis-anonymizer replicate /etc/redis-anonymizer/config.db --cluster default
Sentinel source
INSERT INTO settings(cluster_name,key,value) VALUES
('default','source_type','sentinel'),
('default','source_urls','10.0.0.1:26379,10.0.0.2:26379'),
('default','sentinel_master_name','mymaster');
Cluster source and target
INSERT INTO settings(cluster_name,key,value) VALUES
('default','source_type','cluster'),
('default','source_urls','10.0.1.1:6379,10.0.1.2:6379'),
('default','target_type','cluster'),
('default','target_urls','10.0.2.1:6379,10.0.2.2:6379');
source_tls=1 or target_tls=1 and provide a CA file when the system trust store is not appropriate. Optional client certificate and key settings enable mTLS. A verification failure never falls back to plaintext.MongoDB
| Requirement | What to configure |
|---|---|
| Source | MongoDB 6.0 or later as a replica set or a sharded cluster — change streams need one, and a single-node mongod --replSet is enough. A standalone mongod is refused at init-sync with a message saying so. Tested against MongoDB 7.0 Community. |
| Target | Any MongoDB 4.4 or later, standalone or replica set. It receives ordinary CRUD plus drop, renameCollection, dropDatabase and createIndexes. |
| Driver | libmongoc 1.23 or later. |
| Settings |
source_uri and target_uri (they must differ), hash_key or hash_key_file, and optionally status_interval_ms, batch_size, copy_indexes and full_document. |
| Out of scope | FerretDB, Amazon DocumentDB and Azure Cosmos DB’s MongoDB API. Their change-stream behaviour differs in resume tokens, fullDocument modes and DDL events, and this product does not paper over that. |
mongodb-anonymizer init-config /etc/mongodb-anonymizer/config.db
sqlite3 /etc/mongodb-anonymizer/config.db <<'SQL'
INSERT INTO settings VALUES
('default','source_uri','mongodb://src1,src2,src3/?replicaSet=rs0&tls=true'),
('default','target_uri','mongodb://anon-target:27017'),
('default','hash_key_file','/run/secrets/devreplicate-hmac');
SQL
mongodb-anonymizer init-sync /etc/mongodb-anonymizer/config.db
mongodb-anonymizer replicate /etc/mongodb-anonymizer/config.db
init-sync takes the stream position first, then copies every user collection — every database but admin, local, config and __devreplicate, every collection but views and system.* — dropping the target’s copy first and recreating indexes. replicate then opens the deployment-wide change stream at that position. The state document lives on the target, in __devreplicate.state, so a target restored from a backup carries the checkpoint that matches it.
full_document decides where an update’s document comes from. The default updateLookup has the server fetch the document’s current state per update event. required uses the post-image the source recorded at write time, which needs changeStreamPreAndPostImages enabled on every collection — the stream fails on one where it is not.Apache Kafka / Redpanda
| Requirement | What to configure |
|---|---|
| Source and target | Apache Kafka 3.x in KRaft mode, or Redpanda 24 and later. ZooKeeper-mode Kafka is not supported. |
| Bootstrap |
source_bootstrap and target_bootstrap. They must differ: mirroring onto same-named topics of its own source would feed plaintext back into itself, and an identical string is refused. |
| Security | Per side: <side>_security_protocol (plaintext, ssl, sasl_plaintext, sasl_ssl), SASL PLAIN / SCRAM-SHA-256 / SCRAM-SHA-512 / GSSAPI / OAUTHBEARER, a CA bundle, and a client certificate and key for mTLS. |
| Coverage policy |
unmatched_topic_policy is skip by default: a topic no rule matches is not mirrored at all. passthrough copies it byte-for-byte. Topics whose name starts with _ are never mirrored under either policy. |
| State |
state_topic, a compacted topic on the target, __devreplicate.state by default; it must start with _ and init-sync creates it. |
kafka-anonymizer init-config /etc/kafka-anonymizer/config.db
sqlite3 /etc/kafka-anonymizer/config.db <<'SQL'
INSERT INTO settings(cluster_name,key,value) VALUES
('default','source_bootstrap','10.0.0.1:9092'),
('default','target_bootstrap','10.0.0.9:9092'),
('default','source_security_protocol','sasl_ssl'),
('default','source_sasl_mechanism','SCRAM-SHA-512'),
('default','source_sasl_username','devreplicate'),
('default','source_sasl_password_file','/run/secrets/kafka-source.pw'),
('default','hash_key_file','/run/secrets/devreplicate-hmac');
SQL
kafka-anonymizer init-sync /etc/kafka-anonymizer/config.db
kafka-anonymizer replicate /etc/kafka-anonymizer/config.db
init-sync creates each target topic with the source’s partition count, or empties an existing one with DeleteRecords, copies every record up to the high watermark it saw at the start, and records that watermark per partition. replicate starts at exactly that watermark, so nothing is copied twice and nothing between the two commands is skipped.
init-sync stops the mirror until init-sync --topic re-baselines it. The source consumer runs with auto.offset.reset=error: if retention passed the recorded offset while the mirror was down, replicate stops rather than jumping silently to the earliest or latest offset.Microsoft SQL Server
| Requirement | What to configure |
|---|---|
| Version | SQL Server 2019 or 2022 on both sides; a ProductMajorVersion below 15 is refused. |
| Edition | An edition with Change Data Capture on the source: Developer, Standard or Enterprise. Express has no SQL Server Agent and no CDC and is refused at init-sync. The target can be any edition. |
| Agent | SQL Server Agent must be running on the source — CDC’s capture job is an Agent job. Without it sys.fn_cdc_get_max_lsn() stays NULL and init-sync gives up after 60 s saying so. In a container, MSSQL_AGENT_ENABLED=true. |
| Client | FreeTDS at build and run time; the client speaks TDS 7.4 with a UTF-8 client charset. |
| Target | The target database must already exist. init-sync creates and recreates schemas and tables inside it; it never creates the database. |
| Privileges | The source login needs sysadmin for init-sync (sp_cdc_enable_db requires it), or db_owner where CDC is already enabled. The target login needs db_owner: the baseline drops and recreates tables and every write runs under SET IDENTITY_INSERT. |
mssql-anonymizer init-config --config /etc/mssql-anonymizer/config.db
sqlite3 /etc/mssql-anonymizer/config.db <<'SQL'
INSERT INTO settings (key, value) VALUES
('source_host','prod-sql.internal'), ('source_port','1433'),
('source_user','devreplicate'), ('source_password_file','/run/secrets/mssql-source.pw'),
('source_database','app'),
('target_host','dev-sql.internal'), ('target_port','1433'),
('target_user','devreplicate'), ('target_password_file','/run/secrets/mssql-target.pw'),
('target_database','app_anon'),
('hash_key_file','/run/secrets/devreplicate-hmac');
SQL
mssql-anonymizer init-sync --config /etc/mssql-anonymizer/config.db
mssql-anonymizer replicate --config /etc/mssql-anonymizer/config.db
One source database maps to one target database; a second pair is a second configuration database today. replicate bounds each poll to batch_transactions source transactions, merges every tracked table’s changes into the source’s commit order, and applies them with the moved checkpoint in one target transaction. Latency is the capture job’s interval plus poll_interval_ms.
init-sync runs sp_cdc_enable_db and creates one capture instance, with its change table and capture and cleanup jobs, per table with a primary key. Only keyed tables are replicated by CDC — a heap is copied at init-sync and refreshed only by another one. The target carries no foreign keys, check constraints, defaults or triggers, and a unique index whose key includes an anonymized column is created non-unique. DDL on a tracked table stops replication, because CDC does not follow DDL: run init-sync again. Disable CDC with sys.sp_cdc_disable_db if you stop using the tool.Oracle Database
| Requirement | What to configure |
|---|---|
| Version | Oracle Database 19c, 21c, 23ai or 26ai on the source; the target can be any release that accepts the source’s DDL. Oracle Database Free works for both and includes LogMiner. |
| Supplemental logging |
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS; as SYSDBA in CDB$ROOT. Without it UPDATE and DELETE redo carries no key columns, and both init-sync and replicate check V$DATABASE and refuse to start. |
| ARCHIVELOG | Required for anything beyond development. replicate mines the online logs and the archived logs, and refuses to continue past a gap, naming the SCN it needed. In NOARCHIVELOG mode a log switch that recycles unmined redo stops replication and needs a new init-sync. |
| Container database | In a CDB, LogMiner runs in CDB$ROOT: connect to the root service as a common user (C##…) and name the pluggable database in source_pdb. On a non-CDB leave it empty. |
| Client | Oracle Instant Client (Basic or Basic Lite, 19c or later) on the machine running the tool. ODPI-C is compiled into the binary and loads libclntsh.so at run time. |
| Target | The target schemas must already exist, as users of the same names as the source owners, with quota. init-sync creates and replaces tables inside them, never the users. |
oracle-anonymizer init-config --config /etc/oracle-anonymizer/config.db
sqlite3 /etc/oracle-anonymizer/config.db <<'SQL'
INSERT INTO settings (key, value) VALUES
('hash_key_file','/run/secrets/devreplicate-hmac');
INSERT INTO clusters (name, source_connect, source_user, source_password_file, source_pdb,
target_connect, target_user, target_password_file, schemas) VALUES
('default','db1.example.org:1521/FREE','c##anon','/run/secrets/ora-source.pw','FREEPDB1',
'db2.example.org:1521/ANONPDB','app','/run/secrets/ora-target.pw','APP, HR');
SQL
oracle-anonymizer init-sync --config /etc/oracle-anonymizer/config.db
oracle-anonymizer replicate --config /etc/oracle-anonymizer/config.db
schemas is the comma-separated list of owners to replicate, every table in each. init-sync copies each table AS OF SCN, recreates tables, then plain indexes and finally referential constraints, and records the snapshot SCN and the SCN mining starts from. replicate lists the redo logs covering that range, registers them with DBMS_LOGMNR.ADD_LOGFILE, buffers each transaction until its COMMIT, and applies it with its checkpoint in one target transaction.
init-sync rather than protecting nothing quietly. DDL is not replayed: DDL on a replicated table stops replicate with the statement in the message, so apply it to the target by hand and re-run init-sync. Triggers and views are not recreated, and a table containing an object type, XMLTYPE, SDO_GEOMETRY, VECTOR or BFILE is refused.CouchDB
| Requirement | What to configure |
|---|---|
| Source and target | CouchDB 3.x on both sides. Everything used is stable API; _bulk_get with revs exists since 2.2, so 2.2+ may work, but only 3.x is tested. 1.x has no _bulk_get and is not supported. |
| Authentication | HTTP Basic, through *_username and *_password or *_password_file, or credentials in the URL. The account needs _admin on the target (it creates and deletes databases) and read access to every source database plus /_all_dbs. |
| Transport | http and https through libcurl with the system CA store; verification is always on. There are no client-certificate settings yet. |
| Separation |
target_url must name a different server from source_url unless target_database_prefix is set — otherwise the copy would overwrite the source, and validation refuses it. |
| Selection |
database_pattern is a glob over source database names (default *); discovery_interval_ms is how often replicate re-lists /_all_dbs for new databases. |
couchdb-anonymizer init-config /etc/couchdb-anonymizer/config.db
sqlite3 /etc/couchdb-anonymizer/config.db <<'SQL'
INSERT INTO settings(cluster_name,key,value) VALUES
('default','source_url','https://couch-prod.internal:6984'),
('default','target_url','https://couch-dev.internal:6984'),
('default','source_username','devreplicate'),
('default','source_password_file','/run/secrets/couch-source.pw'),
('default','target_username','devreplicate'),
('default','target_password_file','/run/secrets/couch-target.pw'),
('default','database_pattern','app*'),
('default','hash_key_file','/run/secrets/devreplicate-hmac');
SQL
couchdb-anonymizer init-sync /etc/couchdb-anonymizer/config.db
couchdb-anonymizer replicate /etc/couchdb-anonymizer/config.db
This never uses CouchDB’s own replicator, which copies documents byte-for-byte — exactly the plaintext transfer the tool exists to prevent. Every document crosses through the process instead. init-sync reads update_seq first, deletes and recreates the target database, pages _all_docs, and writes each page with _bulk_docs and new_edits=false. replicate follows a continuous _changes feed, fetches each batch with _bulk_get?revs=true, and checkpoints the batch’s last sequence in _local/devreplicate-state on the target.
init-sync deletes and recreates every target database before copying, so never point target_url at a server holding anything you want to keep under those names. _id and _rev and the whole revision tree are the source’s, which is what makes a replay after a crash a no-op — but 2-abc on the target holds the anonymized body. Attachments are copied verbatim, and _users, _replicator, _global_changes and any other _-prefixed database are never replicated.Run the system continuously
Create the configuration database
Run init-config, store it with restrictive permissions, and place HMAC key material in a protected file or secret mechanism.
Define every sensitive field or key
Add SQL anon_field rows or Redis anon_rule rows before the first copy. Continue with the anonymization guide.
Run the initial sync
This creates the baseline target and applies configured transformations before values are written there.
Supervise replication
Run replicate as a long-lived service under systemd, your orchestrator, or another supervisor.
Backfill or re-baseline policy additions
A rule added after initial sync is pending. In PostgreSQL, MySQL and Redis, run the core backfill workflow; MongoDB Enterprise provides its own document backfill. Kafka resyncs affected topics, while SQL Server, Oracle and CouchDB re-run init-sync for affected data. Either way, do not share the target until pending fields or rules are zero.