Hetzner WordPress vServer Administration

In diesem Artikel geben wir euch hilfreiche Scripte an die Hand, um WordPress / WooCommerce Server auf Hetzner vServern aufzusetzen und zu administrieren. Von PHP Versionswechsel bis zu Backup & Recovery Systemen.

Dieser Artikel setzt fortgeschrittene Linux-Server-Kenntnisse voraus. Sollten diese nicht vorhanden sein, melde dich gerne für Unterstützung.

Backup & Recovery Scripte

Config.xml

Die folgende Konfigurationsdatei wird benötigt:

<config>
    <appname>appname</appname>
    <dbuser>dbuser</dbuser>
    <dbtabl>dbtable</dbtabl>
    <dbpass>dbpass</dbpass>
    <dbhost>dbhost</dbhost>
    <dbport>3306</dbport>
    <fsuser>www-data</fsuser>
    <fsgroup>www-data</fsgroup>
    <websdir>/var/www/html</websdir>
    <storbox>u123456@u123456.your-storagebox.de</storbox>
    <siteurl>https://yourdomain.tld</siteurl>
    <bhburl>false</bhburl>
</config>

Erklärung der Parameter

XMLZweckBeispiel
appnameName des Backups (Bestandteil für Backup-Verzeichnis auf Storage Box und tar.gz & .sql Dateinamensbereich)deinedomain-tld
dbhostDatenbank Server Adresse127.0.0.1
dbnameDatenbank Tabellen-Namewordpress
dbpassDatenbank PasswortsuperSecretPassword
dbuserDatenbank Userwordpress
dbportDatenbank Port3306
fsuserDateisystem-User für Apache Site (Anwendung nach Wiederherstellung eines Backups)www-data
fsgroupDateisystem-Gruppe für Apache Site (Anwendung nach Wiederherstellung eines Backups)www-data
websdirApache2 HT-Docs Root Verzeichnis/var/www/html
storboxHetzner Storage Box User@Hostname (können auch Sub-User sein, für granularere Berechtigungsvergabe)u123456@u123456.your-storagebox.de
siteurlÜberschreibt die SITE_URL in der MySQL Datenbank nach Wiederherstellung eines Backups (hilfreich im Fall eines Domainwechsels)https://yourdomain.tld
bhburlHeartbeat Provider Curl Adresse (z.B. Überwachung der Backupzuverlässigkeit mit BetterUptime)„false“ oder sowas wie „https://uptime.betterstack.com/api/v1/heartbeat/FRfa6qsAs3a65gj43hJc7bNk2“

Server-Recovery Script

Ablage z.B als server-recovery.sh im Verzeichnis ~/shscripts.

#!/bin/bash

# -----------------------------------------
# Backup Recovery Script # Version 5
# -----------------------------------------
#
# Usage: ./server-recovery.sh <config-file> [db-only|files-only]
# Example: ./server-recovery.sh config.xml
#          ./server-recovery.sh myconfig.xml db-only
#          ./server-recovery.sh prod-config.xml files-only
#
# place script in a folder like ~/shscripts/ and give chmod +x rights
# database can be updated on application server or remote database (set dbhost to local 127.0.0.1 or external db lan ip)
# all database tables will be deleted before the sql backup gets injected
#

#
# Precheck if all command line tools are installed
#
require() { command -v "$1" &>/dev/null || { echo "## $1 is not installed, for install use: apt-get install $1"; exit; }; }
require xmlstarlet
require pv

