Bahriya Tersane

Documentation / couchdb

CouchDB

Installs Apache CouchDB, the document database: a single node, or a cluster in CouchDB’s own clustering, in which every database is divided into shards and each shard is kept in copies on different nodes.

The chart provides:

  • one node or several, with a node count that can be changed on a running release: copies of every shard are moved, checked and only then removed;
  • the administrator taken from a Secret you create, which the chart never creates, changes or prints, and a valid user required on every request;
  • Erlang distribution between the nodes over TLS, with a certificate authority of the release’s own made with cert-manager, and optional HTTPS for clients;
  • databases, their security and users declared in values;
  • logical backups of every database to S3-compatible storage on a schedule, and restore into a new release;
  • continuous replication between releases, such as one instance’s regions, each pulling from its peers as a replication user of its own;
  • optional access from outside the cluster through Kong with an allow-list, for the HTTP API, for Fauxton, CouchDB’s web interface, and for peers’ replication;
  • volumes encrypted with LUKS by Longhorn;
  • a NetworkPolicy and Prometheus metrics;
  • the official image from the Mamluk registry, pinned by digest, and every Pod meeting the restricted Pod Security Standard.

Prerequisites

  • Kubernetes 1.33 or later.
  • cert-manager, for the certificates between the nodes. It is included with the Bahriya Kubernetes Engine; on other Kubernetes, install it first.
  • A pull secret for 1x.ax named 1x-ax in the release’s namespace, created from an image token as described in the access documentation.
  • A StorageClass for the volumes: the cluster’s default, or one named in persistence.storageClass. Raising the volume size later needs a class that allows volume expansion.
  • As many nodes of the Kubernetes cluster as CouchDB nodes: no two CouchDB nodes are placed on one node of the cluster.
  • A Secret holding the administrator’s name and password, created before the release:

    kubectl --namespace docs create secret generic couchdb-admin \
      --from-literal=username=admin --from-file=password=./password
    

Installation

helm install docs tersane/couchdb \
  --namespace docs --create-namespace \
  --set auth.existingSecret=couchdb-admin \
  --version <chart version>

A single node:

helm install docs tersane/couchdb --namespace docs \
  --set auth.existingSecret=couchdb-admin \
  --set replicas=1 --set cluster.copies=1

The release’s resources are named <release>-couchdb (here docs-couchdb), or after the release alone if its name already contains couchdb. Check that every node is ready:

kubectl --namespace docs get pods -l app.kubernetes.io/instance=docs

Connecting

Clients in the release’s namespace reach http://docs-couchdb:5984, or https://docs-couchdb:6984 with tls.enabled. Every request needs a valid user; only /_up answers without one.

kubectl --namespace docs port-forward svc/docs-couchdb 5984
curl -u admin http://localhost:5984/_membership

_membership lists every node in cluster_nodes and, when all are connected, in all_nodes.

Databases and users

Databases, who may use them, and users can be declared in values; a Job creates or updates them after each install and upgrade. Each user’s password comes from a Secret you create:

databases:
  - name: orders
    members:
      names: [orders-app]
users:
  - name: orders-app
    existingSecret: orders-app-couchdb   # key: password

Leave both empty to manage databases and users yourself, through the HTTP API.

Shards and copies

cluster.shards (CouchDB’s q, default 2) and cluster.copies (CouchDB’s n, default 3) set how databases created afterwards are divided. A database can set its own, as shards and copies in databases, or as q and n on the request that creates it. With three nodes and three copies, every database stays readable and writable while one node is down. copies may not exceed replicas.

Resizing

Change replicas and upgrade. Take a backup first (see Backups); a resize moves data between nodes.

  • Adding nodes. The new nodes join the cluster, and a Job then moves copies of shards onto them until every node holds a similar share.
  • Removing nodes. Before the StatefulSet shrinks, a Job moves every copy off the nodes that will be removed, checks that each new copy holds as many documents as the copy it was made from, and checks that every database answers in full with the departing nodes in maintenance mode. Only then does the upgrade continue; the removed nodes’ volumes are deleted with them.

Each Job prints the shard maps before and after:

kubectl --namespace docs logs job/docs-couchdb-scale-in

A removal is refused, with nothing changed, when a database has more copies of a shard than the nodes that would remain (lower the database’s copies first, or keep more nodes), or when the copies are not filled within resize.timeoutSeconds; the copies added are then removed again. A refused upgrade leaves the release as it was; correct the cause and upgrade again.

Backups

