DB-Backup per Cronjob bei Domainfactory

Heute möchte ich mal kurz beschreiben, wie ich ein Datenbank-Backup bei meinem Provider domainfactory eingerichtet habe. Ich habe einen Shared-Hosting-Tarif mit SSH-Zugang. Wenn man keinen SSH-Zugang hat, dann klappt der hier beschriebene Vorgang natürlich nicht. Wenn man einen komplett eigenen Server hat, ist das hier auch nicht relevant, denn dann hat man ja die volle Kontrolle über das System.

Das Problem

Prinzipiell sollte das Vorgehen klar sein: Das Programm „mysqldump“ erstellt den Datenbank-Dump, ausgelöst über einen Cronjob. Der Cronjob selber kann auf ein Shell-Script zeigen oder auf ein PHP-Programm. Die Schwierigkeit: Wenn das Backup erstellt wird, sitzt man ja selber nicht vor dem Rechner und kann deswegen das Datenbank-Passwort nicht eintippen. Es ist aber auch keine gute Idee, dieses Passwort einfach in den Scriptaufruf im Cronjob reinzuschreiben (was problemlos ginge), denn dann hat jeder Zugang zur Datenbank, der die Befehlshistorie des Systems oder die laufenden Prozesse anschauen darf. Das Passwort muss also irgendwie anders gelagert werden, und da kommt das Programm „mysql_config_editor“ ins Spiel.

Mit „mysql_config_editor“ kann man den Datenbank-Zugang in einer verschlüsselten Datei namens “.mylogin.cnf“ im eigenen Home-Verzeichnis ablegen. Andere MySQL-Programme können die dort abgelegten Daten dann auslesen. Auf diese Weise steht im Programmaufruf nur ein Verweis auf diese Konfiguration und nicht Nutzername und Passwort selber. Wer die Datei lesen darf, hat dann natürlich wiederum Zugang zur Datenbank. Aber wer meine Dateien lesen darf auf dem Server, hat sowieso Zugang zu allem, denn die Zugangsdaten stehen für WordPress ja auch in einer Konfigurationsdatei.

Vorgehen für das Backup

Zuerst legen wir pro Datenbank eine verschlüsselte Konfiguration an. Dazu einfach über Putty (SSH) aufrufen:

mysql_config_editor set --login-path=appdb_02 --host=mysql5.freudendahl.net --user=db123456_1 --password

Hier müsst ihr folgendes angeben: Einen beliebigen kurzen Namen als „login-path“ (dieser wird dann später im mysqldump-Aufruf stehen), den Servernamen für den MySQL-Server (bei domainfactory bestehend aus MySQL-Version und der Domain) und den Nutzernamen für den Zugriff auf die Datenbank. Letzteres ist auch der Datenbank-Name. Dann natürlich das Passwort noch eingeben. Im Home-Verzeichnis sollte nun die “.mylogin.cnf“-Datei auftauchen. Es ist eine versteckte Datei, deswegen sieht man sie nur mit „ls -lsa“.

Nun brauchen wir einen Ordner für die Backups. Wichtig: Legt diesen Ordner nicht an einer Stelle an, wo er über das Web erreichbar ist! Bei domainfactory sieht die Ordnerstruktur z.B. so aus: Alle meine Dateien befinden sich unterhalb von „/kunden/123456_04711″, alle meine Webseiten-Daten liegen wiederum im Unterorder „/kunden/123456_04711/webseiten“. Der Ordner für die Backups kommt nun auf die oberste erreichbare Ebene neben „webseiten“, also unter „/kunden/123456_04711/backups“. Damit kann sich niemand die dort abgelegten Dateien einfach aus dem Netz herunterladen.

Erstellt euch diesen Ordner und legt darin die Scriptdatei für das Backup ab. Ihr könnt wahlweise ein Shellscript schreiben oder das z.B. mit PHP lösen. Da mir die Befehle zum Durchrotieren der Backups (Wochentag im Dateinamen) in PHP geläufiger sind, habe ich diesen Weg gewählt. Da sich der Ordner außerhalb des Web-Roots befindet, habe ich noch eine php.ini-Datei dazugelegt, da sich bei mir sonst relativ zufällig eine php.ini-Datei von einer meiner Webseiten auswirkte, was nicht gewünscht war. Das Script „db_backup.php“ sieht so aus:

<?php
# Backups erstellen für diese Datenbanken:
$db_configs = array(
	'appdb_01' 	=> 'db123456',
	'appdb_02' 	=> 'db123456_1'
);

