Microsoft SQL Server mit Docker in Linux-Containern installieren

https://oer-informatik.de/docker-install-mssql

tl/dr; (ca. 20 min Lesezeit): Datenbankserver lassen sich auf so ziemlich jedem Betriebssystem aufsetzen. Es ist jedoch häufig wichtig, unterschiedliche Server, definierte Datenbestände oder reproduzierbare Systemumgebungen auf Knopfdruck zu erstellen: für diese Anwendungsfälle spielen Container eine herausragende Rolle. Im folgenden Infotext wird kurz beschrieben, wie ein Container mit einem MS-SQL-Server aufgesetzt und benutzt werden kann. Es werden Voraussetzungen für den Container zusammengetragen, die nötigen Dateien erstellt und Befehle genannt. Am Ende können wir individuelle versionierbare Datenbanken auf Knopfdurck zur Verfügung stellen. Oder, um es zu buzzworden: “Infrastructure as Code”.

Voraussetzungen

Hilfreich (aber nicht zwingend) für dieses Tutorial sind Grundkenntnisse im Umgang mit Docker. Wir benötigen ein Terminal nach Wahl (Powershell, Bash…), eine lauffähige Docker-Umgebung. Ich gebe die Befehle, die auf dem Host-System ausgeführt werden immer in Powershell-Notation an (mit führendem PS>), die Befehle, die im Container abgesetzt werden, mit CT:/$. Ich hoffe, dass erleichtert die Zuordnung auch bei allen, die (sinnvollerweise) in beiden Fällen Linux nutzen.

Zu Beginn: ein Einzeiler - und fertig.

Das schöne an SOcker ist ja, dass alles so einfach ist. Einen Container mit einer aktuellen lauffähigen MS-SQL-Installation erzeuge und starte ich mit folgendem Einzeiler:

Das sind viele Argumente, die alle wichtig sind. Werfen wir einen genaueren Blick darauf:

Option Bedeutung
--name XYZ
--hostname=XYZ
setzt den Containernamen auf XYZ (individualisierbar); sinnvoll ist Zahl am Ende des Containernamens, um mehrere Container des gleichen Images unterschieden zu können. Hostname setzt den internen Namen des Containers und sollte gleichlautend sein.
-d detach: wird im Hintergrund weiter ausgeführt
-p 3309:1433 Der Port 1433 des Containers wird auf den Port 3309 des hosts gemappt.1
-e ACCEPT_EULA=Y Die Lizenzabfrage wird automatisch akzeptiert
-e MSSQL_SA_PASSWORD=_hier%1GutesPWnu+2en setzt eine (leicht auslesbare) Umgebungsvariable für das SQL-Server-Passwort (muss den MS-Regeln entsprechen). (Achtung: alte Bezeichnung war SA_PASSWORD). Sicherheitsrisiko: kann ausgelesen werden und sollte geändert werden!
mcr.microsoft.com/mssql/server:2022-latest nutzt das neuste Image des SQL-Servers; es können auch konkrete Versionen per tag übergeben werden, z.B. statt 2022-latest auch latest, dann werden ggf. auch neue Versionen geladen (die ggf. inkompatibel sind), Übersicht der Versionen im Docker Hub zu MS SQL-Server

Es wird eine Containerinstanz erzeugt, die im Hintergrund läuft. Über die Option -d wird der Container als detached ausgeführt.

Der Rückgabewert dieses Befehls (in meinem Fall 631509...) ist ein Hashwert, über den der Container identifizierbar ist. Im UML-Sequenzdiagramm sieht der Ablauf etwa wie folgt aus:

UML-Sequenzdiagramm: Suche des Images in der DockerEngine und Registry und Instanziierung des Containers
UML-Sequenzdiagramm: Suche des Images in der DockerEngine und Registry und Instanziierung des Containers

Die Ausgabe des obigen docker run-Befehls sieht bei erstmaliger Ausführung etwa so aus:

Zunächst startet der Container und in diesem der SQL Server. In der folgenden Übersicht sollte unter Status “Up” erscheinen.

CONTAINER ID        IMAGE                                   COMMAND                  CREATED             STATUS              PORTS                    NAMES
631509e7b033        mcr.microsoft.com/mssql/server:latest   "/opt/mssql/bin/perm…"   About an hour ago   Up About an hour    0.0.0.0:3309->1433/tcp   mssql22-1

Container und SQL-Server konfigurieren

Der Container mit MS-SQL-Server läuft nun im Hintergrund. Zunächst sollte das Passwort geändert werden, da es sowohl in der Prozesshistorie des Host-Systems als auch in den Umgebungsvariablen des Containers gespeichert wurde. Mit einem einfachen Befehl kann man es dem Container entlocken:

Für unsere ersten Gehversuche ist das natürlich ganz praktisch, dass wir das Passwort in einer Umgebungsvariablen haben. Für Produktivsysteme ist das aber ein No-Go. Wir müssen also einen SQL-Befehl absetzen, um das Passwort zu ändern. Praktisch, SQL-Befehle wollten wir ja ohnehin nutzen. Da wir die Eingabe der Konsole nutzen wollen, kommt es drauf an, aus welchem System heraus wir arbeiten. Es wird interaktiv zunächst nach dem alten, dann nach einem neuen Passwort gefragt.

In der Powershell/Windows:

In der Bash/Linux,OSX:

sh:/$ docker exec -it mssql22-1 /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "$(read -sp "Enter current SA password: "; echo "${REPLY}")" -Q "ALTER LOGIN SA WITH PASSWORD=\"$(read -sp "Enter new SA password: "; echo "${REPLY}")\""

Wenn man diesen Befehl analysiert, lernt man eine Menge über Wege, den SQL-Server-Container zu administrieren.

Eine Linux-Kommandozeile, die ein Programm (oben: sqlcmd) ausführt erhält man mit:

  • -it sorgt dafür, dass die Konsole interaktiv und im Vordergrund bleibt (auf Eingaben wartet)

  • mssql22-1 ist der Containername und muss ggf. angepasst werden

  • statt dem SQL-Kommandozeilentool /opt/mssql-tools/bin/sqlcmd kann jeglicher im Container vorhandene Befehl genutzt werden (z.B. auch bash)