backup:
  enabled: true
  bucket: my-bucket
  basePath: couchdb/docs
  endpoint: s3.example.com        # empty for Amazon S3
  existingSecret: docs-s3         # keys access-key and secret-key
  schedule: "0 2 * * *"
  timezone: UTC
  retentionDays: 30

Each backup is a directory named after its time, such as 20261108T020000Z, holding every database’s documents, with their revisions, attachments and design documents, and its security object. manifest.json is written last; a directory without it is incomplete and is never restored. Deleted documents and conflicting revisions are not kept. Backups older than retentionDays are deleted, except the most recent.

To take a backup now:

kubectl --namespace docs create job docs-backup-now --from=cronjob/docs-couchdb-backup

The platform layout

With backup.layout: platform every storage setting comes from one Secret, and each region of an instance keeps its backups under its own path:

clusterRegion: helsinki-1
backup:
  enabled: true
  layout: platform
  s3:
    existingSecret: platform-backup-s3   # S3_ACCESS_KEY, S3_SECRET_KEY, S3_HOST, S3_BUCKET, S3_REGION
    prefix: couchdb/acme/shop/docs

The backups are then under couchdb/acme/shop/docs/helsinki-1/ in the Secret’s bucket. The render fails without the Secret, the prefix or clusterRegion.

Restore

A backup is restored into a new release at install, with every document’s id and revision kept, so that replication with the original database still recognises them:

helm install docs2 tersane/couchdb --namespace docs \
  --set auth.existingSecret=couchdb-admin \
  --set restore.enabled=true --set restore.backup=20261108T020000Z \
  --set backup.bucket=my-bucket --set backup.basePath=couchdb/docs \
  --set backup.existingSecret=docs-s3

restore.backup: latest takes the most recent complete backup, and restore.databases restores only the databases listed. A database that already holds documents other than design documents is refused. The install fails if the backup cannot be restored; the Job’s log says why.

In the platform layout the backup is read from <restore.prefix>/<clusterRegion>, where restore.prefix defaults to backup.s3.prefix; set it to another instance’s prefix to restore that instance’s backup into a new one.

TLS

The nodes always authenticate one another with certificates from the release’s own authority; interNodeTls.enabled: false turns this off and sends their traffic unencrypted.

For clients, tls.enabled adds HTTPS on port 6984, with a certificate from tls.existingSecret (keys tls.crt, tls.key and ca.crt) or one issued by the release’s authority. Clients verify the server with ca.crt from the Secret <release>-couchdb-http-certs.

tls.httpEnabled: false removes plain HTTP on 5984 from the Service and the NetworkPolicy. CouchDB binds HTTP and HTTPS to one address, so it still listens on 5984 inside the Pod: the NetworkPolicy is what refuses other Pods, and the chart requires networkPolicy.enabled for this setting. It relies on a network plugin that enforces NetworkPolicy.

External access

exposure:
  external:
    enabled: true
    host: docs.example.com
    tlsSecret: docs-example-com-tls
    ipAllowlist: [203.0.113.0/24]

Kong terminates HTTPS on the host name and admits only the listed addresses. The list may not be empty.

Host names can also be composed, and their certificates issued by cert-manager:

clusterRegion: helsinki-1
exposure:
  defaultHostname: docs-x1
  domain: couchdb.example.com
  certIssuer: public-issuer            # a ClusterIssuer
  external:
    enabled: true                      # docs-x1.helsinki-1.couchdb.example.com
    ipAllowlist: [203.0.113.0/24]

Fauxton

client:
  enabled: true                        # ui-docs-x1.helsinki-1.couchdb.example.com
  ipAllowlist: [203.0.113.7/32]
  expiresAt: "2026-11-12T10:00:00Z"    # informational, an annotation

CouchDB’s web interface, at /_utils/ on its own host name. Fauxton runs in the browser and calls CouchDB’s HTTP API, so the host passes the whole API to the listed addresses; CouchDB still requires a valid user. The allow-list may not be empty: the render fails rather than publish CouchDB to everyone.

Replication between releases

Releases holding the same databases, such as one instance’s regions, can keep them in step with CouchDB’s own continuous replication. Each release pulls from its peers:

databases:
  - name: orders
replication:
  user:
    existingSecret: orders-replicator  # this release's replication user
  peers:
    - name: falkenstein-1
      url: https://repl-docs-x1.falkenstein-1.couchdb.example.com
      existingSecret: orders-replicator  # the peer's replication user
  ingress:
    enabled: true                      # repl-docs-x1.helsinki-1.couchdb.example.com
    ipAllowlist: [192.0.2.10, 2001:db8::10]
    requestsPerMinute: 1200

