#!/bin/bash ################################################################################ # Script Name: dokploy-exporter.sh # Version: 1.0 # Description: Prometheus exporter for Dokploy PaaS providing operational # metrics via the Dokploy API — project counts, application status, # database breakdown by type, compose services, server info, # and API health # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # # Prerequisites: # - Dokploy instance running with API enabled # - Dokploy API key (generate in Settings → API) # - curl for API calls # - jq for JSON parsing # - netcat (nc) for HTTP mode # # Usage: # # Output to stdout # ./dokploy-exporter.sh # # # HTTP server mode # ./dokploy-exporter.sh --http -p 9197 # # # Textfile collector mode # ./dokploy-exporter.sh --textfile # # # Custom API token and URL # ./dokploy-exporter.sh --api-url http://dokploy.local:3000 --api-token mytoken # # Metrics Exported: # - dokploy_up - API reachability (1=up, 0=down) # - dokploy_info{version} - Dokploy version info # - dokploy_projects_total - Total project count # - dokploy_applications_total - Total applications across all projects # - dokploy_applications_by_status{status} - Applications by status # - dokploy_compose_services_total - Total Docker Compose services # - dokploy_databases_total - Total managed databases # - dokploy_databases_by_type{type} - Databases by type # - dokploy_servers_total - Total servers managed # - dokploy_exporter_duration_seconds - Script execution time # - dokploy_exporter_last_run_timestamp - Last run timestamp # # Configuration: # Default HTTP port: 9197 # Default API URL: http://localhost:3000 # Textfile directory: /var/lib/node_exporter # ################################################################################ set -euo pipefail # ============================================================================ # CONFIGURATION VARIABLES # ============================================================================ TEXTFILE_DIR="/var/lib/node_exporter" OUTPUT_FILE="" HTTP_MODE=false HTTP_PORT=9197 API_URL="http://localhost:3000" API_TOKEN="" # ============================================================================ # HELPER FUNCTIONS # ============================================================================ show_usage() { cat <&2; exit 1 ;; esac done } # Check prerequisites # Returns: 0 if OK, 1 if error check_prerequisites() { if ! command -v curl >/dev/null 2>&1; then echo "ERROR: curl not found" >&2 return 1 fi if ! command -v jq >/dev/null 2>&1; then echo "ERROR: jq not found (required for JSON parsing)" >&2 return 1 fi if [ -z "$API_TOKEN" ]; then echo "ERROR: --api-token is required" >&2 return 1 fi return 0 } # Escape special characters in Prometheus label values # Args: $1 - string to escape # Returns: escaped string safe for Prometheus labels prom_escape() { local val="$1" val="${val//\\/\\\\}" val="${val//\"/\\\"}" val="${val//$'\n'/}" echo "$val" } # Make an authenticated API call # Args: $1 - API endpoint path (e.g., /api/project.all) # Returns: JSON response on stdout api_call() { local endpoint="$1" curl -s -X GET \ -H "x-api-key: ${API_TOKEN}" \ -H "Accept: application/json" \ "${API_URL}${endpoint}" 2>/dev/null } # ============================================================================ # METRIC GENERATION # ============================================================================ # Generate all Prometheus metrics # Returns: Prometheus text format metrics on stdout generate_metrics() { local script_start script_start=$(date +%s) # Check prerequisites if ! check_prerequisites; then cat </dev/null) if [ -z "$health_response" ]; then cat </dev/null) if [ "$is_error" = "yes" ]; then cat </dev/null) dokploy_version="${dokploy_version:-unknown}" cat </dev/null) total_projects=${total_projects:-0} # Count applications across all projects total_apps=$(echo "$projects_response" | jq '[.[] | (.applications // []) | length] | add // 0' 2>/dev/null) total_apps=${total_apps:-0} # Count applications by status done_apps=$(echo "$projects_response" | jq '[.[] | (.applications // [])[] | select(.applicationStatus == "done")] | length' 2>/dev/null) idle_apps=$(echo "$projects_response" | jq '[.[] | (.applications // [])[] | select(.applicationStatus == "idle")] | length' 2>/dev/null) running_apps=$(echo "$projects_response" | jq '[.[] | (.applications // [])[] | select(.applicationStatus == "running")] | length' 2>/dev/null) error_apps=$(echo "$projects_response" | jq '[.[] | (.applications // [])[] | select(.applicationStatus == "error")] | length' 2>/dev/null) done_apps=${done_apps:-0} idle_apps=${idle_apps:-0} running_apps=${running_apps:-0} error_apps=${error_apps:-0} # Count compose services across all projects total_compose=$(echo "$projects_response" | jq '[.[] | (.compose // []) | length] | add // 0' 2>/dev/null) total_compose=${total_compose:-0} # Count databases by type across all projects pg_count=$(echo "$projects_response" | jq '[.[] | (.postgres // []) | length] | add // 0' 2>/dev/null) mysql_count=$(echo "$projects_response" | jq '[.[] | (.mysql // []) | length] | add // 0' 2>/dev/null) mariadb_count=$(echo "$projects_response" | jq '[.[] | (.mariadb // []) | length] | add // 0' 2>/dev/null) mongo_count=$(echo "$projects_response" | jq '[.[] | (.mongo // []) | length] | add // 0' 2>/dev/null) redis_count=$(echo "$projects_response" | jq '[.[] | (.redis // []) | length] | add // 0' 2>/dev/null) pg_count=${pg_count:-0} mysql_count=${mysql_count:-0} mariadb_count=${mariadb_count:-0} mongo_count=${mongo_count:-0} redis_count=${redis_count:-0} total_databases=$((pg_count + mysql_count + mariadb_count + mongo_count + redis_count)) fi cat </dev/null) total_servers=${total_servers:-0} fi cat <&2 if ! command -v nc >/dev/null 2>&1; then echo "ERROR: netcat (nc) required for HTTP mode" >&2 exit 1 fi # Infinite loop accepting HTTP requests while true; do { read -r request # Check if request is for /metrics endpoint if [[ "$request" =~ ^GET\ /metrics ]]; then echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\n\r" generate_metrics else # Serve HTML landing page for other requests echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r" cat < Dokploy Exporter v1.0

