Skip to main content

Overview

Proper monitoring helps you detect issues early, maintain uptime, and troubleshoot problems efficiently. This guide covers health checks, logging, metrics, and alerting for self-hosted Documenso.

Health Check Endpoints

Documenso provides built-in health check endpoints for monitoring.

Main Health Check

The primary health endpoint is located at /api/health:
curl http://localhost:3000/api/health
Response when healthy:
{
  "status": "ok",
  "timestamp": "2026-03-04T12:00:00.000Z",
  "checks": {
    "database": { "status": "ok" },
    "certificate": { "status": "ok" }
  }
}
Possible statuses:
  • ok: All systems operational
  • warning: Non-critical issue (e.g., certificate missing but app functional)
  • error: Critical issue (e.g., database connection failed)
The health endpoint returns HTTP 200 for “ok” and “warning”, HTTP 500 for “error”. This makes it compatible with most monitoring tools and load balancers.

Health Check Components

The health check verifies (source: /home/daytona/workspace/source/apps/remix/app/routes/api+/health.ts:6):
  1. Database Connectivity: Runs SELECT 1 query to verify PostgreSQL connection
  2. Certificate Status: Checks if signing certificate is available and readable
Example responses:
// Certificate missing (warning)
{
  "status": "warning",
  "timestamp": "2026-03-04T12:00:00.000Z",
  "checks": {
    "database": { "status": "ok" },
    "certificate": { "status": "warning" }
  }
}

// Database connection failed (error)
{
  "status": "error",
  "timestamp": "2026-03-04T12:00:00.000Z",
  "checks": {
    "database": { "status": "error" },
    "certificate": { "status": "ok" }
  }
}

Basic Monitoring Setup

Uptime Monitoring

Use external services or self-hosted solutions:

UptimeRobot (SaaS)

  1. Sign up at uptimerobot.com
  2. Create HTTP monitor:
    • URL: https://your-domain.com/api/health
    • Interval: 5 minutes
    • Alert contacts: Email, SMS, Slack

Self-Hosted Uptime Kuma

# Deploy Uptime Kuma with Docker
docker run -d \
  --name uptime-kuma \
  -p 3001:3001 \
  -v uptime-kuma:/app/data \
  --restart unless-stopped \
  louislam/uptime-kuma:1

# Access at http://localhost:3001
Configuration:
  • Monitor Type: HTTP(s)
  • URL: http://documenso:3000/api/health
  • Heartbeat Interval: 60 seconds
  • Accepted Status Codes: 200

Simple Shell Script Monitor

Create a basic monitoring script:
#!/bin/bash
# monitor-documenso.sh

HEALTH_URL="http://localhost:3000/api/health"
ALERT_EMAIL="admin@example.com"
LOG_FILE="/var/log/documenso-monitor.log"

log() {
  echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# Check health endpoint
RESPONSE=$(curl -s -w "\n%{http_code}" "$HEALTH_URL")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')

if [ "$HTTP_CODE" != "200" ]; then
  log "ERROR: Health check failed with HTTP $HTTP_CODE"
  log "Response: $BODY"
  
  # Send alert email
  echo "Documenso health check failed. Response: $BODY" | \
    mail -s "ALERT: Documenso Down" "$ALERT_EMAIL"
  
  exit 1
fi

# Check if status is 'ok'
STATUS=$(echo "$BODY" | jq -r '.status')
if [ "$STATUS" != "ok" ]; then
  log "WARNING: Health check returned status: $STATUS"
  log "Response: $BODY"
  
  # Send warning email
  echo "Documenso health check warning. Status: $STATUS, Response: $BODY" | \
    mail -s "WARNING: Documenso Issue" "$ALERT_EMAIL"
  
  exit 0
fi

log "Health check passed"
exit 0
Run via cron every 5 minutes:
*/5 * * * * /path/to/monitor-documenso.sh

Logging

Documenso uses Pino for structured logging.

Log Configuration

Configure logging via environment variables (.env):
# Log to file instead of stdout
NEXT_PRIVATE_LOGGER_FILE_PATH="/var/log/documenso/app.log"
When NEXT_PRIVATE_LOGGER_FILE_PATH is set:
  • Logs write to the specified file instead of stdout
  • Parent directories are created automatically
  • Logs are in JSON format for structured querying
In development (NODE_ENV !== production), logs use pretty-print format. In production, logs are JSON unless NEXT_PRIVATE_LOGGER_FILE_PATH is set.

Docker Logging

View Logs

# View recent logs
docker compose logs documenso

# Follow logs in real-time
docker compose logs -f documenso

# View last 100 lines
docker compose logs --tail=100 documenso

# Show timestamps
docker compose logs -t documenso

# Filter by time
docker compose logs --since 2h documenso  # Last 2 hours
docker compose logs --since 2026-03-04T10:00:00 documenso

Configure Log Rotation

Prevent logs from consuming all disk space:
# docker-compose.yml
services:
  documenso:
    image: documenso/documenso:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"      # Max size per log file
        max-file: "3"        # Keep 3 files (30MB total)
        compress: "true"     # Compress rotated logs
Or configure globally in /etc/docker/daemon.json:
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "compress": "true"
  }
}
Restart Docker:
sudo systemctl restart docker

