MDDB Deployment Guide

Production Deployment

System Requirements

Minimum:

  • CPU: 1 core
  • RAM: 512 MB
  • Disk: 1 GB + data storage
  • OS: Linux, macOS, or Windows

Recommended:

  • CPU: 2+ cores
  • RAM: 2 GB
  • Disk: SSD with 10 GB+ free space
  • OS: Linux (Ubuntu 20.04+, Debian 11+, RHEL 8+)

Building for Production

# Build optimized binary
cd services/mddbd
go build -ldflags="-s -w" -o mddbd .

# Or use Make
make build

# Cross-compile for Linux
GOOS=linux GOARCH=amd64 go build -o mddbd-linux .

Systemd Service

Create /etc/systemd/system/mddb.service:

[Unit]
Description=MDDB Markdown Database Server
After=network.target

[Service]
Type=simple
User=mddb
Group=mddb
WorkingDirectory=/opt/mddb
Environment="MDDB_ADDR=:11023"
Environment="MDDB_MODE=wr"
Environment="MDDB_PATH=/var/lib/mddb/mddb.db"
ExecStart=/opt/mddb/mddbd
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=mddb

# Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/mddb

[Install]
WantedBy=multi-user.target

Enable and start:

# Create user and directories
sudo useradd -r -s /bin/false mddb
sudo mkdir -p /opt/mddb /var/lib/mddb
sudo chown mddb:mddb /var/lib/mddb

# Copy binary
sudo cp mddbd /opt/mddb/
sudo chown mddb:mddb /opt/mddb/mddbd
sudo chmod +x /opt/mddb/mddbd

# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable mddb
sudo systemctl start mddb

# Check status
sudo systemctl status mddb

Docker Deployment

Create Dockerfile:

FROM golang:1.27-alpine AS builder

WORKDIR /build
COPY services/mddbd/go.mod services/mddbd/go.sum ./
RUN go mod download

COPY services/mddbd/ ./
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o mddbd .

FROM alpine:latest

RUN apk --no-cache add ca-certificates
RUN addgroup -S mddb && adduser -S mddb -G mddb

WORKDIR /app
COPY --from=builder /build/mddbd .

RUN mkdir -p /data && chown mddb:mddb /data
USER mddb

EXPOSE 11023
VOLUME ["/data"]

ENV MDDB_ADDR=":11023"
ENV MDDB_MODE="wr"
ENV MDDB_PATH="/data/mddb.db"

CMD ["./mddbd"]

Build and run:

# Build image
docker build -t mddb:latest .

# Run container
docker run -d \
  --name mddb \
  -p 11023:11023 \
  -v mddb-data:/data \
  --restart unless-stopped \
  mddb:latest

# Check logs
docker logs -f mddb

Docker Compose

Create docker-compose.yml:

services:
  mddb:
    build: .
    container_name: mddb
    ports:
      - "11023:11023"
    volumes:
      - mddb-data:/data
    environment:
      - MDDB_ADDR=:11023
      - MDDB_MODE=wr
      - MDDB_PATH=/data/mddb.db
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:11023/v1/search"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

volumes:
  mddb-data:

Run:

docker-compose up -d

Reverse Proxy Setup

Nginx

upstream mddb {
    server localhost:11023;
}