# DB-Dump ausführen
foreach ( $db_configs as $config_name => $db_name )
{
	# Kommando ausführen
	$target_file = __DIR__ . "/db_{$db_name}_" . strtolower( date( 'D' ) ) . '.sql.gz';
	$command = "mysqldump --login-path={$config_name} --host=mysql5.freudendahl.net --default-character-set=utf8"
		. "	{$db_name} | gzip > {$target_file}";
	$output = array();
	$return_var = null;
	exec( $command, $output, $return_var );
	print_r( $output );
	
	# Dateigröße ermitteln
	$bytes = filesize( $target_file );
	$available_sizes = array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
	$factor = floor( ( strlen( $bytes ) - 1 ) / 3 );
	$size = sprintf( "%.2f", $bytes / pow( 1024, $factor ) ) . @$available_sizes[$factor];
	
	# Ergebnis loggen	
	echo "Backup done for {$db_name}: {$return_var} (file size: {$size})\n";
	if ( $return_var !== 0 )
	{
		error_log( "Fehler bei Backup für {$db_name}: {$return_var}" );
		exit( 1 );
	}
}
exit( 0 );

In meinem Fall werden hier zwei Datenbanken in jeweils eine Datei gedumpt. Der Dateiname enthält den Wochentag, d.h. am achten Tag wird die Datei des ersten Tages überschrieben. Auf diese Weise hat man mehr als einen Tag rückwärts ein Backup vorliegen, ohne sich die Platte zu sehr zuzumüllen oder manuell Backups entfernen zu müssen.

Anzupassen ist bei euch folgendes: In das Array $db_configs tragt ihr den vorher vergebenen Login-Path ein, gemappt auf den Namen des Datenbank-Nutzers. In dem $command-Aufruf müsst ihr zudem den Namen des MySQL-Servers eintragen. Der Aufruf zippt die Datei im übrigen, damit sie nicht unnötig Platz verbraucht. Entpacken kann man die entstehende .gz-Datei z.B. mit dem Programm 7zip.

Die Lösung für die Domainfactory-Umgebung

Das entstandene Script kann man einfach über SSH ausführen und wird dann ein passendes Backup bekommen. Nun trägt man das Script im Interface von domainfactory als Cronjob ein, testet diesen und stellt fest, dass die erhaltene Datei 0 KB groß ist (bzw. 4 KB wegen Overhead des Zippens). Was ist passiert? Irgendwie klappt die Benutzung des MySQL-Passworts über die “.mylogin.cnf“ nicht.

Die Ursache liegt wohl darin, wie mit diesem „Fake-Cron“ bei domainfactory die Cronjobs ausgeführt werden. Irgendwie laufen die Cronjobs als mein SSH-Nutzer (entstandene Dateien gehören jedenfalls diesem Nutzer), aber nicht so, dass MySQL versteht, in welcher “.mylogin.cnf“-Datei es das Passwort auslesen soll. Genauer kann ich das leider ohne Detailkenntnisse des DF-Systems nicht beschreiben.

Nach ein, zwei Mails mit dem wie immer hilfreichen Support von domainfactory habe ich nun folgendes an der obigen Lösung verändert:

1. Ich habe im DF-Backend nicht mehr direkt die PHP-Datei als Cronjob eingetragen, sondern ein daneben liegendes Shellscript „cronjobs.sh“ mit folgendem Inhalt:

#!/bin/bash
/usr/bin/env -i /usr/local/bin/php7-71LATEST-STANDARD -c /kunden/123456_04711/backups/php.ini /kunden/123456_04711/backups/db_backup.php

Dieser Aufruf sorgt dafür, dass das Script mit einer frischen Umgebung ohne fremde Umgebungsvariablen aufgerufen wird (wegen dem „-i“). Außerdem verwende ich eine definierte PHP-Version und gebe die vorhin mit angelegte php.ini-Datei explizit an (sicher ist sicher). Letztere kann man sich übrigens über das DF-Backend zusammenklicken, herunterladen und ggf. noch um eigene Einträge z.B. zum Errorlogging ergänzen. Dass hier eine PHP-Version hartcodiert ist, gefällt mir dagegen nicht. Bei Gelegenheit muss ich noch herausfinden, wie ich das durch „PHP7-STABLE“ ersetzen kann. Update: Ok, habe das nach dem hilfreichen Hinweis unten so angepasst, dass hier keine spezifische PHP-Version mehr steht.

2. Und jetzt zur wichtigsten Änderung: Das Programm „mysqldump“ lässt sich auch durch die neue Art des Aufrufs nicht dazu bringen, die “.mylogin.cnf“-Datei korrekt auszulesen. Deswegen braucht es im PHP-Script (ganz oben) noch einen Trick: Man kann „mysqldump“ über eine Umgebungsvariable einen konkreten Ort für die Datei mitgeben.

