PodmanでSQL Server 2025コンテナが正常終了せずSIGKILL(137)になる問題の解決ステップ
10/3 12:10 追記
結局イメージ+docker-compose+dockerfileにしました。
はじめに
Podman(特にRootless環境)で SQL Server 2025 (mssql-server) を運用するにあたり、起動・停止の自動化やセキュリティ担保のためのスクリプトを作成しました。
しかし、コンテナ停止時にクリーンシャットダウンが行われず、10分間(タイムアウト)スタックしたのちに SIGKILL (終了コード: 137) で強制終了される問題(SQL Server 2022(CU18以降)の不具合に類似)に直面しました。
イメージを利用していては対策がしにくいので、apt による独自インストール環境への切り替えを試みましたが症状は改善せず。
最終的に 「コンテナの設定と仕様の見直し」を行うことで、わずか5秒で正常終了(終了コード: 0) させることができるようになりました。
そのトラブルシューティングの経緯をまとめます。
トラブル発生までの経緯
1. 初期の運用
当初はMicrosoft公式のSQL Serverイメージを podman run で直接ダウンロードして実行していました。データの永続化のために、-v オプションでホストのディレクトリをマウントする標準的な構成です。
podman run -d \
--name mssql_container \
-p 1433:1433 \
-e "ACCEPT_EULA=Y" \
-e "MSSQL_SA_PASSWORD=foobar" \
-v mssql_data:/var/opt/mssql \
mcr.microsoft.com/mssql/server:2025-latest
2. 運用管理スクリプト(Compose化)の導入
運用の明確化と自動化(起動時のヘルスチェック、sa アカウントの無効化確認、firewalld の自動開閉など)を行うため、以下のファイル群を作成し、堅牢な管理体制を整えました。
-
docker-compose.yaml(コンテナ構成の定義)
services:
mssql:
image: mcr.microsoft.com/mssql/server:2025-latest
container_name: mssql_container
hostname: sql2025
restart: on-failure:3 # 3回まで再起動を試みる
environment:
- ACCEPT_EULA=Y
- MSSQL_SA_PASSWORD=foobar
ports:
- "1433:1433"
volumes:
- ./mssql_data:/var/opt/mssql
-
start-container.sh(起動監視 & ファイアウォール開放スクリプト)
#!/bin/bash
#
# SQL Server を rootless podman compose で起動し、
# sa でログインできないことを確認してから firewalld のポートを開ける。
# rootless 運用のため root での実行は不可 (root と rootless を混ぜない)
if [ "$EUID" -eq 0 ]; then
echo "root では実行できません。通常ユーザーで実行してください。"
exit 1
fi
# 設定項目
CONTAINER="mssql_container"
HOST_PORT="1433" # docker-compose.yaml の ports ("1433:1433") と合わせる
ZONE="public"
SA_PASSWORD="foobar" # docker-compose.yaml の MSSQL_SA_PASSWORD と同じ値
TIMEOUT=120
RECOVERY_TIMEOUT=600 # 全データベースの復旧完了を待つ最大秒数
# 基準パス
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1; pwd)
COMPOSEYML="${SCRIPT_DIR}/docker-compose.yaml"
is_running() {
[ "$(podman inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null)" = "true" ]
}
if [ ! -f "$COMPOSEYML" ]; then
echo "${COMPOSEYML} が見つかりません。"
exit 1
fi
# 起動
if is_running; then
echo "コンテナは既に起動状態です (再作成しません。設定を反映するには stop-container.sh の後に実行してください)。"
else
# podman-compose 1.0.6 は podman 6 で、既存コンテナがあると up が
# 「cannot index slice/array with type string」で失敗するため、先に down して消す
if podman container exists "$CONTAINER"; then
echo "停止中のコンテナを先に削除します。"
podman compose -f "$COMPOSEYML" down -t 60
fi
echo "コンテナを起動します。"
if ! podman compose -f "$COMPOSEYML" up -d; then
echo "起動に失敗しました。ファイアウォールは変更しません。"
exit 1
fi
fi
# 起動待ち
# sa が無効化されていても判定できるよう、「ログイン成功」と「Login failed」のどちらも
# サーバーが応答している印として扱う (接続自体ができない間は待ち続ける)。
echo "SQL Server の起動完了を待機しています..."
COUNTER=0
SA_LOGIN=""
while true; do
OUT=$(podman exec -i "$CONTAINER" /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P "$SA_PASSWORD" -C -l 5 -Q "SELECT 1" 2>&1)
RC=$?
if [ "$RC" -eq 0 ]; then
SA_LOGIN="yes"
break
fi
if echo "$OUT" | grep -qi "Login failed"; then
SA_LOGIN="no"
break
fi
if ! is_running; then
echo "コンテナが停止しました。ログを確認してください: podman logs ${CONTAINER}"
exit 1
fi
sleep 2
COUNTER=$((COUNTER + 2))
if [ "$COUNTER" -ge "$TIMEOUT" ]; then
echo "タイムアウト: SQL Server が ${TIMEOUT} 秒以内に応答しませんでした。"
echo "ログを確認してください: podman logs ${CONTAINER}"
exit 1
fi
done
# sa でログインできる状態は危険なので、公開せずに停止する
if [ "$SA_LOGIN" = "yes" ]; then
echo "==========================================================="
echo "警告: sa アカウントでのログインが可能な状態です!"
echo "セキュリティ保護のため、コンテナを自動停止します。"
echo "一時的な構築時以外は、sa を無効化してから起動してください。"
echo "==========================================================="
podman compose -f "$COMPOSEYML" down -t 60
exit 1
fi
echo "sa アカウントのログイン禁止を確認しました。安全に起動しました。"
# 全データベースの復旧が終わるまで待つ (ポートを開けるのはその後)
# サーバーがログインを受け付けた後も、ユーザーデータベースの復旧は続くため。
echo "データベースの復旧完了を待機しています..."
COUNTER=0
until podman logs "$CONTAINER" 2>&1 | grep -q "Recovery is complete"; do
if ! is_running; then
echo "コンテナが停止しました。ログを確認してください: podman logs ${CONTAINER}"
exit 1
fi
sleep 2
COUNTER=$((COUNTER + 2))
if [ "$COUNTER" -ge "$RECOVERY_TIMEOUT" ]; then
echo "タイムアウト: ${RECOVERY_TIMEOUT} 秒以内にデータベースの復旧が完了しませんでした。ポートは開けません。"
echo "復旧が終わってから、もう一度このスクリプトを実行してください。"
exit 1
fi
done
echo "データベースの復旧が完了しました。"
# ファイアウォールのポートを開放 (runtime のみ)
PORT="${HOST_PORT}/tcp"
if sudo firewall-cmd --zone="$ZONE" --query-port="$PORT" --quiet; then
echo "[-] $PORT はすでに $ZONE ゾーンで有効です。"
else
echo "[+] $PORT を $ZONE ゾーンに追加します。"
sudo firewall-cmd --zone="$ZONE" --add-port="$PORT"
fi
-
stop-container.sh(ファイアウォール閉鎖 & 安全な停止スクリプト)
#!/bin/bash
#
# firewalld のポートを閉じてから SQL Server コンテナを停止する。
#
# podman stop はコンテナへ SIGTERM を送る。
# SQL Server はそれを受けてクリーンシャットダウンを開始する。
#
# 停止後は終了コードを記録し、compose down を実行する。
#
set -euo pipefail
# rootless 運用
if [ "$EUID" -eq 0 ]; then
echo "root では実行できません。"
exit 1
fi
# 設定
CONTAINER="mssql_container"
HOST_PORT="1433"
ZONE="public"
GRACEFUL_TIMEOUT=600
DOWN_TIMEOUT=10
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1; pwd)
COMPOSEYML="${SCRIPT_DIR}/docker-compose.yaml"
is_running() {
[ "$(podman inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null)" = "true" ]
}
if [ ! -f "$COMPOSEYML" ]; then
echo "${COMPOSEYML} が見つかりません。"
exit 1
fi
#
# firewall を閉じる
#
PORT="${HOST_PORT}/tcp"
if sudo firewall-cmd --zone="$ZONE" --query-port="$PORT" --quiet; then
echo "[-] $PORT を $ZONE ゾーンから削除します。"
sudo firewall-cmd --zone="$ZONE" --remove-port="$PORT"
echo "ネットワーク切断後の残処理を待機しています。(5s)..."
sleep 5
else
echo "[ ] $PORT は $ZONE ゾーンで有効ではありません。"
fi
#
# コンテナ確認
#
if ! podman container exists "$CONTAINER"; then
echo "コンテナはありません。"
exit 0
fi
#
# 停止
#
if is_running; then
echo "SQL Server を停止します (timeout=${GRACEFUL_TIMEOUT}s)。"
START_TIME=$(date +%s)
podman stop -t "$GRACEFUL_TIMEOUT" "$CONTAINER"
END_TIME=$(date +%s)
ELAPSED=$((END_TIME - START_TIME))
echo "停止要求完了 (${ELAPSED} 秒)"
fi
#
# ログ確認
#
echo "--- 終了時ログ ---"
podman logs "$CONTAINER" 2>&1 \
| grep -iE "shutdown|terminating|checkpoint|recoveryunit" \
| tail -20
echo "--------------------"
#
# 終了コード確認
#
EXIT_CODE=$(podman inspect \
-f '{{.State.ExitCode}}' \
"$CONTAINER" 2>/dev/null)
echo "コンテナ終了コード: ${EXIT_CODE}"
case "${EXIT_CODE}" in
0)
echo "正常終了しました。"
;;
137)
echo "警告: SIGKILL により終了しました。"
;;
*)
echo "終了コード: ${EXIT_CODE}"
;;
esac
#
# 削除
#
podman compose -f "$COMPOSEYML" down -t "$DOWN_TIMEOUT"
RC=$?
if podman container exists "$CONTAINER"; then
echo "コンテナ ${CONTAINER} が残っています。"
podman ps -a
exit 1
fi
if [ "$RC" -ne 0 ]; then
echo "注意: compose down の終了コードは ${RC} でした。"
fi
echo "停止しました。"
3. 【障害の確認】クリーンシャットダウンにならない
作成した stop-container.sh でコンテナの終了状態を厳密に監視し始めたところ、SQL Serverが通常の終了プロセスを通過していないことに気づきました。
stop-container.sh の監視ログ:
SQL Server を停止します (timeout=600s)。
WARN StopSignal SIGTERM failed to stop container mssql_container in 600 seconds, resorting to SIGKILL
コンテナ終了コード: 137
警告: SIGKILL により終了しました。
podman stop が送られてから10分間(600秒)コンテナがスタックし、最終的に強制終了されていました。
迷走と検証:イメージのバグを疑い aptパッケージ追加構成へ
過去の SQL Server 2022 のコンテナイメージにおいて、内部のシグナルハンドリングに起因する終了不具合の報告がありました。
今回の問題はこれと類似していたため、「2025の公式イメージにもまだ何らかのバグがあるのではないか」と考えました。
そこで、公式イメージをベースにしながらも、コンテナ内の環境を切り分けるためにUSER rootで一時的にルート権限へ切り替え、apt経由でパッケージを追加・カスタマイズする構成へと変更を試みました。
docker-compose.yaml
services:
mssql:
build: .
container_name: mssql_container
hostname: sql2025
restart: on-failure:3 # 3回まで再起動を試みる
environment:
- ACCEPT_EULA=Y
- MSSQL_SA_PASSWORD=foobar
ports:
- "1433:1433"
volumes:
- ./mssql_data:/var/opt/mssql
dockerfile
FROM mcr.microsoft.com/mssql/server:2025-latest
USER root
RUN apt-get update && apt-get install -y \
curl \
nano \
vim \
&& rm -rf /var/lib/apt/lists/*
USER mssql
しかし、apt 構成に変更しても症状は一切変わりませんでした。
やはり同様に10分間スタックしたのちに SIGKILL で終了してしまいます。
ここで「イメージ自体のバグ」ではなく、**「コンテナの設定方法、およびPodman環境における仕様のミスマッチ」**に真因があるのではないかと仮説を立て直し、設定を深く見直すことにしました。
真因の特定:何が起きていたのか?
詳細ログ(errorlog)を時系列で追ったところ、以下の2つの問題が連鎖していることが判明しました。
-
シグナル(SIGTERM)の不達
独自Dockerfile(またはComposeのラップ構造)によって、コンテナのメインプロセス(PID 1)とSQL Serverの実行バイナリ(sqlservr)の紐付けが曖昧になっており、Podmanからの停止要求(SIGTERM)がSQL Serverへダイレクトに届いていなかった。 -
停止シーケンス中の内部競合(ライブロック)
シグナルが正常に届かないままネットワーク(firewalld)だけが閉じたため、接続を切断された外部のアプリケーションやツールが超高頻度で再接続を試行。これによりSQL Server内部でシャットダウンの準備処理と、データベースの起動・初期化処理(Starting up...)が最悪のタイミングで秒間ループし、デッドロック(ライブロック)状態に陥っていた。
解決策:設定の見直し
これまでの経緯を踏まえ、「停止シグナルを確実にSQL Serverへ届ける」 ために以下の設定見直しを行いました。
1. Dockerfile の修正(ENTRYPOINTの明示)
コンテナ起動時に実行するバイナリを ENTRYPOINT で明示的に指定し、Podmanからの SIGTERM を sqlservr が直接受け取れる(PID 1として動作させる)ようにします。
FROM mcr.microsoft.com/mssql/server:2025-latest
USER root
# 必要なパッケージを apt でインストール
RUN apt-get update && apt-get install -y \
curl \
nano \
vim \
&& rm -rf /var/lib/apt/lists/*
# データの永続化領域の権限を担保
RUN mkdir -p /var/opt/mssql && \
chown -R 10001:0 /var/opt/mssql
# 元の一般ユーザー(UID: 10001)に戻す
USER mssql
# 【最重要】シグナルを直接受け取るためのエントリーポイント指定
ENTRYPOINT ["/opt/mssql/bin/sqlservr"]
2. docker-compose.yaml の修正(stop_signalの明示)
compose側にも、停止時に送信すべきシグナルが SIGTERM であることを念押しして明示します。
services:
mssql:
build: .
container_name: mssql_container
hostname: sql2025
restart: on-failure:3
stop_signal: SIGTERM # 【重要】明示的にSIGTERMを送信する
environment:
- ACCEPT_EULA=Y
- MSSQL_SA_PASSWORD=foobar
ports:
- "1433:1433"
volumes:
- ./mssql_data:/var/opt/mssql
修正後の結果
設定を見直し、再ビルドした環境で再度 stop-container.sh を実行した結果です。
[-] 1433/tcp を public ゾーンから削除します。
success
ネットワーク切断後の残処理を待機しています。(5s)...
SQL Server を停止します (timeout=600s)。
mssql_container
停止要求完了 (5 秒)
--- 終了時ログ ---
2026-10-03 00:08:03.61 spid20s SQL Server is terminating in response to a 'stop' request from Service Control Manager.
[DeleteDbName] Database HoujinBangouDB with id 15 is deleted.
...
--------------------
コンテナ終了コード: 0
正常終了しました。
あれほど頑なに10分間スタックしていたコンテナが、わずか5秒で安全にクリーンシャットダウン(終了コード0) できるようになりました!
ログからも、外部からのリトライアクセスに邪魔されることなく、Service Control Manager からの停止要求として正しく認識し、各データベースが安全にアンロード(deleted)されていることが確認できます。
まとめ
SQL Serverコンテナの終了問題に直面した際、イメージ自体の不具合を疑いたくなりますが、独自に apt 等でラップした環境やPodman構成においては、**「停止シグナルがメインプロセスへダイレクトに届いているか」**というコンテナ設計の基本部分が非常にシビアに影響します。
-
ENTRYPOINTをシェル形式(CMDなど)に頼らず、大カッコ形式の["..."]で実行バイナリを固定する -
stop_signal: SIGTERMを明示する
この2点を徹底することで、データベースのデータ整合性を保ったまま、安全で高速なコンテナ運用が可能になります。同じように「コンテナが正常終了しない」とお悩みの方は、イメージの変更ではなく、まずプロセスのシグナル経路を見直してみることをお勧めします。
結局イメージに戻る
投稿後に、結局イメージでも大丈夫なんじゃないかと思いなおしました。
次のようにdockerfileとdocker-compose.yamlを次のように書き換えても動きました。
終了にかかる時間も十分短いままです。
-
Dockerfile(一部の削除のみ)
FROM mcr.microsoft.com/mssql/server:2025-latest
ENTRYPOINT ["/opt/mssql/bin/sqlservr"]
-
docker-compose.yaml(変更なし)
services:
mssql:
build: .
container_name: mssql_container
hostname: sql2025
restart: on-failure:3
stop_signal: SIGTERM # 【重要】明示的にSIGTERMを送信する
environment:
- ACCEPT_EULA=Y
- MSSQL_SA_PASSWORD=foobar
ports:
- "1433:1433"
volumes:
- ./mssql_data:/var/opt/mssql