server {
    listen 80;
    server_name mddb.example.com;

    # Redirect to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name mddb.example.com;

    ssl_certificate /etc/letsencrypt/live/mddb.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mddb.example.com/privkey.pem;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=mddb_limit:10m rate=10r/s;
    limit_req zone=mddb_limit burst=20 nodelay;

    location / {
        proxy_pass http://mddb;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

Caddy

mddb.example.com {
    reverse_proxy localhost:11023
    
    # Rate limiting
    rate_limit {
        zone dynamic {
            key {remote_host}
            events 100
            window 1m
        }
    }
}

Persistence Guarantees

What MDDB promises about a write that returned success, and what it does not.

Surface On acknowledgement the write is… Survives kill -9?
POST /v1/add (REST) committed to bbolt and fsynced Yes
gRPC Add committed to bbolt and fsynced Yes
Batch (POST /v1/batch, gRPC batch) committed in one transaction and fsynced Yes
MCP write tools same path as REST Yes
FTS index for the above written in the same batch transaction Yes
Async bulk job (POST /v1/bulk) queued, not written — the payload lives in memory until the worker reaches it No — an in-flight job is marked failed on restart
Embeddings queued to a background worker after the document commits No — re-embed by reindexing; the document itself is safe

bbolt fsyncs on every commit, so durability does not depend on a graceful shutdown. Verified rather than assumed: a document written over REST, then kill -9 with no shutdown hooks, is present — content and full-text index intact — after the process restarts.

NoFreelistSync is enabled, which skips syncing the free-page list. That is a recovery-time trade, not a durability one: the freelist is rebuilt from the page tree when the file is opened.

What breaks the guarantee

The engine fsyncs to whatever storage it was given. If that storage does not outlive the process, neither does the data — which is what happens when a container runs without a volume mounted at the data directory.

MDDB checks this at startup and says so, rather than accepting writes quietly:

{"level":"WARN","msg":"persistence warning","code":"ephemeral_storage",
 "detail":"the data directory is on tmpfs: an in-memory filesystem — its contents are lost on restart",
 "path":"/data"}

GET /health reports the same, so a probe or dashboard can see it:

{
  "status": "healthy",
  "durable": false,
  "persistence": {
    "path": "/data",
    "filesystem": "overlay",
    "ephemeral": true,
    "writable": true,
    "warnings": [{"code": "ephemeral_storage", "detail": "…mount a volume at this path"}]
  }
}
Warning Meaning
ephemeral_storage The data directory is on tmpfs, ramfs or a container's own overlay layer. Mount a volume.
not_writable The directory cannot be written to — checked by creating a file, not by reading mode bits, since a read-only mount or a user mismatch looks writable otherwise.
low_disk_space Free space is below MDDB_DISK_MIN_FREE (default 100 MB). Set it to 0 to disable the check.

Backup Strategy

Automated Backups

#!/bin/bash
# /opt/mddb/backup.sh

BACKUP_DIR="/backups/mddb"
RETENTION_DAYS=30
DATE=$(date +%Y-%m-%d-%H%M%S)

# Create backup directory
mkdir -p ${BACKUP_DIR}

# Create backup
curl -s "http://localhost:11023/v1/backup?to=${BACKUP_DIR}/backup-${DATE}.db"

# Compress old backups
find ${BACKUP_DIR} -name "backup-*.db" -mtime +1 -exec gzip {} \;

# Remove old backups
find ${BACKUP_DIR} -name "backup-*.db.gz" -mtime +${RETENTION_DAYS} -delete

# Log
echo "$(date): Backup completed - backup-${DATE}.db" >> /var/log/mddb-backup.log

Add to crontab:

# Daily backup at 2 AM
0 2 * * * /opt/mddb/backup.sh

Offsite Backup

#!/bin/bash
# Sync to S3
aws s3 sync /backups/mddb s3://my-bucket/mddb-backups/ \
  --storage-class STANDARD_IA \
  --exclude "*" \
  --include "backup-*.db.gz"

# Or use rsync
rsync -avz /backups/mddb/ backup-server:/backups/mddb/

Monitoring

Health Check Script

#!/bin/bash
# /opt/mddb/healthcheck.sh

ENDPOINT="http://localhost:11023/v1/search"
TIMEOUT=5

response=$(curl -s -o /dev/null -w "%{http_code}" --max-time ${TIMEOUT} \
  -X POST ${ENDPOINT} \
  -H 'Content-Type: application/json' \
  -d '{"collection":"_health","limit":1}')

if [ "$response" = "200" ] || [ "$response" = "400" ]; then
    echo "OK"
    exit 0
else
    echo "FAIL: HTTP $response"
    exit 1
fi

Prometheus Metrics (Future)

# prometheus.yml
scrape_configs:
  - job_name: 'mddb'
    static_configs:
      - targets: ['localhost:11023']

Performance Tuning

OS Tuning

# Increase file descriptors
echo "mddb soft nofile 65536" >> /etc/security/limits.conf
echo "mddb hard nofile 65536" >> /etc/security/limits.conf

# Kernel parameters
cat >> /etc/sysctl.conf <<EOF
net.core.somaxconn = 1024
net.ipv4.tcp_max_syn_backlog = 2048
EOF

sysctl -p

Database Optimization

# Regular maintenance
# Truncate old revisions weekly
curl -X POST http://localhost:11023/v1/truncate \
  -H 'Content-Type: application/json' \
  -d '{"collection":"blog","keepRevs":10,"dropCache":true}'

Security Hardening

Firewall Rules

# UFW
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

# iptables
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -j DROP

API Authentication (Nginx)

location / {
    # Basic auth
    auth_basic "MDDB API";
    auth_basic_user_file /etc/nginx/.htpasswd;
    
    proxy_pass http://mddb;
}

Create password file:

htpasswd -c /etc/nginx/.htpasswd admin

Troubleshooting

Check Logs

# Systemd
sudo journalctl -u mddb -f

# Docker
docker logs -f mddb

# File logs (if configured)
tail -f /var/log/mddb.log

Common Issues

Database locked:

# Check for multiple instances
ps aux | grep mddbd

# Stop all instances
sudo systemctl stop mddb

RSS grows under sustained load — is that a leak?

Usually not, and the way to tell is to stop looking at RSS.

MDDB stores its data in a bbolt file that the process memory-maps. Those pages count towards RSS and grow with the database, but they are file-backed and reclaimable: the kernel drops them under pressure and reads them back from disk. A process holding a 160 MB database will show a couple of hundred MB of RSS and be using almost none of it as heap.

What distinguishes the two is memoryHeap — Go's HeapInuse, which counts only what the program has allocated and not returned:

curl -s http://localhost:11023/v1/system/info | jq '{memoryHeap, memorySystem, numGoroutines}'

Measured over 45 minutes of mixed traffic — 1.26 million operations of adds, updates, reads, keyword and hybrid search — against a database that grew to 160 MB:

Start After 45 min
RSS 190 MB 247 MB
Heap in use 43 MB 47 MB
Goroutines 38 42

RSS rose 57 MB and flattened; the heap did not move outside its normal oscillation, and the largest single allocator grew during the first half and shrank during the second — a buffer reaching steady state. The reproduction is tools/bench/soak if you want to run it against your own workload.

Watch the heap and the goroutine count, not RSS. A heap that climbs across successive readings hours apart, or a goroutine count that only goes up, is worth reporting. RSS tracking the database file is the design working.

If the database itself is larger than you expect, old revisions are the usual reason:

ls -lh /var/lib/mddb/mddb.db

curl -X POST http://localhost:11023/v1/truncate \
  -H 'Content-Type: application/json' \
  -d '{"collection":"blog","keepRevs":5}'

Slow queries:

  • Add metadata indices
  • Use pagination
  • Optimize filters
  • Consider caching layer

Scaling

Vertical Scaling

  • Increase CPU/RAM
  • Use SSD storage
  • Optimize OS settings

Horizontal Scaling

  • Read replicas (file-based replication)
  • Load balancer for reads
  • Single write instance
  • Consider sharding by collection

Read Replicas

# On primary server
0 */6 * * * curl "http://localhost:11023/v1/backup?to=/replication/mddb.db"

# On replica servers
*/5 * * * * rsync -avz primary:/replication/mddb.db /var/lib/mddb/mddb.db

Run replicas in read-only mode:

MDDB_MODE="read" MDDB_ADDR=":11024" ./mddbd