# für die Ausführung via Cron noch den Pfad zur .mylogin.ncf-Datei setzen
putenv( "MYSQL_TEST_LOGIN_FILE=/kunden/123456_04711/.mylogin.cnf" );

Dem Namen und dem dämlichen Handling als Umgebungsvariable anstatt als Aufruf-Parameter nach zu urteilen ist das nur für Testzwecke gedacht. Es funktioniert jedoch und steht auch unter Shared-Hosting-Bedingungen nur dem eigenen Script zur Verfügung. Insofern habe ich damit kein Problem. Wichtig ist aber natürlich, dass auf der cnf-Datei im Home-Verzeichnis niemand außer dem eigenen Benutzer Leserechte hat, sonst könnte das ja auch ein anderer Nutzer auf der gleichen Servermaschine aufrufen!

Und siehe da: Mit diesen Änderungen klappt nun auch der Aufruf als Cronjob! Fortan muss man natürlich auch dran denken, diese Backups ab und zu mal herunterzuladen. Wessen finanzielle Existenz an seiner Webseite hängt, wird das sicher anders lösen. Aber für eine Hobbyseite mit wenigen Beiträgen pro Monat reicht es völlig, einmal im Quartal das Backup auf dem Laptop zu sichern. Die WordPress-Uploads sind dann ebenfalls manuell herunterzuziehen, da dieses Backup nur die Datenbank erfasst.

