SQL Server in Docker: Was im Container schiefgeht
Sie stoppen den Container mit docker stop. Zehn Sekunden passiert nichts, dann ist er weg, Exitcode 137. Beim nächsten Start läuft eine Recovery, als wäre der Strom ausgefallen. In unserem Lab war das bei SQL Server 2022 und 2025 bei jedem einzelnen Stopp so.
SQL Server läuft im Container mit derselben Engine wie überall. Was sich ändert, ist alles drumherum: wer Signale bekommt, welche CPUs und wie viel Speicher die Engine zu sehen glaubt, wem die Dateien gehören. Die Schnellstarts von Microsoft zeigen, wie man den Container startet. Diese Seite zeigt sechs Stellen, an denen es danach schiefgeht, gemessen auf SQL Server 2017, 2019, 2022 und 2025.
In 60 Sekunden
docker stopbeendet SQL Server 2022 und 2025 nicht sauber. Das Startskript im Image gibt das Stoppsignal nicht weiter. Docker wartet zehn Sekunden und bricht dann hart ab.--initmacht es schlimmer. Sauber endet SQL Server nur, wenn das Signal direkt an densqlservr-Prozess geht.- Eine CPU-Quote (
--cpus) sieht SQL Server nicht. Er plant mit allen CPUs des Hosts. Feste CPUs (--cpuset-cpus) beachten erst SQL Server 2022 und 2025. - SQL Server 2017 und 2019 sehen das Speicherlimit des Containers nicht (auf Hosts mit cgroup v2). In einem 4-GB-Container planten sie mit 60 GB. Begrenzen Sie den Speicher immer selbst und lassen Sie Kopfraum.
- Volumes: Ab 2019 läuft SQL Server im Container nicht als root. Ab 2022 muss das Datenverzeichnis dem Benutzer mit der ID 10001 gehören, sonst startet SQL Server nicht.
- Images:
latestwandert mit jedem Update. Für reproduzierbare Umgebungen gehört der CU-Stand in den Tag. Bei 2025 ergibtMSSQL_PID=Developerdie Enterprise Developer Edition. - Bereit ist SQL Server, wenn er auf eine Abfrage antwortet, nicht wenn der Container läuft. Im Lab dauerte das 7 bis 14 Sekunden.
1 · Herunterfahren: docker stop ist kein Shutdown
docker stop schickt dem Prozess mit der Nummer 1 im Container das Signal SIGTERM, wartet eine Frist (Standard zehn Sekunden) und beendet den Container danach mit SIGKILL. Das funktioniert nur, wenn Prozess 1 das Signal versteht oder weitergibt.
In den Images von SQL Server 2022 und 2025 ist Prozess 1 nicht SQL Server, sondern ein Startskript (/opt/mssql/bin/launch_sqlservr.sh), das SIGTERM weder verarbeitet noch an sqlservr weiterreicht. Das ist bei Microsoft als Fehler bekannt (mssql-docker #968, betrifft die Images seit 2022 CU18 und 2025-latest); eine Korrektur ist für kommende CUs angekündigt, ohne Nummer. In 2022 CU27 und 2025 CU10 war sie im Lab noch nicht enthalten.
| Version | Prozess 1 | docker stop (Frist 10 s) | docker stop -t 30 | --init | SIGTERM direkt an sqlservr |
|---|---|---|---|---|---|
| 2017, 2019 | sqlservr | 10,3 s, Exit 137 | 10,4 s, Exit 0 | 10,3 s, Exit 137 | 10,3 s, Exit 0 |
| 2022 CU27 | Startskript | 10,3 s, Exit 137, Recovery | 30,3 s, Exit 137, Recovery | 0,3 s, Exit 143, Recovery | 1,1 s, Exit 0, sauber |
| 2025 CU9 und CU10 | Startskript | 10,3 s, Exit 137, Recovery | 30,3 s, Exit 137, Recovery | 0,2–0,3 s, Exit 143, Recovery | 0,7 s, Exit 0, sauber |
„Recovery“ heißt: Im Errorlog des beendeten Laufs fehlt SQL Server is terminating in response to a 'stop' request, und beim nächsten Start stehen Zeilen wie … transactions rolled forward in database …. Bestätigte Transaktionen gehen dabei nicht verloren; SQL Server holt sie aus dem Log nach. Aber jeder Stopp ist ein Abbruch, und mit großen offenen Transaktionen dauert der nächste Start entsprechend länger.
Warum --init nicht hilft: Mit --init setzt Docker einen kleinen Init-Prozess als Nummer 1 ein, der Signale weitergibt. Gemessen haben wir: Der Container ist nach 0,2 bis 0,3 Sekunden weg, ohne Stop-Meldung, und beim nächsten Start läuft die Recovery. Unsere Vermutung, nicht geprüft: Der Init-Prozess gibt das Signal an das Startskript weiter, nicht an SQL Server; das Skript endet sofort, und SQL Server endet mit ihm ohne geordnetes Herunterfahren.
Was hilft (2022 und 2025): SIGTERM direkt an den SQL-Server-Prozess schicken und warten, bis der Container endet:
# Prozessnummern von sqlservr im Container anzeigen (es gibt zwei)
docker exec mein-sql sh -c '
for p in /proc/[0-9]*; do
[ "$(cat $p/comm 2>/dev/null)" = sqlservr ] && echo ${p#/proc/}
done'
# Eine der beiden genügt (hier 9 als Beispiel); im Lab endeten beide Varianten sauber
docker exec mein-sql kill -TERM 9
docker wait mein-sqlIm Lab war SQL Server danach in 0,7 bis 1,1 Sekunden beendet, mit Stop-Meldung und ohne Recovery beim nächsten Start. Dasselbe Prinzip gilt für Orchestrierungen: Ein preStop-Schritt, der das Signal an sqlservr schickt, ersetzt das fehlende Weiterreichen. Das haben wir unter Kubernetes nicht gemessen.
SQL Server 2017 und 2019 haben das Problem nicht: Dort ist sqlservr selbst Prozess 1 und fährt auf SIGTERM geordnet herunter. Er braucht dafür aber gut zehn Sekunden, also genau die Standardfrist von Docker. Mit docker stop ohne Zusatz endete es im Lab deshalb trotzdem mit Exit 137. Geben Sie mehr Zeit: docker stop -t 30 oder beim Start --stop-timeout 30.
2 · CPU: Die Quote sieht SQL Server nicht
Docker kennt zwei Arten, CPUs zu begrenzen: eine Quote (--cpus=2, der Container bekommt Rechenzeit für zwei CPUs, verteilt auf alle) und feste CPUs (--cpuset-cpus=22-23, der Container läuft nur auf diesen beiden). Wir haben gemessen, was SQL Server jeweils sieht, auf einem Host mit 24 logischen CPUs:
--cpus) oder als feste CPUs (--cpuset-cpus).| Version | ohne Grenze | --cpus=2 | --cpuset-cpus (2 CPUs) |
|---|---|---|---|
| 2017, 2019 | 24 CPUs, 24 Scheduler | 24 CPUs, 24 Scheduler | 24 CPUs, 24 Scheduler |
| 2022, 2025 | 24 CPUs, 24 Scheduler | 24 CPUs, 24 Scheduler | 2 CPUs, 2 Scheduler |
- Die Quote sieht keine Version. Microsoft beschreibt das so; im Lab bestätigt. SQL Server legt für jede Host-CPU einen Scheduler an und plant Parallelität danach, obwohl der Container nur zwei CPUs Rechenzeit bekommt.
- Feste CPUs beachten erst 2022 und 2025. Dazu fanden wir keine Quelle; 2017 und 2019 sehen trotz Pinning alle 24.
- Die Folge ist messbar: Eine Tabellensperre mit
TABLOCKXbelegte bei 24 sichtbaren CPUs 24 Lock-Partitionen, bei zwei sichtbaren eine. Und mit der Voreinstellungmax degree of parallelism = 0darf eine Abfrage so viele Threads nutzen, wie SQL Server CPUs sieht. Wie stark das die Laufzeit unter einer Quote verschlechtert, haben wir nicht gemessen.
Was hilft: Auf 2022 und 2025 feste CPUs statt einer Quote verwenden, wenn der Host das zulässt. Auf allen Versionen MAXDOP passend zu den tatsächlich verfügbaren CPUs setzen. Microsoft nennt für Quoten zusätzlich ALTER SERVER CONFIGURATION SET PROCESS AFFINITY zusammen mit Trace Flag 8002; das haben wir nicht geprüft. CPU-Limits unter Kubernetes sind in der Regel Quoten in diesem Sinn; unter Kubernetes haben wir nicht gemessen.
3 · Speicher: Drei Grenzen, die man auseinanderhalten muss
--memorybegrenzt den ganzen Container. Wird es überschritten, beendet der Kernel den Prozess (OOM-Kill).MSSQL_MEMORY_LIMIT_MBsagt SQL Server, wie viel Speicher er insgesamt als vorhanden betrachten soll.max server memorybegrenzt innerhalb davon den Speicher der Engine.
Ohne Einstellung nimmt SQL Server laut Microsoft 80 Prozent des kleineren Werts aus Host-Speicher und Container-Grenze. Was wir auf einem Host mit cgroup v2 (dem Standard aktueller Linux-Distributionen) gemessen haben:
committed_target_kb) und gemeldeter Speicher (physical_memory_kb) im Container mit 4 GB, gemessen auf SQL Server 2017 bis 2025. 64.417 MB sind 80 Prozent des Host-Speichers.| Version | --memory=4g | 4g und MSSQL_MEMORY_LIMIT_MB=3200 | 4g und MSSQL_MEMORY_LIMIT_MB=6000 |
|---|---|---|---|
| 2017 | sieht 64.417 MB, Ziel 60.480 MB | 3.200 MB, Ziel 2.297 MB | 6.000 MB, Ziel 4.935 MB |
| 2019 | sieht 64.417 MB, Ziel 61.037 MB | 3.200 MB, Ziel 2.506 MB | 6.000 MB, Ziel 5.112 MB |
| 2022 | 3.277 MB, Ziel 2.617 MB | 3.200 MB, Ziel 2.573 MB | 4.096 MB, Ziel 3.433 MB |
| 2025 | 3.277 MB, Ziel 2.721 MB | 3.200 MB, Ziel 2.641 MB | 4.096 MB, Ziel 3.618 MB |
„Sieht“ ist der Speicher, den SQL Server als vorhanden meldet (physical_memory_kb), „Ziel“ das Speicherziel (committed_target_kb). 64.417 MB sind 80 Prozent des Host-Speichers.
- 2017 und 2019 erkennen die Container-Grenze unter cgroup v2 nicht. In einem Container mit 4 GB planen sie mit 60 GB. Sobald der Puffer-Pool wächst, endet das mit einem OOM-Kill. Microsoft nennt die Unterstützung für cgroup v2 ab SQL Server 2022 CU20.
- Ein zu hohes
MSSQL_MEMORY_LIMIT_MBist gefährlich. 2017 und 2019 übernehmen es ungeprüft. 2022 und 2025 kappen es auf die Container-Grenze, lassen dann aber keinen Kopfraum mehr für alles, was außerhalb der Engine Speicher braucht.
Was hilft: Immer beides setzen, Container-Grenze und MSSQL_MEMORY_LIMIT_MB, und dazwischen 10 bis 20 Prozent Kopfraum lassen, wie Microsoft empfiehlt. Auf 2017 und 2019 ist das Pflicht. Den OOM-Kill selbst haben wir nicht provoziert.
4 · Volumes und Rechte: Wem gehört das Datenverzeichnis?
Ab SQL Server 2019 läuft SQL Server im Container als Benutzer mssql mit der ID 10001, nicht als root. Wer ein Verzeichnis des Hosts als Datenverzeichnis einhängt, muss es diesem Benutzer zugänglich machen. Wir haben vier Varianten probiert:
| Eigentümer des Verzeichnisses auf dem Host | 2017 | 2019 | 2022 | 2025 |
|---|---|---|---|---|
| root, Rechte 755 | startet | scheitert | scheitert | scheitert |
| ID 10001, Gruppe 0, Rechte 770 | startet | startet | startet | startet |
| fremder Benutzer (4000), Rechte 700 | startet | scheitert | scheitert | scheitert |
| root, Gruppe 0 schreibbar (770) | startet | startet | scheitert | scheitert |
SQL Server 2017 läuft als root und startet mit jedem Verzeichnis. Die Variante „Gruppe 0 schreibbar“, die Microsoft für Plattformen mit zufälligen Benutzer-IDs nennt, funktionierte nur bei 2019; dort läuft mssql in der Gruppe 0, bei 2022 und 2025 in der eigenen Gruppe 10001. Beim Scheitern endet der Container sofort, und docker logs zeigt:
ERROR: Setup FAILED copying system data file 'C:\templatedata\master.mdf' to '/var/opt/mssql/data/master.mdf': 5(Access is denied.)
ERROR: BootstrapSystemDataDirectories() failure (HRESULT 0x80070005)Der Windows-Pfad und der Windows-Fehlercode 5 sind kein Druckfehler: SQL Server läuft unter Linux auf einer Abstraktionsschicht, die ihm Windows-Pfade zeigt.
Was hilft:
sudo install -d -o 10001 -g 0 -m 770 /srv/sql/data/mein-sql
docker run … -v /srv/sql/data/mein-sql:/var/opt/mssql/data …5 · Images, Tags und Editionen
latestwandert.2025-latestzeigte bei uns noch auf CU9, als CU10 schon als eigener Tag bereitstand; nach dem nächstendocker pullzeigt er woanders hin. Für Tests, die morgen dasselbe Ergebnis liefern sollen, gehört der CU-Stand in den Tag (zum Beispiel2025-CU10-ubuntu-24.04) oder das Image wird über seinen Digest festgelegt.- Ein Update ist ein neues Image. Die Datenbanken liegen auf dem Volume; der Container wird mit dem neuen Tag neu erstellt.
- Editionen bei 2025:
MSSQL_PID=Developerergab im Lab die Enterprise Developer Edition, obwohl die Tabelle in der Dokumentation diesen Wert nicht mehr nennt. Wer realistisch gegen Standard testen will, setztMSSQL_PID=StandardDeveloper(meldet Standard Developer Edition). - Nur x86-64: Die Images laufen laut Microsoft nur auf x86-64-Hosts; Emulation über Rosetta oder QEMU, etwa auf Apple Silicon, ist nicht unterstützt.
6 · Entwicklung und CI
- Startzeit: Bis SQL Server auf eine Abfrage antwortete, vergingen im Lab 7 bis 8 Sekunden (2017, 2019) bzw. 9 bis 14 Sekunden (2022, 2025). Ein laufender Container heißt noch nicht, dass SQL Server bereit ist; Tests sollten auf ein erfolgreiches
SELECT 1warten. - Das Kommandozeilenwerkzeug liegt je Version woanders: in den Images 2022 und 2025 unter
/opt/mssql-tools18/bin/sqlcmd(mit-C, weil das Zertifikat selbst ausgestellt ist), in 2017 und 2019 unter/opt/mssql-tools/bin/sqlcmd. - Sporadische Startabstürze: Microsoft hat einen seltenen Absturz kurz nach dem Start bestätigt, der vor allem in CI-Umgebungen mit vielen gleichzeitigen Starts auffiel (mssql-docker #974); behoben laut Microsoft in 2022 CU29 und 2025 CU12. Wir haben ihn nicht beobachtet.
- Das Errorlog liegt unter
/var/opt/mssql/log/errorlogund war in allen geprüften Images einfacher Text (die Dokumentation nennt UTF-16).
Lagebild: Was sieht meine Instanz?
Die folgende Abfrage zeigt, was SQL Server von seiner Plattform hält: Betriebssystem, Edition, CPUs und Scheduler, Speicher, wichtige Einstellungen. Vergleichen Sie das Ergebnis mit den Grenzen, die Sie dem Container gegeben haben.
Abfrage: Lagebild Container
/* PSG Stack · SQL Server unter Linux und Docker · Abfrage 2 Was sieht diese Instanz von ihrer Plattform? Betriebssystem, Edition, CPUs und Scheduler, Speicherziel, wichtige Einstellungen, Pfade und ob der SQL Server Agent läuft. !!! WARNUNG — bitte vor dem Ausführen lesen !!! - Nur lesend. Die Abfrage ändert keine Daten und keine Einstellung. - Rechte: VIEW SERVER STATE (ab SQL Server 2022 genügt VIEW SERVER PERFORMANCE STATE). - MAXDOP 1: Jede Teilabfrage läuft seriell und belegt keine weiteren Kerne. - Ab SQL Server 2017. Spalten, die eine Version nicht kennt, erscheinen als n/a; die Abfrage prüft dazu, ob die Spalte existiert. Geprüft im PSG-Lab auf 2017, 2019, 2022 und 2025 in Linux-Containern, als sa und ohne sysadmin (11.11.2026); unter Windows nicht geprüft. - container_type_desc meldet einen Linux-Container, sofern die Version die Spalte kennt. Vergleichen Sie cpu_count und physical_memory_mb mit den Grenzen, die Sie dem Container gegeben haben: Eine CPU-Quote (--cpus) sieht keine Version, ein CPU-Pinning (--cpuset-cpus) erst SQL Server 2022 und 2025; das Speicherlimit unter cgroup v2 sehen 2017 und 2019 nicht.*/SET NOCOUNT ON; DECLARE @sql nvarchar(max), @spalten nvarchar(max) = N''; /* 1 · Plattform */SET @sql = N'SELECT ' + CASE WHEN OBJECT_ID(N'sys.dm_os_host_info') IS NULL THEN N'N''n/a'' AS host_platform, N''n/a'' AS host_distribution, N''n/a'' AS host_release' ELSE N'h.host_platform, h.host_distribution, h.host_release' END + N', CONVERT(nvarchar(20), SERVERPROPERTY(''ProductVersion'')) AS version, CONVERT(nvarchar(30), SERVERPROPERTY(''ProductUpdateLevel'')) AS update_level, CONVERT(nvarchar(60), SERVERPROPERTY(''Edition'')) AS edition FROM ' + CASE WHEN OBJECT_ID(N'sys.dm_os_host_info') IS NULL THEN N'(SELECT 1 AS x) AS h' ELSE N'sys.dm_os_host_info AS h' END + N' OPTION (MAXDOP 1);';EXEC sys.sp_executesql @sql; /* 2 · CPU und Speicher, wie SQL Server sie sieht */SELECT @spalten = @spalten + N', ' + CASE WHEN EXISTS (SELECT 1 FROM sys.all_columns WHERE object_id = OBJECT_ID(N'sys.dm_os_sys_info') AND name = v.n) THEN N'CONVERT(nvarchar(60), i.' + v.n + N')' ELSE N'N''n/a''' END + N' AS ' + v.nFROM (VALUES (1, N'socket_count'), (2, N'cores_per_socket'), (3, N'numa_node_count'), (4, N'sql_memory_model_desc'), (5, N'container_type_desc')) AS v(k, n)ORDER BY v.k;SET @sql = N'SELECT i.cpu_count, i.scheduler_count, i.hyperthread_ratio, i.affinity_type_desc' + @spalten + N', i.physical_memory_kb / 1024 AS physical_memory_mb, i.committed_target_kb / 1024 AS committed_target_mbFROM sys.dm_os_sys_info AS i OPTION (MAXDOP 1);';EXEC sys.sp_executesql @sql; SELECT COUNT(*) AS scheduler_sichtbar_onlineFROM sys.dm_os_schedulersWHERE status = N'VISIBLE ONLINE'OPTION (MAXDOP 1); /* 3 · Einstellungen, TempDB, Agent, Pfade */SELECT (SELECT CONVERT(int, value_in_use) FROM sys.configurations WHERE name = N'max server memory (MB)') AS max_server_memory_mb, (SELECT CONVERT(int, value_in_use) FROM sys.configurations WHERE name = N'max degree of parallelism') AS maxdop, (SELECT CONVERT(int, value_in_use) FROM sys.configurations WHERE name = N'cost threshold for parallelism') AS cost_threshold, (SELECT COUNT(*) FROM tempdb.sys.database_files WHERE type = 0) AS tempdb_datendateien, CASE WHEN EXISTS (SELECT 1 FROM sys.dm_exec_sessions WHERE program_name LIKE N'SQLAgent%') THEN N'läuft' ELSE N'läuft nicht oder unsichtbar' END AS sql_server_agent, CONVERT(nvarchar(260), SERVERPROPERTY('InstanceDefaultDataPath')) AS standard_datenpfad, CONVERT(nvarchar(260), SERVERPROPERTY('InstanceDefaultLogPath')) AS standard_logpfad, CONVERT(nvarchar(260), SERVERPROPERTY('ErrorLogFileName')) AS errorlogOPTION (MAXDOP 1);Beispielausgabe
Aus dem Lab: SQL Server 2022 CU27 in einem Container mit zwei festen CPUs (--cpuset-cpus) und 4 GB Speichergrenze, Agent eingeschaltet:
host_platform host_distribution host_release version update_level edition
------------- ----------------- ------------ ---------- ------------ --------------------------
Linux Ubuntu 22.04 16.0.4295.3 CU27 Developer Edition (64-bit)
cpu_count scheduler_count affinity_type_desc container_type_desc physical_memory_mb committed_target_mb
--------- --------------- ------------------ ------------------- ------------------ -------------------
2 2 AUTO LINUX CONTAINER 3277 2551
max_server_memory_mb maxdop tempdb_datendateien sql_server_agent standard_datenpfad errorlog
-------------------- ------ ------------------- ---------------- -------------------- ---------------------------
2147483647 0 2 läuft /var/opt/mssql/data/ /var/opt/mssql/log/errorlogSpalten gekürzt. So lesen Sie das: Zwei CPUs und 3.277 MB entsprechen den Container-Grenzen (80 Prozent von 4 GB); max server memory und MAXDOP stehen noch auf der Voreinstellung. Zeigt cpu_count bei Ihnen die CPU-Zahl des Hosts, obwohl der Container begrenzt ist, oder physical_memory_mb mehr als die Container-Grenze, dann trifft Sie Abschnitt 2 oder 3.
Methode und Grenzen
Gemessen am 11.11.2026 auf einer Linux-VM (Ubuntu, cgroup v2, 24 logische CPUs, 78 GB Speicher, Docker 29.8) mit den Microsoft-Images SQL Server 2017 CU31, 2019 CU32, 2022 CU27, 2025 CU9 und CU10 (Ubuntu-Varianten), Developer Edition; je Fall ein frischer Container ohne Netzwerk, je Fall ein Lauf, kleine Datenbanken. Das Herunterfahren auf 2022 und 2025 hatten wir am 21.10.2026 bereits einmal gemessen; die Ergebnisse stimmen überein. Was wir nicht selbst gemessen haben, stammt aus Microsoft Learn und dem GitHub-Repository der Images. Grenzen:
- Neuere CU-Images als 2022 CU27 und 2025 CU10 haben wir nicht geprüft; ob die angekündigte Korrektur des Startskripts dort enthalten ist, ist offen.
- Recovery-Dauer nach einem harten Ende, Laufzeitwirkung zu vieler Scheduler unter einer Quote und der OOM-Kill sind nicht gemessen.
- Nur Ubuntu-Images, nur Docker auf einem Host mit cgroup v2. RHEL-Images, Podman, Kubernetes und Hosts mit cgroup v1 haben wir nicht geprüft; unter cgroup v1 kann die Speichererkennung von 2017 und 2019 anders ausfallen.
- Zeiten sind Einzelmessungen und zeigen Größenordnungen.
Weiterlesen
- Microsoft Learn: Quickstart „Run SQL Server Linux container images with Docker“; „Configure SQL Server container images on Docker“; „Performance best practices and configuration guidelines for SQL Server on Linux“.
- GitHub
microsoft/mssql-docker: Issues #968 (Startskript gibt SIGTERM nicht weiter), #974 (Absturz kurz nach dem Start). - Docker-Dokumentation:
docker stopunddocker run --init. - In dieser Reihe: Übersicht und Teil 1: Linux, Container oder Windows? mit der Prüfabfrage „Was fehlt unter Linux?“.
- Bei PSG: Neustart hilft nur kurz (was ein Neustart in SQL Server wirklich zurücksetzt), SQL Server Locking verstehen.
Sie betreiben SQL Server in Containern?
Prüfen Sie zuerst drei Dinge: ob Ihre Container mit Exitcode 0 oder 137 enden, ob cpu_count und physical_memory_mb aus Abfrage 2 zu den Grenzen passen, die Sie gesetzt haben, und wem das Datenverzeichnis gehört. Stimmt eines davon nicht, lohnt ein zweiter Blick auf die Container-Definition, bevor das nächste Update oder der nächste Stopp ansteht.