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.
Inhalte
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
XML | Zweck | Beispiel |
---|---|---|
appname | Name des Backups (Bestandteil für Backup-Verzeichnis auf Storage Box und tar.gz & .sql Dateinamensbereich) | deinedomain-tld |
dbhost | Datenbank Server Adresse | 127.0.0.1 |
dbname | Datenbank Tabellen-Name | wordpress |
dbpass | Datenbank Passwort | superSecretPassword |
dbuser | Datenbank User | wordpress |
dbport | Datenbank Port | 3306 |
fsuser | Dateisystem-User für Apache Site (Anwendung nach Wiederherstellung eines Backups) | www-data |
fsgroup | Dateisystem-Gruppe für Apache Site (Anwendung nach Wiederherstellung eines Backups) | www-data |
websdir | Apache2 HT-Docs Root Verzeichnis | /var/www/html |
storbox | Hetzner 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 |
bhburl | Heartbeat 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!"
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!"
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