# Get the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# Check if config file parameter is provided
if [ $# -eq 0 ]; then
    echo "## Error: No config file specified!"
    echo "## Usage: $0 <config-file> [db-only|files-only]"
    echo "## Example: $0 config.xml"
    echo "##          $0 myconfig.xml db-only"
    exit 1
fi

# Get config file from first parameter and look for it in script directory
CONFIG_FILE="$SCRIPT_DIR/$1"

# Shift parameters so $1 becomes the optional mode (db-only/files-only)
shift

# Check if config file exists
if [ ! -f "$CONFIG_FILE" ]; then
  echo "## Error: Configuration file '$1' not found in script directory!"
  echo "## Looking for: $CONFIG_FILE"
  echo "## Please ensure the configuration file exists in the same directory as this script."
  exit 1
fi

#
# Get Parameters from $CONFIG_FILE (examples see comments)
#

# Define required config keys
configs=(appname dbuser dbtabl dbpass dbhost dbport fsuser fsgroup websdir storbox siteurl)

# Load and validate all configs
for key in "${configs[@]}"; do
    value=$(xmlstarlet sel -t -v "/config/$key" "$CONFIG_FILE")
    if [ -z "$value" ]; then
        echo "!! $CONFIG_FILE $key not set !! stopping process!"
        exit 1
    fi
    declare "$key=$value"
done

#
# check options (now $1 is the mode after shift)
#

if [ "$1" = "" ]; then
  echo "## launched full-recovery for ${appname} startet at ${date}"
elif [ "$1" = "db-only" ]; then
  echo "## launched db-only recovery for ${appname} startet at ${date}"
elif [ "$1" = "files-only" ]; then
  echo "## launched files-only recovery for ${appname} startet at ${date}"
else
  echo "!! unknown recovery mode: $1"
  echo "!! valid modes are: db-only, files-only (or leave empty for full recovery)"
  exit 0
fi

#
# get latest .sql and .tar.gz from backup server
#

if [ "$1" = "" ]; then
	latestsql=$(ssh -p 23 ${storbox} ls -t1 ${appname}/ | grep .sql | head -n 1)
	latesttar=$(ssh -p 23 ${storbox} ls -t1 ${appname}/ | grep .tar.gz | head -n 1)
elif [ "$1" = "db-only" ]; then
	latestsql=$(ssh -p 23 ${storbox} ls -t1 ${appname}/ | grep .sql | head -n 1)
elif [ "$1" = "files-only" ]; then
	latesttar=$(ssh -p 23 ${storbox} ls -t1 ${appname}/ | grep .tar.gz | head -n 1)
else
	echo "!! unknown command, stopping process!"
    exit 0
fi

echo -e "\n## Recover latest PRD Version on DEV with:"
echo ${latestsql}
echo ${latesttar}

echo -e "\n## Downloading files"

if [ "$1" != "files-only" ]; then
	rsync --progress -e 'ssh -p23' ${storbox}:${appname}/${latestsql} ${SCRIPT_DIR}/tmp/
fi

if [ "$1" != "db-only" ]; then
	rsync --progress -e 'ssh -p23' ${storbox}:${appname}/${latesttar} ${SCRIPT_DIR}/tmp/
fi

#
# restore files
#

if [ "$1" != "db-only" ]; then
echo -e "\n## Restoring Files"
	filenametar=$(echo ${latesttar} | sed s/"\/data\/${appname}\/"//)
	echo "-- deleting ${websdir}/*"
	rm -R -f ${websdir}/*
  echo "-- untar the tar.gz to ${websdir}/."
  pv ${SCRIPT_DIR}/tmp/${filenametar} | tar -xzf - -C ${websdir}/.
	echo "-- restored the files"
fi

#
# restore database
#

echo -e "\n## Read WordPress Table Prefix"
prefix=$(sed -n "s/^.*\$table_prefix *= *'\([^']*\)'.*\$/\1/p" ${websdir}/wp-config.php)
echo "-- table prefix: ${prefix}"


echo -e "\n## Restoring Database & change siteurl"

if [ "$1" != "files-only" ]
then

    echo "-- deleting Database Tables..."
    TABLES=$(mysql -h ${dbhost} -P ${dbport} -u ${dbuser} -p ${dbtabl} --password="${dbpass}" -e 'show tables' | awk '{ print $1}' | grep -v '^Tables' )

    for t in $TABLES
    do
        mysql -h ${dbhost} -P ${dbport} -u ${dbuser} -p ${dbtabl} --password="${dbpass}" -e "drop table $t" 2> /dev/null
    done

    echo "-- recover Database from SQL..."
	  filenamesql=$(echo ${latestsql} | sed s/"\/data\/${appname}\/"//)
    mysql -h ${dbhost} -P ${dbport} -u ${dbuser} -p ${dbtabl} --password="${dbpass}" < ${SCRIPT_DIR}/tmp/${filenamesql}
    # rewrite siteurl
    mysql -h ${dbhost} -P ${dbport} -u ${dbuser} -p ${dbtabl} --password="${dbpass}" -e "UPDATE ${prefix}options SET option_value = '${siteurl}' WHERE option_name = 'home' OR option_name = 'siteurl';"
    # disable indexing
    mysql -h ${dbhost} -P ${dbport} -u ${dbuser} -p ${dbtabl} --password="${dbpass}" -e "UPDATE ${prefix}options SET option_value = '0' WHERE option_name = 'blog_public';"
    echo "-- disabled wordpress indexing for search machines..."
fi

#
# change wp-config.php to new server
#

if [ "$1" != "db-only" ]
then
  sed "s/.*define( 'DB_NAME.*/define( 'DB_NAME', '${dbtabl}' );/" -i ${websdir}/wp-config.php
  sed "s/.*define( 'DB_USER.*/define( 'DB_USER', '${dbuser}' );/" -i ${websdir}/wp-config.php
  sed "s/.*define( 'DB_PASSWORD.*/define( 'DB_PASSWORD', '${dbpass}' );/" -i ${websdir}/wp-config.php
  sed "s/.*define( 'DB_HOST.*/define( 'DB_HOST', '${dbhost}:${dbport}' );/" -i ${websdir}/wp-config.php
fi


#
# rewrite filesystem user, group and permissions
#

if [ "$1" != "db-only" ]; then
  echo -e "\n## Set Filesystem User:Group and Permissions"
  chown ${fsuser}:${fsgroup} -hR ${websdir} ## Set ownership for all files and directories (including hidden)
  find ${websdir} -type f -exec chmod 644 {} + ## Set file permissions
  find ${websdir} -type d -exec chmod 755 {} + ## Set directory permissions
fi


### delete local copy

echo -e "\n## remove ${SCRIPT_DIR}/tmp files..."
rm ${SCRIPT_DIR}/tmp/*

echo -e "\n## Recovery finished!"
Erweitern

Server-Backup Script

Ablage z.B als server-backup.sh im Verzeichnis ~/shscripts.

#!/bin/bash

# -----------------------------------------
# Backup Creation Script # Version 5
# -----------------------------------------

#
# Precheck if all command line tools are installed
#
require() { command -v "$1" &>/dev/null || { echo "## $1 is not installed, for install use: apt-get install $1"; exit; }; }
require xmlstarlet
require pv

# Get the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# Check if config file parameter is provided
if [ $# -eq 0 ]; then
    echo "## Error: No config file specified!"
    echo "## Usage: $0 <config-file> [-m db-only|files-only] [-t daily|monthly]"
    echo "## Example: $0 config.xml"
    echo "##          $0 config.xml -m db-only"
    echo "##          $0 config.xml -m files-only -t daily"
    exit 1
fi

# Get config file from first parameter and look for it in script directory
CONFIG_FILE="$SCRIPT_DIR/$1"

# Shift parameters so $1 becomes the optional mode (db-only/files-only)
shift

# Check if config file exists
if [ ! -f "$CONFIG_FILE" ]; then
  echo "## Error: Configuration file '$1' not found in script directory!"
  echo "## Looking for: $CONFIG_FILE"
  echo "## Please ensure the configuration file exists in the same directory as this script."
  exit 1
fi


# change to working directory (needed for cron)
cd "$SCRIPT_DIR"
mkdir -p tmp

#
# Get Parameters from $CONFIG_FILE (examples see comments)
#

# Define required config keys
configs=(appname dbuser dbtabl dbpass dbhost dbport websdir storbox bhburl)

# Load and validate all configs
for key in "${configs[@]}"; do
    value=$(xmlstarlet sel -t -v "/config/$key" "$CONFIG_FILE")
    if [ -z "$value" ]; then
        echo "!! $CONFIG_FILE $key not set !! stopping process!"
        exit 1
    fi
    declare "$key=$value"
done

# at first, we read all the parameters which the command line gave us
# with those parameters, we control the backup process, add file prefixes and other stuff

## standards
FLAGBTYPE=tmp
FLAGBMODE=""

while getopts m:t: flag; do
	case "${flag}" in
    m)
      case "${OPTARG}" in
      db-only|files-only)
          FLAGBMODE=${OPTARG};;
      *)
        echo "## ERROR: Unknown backup mode! Please provide -m files-only or -m db-only or leave blank for full backup!"
        exit 1;;
      esac;;
    t)
      case "${OPTARG}" in
      daily|monthly)
          FLAGBTYPE=${OPTARG}
          echo "Setting: Backuptype -> ${OPTARG}";;
      *)
        echo "## ERROR: Unknown Backup Type. Please provide -t daily (begin of day), -t monthly (begin of month)."
        exit 1;;
      esac;;
    *)
      echo "## ERROR: Unknown Parameter."
      echo "You can provide the following Parameters:"
      echo ""
      echo "-m (for Backup Mode like: 'files-only' or 'db-only'"
      echo "-t (for Backup Type like: 'daily' [begin of day] or 'monthly' [begin of month]"
      exit 1;;
  esac
done


## message
date=$(date '+%Y-%m-%d_%H-%M')

if [ "$FLAGBMODE" = "" ]; then
  echo "## launching full-backup for ${appname} started at ${date}"
elif [ "$FLAGBMODE" = "db-only" ]; then
  echo "## launching db-only backup for ${appname} started at ${date}"
elif [ "$FLAGBMODE" = "files-only" ]; then
  echo "## launching files-only backup for ${appname} started at ${date}"
else
  echo "## Unknown backup mode! Please provide -m files-only or -m db-only or leave blank for full backup!"
  exit 1
fi

### mysql dump

if [ "$FLAGBMODE" != "files-only" ]; then
  echo -e "\n## start mysql dump creation..."
  # get actual database size
  db_size=$(mysql -h ${dbhost} -P ${dbport} -u ${dbuser} --password="${dbpass}" --silent --skip-column-names -e "SELECT ROUND(SUM(data_length) / 1024 / 1024, 0) FROM information_schema.TABLES WHERE table_schema='${dbtabl}';")
  # do the mysqldump, piped in pv with the db_size to get a progress bar
  mysqldump --column-statistics=0 -h ${dbhost} -P ${dbport} -u ${dbuser} -p ${dbtabl} --add-drop-table --password="${dbpass}" | pv -s "$db_size"m > ${SCRIPT_DIR}/tmp/${FLAGBTYPE}_${date}_${appname}.sql  && echo "-- mysqldump successful" || exit 1
fi

### fullbackup of page

if [ "$FLAGBMODE" != "db-only" ]; then
  echo -e "\n## start tarball creation..."
  tar cf - -C ${websdir}/ . | pv -s $(du -sb ${websdir}/ | awk '{print $1}') | gzip > ${SCRIPT_DIR}/tmp/${FLAGBTYPE}_${date}_${appname}.tar.gz  && echo "-- tarball successful" || exit 1
fi

### copy to lkfdbackup
echo -e "\n## start backup transfer..."
rsync --progress -e 'ssh -p23' -rt ${SCRIPT_DIR}/tmp/ ${storbox}:${appname}/  && echo "-- transfer successful" || exit 1

### delete local copy
echo -e "\n## remove /tmp files..."
rm ${SCRIPT_DIR}/tmp/*

### contact heartbeat (only if full-backup and heartbeat url is set)
if [ "$FLAGBMODE" == "" ] && [ "$bhburl" != "" ] && [ "$bhburl" != "false" ]; then
  echo -e "\n## sending heartbeat..."
  curl ${bhburl}
fi

echo -e "\n## Backup finished!"
Erweitern

Update Helper

Plugin Sicherung vor Update

Mit dem folgenden Befehl lässt sich im /wp-content/plugins Ordner ein .tar.gz Archiv eines Plugins erstellen.

tar -czvf woocommerce-paypal-payments.tar.gz woocommerce-paypal-payments

Sollte nach dem Update etwas nicht mehr funktionieren, kann die vorherige Version des Plugins aus dem .tar.gz Paket wieder entpackt und die neue überschrieben werden.

tar -xzvf woocommerce-paypal-payments.tar.gz --overwrite

Sollte ein Pluginupdate auch ein Datenbankupdate ausgelöst haben, kann es notwendig sein, ein Backup der gesamten Seite wiederherzustellen.

Server Konfiguration

Zeitzone ändern

Abfrage der Timezone für Berlin

timedatectl list-timezones | grep Berlin

Umstellung der Zeitzone

sudo timedatectl set-timezone [timezone]

Test der Zeitzone

timedatectl

Apache Konfiguration

Server Alias

Zur Erteilung von Zertifikaten für www.domain.tl und domain.tld wird eine Einstellung namens ServerAlias benötigt.

Gehe dazu in /etc/apache2/sites-enabled# vi 000-default-le-ssl.conf & /etc/apache2/sites-enabled# vi 000-default.conf und füge die nicht hauptsächlich verwendete Domain als ServerAlias hinzu.

Anschließend systemctl restart apache2 und certbot ausführen, um die neuen Einstellungen zu aktivieren und das SSL Zertifikat zu erneuern.

PHP Version wechseln

Downgrade auf PHP 7.4 von PHP 8.3

sudo apt-get install software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo add-apt-repository ppa:ondrej/apache2
sudo apt-get update
sudo apt-get install php7.4

Zuletzt installieren wir einige grundlegende PHP-Erweiterungen, die für die Bereitstellung von WordPress erforderlich sind:

sudo apt-get install libapache2-mod-php7.4
sudo apt-get install php-mysql php7.4-xml php7.4-gd php7.4-mysql php7.4-mbstring php7.4-zip php7.4-curl php7.4-soap
sudo a2enmod rewrite

Wechsel auf PHP 7.4 ausführen

sudo a2dismod php8.3
sudo a2enmod php7.4
sudo service apache2 restart
systemctl restart apache2

Falls es trotzdem Probleme gibt, kann der folgende Befehl helfen:

sudo update-alternatives --set php /usr/bin/php7.4

Downgrade auf PHP 7.3 von PHP 8.3

sudo apt-get install software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo add-apt-repository ppa:ondrej/apache2
sudo apt-get update
sudo apt-get install php7.3
php -v

sudo apt-get install libapache2-mod-php7.3
sudo apt-get install php-mysql php7.3-xml php7.3-gd php7.3-mysql php7.3-mbstring php7.3-zip php7.3-curl php7.3-soap
sudo a2enmod rewrite

sudo a2dismod php8.3
sudo a2enmod php7.3
sudo service apache2 restart
systemctl restart apache2

Downgrade auf PHP 8.1 von PHP 8.3

sudo apt-get install software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo add-apt-repository ppa:ondrej/apache2
sudo apt-get update
sudo apt-get install php8.1
php -v

sudo apt-get install libapache2-mod-php8.1
sudo apt-get install php-mysql php8.1-xml php8.1-gd php8.1-mysql php8.1-mbstring php8.1-zip php8.1-curl php8.1-soap
sudo a2enmod rewrite

sudo a2dismod php8.3
sudo a2enmod php8.1
sudo service apache2 restart
systemctl restart apache2

sudo update-alternatives --set php /usr/bin/php8.1

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert