diff --git a/docker/container-snippet-docker-compose.yml b/docker/container-snippet-docker-compose.yml
new file mode 100644
index 0000000..fdc7149
--- /dev/null
+++ b/docker/container-snippet-docker-compose.yml
@@ -0,0 +1,14 @@
+# --- add this snippet to your existing docker-compose.yml file ---
+# --- this will define a small container that will serve the maintenance page you define in /maintenance/index.html---
+# --- this assumes you have a docker network by the name nginxproxymanager ---
+# --- this container must have access to the same docker network as your nginx proxy manager container to function ---
+
+ # --- Maintenance page container ---
+ maintenance:
+ image: nginx:alpine
+ restart: unless-stopped
+ volumes:
+ - ./maintenance:/usr/share/nginx/html:ro
+ networks:
+ - nginxproxymanager
+ # ----------------------------------
\ No newline at end of file
diff --git a/docker/example-docker-compose.yml b/docker/example-docker-compose.yml
new file mode 100644
index 0000000..d3acb1a
--- /dev/null
+++ b/docker/example-docker-compose.yml
@@ -0,0 +1,54 @@
+services:
+ app:
+ build: .
+ image: jc21/nginx-proxy-manager:custom
+ restart: unless-stopped
+ ports:
+ - '80:80' # Public HTTP Port
+ - '443:443' # Public HTTPS Port
+ - '81:81' # Admin Web Port
+ environment:
+ DB_MYSQL_HOST: 'db'
+ DB_MYSQL_PORT: '3306'
+ DB_MYSQL_USER: 'npm'
+ DB_MYSQL_PASSWORD: 'your_secure_password' # Replace with your secure password
+ DB_MYSQL_NAME: 'npm'
+ TZ: 'your_timezone_here' # Set the timezone
+ # Uncomment this if IPv6 is not enabled on your host
+ # DISABLE_IPV6: 'true'
+ volumes:
+ - ./data:/data
+ - ./letsencrypt:/etc/letsencrypt
+ - /root/nginx-proxy-manager/logs:/data/logs # Bind host logs directory
+ depends_on:
+ - db
+ networks:
+ - nginxproxymanager
+
+ db:
+ image: 'jc21/mariadb-aria:latest'
+ restart: unless-stopped
+ environment:
+ MYSQL_ROOT_PASSWORD: 'your_secure_root_password' # Replace with your secure root password
+ MYSQL_DATABASE: 'npm'
+ MYSQL_USER: 'npm'
+ MYSQL_PASSWORD: 'your_secure_password' # Replace with your secure password
+ MARIADB_AUTO_UPGRADE: '1'
+ volumes:
+ - ./mysql:/var/lib/mysql
+ networks:
+ - nginxproxymanager
+
+ # --- Maintenance page container ---
+ maintenance:
+ image: nginx:alpine
+ restart: unless-stopped
+ volumes:
+ - ./maintenance:/usr/share/nginx/html:ro
+ networks:
+ - nginxproxymanager
+ # ----------------------------------
+
+networks:
+ nginxproxymanager:
+ external: true
diff --git a/npm-maintenance/index.html b/npm-maintenance/index.html
new file mode 100644
index 0000000..851c832
--- /dev/null
+++ b/npm-maintenance/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+ Down for Maintenance
+
+
+
+
+
Down for Maintenance
+
This resource is currently unavailable while we perform scheduled maintenance.
+
Please contact the administrator with any questions.
+
+
+
diff --git a/npm-maintenance/npm-maintenance.sh b/npm-maintenance/npm-maintenance.sh
new file mode 100644
index 0000000..9471d8f
--- /dev/null
+++ b/npm-maintenance/npm-maintenance.sh
@@ -0,0 +1,237 @@
+#!/usr/bin/env bash
+# npm-maintenance.sh
+# Toggles maintenance mode across all NPM proxy hosts via the NPM REST API
+
+set -euo pipefail
+
+# ============================================================
+# Configuration
+# ============================================================
+NPM_URL="http://localhost:81"
+NPM_USER="username"
+NPM_PASS="password"
+
+MAINTENANCE_HOST="maintenance"
+MAINTENANCE_PORT=80
+MAINTENANCE_SCHEME="http"
+
+SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
+BACKUP_FILE="${SCRIPT_DIR}/proxy_backup.json"
+LOG_DIR="${SCRIPT_DIR}"
+LOG_OUT="${LOG_DIR}/npm-maintenance.out"
+LOG_ERR="${LOG_DIR}/npm-maintenance.err"
+
+TRIGGERED_BY="$(whoami)"
+
+# ============================================================
+# Logging
+# ============================================================
+log_separator() {
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] [----] ----------------------------------------" | tee -a "$LOG_OUT"
+}
+
+log_info() {
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $1" | tee -a "$LOG_OUT"
+}
+
+log_warn() {
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $1" | tee -a "$LOG_OUT" >&2
+}
+
+log_error() {
+ local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $1"
+ echo "$msg" | tee -a "$LOG_OUT"
+ echo "$msg" >> "$LOG_ERR"
+}
+
+# ============================================================
+# Preflight checks
+# ============================================================
+preflight_checks() {
+ if [ ! -d "$LOG_DIR" ]; then
+ mkdir -p "$LOG_DIR"
+ log_info "[PREFLIGHT] Log directory not found, created: $LOG_DIR"
+ fi
+
+ if [ ! -f "$LOG_OUT" ]; then
+ touch "$LOG_OUT"
+ log_info "[PREFLIGHT] Log file not found, created: $LOG_OUT"
+ fi
+
+ if [ ! -f "$LOG_ERR" ]; then
+ touch "$LOG_ERR"
+ log_info "[PREFLIGHT] Error log file not found, created: $LOG_ERR"
+ fi
+
+ if [ ! -f "$BACKUP_FILE" ]; then
+ echo "[]" > "$BACKUP_FILE"
+ log_info "[PREFLIGHT] Backup file not found, created empty: $BACKUP_FILE"
+ fi
+}
+
+# ============================================================
+# API helpers
+# ============================================================
+get_token() {
+ local response
+ response=$(curl -s -X POST "${NPM_URL}/api/tokens" \
+ -H "Content-Type: application/json" \
+ -d "{\"identity\":\"${NPM_USER}\",\"secret\":\"${NPM_PASS}\"}")
+
+ local token
+ token=$(echo "$response" | jq -r '.token // empty')
+
+ if [ -z "$token" ]; then
+ log_error "Failed to authenticate with NPM API. Check NPM_USER and NPM_PASS."
+ exit 1
+ fi
+
+ echo "$token"
+}
+
+get_proxy_hosts() {
+ local token="$1"
+ curl -s -X GET "${NPM_URL}/api/nginx/proxy-hosts" \
+ -H "Authorization: Bearer ${token}"
+}
+
+update_proxy_host() {
+ local token="$1"
+ local id="$2"
+ local body="$3"
+
+ curl -s -X PUT "${NPM_URL}/api/nginx/proxy-hosts/${id}" \
+ -H "Authorization: Bearer ${token}" \
+ -H "Content-Type: application/json" \
+ -d "$body"
+}
+
+# ============================================================
+# Enable maintenance
+# ============================================================
+enable_maintenance() {
+ log_separator
+ log_info "START: Enabling maintenance mode (triggered by ${TRIGGERED_BY})"
+
+ local token
+ token=$(get_token)
+
+ local hosts
+ hosts=$(get_proxy_hosts "$token")
+
+ local total
+ total=$(echo "$hosts" | jq length)
+
+ if [ "$total" -eq 0 ]; then
+ log_warn "No proxy hosts found. Nothing to do."
+ log_separator
+ exit 0
+ fi
+
+ # Save backup: id, forward_host, forward_port, forward_scheme
+ echo "$hosts" | jq '[.[] | {id, forward_host, forward_port, forward_scheme}]' > "$BACKUP_FILE"
+ log_info "Backed up $total proxy host(s) to $BACKUP_FILE"
+
+ local success=0
+ local failed=0
+
+ for i in $(seq 0 $((total - 1))); do
+ local id domain body response
+ id=$(echo "$hosts" | jq -r ".[$i].id")
+ domain=$(echo "$hosts" | jq -r ".[$i].domain_names[0]")
+
+ # Build update payload from existing host, only swapping forward fields
+ body=$(echo "$hosts" | jq -c \
+ ".[$i] | del(.id, .created_on, .modified_on, .owner_user_id) | .forward_host = \"${MAINTENANCE_HOST}\" | .forward_port = ${MAINTENANCE_PORT} | .forward_scheme = \"${MAINTENANCE_SCHEME}\"")
+
+ response=$(update_proxy_host "$token" "$id" "$body")
+
+ if echo "$response" | jq -e '.id' > /dev/null 2>&1; then
+ log_info " ✓ [$((i+1))/$total] $domain (id: $id) → ${MAINTENANCE_HOST}:${MAINTENANCE_PORT}"
+ success=$((success + 1))
+ else
+ log_error " ✗ [$((i+1))/$total] $domain (id: $id) — API error: $(echo "$response" | jq -r '.error.message // .message // "unknown"')"
+ fi
+ done
+
+ log_info "DONE: $success succeeded, $failed failed out of $total host(s)"
+ log_separator
+}
+
+# ============================================================
+# Disable maintenance
+# ============================================================
+disable_maintenance() {
+ log_separator
+ log_info "START: Disabling maintenance mode (triggered by ${TRIGGERED_BY})"
+
+ local count
+ count=$(jq length "$BACKUP_FILE")
+
+ if [ "$count" -eq 0 ]; then
+ log_error "Backup file exists at $BACKUP_FILE but contains no hosts. Was enable ever run? Aborting."
+ log_separator
+ exit 1
+ fi
+
+ local token
+ token=$(get_token)
+
+ local hosts
+ hosts=$(get_proxy_hosts "$token")
+
+ local success=0
+ local failed=0
+
+ for i in $(seq 0 $((count - 1))); do
+ local id fwd_host fwd_port fwd_scheme current_host domain body response
+ id=$(jq -r ".[$i].id" "$BACKUP_FILE")
+ fwd_host=$(jq -r ".[$i].forward_host" "$BACKUP_FILE")
+ fwd_port=$(jq -r ".[$i].forward_port" "$BACKUP_FILE")
+ fwd_scheme=$(jq -r ".[$i].forward_scheme" "$BACKUP_FILE")
+
+ # Get the current live config for this host and restore only the forward fields
+ current_host=$(echo "$hosts" | jq -c ".[] | select(.id == $id)")
+ domain=$(echo "$current_host" | jq -r '.domain_names[0]')
+
+ body=$(echo "$current_host" | jq -c \
+ "del(.id, .created_on, .modified_on, .owner_user_id) | .forward_host = \"${fwd_host}\" | .forward_port = ${fwd_port} | .forward_scheme = \"${fwd_scheme}\"")
+
+ response=$(update_proxy_host "$token" "$id" "$body")
+
+ if echo "$response" | jq -e '.id' > /dev/null 2>&1; then
+ log_info " ✓ [$((i+1))/$count] $domain (id: $id) → ${fwd_host}:${fwd_port}"
+ success=$((success + 1))
+ else
+ log_error " ✗ [$((i+1))/$total] $domain (id: $id) — API error: $(echo "$response" | jq -r '.error.message // .message // "unknown"')"
+ fi
+ done
+
+ if [ "$failed" -eq 0 ]; then
+ echo "[]" > "$BACKUP_FILE"
+ log_info "Backup cleared after full successful restore"
+ else
+ log_warn "Backup retained at $BACKUP_FILE due to $failed failure(s)"
+ fi
+
+ log_info "DONE: $success succeeded, $failed failed out of $count host(s)"
+ log_separator
+}
+
+# ============================================================
+# Entry point
+# ============================================================
+case "$1" in
+ enable)
+ preflight_checks
+ enable_maintenance
+ ;;
+ disable)
+ preflight_checks
+ disable_maintenance
+ ;;
+ *)
+ log_error "Invalid or missing argument. Usage: npm-maintenance {enable|disable}"
+ exit 1
+ ;;
+esac