Centralized Logging

Send logs to a centralized logging system: Loki + Grafana:
# docker-compose.yml
services:
  documenso:
    logging:
      driver: loki
      options:
        loki-url: "http://loki:3100/loki/api/v1/push"
        loki-batch-size: "400"
Syslog:
services:
  documenso:
    logging:
      driver: syslog
      options:
        syslog-address: "tcp://192.168.1.10:514"
        tag: "documenso"

Parse and Query Logs

Since logs are JSON, use jq to parse:
# View all errors
docker compose logs documenso | grep '"level":50'

# Extract specific fields
docker compose logs documenso | jq -r '.msg'

# Filter by user ID
docker compose logs documenso | jq 'select(.userId == 123)'

# Count errors by type
docker compose logs documenso | \
  jq -r 'select(.level == 50) | .err.code' | \
  sort | uniq -c

Application Log Levels

Pino log levels:
LevelValueDescription
trace10Very detailed debug info
debug20Debugging information
info30General information (default)
warn40Warning messages
error50Error messages
fatal60Fatal errors (app crash)
Configure log level:
# In .env or environment
LOG_LEVEL="info"  # Options: trace, debug, info, warn, error, fatal

Database Monitoring

PostgreSQL Logs

Enable and configure PostgreSQL logging:
# View PostgreSQL logs
docker compose logs documenso-db

# Configure logging in postgresql.conf
logging_collector = on
log_directory = 'pg_log'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_statement = 'all'          # Log all queries (use 'ddl' for production)
log_duration = on              # Log query duration
log_min_duration_statement = 1000  # Log queries slower than 1 second

Connection Monitoring

Monitor active database connections:
-- View active connections
SELECT 
  pid,
  usename,
  application_name,
  client_addr,
  state,
  query_start,
  state_change
FROM pg_stat_activity
WHERE datname = 'documenso';

-- Count connections by state
SELECT state, COUNT(*)
FROM pg_stat_activity
WHERE datname = 'documenso'
GROUP BY state;

Slow Query Monitoring

-- Enable pg_stat_statements extension
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- Find slow queries
SELECT 
  query,
  calls,
  total_exec_time,
  mean_exec_time,
  max_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

Database Size Monitoring

-- Check database size
SELECT 
  pg_size_pretty(pg_database_size('documenso')) as db_size;

-- Table sizes
SELECT 
  tablename,
  pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 10;

Metrics and Observability

Prometheus Metrics

While Documenso doesn’t expose Prometheus metrics natively, you can add them: PostgreSQL Exporter:
# docker-compose.yml
services:
  postgres-exporter:
    image: prometheuscommunity/postgres-exporter
    environment:
      DATA_SOURCE_NAME: "postgresql://documenso:password@documenso-db:5432/documenso?sslmode=disable"
    ports:
      - "9187:9187"
Node Exporter (system metrics):
services:
  node-exporter:
    image: prom/node-exporter
    ports:
      - "9100:9100"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'

Grafana Dashboard

Create a Grafana dashboard with key metrics:
  1. System Metrics:
    • CPU usage
    • Memory usage
    • Disk I/O
    • Network traffic
  2. Database Metrics:
    • Active connections
    • Query performance
    • Database size
    • Replication lag (if using replicas)
  3. Application Metrics:
    • HTTP response times
    • Error rates
    • Request volume
  4. Business Metrics:
    • Documents signed per hour
    • Active users
    • API calls

Alerting

Alert Rules

