initial commit
This commit is contained in:
@@ -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
|
||||
# ----------------------------------
|
||||
@@ -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
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Down for Maintenance</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
.box {
|
||||
text-align: center;
|
||||
background: white;
|
||||
padding: 2rem 3rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 { color: #e74c3c; }
|
||||
p { color: #555; }
|
||||
a { color: #3498db; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<h1>Down for Maintenance</h1>
|
||||
<p>This resource is currently unavailable while we perform scheduled maintenance.</p>
|
||||
<p>Please contact the administrator with any questions.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user