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):
Database Connectivity : Runs SELECT 1 query to verify PostgreSQL connection
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)
Sign up at uptimerobot.com
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
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:
Level Value Description trace 10 Very detailed debug info debug 20 Debugging information info 30 General information (default) warn 40 Warning messages error 50 Error messages fatal 60 Fatal 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:
System Metrics :
CPU usage
Memory usage
Disk I/O
Network traffic
Database Metrics :
Active connections
Query performance
Database size
Replication lag (if using replicas)
Application Metrics :
HTTP response times
Error rates
Request volume
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
Database Connection Pool Exhausted
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
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.
Set meaningful thresholds
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.
Implement redundant monitoring
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