alle weitern Argumente legen die Optionen für sqlcmd fest:

  • -S localhost: das Tool soll den lokalen SQL-Server nutzen

  • -U SA: der Username lautet SA (Standard bei MS-SQL)

  • -P : das Anmeldepasswort, hier etwas tricky: die folgenden Befehle fragen es von der Konsole ab und setzen es hier ein.

    • in der Shell/*nix: "$(read -sp "Enter current SA password: "; echo "${REPLY}")"

    • in der Powershell/Windows: "$(read-host "Enter current SA password")"

  • -Q: diese Query (SQL-Befehl) soll ausgeführt werden. Es wird doch nicht einfach die Query mit Passwort übergeben, etwa "ALTER LOGIN SA WITH PASSWORD='text', sondern wieder (wie oben) das Passwort über die jeweilige Konsole abgefragt.

SQL Befehlszeile im Container öffnen

Der Befehl zum Passwortändern zeigt auch unmittelbar einen Weg, um die SQL-Commandozeile zu erhalten. In der Container-Bash:

CT:/$ /opt/mssql-tools/bin/sqlcmd -S localhost -U SA

Nach Eingabe des Passworts kann man zeilenweise SQL-Befehle eingeben und mit go abschließen:

1> SELECT Name from sys.Databases
2> go
Name
--------------------------------------------------------------------------------------------------------------------------------
master
tempdb
model
msdb
testDB

(5 rows affected)

En dieser Stelle könnten jetzt die Befehle folgen, die eigene Datenbanken erzeugen (CREATE DATABASE...), mit Tabellen befüllen (CREATE TABLE...), diese mit Datensätzen versehen (INSERT INTO ...) und schließlich auswerten (SELECT). Wenn alle Befehle mit go abgeschlossen werden hat man bald eine befüllte nutzbare Datenbank. Aber in der Konsole ist das aber alles etwas mühsam. Wir brauchen ein Frontend.

Verbindung mit einem DB-Frontend aufbauen

Der so eingerichtete und gestartete Container kann direkt über ein Frontend (wie HeidiSQL, DBeaver o.ä.) genutzt werden. Die Einstellungen sind eigentlich in allen Frontends identisch:

  • Hostname: 127.0.0.1 (die lokale Maschine)

  • Port: 3309 (wie oben angegeben - ggf. anpassen!)

  • Benutzername: sa (SQL-Server-Standard)

  • Passwort: (wie oben eingegeben)

HeidiSQL-Konfiguration für MS-SQL
HeidiSQL-Konfiguration für MS-SQL

Schick. Mit dem SQL-Frontend können wir jetzt schon ganz gut arbeiten. Insbesondere, wenn Daten nur lesend verwendet werden oder immer gleiche Testdaten genutzt werden sollen, wäre es sehr praktisch, wenn wir direkt eine gefüllte Datenbank erhalten könnten.

Das ganze geht natürlich auch mit dem MS-SQL-Managementstudio. Hier muss aus unerfindlichen Gründen der Port mit Komma abgetrennt werden:

Login-Maske des MS-Managementstudios erwartet IP und Port durch Komma getrennt: 127.0.0.1,3309
Login-Maske des MS-Managementstudios erwartet IP und Port durch Komma getrennt: 127.0.0.1,3309

Einen Docker-Container erzeugen, der bereits Daten enthält

Mit dem obigen Beispiel wurde ein Container mit einer leeren Datenbank erzeugt. Es wäre jedoch schön, wenn wir eine Möglichkeit hätten, direkt einen befüllten Container zu erzeugen. Dazu müssen wir ein eigenes Docker-Image erstellen. Hierzu wird eine Art Anleitung erstellt, was alles in das Image aufgenommen werden soll - diese Anleitung nennt sich Dockerfile. Angelehnt an das Beispiel hier benötigen wir in einem Verzeichnis insgesamt drei Dateien:

  • Dockerfile: Diese Datei beschreibt, welche Komponenten in dem Image enthalten sein sollen

  • entrypoint.sh: Legt fest, was beim Containerstart passieren soll. (MS-SQL starten und configure.sh aufrufen). Diese Datei wird automatisch beim Erstellen des Containers gestartet

  • configure-db-sh: Prüft, ob der MS-SQL-Server gestartet ist und startet dann den Import von setup.sql

  • setup.sql: Enthält die SQL-Befehle, die zu Beginn ausgeführt werden sollen (also den DB-Dump)

Der Bauplan des Images: Dockerfile

In der Datei namens Dockerfile wird festgelegt auf welcher Basis mit welchen Anpassungen das Image erstellt wird. Die Kommentare erklären hoffentlich das wesentliche. Das File kann auch hier direkt heruntergeladen werden.

Inhalt der Datei Dockerfile:

Die Importroutine configure-db.sh

Die configure-db.sh ist auf den aktuellen MS-SQL-Server zugeschnitten. Sie prüft nach 20s alle 10s, ob der SQL-Server schon online ist und importiert dann den Inhalt aller *.sql-Dateien im aktuellen Ordner.

Das File kann auch hier direkt heruntergeladen werden.

Inhalt der Datei configure-db.sh:

#!/bin/bash
# customized from https://github.com/microsoft/mssql-docker/blob/master/linux/preview/examples/mssql-customize/
# added ability to import multiple sql-files

DBSTATUS=1       # 0 means all DBs are online
ERRCODE=1        # 0 means no errors
LOOPCOUNT=0      # how many 10s sequences are needet to boot DBMS?
LOOPCONDITION=1  # becomes 0 if DBMS is reachable

echo ""
echo ""
echo "[entrypoint SQL] Importscript for SQL-Files"
echo "#=========================================="
echo ""
echo "Waiting 20s for DB to start..."
sleep 20
echo "[entrypoint SQL] Trying to connect to DB prior to start imports"


while [ ${LOOPCONDITION} -eq 1 ]; do
        if [ ${LOOPCOUNT} -lt 20 ]; then      # trying max 20 times (200s)
            LOOPCOUNT=$((LOOPCOUNT+1))       # incrementing no of loops

            # This query checks, whether all db are online, see
            # https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-databases-transact-sql?view=sql-server-2017
            DBSTATUS=$(/opt/mssql-tools/bin/sqlcmd -h -1 -t 1 -U sa -P "$MSSQL_SA_PASSWORD" -Q "SET NOCOUNT ON; Select SUM(state) from sys.databases")
            ERRCODE=$?  # in case of errors
            echo "[entrypoint SQL] Checked DBMS: Loop ${loopcount} Waiting for DBs: ${DBSTATUS} / Error-Code ${ERRCODE} (0 means ok)"
        else
            echo "[entrypoint SQL] Tried 20 times (200 seconds) - Imports aborted"
            exit 1
        fi

        if [ ${DBSTATUS} -eq 0 ] && [ ${ERRCODE} -eq 0 ]; then
            LOOPCONDITION=0
            echo "Everthing seems fine - starting Import"
        else
            echo "[entrypoint SQL] Wait another 10s"
            sleep 10
        fi
done
echo ""
echo "#==================================================="
echo "Trying to gather all SQL-files in working-directory."
echo ""
for FILENAME in ./*.sql; do  #Regex Filterung nach Dateiendung und Sortierung
   echo ""
   echo "[entrypoint SQL] File ${FILENAME}"
   echo "----------------------------------------"
   echo "Found file ${FILENAME}, sanitizing name (whitespaces)"
   FILENAME=$(echo "${FILENAME}" | sed 's/ /\\ /g')
   echo "Importing file as ${FILENAME}"
   /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -d master -i "${FILENAME}"
   echo "Finished import of ${FILENAME}"
done

echo ""
echo ""
echo "[entrypoint SQL] All imports finished - container status should change to healthy now"
echo "#==================================================================="

Das Startscript entrypoint.sh

Gegenüber den recht langen Dateien Dockerfile und configure-db.sh ist die entrypoint.sh unspektakulär: sie startet nur die Importdatei und den SQL-Server. Auch dieses File kann auch hier direkt heruntergeladen werden.

Inhalt der Datei entrypoint.sh:

Die Importdatei setup.sql

Schließlich wird noch eine SQL-Datei benötigt, die alle Befehle für Datenstrukturen (DDL) und Dateninhalte (DML) enthält. Sie muss die Dateiendung *.sql haben und im aktuellen Ordner liegen. Wenn mehrere Dateien exitieren, werden sie in alphabetischer Reihenfolge importiert. Ein einfaches Beispiel ist hier abgedruckt, es kann aber natürlich jeder MS-SQL-Datenbankdump verwendet werden. Auch dieses File kann hier direkt heruntergeladen werden.

Inhalt der Datei setup.sql:

Bauen des ersten eigenen Docker-Images:

Wenn sich alle vier Dateien (und die eigene Konsole) in einem Verzeichnis befinden kann das neue Image (hier mit dem von mir vergebenen Namen mssql-custom) über den folgenden Befehl erstellt werden (der Punkt am Ende ist wichtig!):

Wie so häufig verrät uns die Ausgabe eine ganze Menge über die Hintergründe von Docker:

[+] Building 4.1s (12/12) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                     0.1s
 => => transferring dockerfile: 32B                                                                                                                      0.0s
 => [internal] load .dockerignore                                                                                                                        0.1s
 => => transferring context: 2B                                                                                                                          0.0s
 => [internal] load metadata for mcr.microsoft.com/mssql/server:2022-latest                                                                              0.0s
 => [1/7] FROM mcr.microsoft.com/mssql/server:2022-latest                                                                                                0.0s
 => [internal] load build context                                                                                                                        0.1s
 => => transferring context: 781B                                                                                                                        0.0s
 => CACHED [2/7] RUN apt update && apt install -y vim nano                                                                                               0.0s
 => CACHED [3/7] RUN mkdir -p /usr/config                                                                                                                0.0s
 => CACHED [4/7] WORKDIR /usr/config                                                                                                                     0.0s
 => [5/7] COPY . /usr/config                                                                                                                             0.2s
 => [6/7] RUN chmod +x /usr/config/entrypoint.sh                                                                                                         1.8s
 => [7/7] RUN chmod +x /usr/config/configure-db.sh                                                                                                       1.0s
 => exporting to image                                                                                                                                   0.5s
 => => exporting layers                                                                                                                                  0.3s
 => => writing image sha256:2726f94e6126e3f6c109b8ff9ac72f60c1dbcd780582978c9076e5c2911bddca                                                             0.0s
 => => naming to docker.io/library/mssql-custom                                                                                                          0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

Wir erkennen sieben Zeilen aus dem Dockerfile, die jeweils durchnummeriert (bis [7/7]) dargestellt werden. Jede dieser Zeilen stellt eine Schicht, einen Layer dar, der für andere Images wiederverwendet werden könnte, deren Dockerfile sich bis dahin gleicht. Das ist einer der Gründe, warum Docker so sparsam mit Ressourcen umgehen kann. Alle Layer werden am Ende zu dem neuen Image (wie gewünscht benannt: mssql-custom)zusammengefügt.

Das Image wurde erstellt und sollte nun per docker image ls angezeigt werden.

REPOSITORY                       TAG           IMAGE ID       CREATED          SIZE
mssql-custom                     latest        4e06b70b0d05   22 seconds ago   1.47GB

Ab hier wie immer: Einen Container erstellen und starten

Wir haben ein Image (mssql-custom), wir benötigen einen laufenden Container. Das kennen wir ja schon. Wir müssen nur darauf achten, dass der Name (ich wähle mssql22c1) und der externe Port (ich wähle 3314) ein anderer ist wie bei unseren anderen Containern.

Relativ zügig wird auch der Hashwert zurückgegeben, der Container scheint also gebaut und zu starten.

f9452a082fa65645d02897527bfeef2a0cb35da0303bc9244f27acc1b262479d

Dem Container beim Starten zuschauen

Im Inneren des Containers startet jetzt MS-SQL - und wenn das online ist, wird der Import gestartet. Dass der Container startet erkennen wir an docker ps (oder mit der Langform des Befehls: docker container ls):

CONTAINER ID   IMAGE                                        COMMAND                  CREATED         STATUS                            PORTS                    NAMES
f9452a082fa6   mssql-custom                                 "./entrypoint.sh"        6 seconds ago   Up 4 seconds (health: starting)   0.0.0.0:3314->1433/tcp   mssql22c1
0f838125b001   mcr.microsoft.com/mssql/server:2022-latest   "/opt/mssql/bin/perm…"   5 hours ago     Up 5 hours                        0.0.0.0:3309->1433/tcp   mssql22-1

In der Spalte STATUS wird auch ausgegeben, dass der von uns eingebaute HEALTHY-Test im Dockerfile wohl noch nicht erfüllt wurde. Das System scheint noch zu starten. Wenn wir es etwas genauer wissen wollen, dann schauen wir mal in die Log-Dateien des Containers:

Importiere SQL-Kommandos
Starte SQL-Server


[entrypoint SQL] Importscript for SQL-Files
#==========================================

Am Anfang erkennen wir nur die Meldungen aus entrypoint.sh und configure-db.sh. Nach einiger Zeit (bei mit ca. 32s) versucht er das erste Mal, die SQL-Dateien zu importieren, aber MS-SQL ist noch nicht gestartet:

[entrypoint SQL] Trying to connect to DB prior to start imports
[entrypoint SQL] Checked DBMS: Loop  Waiting for DBs:           0 / Error-Code 0 (0 means ok)
Everthing seems fine - starting Import

Dann folgen nach ca. 40s zahllosen MS-SQL-Meldungen, an deren Ende nach ca. 1min die Erfolgsmeldung des Imports steht:

[entrypoint SQL] File ./setup_importtest_mssql.sql
----------------------------------------
Found file ./setup_importtest_mssql.sql, sanitizing name (whitespaces)
Importing file as ./setup_importtest_mssql.sql
...
(8 rows affected)
Finished import of ./setup_importtest_mssql.sql


[entrypoint SQL] All imports finished - container status should change to healthy now
#===================================================================

Acht Datenzeilen wurden importiert (8 rows affected).

Der Container soll jetzt healthy sein, schauen wir doch mal nach:

CONTAINER ID   IMAGE                                        COMMAND                  CREATED         STATUS                   PORTS                    NAMES
f9452a082fa6   mssql-custom                                 "./entrypoint.sh"        3 minutes ago   Up 3 minutes (healthy)   0.0.0.0:3314->1433/tcp   mssql22c1

Mit angepasster Port-Bezeichnung klappt die Verbindung mit dem Frontend in einem Rutsch.

Zum Schluss: Container-Admin in 20s: ausschalten, wieder einschalten oder löschen

Irgendwann ist Feierabend und auch der Container soll schlafen. Wie das bei Containern grundsätzlich geht habe ich hier zusammengefasst, im Schnelldurchgang seien hier aber die wichtigsten Befehle nochmals genannt:

Welche Container wurden erzeugt, welche laufen und wie viel Speicher benötigen sie?

Welche Images wurden geladen und wie viel Speicher verwenden sie?

Einen laufenden Container ausschalten, um ihn später wieder zu nutzen (hier: Name mssql22c1, ginge auch per Hash/ID:

Einen gestoppten Container wieder aktivieren, um ihn am nächsten Tag weiter zu nutzen (hier: Name mssql22c1, ginge auch per Hash/ID):

Einen Container, den man nicht mehr benötigt, löschen (hier: Name mssql22c1, ginge auch per Hash/ID)

Ein Image, das man nicht mehr benötigt, löschen (hier: Name server:2022-latest)

Fazit

So langsam fängt Docker an Spaß zu machen: wir können einen Container mit einem DBMS aus vorhandenen Images erstellen, per Console darauf zugreifen, ein DB-Frontend damit verknüpfen. Das ist alles ganz nett.

Wirklich mächtig wird es aber erst dadurch, dass wir per Knopfdruck ein befülltes Datenbanksystem erhalten können. Wenn wir den Code dafür noch versionieren, haben wir alle Versprechen umgesetzt, die sich hinter dem Buzzword “Infrastructure as Code (IaS)” verstecken.

Quellen und offene Ressourcen (OER)

Die Ursprungstexte (als Markdown), Grafiken und zugrunde liegende Diagrammquelltexte finden sich (soweit möglich in weiterbearbeitbarer Form) in folgendem git-Repository:

https://gitlab.com/oer-informatik/db-sql/dbms.

Sofern nicht explizit anderweitig angegeben sind sie zur Nutzung als Open Education Resource (OER) unter Namensnennung (H. Stein, oer-informatik.de) freigegeben gemäß der Creative Commons Namensnennung 4.0 International Lizenz (CC BY 4.0).

Creative Commons Lizenzvertrag


  1. In diesen Beispielen mappe ich die Ports unterschiedlicher DBMS auf die lokalen Ports 3307-3312, da diese durch kein lokales DBMS belegt sind, aber direkt hinter dem MySQL-Standardport folgen. Jeder andere freie Port ist ebenso möglich, muss nur bei der DB-Verbindung anpasst werden.

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: