SMF - Just Installed!
nano /usr/local/sbin/docker-galera-upgrade.sh
#!/bin/bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
LOG="/var/log/docker-galera-upgrade.log"
STATE_FILE="/var/run/docker-galera-upgrade.state"
NODE=$(hostname)
log() {
echo "[$(date '+%F %T')] $*" | tee -a "$LOG"
}
log "===== START NODE UPGRADE (DOCKER + GALERA) ====="
# --- функции Galera ---
galera_size() {
mysql -Nse "SHOW STATUS LIKE 'wsrep_cluster_size'" | awk '{print $2}'
}
galera_ready() {
mysql -Nse "SHOW STATUS LIKE 'wsrep_ready'" | grep -q ON
}
# --- swarm check ---
if docker info 2>/dev/null | grep -q "Swarm: active"; then
SWARM=1
else
SWARM=0
fi
# --- POST REBOOT ---
if [ -f "$STATE_FILE" ]; then
log "Post-reboot stage"
```
# ждём Galera
until galera_ready; do
log "Waiting Galera..."
sleep 5
done
# возвращаем Docker
if [ "$SWARM" -eq 1 ]; then
docker node update --availability active "$NODE" || true
fi
rm -f "$STATE_FILE"
log "Node restored"
exit 0
```
fi
# --- PRECHECK Galera ---
SIZE=$(galera_size)
if [ "$SIZE" -le 1 ]; then
log "Cluster size too small → abort"
exit 1
fi
log "Galera size OK: $SIZE"
# --- drain docker ---
if [ "$SWARM" -eq 1 ]; then
docker node update --availability drain "$NODE"
fi
# --- graceful Galera leave ---
log "Stopping MariaDB (leave cluster)"
systemctl stop mariadb
touch "$STATE_FILE"
# --- upgrade ---
apt update >> "$LOG" 2>&1
apt upgrade -y >> "$LOG" 2>&1
# --- reboot ---
if [ -f /var/run/reboot-required ]; then
reboot
fi
# --- start MariaDB ---
systemctl start mariadb
# ждём Galera
until galera_ready; do
sleep 5
done
# --- restore docker ---
if [ "$SWARM" -eq 1 ]; then
docker node update --availability active "$NODE"
fi
rm -f "$STATE_FILE"
log "===== DONE ====="
nano /etc/systemd/system/docker-galera-upgrade.service
[Unit]
Description=Docker + Galera node upgrade
After=network-online.target docker.service mariadb.service
Requires=docker.service mariadb.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/docker-galera-upgrade.sh
[Install]
WantedBy=multi-user.target
nano /etc/systemd/system/docker-galera-upgrade.timer
[Unit]
Description=Node upgrade timer
[Timer]
OnCalendar=*-*-* 04:00:00
Persistent=true
[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now docker-galera-upgrade.timer
systemctl enable docker-galera-upgrade.service
mysql -e "SHOW STATUS LIKE 'wsrep_cluster_size';"
docker node ls
tail -f /var/log/docker-apt-upgrade.log
tail -f /var/log/haproxy-upgrade.log
systemctl status docker-apt-upgrade.service
systemctl status haproxy-upgrade.service
curl -s -X POST https://api.telegram.org/botTOKEN/sendMessage
-d chat_id=CHAT_ID
-d text="Upgrade done on $(hostname)"
docker node ls
docker service ls
echo OK | nc -l -p 8080
nano /usr/local/sbin/haproxy-ha-upgrade.sh
#!/bin/bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
LOG="/var/log/haproxy-upgrade.log"
LOCK="/var/run/haproxy-upgrade.lock"
VIP="10.10.1.100"
PEER="haproxy-2" # поменять на вторую ноду
log() {
echo "[$(date '+%F %T')] $*" | tee -a "$LOG"
}
if [ -f "$LOCK" ]; then
exit 0
fi
touch "$LOCK"
trap "rm -f $LOCK" EXIT
# self check
curl -fs http://127.0.0.1/health || exit 1
# peer check
ssh "$PEER" "curl -fs http://127.0.0.1/health" || exit 1
# upgrade
apt update >> "$LOG" 2>&1
apt upgrade -y >> "$LOG" 2>&1
if [ -f /var/run/reboot-required ]; then
reboot
fi
sleep 30
curl -fs http://127.0.0.1/health || exit 1
nano /etc/systemd/system/haproxy-upgrade.service
[Unit]
Description=HAProxy upgrade
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/haproxy-ha-upgrade.sh
nano /etc/systemd/system/haproxy-upgrade.timer
[Unit]
Description=HAProxy upgrade timer
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now haproxy-upgrade.timer
nano /usr/local/sbin/docker-apt-upgrade.sh
#!/bin/bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
LOG="/var/log/docker-apt-upgrade.log"
STATE_FILE="/var/run/docker-upgrade.state"
NODE=$(hostname)
log() {
echo "[$(date '+%F %T')] $*" | tee -a "$LOG"
}
log "===== START DOCKER NODE UPGRADE ====="
if docker info 2>/dev/null | grep -q "Swarm: active"; then
SWARM=1
else
SWARM=0
fi
if [ -f "$STATE_FILE" ]; then
log "Post-reboot detected → restoring node"
```
if [ "$SWARM" -eq 1 ]; then
docker node update --availability active "$NODE" || true
fi
rm -f "$STATE_FILE"
exit 0
```
fi
if [ "$SWARM" -eq 1 ]; then
docker node update --availability drain "$NODE"
fi
touch "$STATE_FILE"
apt update >> "$LOG" 2>&1
apt upgrade -y >> "$LOG" 2>&1
if [ -f /var/run/reboot-required ]; then
reboot
fi
if [ "$SWARM" -eq 1 ]; then
docker node update --availability active "$NODE"
fi
rm -f "$STATE_FILE"
chmod +x /usr/local/sbin/docker-apt-upgrade.sh
nano /etc/systemd/system/docker-apt-upgrade.service
[Unit]
Description=Docker node upgrade
After=network-online.target docker.service
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/docker-apt-upgrade.sh
[Install]
WantedBy=multi-user.target
nano /etc/systemd/system/docker-apt-upgrade.timer
[Unit]
Description=Docker node upgrade timer
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now docker-apt-upgrade.timer
systemctl enable docker-apt-upgrade.service
/usr/local/bin/haproxy-map-update.sh
/usr/local/bin/haproxy-cert.sh
#!/bin/bash
set -euo pipefail
DOMAIN=${1:-}
if [ -z "$DOMAIN" ]; then
echo "[CERT] no domain!"
exit 1
fi
echo "[CERT] Issuing certificate for $DOMAIN"
certbot certonly --standalone \
--http-01-port=8888 \
-d "$DOMAIN" \
--non-interactive \
--agree-tos \
--email george@cloud-life.site \
--expand
echo "[CERT] Building PEM"
cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem \
/etc/letsencrypt/live/$DOMAIN/privkey.pem \
> /etc/ssl/haproxy/$DOMAIN.pem
echo "[CERT] Done"
#!/bin/bash
GREEN="\033[0;32m"
NC="\033[0m"
RED="\033[0;31m"
CFG="/etc/haproxy/haproxy.cfg"
HOSTS_MAP="/etc/haproxy/maps/hosts.map"
PASSTH_MAP="/etc/haproxy/maps/passth.map"
ACME_MAP="/etc/haproxy/maps/acme_backends.map"
usage() {
echo -e "${GREEN}"
echo "Usage:"
echo " $0 set DOMAIN BACKEND MODE IP_LIST PORT [-y]"
echo " $0 del DOMAIN"
echo -e "${NC}"
exit 1
}
if [ $# -lt 2 ]; then
usage
fi
ACTION=$1
DOMAIN=$2
BACKEND=${3:-}
MODE=${4:-}
IP_LIST=${5:-}
PORT=${6:-}
AUTO_YES=${7:-}
set -euo pipefail
backend_exists() {
grep -q "^backend $BACKEND" "$CFG"
}
build_servers() {
IFS=',' read -ra IPS <<< "$IP_LIST"
i=1
SERVERS=""
for ip in "${IPS[@]}"; do
name="${BACKEND}${i}"
if [ "$PORT" == "443" ]; then
SERVERS+=" server ${name} ${ip}:${PORT} ssl verify none check\n"
else
SERVERS+=" server ${name} ${ip}:${PORT} check\n"
fi
i=$((i+1))
done
}
create_backend() {
echo "[BACKEND] Creating: $BACKEND"
if [ -z "${IP_LIST:-}" ]; then
read -p "IP(s): " IP_LIST
fi
if [ -z "${PORT:-}" ]; then
read -p "PORT: " PORT
fi
build_servers
TMP=$(mktemp)
awk -v block="$(cat <<EOF
backend $BACKEND
mode http
balance roundrobin
option forwardfor header X-Real-IP
http-request set-header X-Forwarded-For %[src]
http-request set-header X-Forwarded-Port %[dst_port]
$SERVERS
EOF
)" '
/# --- AUTO_BACKENDS_END ---/ {
print block
}
{ print }
' "$CFG" > "$TMP"
mv "$TMP" "$CFG"
}
issue_cert() {
/usr/local/bin/haproxy-cert.sh "$DOMAIN"
}
update_map() {
local MAP=$1
local KEY=$2
local VALUE=$3
TMP=$(mktemp)
[ -f "$MAP" ] && cp "$MAP" "$TMP"
grep -v "^$KEY " "$TMP" > "$TMP.new" || true
echo "$KEY $VALUE" >> "$TMP.new"
mv "$TMP.new" "$MAP"
rm -f "$TMP"
}
delete_maps() {
for m in "$HOSTS_MAP" "$PASSTH_MAP" "$ACME_MAP"; do
[ -f "$m" ] && grep -v "^$DOMAIN " "$m" > "$m.tmp" && mv "$m.tmp" "$m"
done
}
engine() {
for MAP in "$HOSTS_MAP" "$PASSTH_MAP" "$ACME_MAP"; do
[ -f "$MAP" ] || continue
awk '{ print length($1), $0 }' "$MAP" | sort -nr | cut -d" " -f2- > "$MAP.tmp"
mv "$MAP.tmp" "$MAP"
done
}
echo "[MAP] $DOMAIN"
if [ "$ACTION" == "set" ]; then
if ! backend_exists; then
if [[ "$AUTO_YES" == "-y" ]]; then
create_backend
else
read -p "Create backend $BACKEND? [Y/n]: " c
[[ "$c" =~ ^[Nn]$ ]] && exit 1
create_backend
fi
fi
if [ "$MODE" == "http" ]; then
issue_cert
update_map "$HOSTS_MAP" "$DOMAIN" "$BACKEND"
elif [ "$MODE" == "passth" ]; then
update_map "$PASSTH_MAP" "$DOMAIN" "$BACKEND"
update_map "$ACME_MAP" "$DOMAIN" "acme_${BACKEND}"
fi
delete_maps
engine
systemctl reload haproxy
elif [ "$ACTION" == "del" ]; then
delete_maps
engine
systemctl reload haproxy
fi
echo -e "${GREEN}[DONE]${NC}"
# --- AUTO_BACKENDS_END ---
Client
↓
HAProxy (:80 / :443)
↓
Maps routing:
├── hosts.map (HTTP backend)
├── passth.map (TLS SNI backend)
└── acme_backends.map (ACME HTTP backend)
↓
Backends (auto-generated)
/usr/local/bin/haproxy-map-update.sh
haproxy-map-update.sh set DOMAIN BACKEND MODE IP_LIST PORT [-y]
haproxy-map-update.sh del DOMAIN
haproxy-map-update.sh set ftp.cloud-life.site ftp http 10.10.1.37 80 -y
haproxy-map-update.sh set games.cloud-life.site games passth "10.10.1.46,10.10.1.47,10.10.1.48" 443 -y
haproxy-map-update.sh del games.cloud-life.site
Internet → HAProxy :80
→ /.well-known/acme-challenge/
→ 127.0.0.1:8888 (certbot standalone)
/etc/haproxy/maps/hosts.map
/etc/haproxy/maps/passth.map
/etc/haproxy/maps/acme_backends.map
IP_LIST="10.10.1.46,10.10.1.47,10.10.1.48"
PORT=443
server games1 10.10.1.46:443 ssl verify none check
server games2 10.10.1.47:443 ssl verify none check
server games3 10.10.1.48:443 ssl verify none check
-y
Internet
↓
:80 (HAProxy)
↓
ACME path → 127.0.0.1:8888 (certbot standalone)
↓
остальной трафик → maps → backend
frontend http_80
bind *:80
mode http
acl letsencrypt-acl path_beg /.well-known/acme-challenge/
use_backend letsencrypt-backend if letsencrypt-acl
http-request redirect scheme https code 301 if !letsencrypt-acl
backend letsencrypt-backend
mode http
server letsencrypt 127.0.0.1:8888
#!/bin/bash
set -euo pipefail
DOMAIN=${1:-}
if [ -z "$DOMAIN" ]; then
echo "[CERT] no domain!"
exit 1
fi
echo "[CERT] Issuing certificate for $DOMAIN"
certbot certonly --standalone \
--http-01-port=8888 \
-d "$DOMAIN" \
--non-interactive \
--agree-tos \
--email george@cloud-life.site \
--expand
echo "[CERT] Building PEM"
cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem \
/etc/letsencrypt/live/$DOMAIN/privkey.pem \
> /etc/ssl/haproxy/$DOMAIN.pem
echo "[CERT] Done"
#!/bin/bash
GREEN="\033[0;32m"
NC="\033[0m"
RED="\033[0;31m"
usage() {
echo -e "${GREEN}"
echo "Usage:"
echo " $0 set DOMAIN BACKEND MODE"
echo " $0 del DOMAIN"
echo ""
echo "Modes:"
echo " http - обычный HTTP (hosts.map)"
echo " passth - TLS passthrough (passth.map + acme_backends.map)"
echo -e "${NC}"
exit 1
}
if [ $# -lt 2 ]; then
echo -e "${RED}[ERROR] Invalid arguments${NC}"
usage
fi
ACTION=$1
DOMAIN=$2
BACKEND=${3:-}
MODE=${4:-}
if [ "$ACTION" == "set" ]; then
if [ -z "$BACKEND" ] || [ -z "$MODE" ]; then
echo -e "${RED}[ERROR] Invalid arguments${NC}"
usage
fi
fi
set -euo pipefail
HOSTS_MAP="/etc/haproxy/maps/hosts.map"
PASSTH_MAP="/etc/haproxy/maps/passth.map"
ACME_MAP="/etc/haproxy/maps/acme_backends.map"
ACME_BACKEND="acme_${BACKEND}"
issue_cert() {
echo "[CERT] Ensuring certificate for $DOMAIN"
if /usr/local/bin/haproxy-cert.sh "$DOMAIN"; then
echo -e "${GREEN}[CERT] OK${NC}"
else
echo -e "${RED}[CERT] FAILED → aborting${NC}"
exit 1
fi
}
update_file() {
local MAP=$1
local KEY=$2
local VALUE=$3
TMP=$(mktemp)
[ -f "$MAP" ] && cp "$MAP" "$TMP"
grep -v "^$KEY " "$TMP" > "$TMP.new" || true
if [ "$ACTION" == "set" ]; then
echo "$KEY $VALUE" >> "$TMP.new"
fi
mv "$TMP.new" "$MAP"
rm -f "$TMP"
}
delete_from_all() {
update_file "$HOSTS_MAP" "$DOMAIN" ""
update_file "$PASSTH_MAP" "$DOMAIN" ""
update_file "$ACME_MAP" "$DOMAIN" ""
}
engine() {
for MAP in "$HOSTS_MAP" "$PASSTH_MAP" "$ACME_MAP"; do
if [ -f "$MAP" ]; then
awk '{ print length($1), $0 }' "$MAP" \
| sort -nr \
| cut -d" " -f2- \
> "$MAP.tmp"
mv "$MAP.tmp" "$MAP"
fi
done
}
echo "[MAP] Processing: $DOMAIN"
# 🔥 1. СНАЧАЛА сертификат
if [ "$ACTION" == "set" ]; then
issue_cert
fi
# 🔥 2. потом routing
delete_from_all
if [ "$ACTION" == "set" ]; then
if [ "$MODE" == "http" ]; then
update_file "$HOSTS_MAP" "$DOMAIN" "$BACKEND"
elif [ "$MODE" == "passth" ]; then
update_file "$PASSTH_MAP" "$DOMAIN" "$BACKEND"
update_file "$ACME_MAP" "$DOMAIN" "$ACME_BACKEND"
else
echo "Mode must be: http | passth"
exit 1
fi
fi
echo "[MAP] Sorting maps"
engine
echo "[MAP] Reloading HAProxy"
systemctl reload haproxy
echo -e "${GREEN}[MAP] DONE${NC}"
# HTTP сайт
haproxy-map-update.sh set site.com backend_name http
# TLS passthrough
haproxy-map-update.sh set site.com backend_name passth
# удалить
haproxy-map-update.sh del site.com
nano /usr/local/bin/haproxy-safe-reload.sh
#!/bin/bash
set -euo pipefail
VIP="10.10.10.10"
# Проверяем, что нода активная (есть VIP)
ip addr | grep -q "$VIP"
if [ $? -ne 0 ]; then
echo "[HAProxy] Not active node → skip reload"
exit 0
fi
LOCK=/var/lock/haproxy-reload.lock
flock -n "$LOCK" bash -c '
echo "[HAProxy] validating config..."
haproxy -c -f /etc/haproxy/haproxy.cfg
if [ $? -ne 0 ]; then
echo "[HAProxy] CONFIG INVALID → abort"
exit 1
fi
echo "[HAProxy] reloading..."
systemctl reload haproxy
echo "[HAProxy] reload OK"
'
chmod +x /usr/local/bin/haproxy-safe-reload.sh
apt install inotify-tools -y
nano /usr/local/bin/haproxy-watch.sh
#!/bin/bash
WATCH="/etc/haproxy"
LOCK=/var/lock/haproxy-watch.lock
LAST_RUN=0
inotifywait -m -r -e close_write --format '%f' $WATCH | while read file; do
```
# игнор временных файлов
case "$file" in
*.swp|*.swx|*.tmp|4913)
continue
;;
esac
# реагируем только на нужные файлы
if [[ "$file" != "haproxy.cfg" && "$file" != *.map ]]; then
continue
fi
NOW=$(date +%s)
# debounce (защита от повторов)
if (( NOW - LAST_RUN < 3 )); then
continue
fi
LAST_RUN=$NOW
echo "[WATCH] Change detected: $file"
flock -n $LOCK /usr/local/bin/haproxy-safe-reload.sh
```
done
chmod +x /usr/local/bin/haproxy-watch.sh
nano /etc/systemd/system/haproxy-watch.service
[Unit]
Description=HAProxy config watcher
After=network.target
[Service]
ExecStart=/usr/local/bin/haproxy-watch.sh
Restart=always
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now haproxy-watch
systemctl status haproxy-watch
nano /etc/letsencrypt/renewal-hooks/deploy/haproxy-deploy-cert.sh
#!/bin/bash
set -euo pipefail
CERT_DIR="/etc/haproxy/certs"
if [ -n "${RENEWED_LINEAGE:-}" ]; then
DOMAIN=$(basename "$RENEWED_LINEAGE")
```
cat "$RENEWED_LINEAGE/fullchain.pem" \
"$RENEWED_LINEAGE/privkey.pem" \
> "$CERT_DIR/$DOMAIN.pem"
```
else
for d in /etc/letsencrypt/live/*/; do
DOMAIN=$(basename "$d")
cat "$d/fullchain.pem" "$d/privkey.pem" > "$CERT_DIR/$DOMAIN.pem"
done
fi
/usr/local/bin/haproxy-safe-reload.sh
chmod +x /etc/letsencrypt/renewal-hooks/deploy/haproxy-deploy-cert.sh
nano /etc/haproxy/haproxy.cfg
[WATCH] Change detected: haproxy.cfg
[HAProxy] reload OK