After each install and upgrade a Job writes one document in _replicator for every peer and every replicated database (replication.databases, or every database in databases), each pulling the peer’s copy into this one, and deletes the ones of peers or databases no longer listed. It manages only documents whose id starts with bahriya-. Set replication.reconcile: true so that removing the last peer also deletes its documents.

The setup Job creates the replication user and makes it a member of every replicated database; peers authenticate as that user, never as the administrator. replication.ingress publishes the release to its peers on its own host name, to the listed addresses only, with a limit on requests per minute from each address. Peers’ certificates are verified against the image’s public authorities unless replication.verifyPeer is false.

Replication is multi-primary: each release accepts writes. When the same document is changed in two releases before they replicate, CouchDB keeps both revisions, chooses the same winner in each, and lists the other in _conflicts; resolving the conflict is the application’s.

Encryption at rest

encryption:
  volume:
    enabled: true

Longhorn encrypts every volume of the release with LUKS, using a passphrase in the Secret <release>-couchdb-volume-encryption, generated at install unless encryption.volume.passphrase or existingSecret gives one, and kept when the release is uninstalled, as the volumes are. A passphrase that differs from the one the volumes were encrypted with is refused. CouchDB itself has no encryption at rest. Decide at install: the StorageClass of an existing release’s volumes cannot change. Needs Longhorn.

Network policy

Admits the HTTP API from Pods in the release’s namespace, from the chart’s Jobs and test, from the sources in networkPolicy.from, and from the ingress controller’s namespace when external access, Fauxton or the replication host is on; admits the nodes to one another on 4369 and 9100; and admits the metrics port from networkPolicy.metrics.from, or every source when that is empty.

Outgoing traffic is not restricted unless networkPolicy.egress.enabled: then the nodes reach DNS, the release’s Pods, HTTPS anywhere (for peers’ replication hosts; egress.https: false removes it) and the destinations in egress.to. The Jobs stay unrestricted, since backups reach object storage.

Metrics

