docker『Service Unavailable』の原因と対処法

docker『Service Unavailable』の原因と対処法

「Service Unavailable」は、主にHTTPステータスコード 503 として現れるエラーで、「サーバー自体は生きているが、今はリクエストを処理できない」という状態を示すことが多い。Docker環境では、コンテナ内のアプリが落ちている、リバースプロキシの背後にあるコンテナがすべてダウンしている、ヘルスチェック失敗でバックエンドが切り離されている、リソース不足や外部サービスの503など、さまざまな要因が絡む。どのレイヤ(アプリ・コンテナ・リバプロ・オーケストレーション・外部サービス)で503が発生しているのかを切り分けることが重要になる。

エラーの意味とよくある発生条件

Service Unavailable(HTTP 503)は、典型的には以下のような条件で現れる。

・NginxやTraefikなどのリバースプロキシが、バックエンドコンテナに接続できず503を返している
・Docker Swarm / Kubernetesなどのロードバランサが、すべてのタスク/Podを unhealthy と判断し503を返している
・コンテナ内アプリがクラッシュしている、起動中だがポートをリッスンしていない
・CPU/メモリ枯渇・接続数上限などでアプリがリクエストを処理できない
・外部APIやDBが503を返しており、ゲートウェイ/バックエンドがそのまま503を返している
・Docker Registryなど、外部サービス側がメンテナンス中・過負荷で503を返している

まず「どのURLにアクセスしたとき、誰が503を返しているのか」(Nginxなのかアプリなのか)をログで確認する。

コンテナ内アプリが落ちている・起動していない場合

もっともシンプルな原因は、バックエンドアプリのコンテナが落ちている、もしくは起動直後でまだポートを開いていないケース。

確認手順の例(docker-compose想定)。

# コンテナの状態確認
docker compose ps

# ログ確認
docker compose logs app

# コンテナ内でポートを開いているか確認
docker exec -it app sh -lc "ss -ltnp | grep 8080 || echo 'no listener'"

# ホスト側から直接叩いてみる
curl -v http://localhost:8080/health

ログにスタックトレース・エラー・即時終了などが記録されていれば、アプリ自体のバグや設定ミスが原因であり、Docker以前の問題になる。アプリを正常起動できるようにしたうえで、リバースプロキシ経由の挙動を再確認する。

ヘルスチェック失敗や再起動ループによる503

ヘルスチェックが導入されている環境では、「ヘルスチェックNG→ロードバランサから外される→すべてのバックエンドがNG→503」という流れになりやすい。

Dockerfile / composeでのヘルスチェック例。

# Dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

# docker-compose.yml
services:
  app:
    image: myorg/app
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3

ヘルスチェック失敗時の確認ポイント。

・ヘルスチェックURLが正しいか(/health なのに /status を見ていないかなど)
・ヘルスチェックのtimeoutが短すぎないか
・起動直後のウォームアップ時間を考慮しているか(start_period など)

# 起動直後のウォームアップを待つ設定例
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
  interval: 30s
  timeout: 5s
  retries: 3
  start_period: 60s

ヘルスチェックを一時的に無効化して様子を見るのも切り分けとして有効。

リバースプロキシ(Nginx/Traefik)越しのService Unavailable

Nginxを使う典型的な構成では、バックエンドへのproxy_pass先に接続できないとき、503を返す。

Nginxの設定例。

upstream app_upstream {
    server app:8080;
}

server {
    listen 80;
    location / {
        proxy_pass http://app_upstream;
    }
}

ここで503が出る場合のチェック項目。

・NginxコンテナからappコンテナへDNS解決できるか(app という名前が解決されるか)
・コンテナ間ネットワークでポート8080に接続できるか
・upstream定義のserver名・ポートがcomposeのサービス名・ポートと一致しているか

Nginxコンテナ内から直接バックエンドを叩くと切り分けしやすい。

docker exec -it nginx sh -lc "
  ping -c 3 app || echo 'ping NG';
  curl -v http://app:8080/health || echo 'curl NG';
"

Nginxのエラーログ(error.log)にも、上流への接続エラーの詳細が記録される。

docker exec -it nginx sh -lc "tail -n 50 /var/log/nginx/error.log"

Docker Swarm / オーケストレーションでのService Unavailable

Docker Swarmや他のオーケストレーションを利用している場合、サービスのタスクがすべて落ちている、またはunhealthyになっていると、VIPやロードバランサが503を返す。

Swarmでの状態確認例。

# サービスの状態
docker service ls
docker service ps myservice

# タスクの詳細
docker inspect <task_id> | jq '.[0].Status'

チェックすべき点。

・desired state が Running なのに tasks が Restarting / Failed になっていないか
・healthcheck が失敗し続けていないか
・ノードが down/unreachable になっていないか

必要に応じて、レプリカ数を増やす・特定ノードからタスクを退避するなどで、503の頻度を下げられる。

# レプリカ数を増やす例
docker service scale myservice=4

リソース不足(CPU/メモリ/接続数)が原因の503

コンテナは起動しているが、負荷やリソース制限によりアプリがリクエストを処理できず503を返すことも多い。

典型的な症状。

・CPU使用率が常に100%近く張り付いている
・メモリ不足でアプリが頻繁にOOM Killerの対象になる
・DB接続プールや外部APIの同時接続数制限に達している

docker statsやコンテナログで負荷状況を確認する。

docker stats