Set up alerts for critical issues:
Condition: Health check returns non-200 status for 2 consecutive checksAlert: Page on-call engineer immediatelyAction: Check logs, restart service if needed
Condition: All database connections in use for >1 minuteAlert: Email + Slack notificationAction: Check for long-running queries, scale connections if needed
Condition: Disk usage >90%Alert: Email + Slack notificationAction: Clean up logs, expand storage, archive old data
Condition: >10 errors per minute for 5 minutesAlert: Slack notificationAction: Check logs for patterns, investigate root cause
Condition: SSL or signing certificate expires in <30 daysAlert: Email notificationAction: Renew certificate
Condition: Average response time >2 seconds for 5 minutesAlert: Slack notificationAction: Check database performance, review slow queries

Alert Configuration Examples

Alertmanager (Prometheus):
# alertmanager.yml
route:
  group_by: ['alertname']
  receiver: 'team-email'
  routes:
    - match:
        severity: critical
      receiver: 'pagerduty'

receivers:
  - name: 'team-email'
    email_configs:
      - to: 'team@example.com'
        from: 'alerts@example.com'
  
  - name: 'pagerduty'
    pagerduty_configs:
      - service_key: 'your-pagerduty-key'
Simple Shell Script Alert:
#!/bin/bash
# alert-on-error.sh

LOG_FILE="/var/log/documenso/app.log"
ERROR_THRESHOLD=10
TIME_WINDOW=300  # 5 minutes in seconds

# Count errors in last 5 minutes
ERROR_COUNT=$(tail -n 1000 "$LOG_FILE" | \
  jq -r 'select(.level >= 50 and (.time | tonumber) > (now - '$TIME_WINDOW'))' | \
  wc -l)

if [ "$ERROR_COUNT" -ge "$ERROR_THRESHOLD" ]; then
  echo "High error rate: $ERROR_COUNT errors in last 5 minutes" | \
    mail -s "ALERT: Documenso High Error Rate" admin@example.com
fi

Performance Monitoring

Response Time Monitoring

Monitor endpoint response times:
#!/bin/bash
# monitor-response-time.sh

URL="http://localhost:3000/api/health"

for i in {1..10}; do
  RESPONSE_TIME=$(curl -o /dev/null -s -w '%{time_total}' "$URL")
  echo "Response time: ${RESPONSE_TIME}s"
  sleep 1
done

Resource Usage

Monitor Docker container resources:
# Real-time resource usage
docker stats documenso

# Get stats in JSON format
docker stats documenso --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
Set resource limits:
# docker-compose.yml
services:
  documenso:
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G
        reservations:
          cpus: '1'
          memory: 1G

Monitoring Best Practices

Focus on:
  • Availability: Is the service up?
  • Performance: How fast is it responding?
  • Errors: Are errors occurring?
  • Saturation: Are resources near capacity?
This is the “Four Golden Signals” of monitoring.
Base alert thresholds on:
  • Historical data (what’s normal for your instance)
  • Business requirements (acceptable downtime)
  • User experience (response time expectations)
Avoid arbitrary thresholds that cause alert fatigue.
  • Internal monitoring: Health checks from within your network
  • External monitoring: Third-party service checking from outside
  • Synthetic monitoring: Automated tests simulating user actions
For each alert, document:
  • What the alert means
  • How to investigate
  • Common causes and fixes
  • Escalation procedures
Store runbooks in a wiki or repository.
Regularly review:
  • Alert frequency (too many? too few?)
  • False positive rate
  • Time to detection vs. time to resolution
  • Monitoring coverage gaps

Complete Monitoring Stack Example

Full monitoring setup with Prometheus, Grafana, and Loki:
# docker-compose.monitoring.yml
version: '3.8'

services:
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    ports:
      - "9090:9090"
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'

  grafana:
    image: grafana/grafana
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - "3001:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin

  loki:
    image: grafana/loki
    ports:
      - "3100:3100"
    volumes:
      - loki-data:/loki
    command: -config.file=/etc/loki/local-config.yaml

  promtail:
    image: grafana/promtail
    volumes:
      - /var/log:/var/log
      - ./promtail-config.yml:/etc/promtail/config.yml
    command: -config.file=/etc/promtail/config.yml

  postgres-exporter:
    image: prometheuscommunity/postgres-exporter
    environment:
      DATA_SOURCE_NAME: "postgresql://documenso:password@documenso-db:5432/documenso?sslmode=disable"
    ports:
      - "9187:9187"

  node-exporter:
    image: prom/node-exporter
    ports:
      - "9100:9100"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'

volumes:
  prometheus-data:
  grafana-data:
  loki-data:

Troubleshooting

Common issues and how to diagnose them

Updates

Keep your instance up to date

Backups

Backup and restore procedures

Environment Variables

Configure logging and monitoring settings