Dokploy Prometheus Exporter v1.0

Metrics

Operational metrics from the Dokploy API.

EOF fi } | nc -l -p "$HTTP_PORT" -q 1 2>/dev/null done } # ============================================================================ # MAIN EXECUTION # ============================================================================ # Main entry point - routes to appropriate output mode main() { parse_args "$@" if [ "$HTTP_MODE" = true ]; then # Run HTTP server (blocks until killed) run_http_server elif [ -n "$OUTPUT_FILE" ]; then # Textfile collector mode: write atomically using temp file local output_dir output_dir="$(dirname "$OUTPUT_FILE")" mkdir -p "$output_dir" # Create temp file in SAME directory for atomic rename (same filesystem) local temp_file temp_file=$(mktemp "${output_dir}/.dokploy_metrics.XXXXXX") # Generate metrics to temp file if ! generate_metrics > "$temp_file" 2>/dev/null; then rm -f "$temp_file" echo "ERROR: Failed to generate metrics" >&2 exit 1 fi # Validate: file must exist, have content local file_lines file_lines=$(wc -l < "$temp_file" 2>/dev/null || echo 0) if [ "$file_lines" -lt 10 ]; then rm -f "$temp_file" echo "ERROR: Metrics file too small ($file_lines lines), keeping previous" >&2 exit 1 fi # Set permissions before move chmod 644 "$temp_file" # Atomic rename - no gap where file is missing mv -f "$temp_file" "$OUTPUT_FILE" echo "Metrics written to $OUTPUT_FILE ($file_lines lines)" >&2 else # Default: output to stdout generate_metrics fi } # Execute main function with all script arguments main "$@"