# OOMになっていないか確認
docker inspect <container> --format '{{.State.OOMKilled}}'

アプリ側での対処例。

・Webサーバ(例: Node.jsならPM2、PythonならGunicorn)のワーカー数・キューサイズ調整
・DB接続プールのサイズ調整
・タイムアウトを適切に設定して、過負荷時は早めに503/429を返す

コンテナ定義側でのリソース制限も見直す。

# docker-compose.yml のリソース制限例 (Swarm)
deploy:
  resources:
    limits:
      cpus: '1.0'
      memory: 512M
    reservations:
      cpus: '0.5'
      memory: 256M

Docker Registryや外部サービスからの503 (pull/pushエラー)

docker pull/push 時に 503 Service Unavailable が出るケースもある。

docker pull myregistry.example.com/myorg/app:1.0
# → Error response from daemon: Get
#   https://myregistry.example.com/v2/: Service Unavailable

この場合の原因候補。

・プライベートレジストリや外部レジストリがメンテナンス中・過負荷
・プロキシやロードバランサの背後にあるレジストリのバックエンドが落ちている
・ネットワーク機器やFWが断続的にレジストリへのアクセスを遮断している

対処の流れ。

・レジストリのステータスページ・監視を確認
・少し時間をおいてリトライ(CI/CDならエクスポネンシャルバックオフ付きリトライを実装)
・オンプレレジストリなら、そのコンテナ/サービスのログと状態を確認し、必要なら再起動

# CI上での簡易リトライ例(シェル)
for i in 1 2 3; do
  docker pull myregistry.example.com/myorg/app:1.0 && break
  echo "pull failed, retry $i..."
  sleep $((i * 10))
done

アプリ側のタイムアウト・サーキットブレーカによる503

マイクロサービス間通信やAPIゲートウェイを使う構成では、内部でサーキットブレーカやタイムアウトを実装していて、バックエンドの遅延やエラーに応じて明示的に503を返すことがある。

例: Node.js のAPIゲートウェイが下流サービスからのレスポンス遅延時に 503 を返す、など。

// 概念例(Node.js)
if (!backendResponseOk) {
  res.status(503).json({ error: "Service temporarily unavailable" });
}

この場合、Docker自体ではなくアプリケーションの保護機構が動いているだけなので、下流サービスの復旧・スロットリング・キャッシュ導入などのアプリケーションアーキテクチャ側の対策が必要になる。

・タイムアウト値の調整(短すぎても長すぎても問題)
・リトライ回数・バックオフの設計
・キャッシュやフォールバックレスポンスの導入

実務で役立つログ・メトリクスの取り方

503の原因を追うためには、ログとメトリクスを「どのレイヤで」「何が起きたか」が分かるように集めることが重要。

基本セットの例。

・リバプロのアクセスログとエラーログ(Nginxならaccess.log / error.log)
・アプリケーションログ(リクエストID・ルーティング・外部API呼び出し結果)
・コンテナのリソースメトリクス(CPU/メモリ/ネットワーク)
・外部サービスへの呼び出し件数・失敗率・応答時間

# NginxのログにリクエストIDを出す例(概念)
log_format main '$remote_addr - $request [$request_time] '
                '$status $body_bytes_sent "$http_user_agent" '
                'req_id=$request_id';

access_log /var/log/nginx/access.log main;

ログに「503が返されたタイミング」と「バックエンドの状態」を紐付けて監視できると、原因特定がかなり楽になる。

NG→OK 早見表(よくあるパターンと修正例)

# 1) バックエンドコンテナが落ちている
# NG
curl http://localhost/     # 503
docker compose ps          # app が exited

# OK: app を修正・再起動
docker compose logs app
# バグを修正後
docker compose up -d


# 2) Nginxのupstream設定ミス
# NG (設定)
upstream app_upstream {
    server api:8080;   # 実際のサービス名はapp
}

# OK
upstream app_upstream {
    server app:8080;
}


# 3) ヘルスチェックが厳しすぎる
# NG(タイムアウト短すぎ)
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
  interval: 5s
  timeout: 1s
  retries: 1

# OK(余裕を持たせる)
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
  interval: 30s
  timeout: 5s
  retries: 3
  start_period: 60s


# 4) Registryの一時的な503
# NG
docker pull myregistry.example.com/myorg/app:1.0   # 503になったら即失敗扱い

# OK: リトライを入れる
for i in 1 2 3; do
  docker pull myregistry.example.com/myorg/app:1.0 && break
  sleep $((i * 10))
done

チェックリスト(上から順に確認する)

1) 503を返しているのはどのコンポーネントか(Nginx/Traefik/アプリ本体/外部サービス)をログで特定したか
2) バックエンドコンテナが正常起動し、対象ポートをリッスンしているか
3) コンテナ間のDNS解決やネットワーク(service名:port で接続)が正しく動いているか
4) ヘルスチェックのURL・タイミング・タイムアウト設定が妥当か
5) Swarmや他オーケストレーションを使っている場合、タスク/Podがunhealthyや再起動ループになっていないか
6) CPU/メモリ/接続数などのリソース不足がなく、OOMやスロットルが頻発していないか
7) 外部レジストリやAPI、DBが503を返していないか(ステータス・監視・ログで確認したか)
8) アプリ側で意図的に503を返す実装(サーキットブレーカ・ゲートウェイなど)が発動していないか
9) ログとメトリクスが、503発生タイミングとバックエンド状態を紐付けて追えるようになっているか