10 Gedanken zu „DB-Backup per Cronjob bei Domainfactory

  1. Die aktuelle php-Version 7.1 kann hiermit aufgerufen werden:

    /usr/local/bin/php7-71LATEST-STANDARD

  2. Vielen Dank für die Information! Ich habe das getestet und oben mit eingebaut.

  3. Hallo Johannes, vielen Dank für das Script – es hat mir bei meinem Projekt sehr weitergeholfen. Eine kurze Verständnisfrage noch:

    Welche Funktion erfüllt die Variable „$output“?
    Wenn ich sie mit echo ausgebe, wird nur ein leeres Array zurückgegeben.

    Danke & VG

  4. In $output steht theoretisch, was das per exec aufgerufene Script ausgegeben hat, falls es denn etwas ausgibt. Ich habe die Ausgabe jetzt mal zu print_r( $output ) angepasst, da ein direktes echo auf einem Array natürlich nicht funktioniert. Danke also für den Hinweis!

  5. Hallo,

    vielen Dank für dafür. Ich habe auch ein paar Seiten bei DF gehostet. Für ein aktuelles Projekt muss ich jede Nacht 2 Tabellen als CSV speichern, die ich dann automatisch per FTP abholen möchte (in die Warenwirtschaft) und auch 2 Tabellen wieder hochladen.
    Mit der Methode müsste ich doch auch statt dem dump ein beliebiges SQL-Statement absetzen können und in eine Datei exportieren können?!
    „Select * from Tabelle“…

    danke
    alex

  6. mysqldump ist erst mal ein eigenständiges Programm, dem man nicht einfach eine andere Query mitgeben kann. Für Deinen Anwendungsfall erstellst Du vielleicht doch besser ein eigenes PHP-Script, das z.B. per PDO mit der Datenbank kommunizieren könnte. Dann hat man mehr Kontrolle darüber, was wann und wie passiert und kann z.B. auch eine Fehlerbehandlung einbauen.

  7. Hallo!

    Danke für die super Anleitung – die ist sehr verständlich und genau geschrieben! Trotzdem stoße ich leider immer wieder zu einem Problem – Bei mir wird das SQL Backupfile nachwievor leer erstellt. Das putenv habe ich natürlich auch miteingebaut.
    Ich teste gerade mit einer Datenbank die gerade mal 8 MB groß ist. Das direkte Backup bei der DF geht durch.
    Das Script wird aufgerufen, wie gesagt ein Backupfile erstellt, aber ohne Inhalt. Nachdem ich nun schon verzweifelt richtig viel versucht habe, bitte eine Frage:
    Funktioniert das bei Dir noch einwandfrei, oder gab es eine weitere Änderung?

    Danke! LG Yoshi

  8. Hallo!

    Ich bin gerade von domainfactory weg gewechselt, aber das oben beschriebene Setup hatte bis zuletzt gut funktioniert.

    So auf Anhieb fällt mir nur ein, mal zu probieren, was passiert wenn Du das Script A) selber auf der Konsole aufrufst und B) den Cron-Job über die Oberfläche manuell anstößt, im Unterschied zu C) nämlich der zeitgesteuerten Ausführung durch DF. Soweit ich mich erinnere, hatte das teilweise verschiedene Resultate. Wenn es bei direktem Aufruf geht, ist das Script schon mal an sich nicht kaputt. Wenn es bei manuellem Start des Cron-Jobs geht, aber nicht in der zeitgesteuerten Version, dann liegt es an der komischen Art, wie DF die Cronjobs startet. Ich hatte dabei das Problem, dass willkürlich irgendeine der php.inis meiner Webseiten benutzt wurde, bis ich dann lieber eine eigene mitgegeben habe. Vielleicht ist es ja so ein Effekt?

    Viele Glück und Grüße,
    Johannes

  9. Hallo Johannes!

    Danke für Deine Antwort – ich habe es mittlerweile geschafft 🙂

    Nach einigen Test kann ich noch folgende Infos zur sehr guten Anleitung dazugeben:
    Als es nicht funktioniert hat habe ich den Benutzernamen und das Passwort mal hartkodiert in das command mysqldump eingetragen. Damit wusste ich nun dass der Cronjob grundsätzlich richtig startet und das php Script auch richtig ausgeführt wird. Also dachte ich mir, muss der Fehler in der Richtung des erstellten .mylogin.cnf liegen.
    Also fing ich mit der weiteren Fehlereingrenzung an.
    Zum einen habe ich dem command mysqldump noch –log-error=backerror.log hinzugefügt. Dadurch konnte ich feststellen, dass die .mylogin.cnf nicht richtig ausgelesen wurde. (Es wird direkt im Ordner ein Textfile backerror.log erzeugt)
    Einerseits hatte ich einen Fehler bei den Rechten des Files .mylogin.cnf. Das File hat bei mir nun die Berechtigung 0600. Damit funktioniert das schonmal.
    Zum anderen hatte ich trotzdem das Problem, dass der Login über das .mylogin.cnf nicht funktioniert hat. (Im Fehlerfall wird eben dieses leere Backup-File erstellt)
    Als Lösung habe ich der Datenbank ein neues Passwort gegeben, das .mylogin.cnf nochmal neu erstellt, und siehe da – es funktioniert! 🙂

    Auch einen Zusatz möchte ich noch unterbringen:
    Als zweite Sicherung wollte ich eines der Tages-Backups noch in einen anderen Ordner wegsichern. Hierzu habe ich einfach einen zweiten Cronjob eingerichtet, welcher einmal in der Woche ein Tagesbackupfile in einen zweiten Backupordner kopiert, und zum Filenamen noch das Datum hinzufügt.

    Hierzu habe ich ein move.sh erstellt – auf dieses wird dann der Cronjob verwiesen.
    Hier der Inhalt des Files:

    #!/bin/sh
    cp -a
    /kunden/Ordner Kundennummer/Ordner des Tagesbackups/Filename (z.B. db_db#_wed.sql.gz)
    /kunden/Ordner Kundennummer/Ordner des Wochenbackups/Filename (z.B. db_db#_wed.sql.gz)

    mv /kunden/Ordner Kundennummer/Ordner des Wochenbackups/Filename (z.B. db_db#_wed.sql.gz)
    /kunden/Ordner Kundennummer/Ordner des Wochenbackups/$(date +%F-%T)Filename (z.B. db_db#_wed.sql.gz)

    Was passiert hier:
    Mit dem cp -a kopiere ich die Tagessicherung vom Mittwoch (_wed.sql.gz) in einen zweiten Wochenbackup Ordner.
    Danach mit dem mv Befehl benenne ich den Dateinamen um. Mit dem Zusatz $(date +%F-%T) wird das aktuelle Datum mit Uhrzeit dem Namen hinzugefügt.

    Das Script führe ich dann natürlich erst am Donnerstag jede Woche über den Cronjob aus.

    OK, soweit meine Info von meinem Test.

    Ich hoffe damit ist die Anleitung noch für weitere Suchende um einen interessanten Aspekt erweitert. Vermutlich gäbe es noch einen einfacheren Weg, ich für meinen Teil war dann einfach froh, dass alles funktioniert hat 🙂

    Danke nochmal!
    LG Yoshi

  10. Vielen Dank für diese ausführlichen Infos! Freut mich, dass es mit dem Backup nun klappt. 🙂

Schreibe einen Kommentar

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

Bitte beachte die Kommentarregeln: 1) Kein Spam, und bitte höflich bleiben. 2) Ins Namensfeld gehört ein Name. Gerne ein Pseudonym, aber bitte keine Keywords. 3) Keine kommerziellen Links, außer es hat Bezug zum Beitrag. mehr Details...

So, noch mal kurz drüber schauen und dann nichts wie ab damit. Vielen Dank fürs Kommentieren! :-)