CouchDB’s Prometheus metrics are on port 17986 at /_node/_local/_prometheus, without authentication, with the prometheus.io/* annotations on the Pods, and a ServiceMonitor with metrics.serviceMonitor.enabled.

Storage

persistence.size can be raised on a running release: a Job asks for the larger size claim by claim, which the StorageClass must allow. It can never be lowered; the upgrade is refused.

persistentVolumeClaimRetentionPolicy decides whether a volume is deleted when resizing removes its node (whenScaled, Delete by default) and when the release is uninstalled (whenDeleted, Retain by default).

Upgrades

The image is pinned by digest in each chart version. A new chart version restarts the nodes one at a time; with three or more nodes and three copies, the cluster stays available.

Upgrading from the previous release changes nothing with the same values: every addition is off by default. The nodes restart once, since the Jobs’ scripts moved to a ConfigMap of their own; after that, a change to a script no longer restarts the nodes. Upgrading within CouchDB 3 needs no other step: the data files are compatible, and views are rebuilt when first queried after a change of their design document. Compaction is optional and can be run afterwards.

Pod Security

Every Pod meets the restricted Pod Security Standard: CouchDB runs as user 5984 and the Jobs as 10001, with a read-only root filesystem, no privilege escalation, every capability dropped and the runtime’s default seccomp profile.

Testing

helm test docs --namespace docs

The test checks that an anonymous request is refused, and writes a document in a new database as the administrator, reads it back with its revision and deletes the database.

Uninstalling

helm uninstall docs --namespace docs

The volumes are kept; delete them to remove the data:

kubectl --namespace docs delete pvc -l app.kubernetes.io/instance=docs

Parameters

CouchDB

Parameter Description Default
replicas Nodes: 1 is a single node 3
cluster.shards / cluster.copies Shards and copies of databases created afterwards 2 / 3
image.repository / image.tag CouchDB, the official image 1x.ax/tersane/core/couchdb/couchdb / the release’s
toolsImage.repository / toolsImage.tag The Jobs’ and test’s image 1x.ax/tersane/core/couchdb/couchdb-backup / the release’s
imagePullSecrets Pull secrets of every Pod [{name: 1x-ax}]
auth.existingSecret The Secret with the administrator’s name and password; required ""
auth.usernameKey / auth.passwordKey Its keys username / password
auth.cookieSecret / auth.cookieKey A Secret with the Erlang cookie; empty generates one "" / cookie
config CouchDB settings, as INI sections {}
clusterDomain The cluster’s DNS domain cluster.local
clusterRegion The region served, for the platform backup layout and composed host names ""

TLS

Parameter Description Default
interNodeTls.enabled Distribution between the nodes over TLS true
tls.enabled HTTPS for clients on 6984 false
tls.existingSecret A Secret with the HTTPS certificate; empty issues one ""
tls.httpEnabled Plain HTTP on 5984 in the Service and NetworkPolicy true
tls.extraDnsNames Names the HTTPS certificate carries besides the Service’s []

Storage, resources and scheduling

Parameter Description Default
persistence.size / .storageClass Volume size per node; StorageClass 10Gi / the default class
persistentVolumeClaimRetentionPolicy.whenScaled / .whenDeleted Volumes on resizing; on uninstall Delete / Retain
encryption.volume.enabled LUKS-encrypted volumes, by Longhorn false
encryption.volume.passphrase / .existingSecret The passphrase, or a Secret with it; empty generates one ""
encryption.volume.storageClass.* reclaimPolicy, numberOfReplicas, dataLocality of the encrypted class Delete, 3, disabled
resources Requests and limits of each node 250m/512Mi, limits 1000m/1Gi
jobResources Of the init container, the Jobs and the test 10m/32Mi, limits 500m/256Mi
podDisruptionBudget.enabled / .maxUnavailable A budget for node drains true / 1
topologySpread.topologyKey / .whenUnsatisfiable How nodes are spread kubernetes.io/hostname / DoNotSchedule
nodeSelector, tolerations, priorityClassName Scheduling none
terminationGracePeriodSeconds   60
maxOpenFiles Files each node keeps open 65536

Databases, users and resizing

Parameter Description Default
databases Databases with their members and admins []
users Users, each with a Secret holding its password []
resize.timeoutSeconds How long a resize waits for copies to fill 1800

Backups and restore

Parameter Description Default
backup.enabled Scheduled backups false
backup.layout legacy (the settings below) or platform (one Secret, <prefix>/<clusterRegion>) legacy
backup.s3.existingSecret / .prefix The platform layout’s Secret and instance prefix ""
backup.bucket / .basePath Where backups are kept ""
backup.endpoint / .region S3-compatible storage other than Amazon S3; region "" / us-east-1
backup.existingSecret A Secret with access-key and secret-key ""
backup.schedule / .timezone When backups are taken 0 2 * * * / UTC
backup.retentionDays Days a backup is kept 30
backup.exclude Databases not backed up []
restore.enabled Restore a backup at install false
restore.backup The backup’s directory, or latest latest
restore.bucket / .basePath Where to restore from, when not backup’s ""
restore.prefix The instance prefix to restore from in the platform layout backup.s3.prefix
restore.databases The databases to restore; empty for all []

Access and metrics

Parameter Description Default
exposure.defaultHostname / .domain Compose host names as [ui-\|repl-]<defaultHostname>.<clusterRegion>.<domain> ""
exposure.certIssuer A ClusterIssuer for every host’s certificate ""
exposure.external.enabled The HTTP API on a host name through Kong false
exposure.external.host / .tlsSecret Host name (or composed); its TLS Secret ""
exposure.external.ipAllowlist Addresses admitted; required []
exposure.external.ingressClassName / .ingressNamespace The ingress controller, for every host kong / kong
client.enabled Fauxton, at /_utils/ on its own host false
client.host / .tlsSecret Its host name (or composed); its TLS Secret ""
client.ipAllowlist Addresses admitted; required, never empty []
client.expiresAt When the platform switches it off; an annotation ""
replication.peers Peers to pull from: name, url, existingSecret []
replication.databases Databases replicated; empty for every database in databases []
replication.user.existingSecret This release’s replication user, which peers pull as ""
replication.reconcile Run the replication Job with no peers, to delete removed peers’ documents false
replication.verifyPeer Verify peers’ certificates true
replication.ingress.enabled The host peers pull from false
replication.ingress.host / .tlsSecret Its host name (or composed); its TLS Secret ""
replication.ingress.ipAllowlist The peers’ addresses; required, never empty []
replication.ingress.requestsPerMinute Requests a minute from each address 1200
networkPolicy.enabled The NetworkPolicy true
networkPolicy.from Further sources admitted to the HTTP API []
networkPolicy.metrics.from Sources admitted to the metrics port; empty admits every source []
networkPolicy.egress.enabled / .https / .to Restrict the nodes’ outgoing traffic; HTTPS anywhere; further destinations false / true / []
metrics.enabled The Prometheus port true
metrics.annotations prometheus.io annotations on the Pods true
metrics.serviceMonitor.enabled A ServiceMonitor false