Перейти к содержимому
RootPoligon

Диагностика: найти и починить

Ситуация

Коллега поменял что-то в проекте «Заметки» и ушёл. Сайт не открывается. Хочется сделать docker compose down -v и поднять всё заново — но это удалит заметки и не объяснит, что сломалось. Если причина в файлах проекта, после пересоздания всё сломается снова.

Главная мысль

Тот же цикл, что в курсе «Агент чинит сервер»: симптом → факты → гипотеза → минимальное исправление → проверка. Для Compose у фактов три источника, и все три ничего не меняют:

  1. Симптом снаружи — curl -sS -i http://127.0.0.1:8080/: какая ошибка и кто её выдал.
  2. Состояние — docker compose ps -a: какой сервис не Up, есть ли unhealthy, Restarting, код завершения.
  3. Журнал — docker compose logs --tail 20 сервис: что сказал сам процесс.

Если нужно увидеть итоговые настройки — docker compose config. Исправляют файл проекта, а не контейнер: изменения внутри контейнера пропадут при пересоздании.

Словарь симптомов

Симптом curlЧто значитГде искать
(7) Couldn't connect to serverна порту хоста никто не слушаетps -a: работает ли web, опубликован ли порт
(56) Connection reset / (52) Empty replyпорт опубликован, но переслать некудаps: в какой порт контейнера идёт пересылка
502 Bad Gateway от nginxnginx работает, приложение отказало в соединениижурнал web: адрес upstream; состояние app
504 Gateway Time-out от nginx (через минуту)nginx не дождался соединениясостояние app: не остановлен ли

Разбор: три поломки по очереди

Папка ~/docker-course/rpd-notes, проект поднят с итоговым compose.yaml (глава 7), curl -sS http://127.0.0.1:8080/ отвечает Заметки (первая строка — заголовок из вашего .env; если в главе 6 вы его меняли, он будет другим). Каждую поломку создаём сами и сразу чиним.

1. Опечатка в имени сервиса

sed -i 's|proxy_pass http://app:8000;|proxy_pass http://notes:8000;|' nginx/default.conf
docker compose restart web
sleep 3
curl -sS http://127.0.0.1:8080/
curl: (7) Failed to connect to 127.0.0.1 port 8080 after 0 ms: Couldn't connect to server

На порту никто не слушает. Состояние:

docker compose ps -a --format 'table {{.Name}}\t{{.Status}}'
NAME        STATUS
rpd-app-1   Up 10 seconds (healthy)
rpd-web-1   Restarting (1) 1 second ago

web падает и перезапускается. Журнал:

docker compose logs web --tail 3
web-1  | 2026/10/16 12:06:04 [emerg] 1#1: host not found in upstream "notes" in /etc/nginx/conf.d/default.conf:7

При запуске nginx ищет адрес каждого upstream; имени notes в сети проекта нет — nginx не стартует. Файл и строка названы. Исправление — одна строка:

sed -i 's|proxy_pass http://notes:8000;|proxy_pass http://app:8000;|' nginx/default.conf
docker compose restart web
sleep 3
curl -sS http://127.0.0.1:8080/

Заметки — восстановлено.

2. Не тот порт приложения

sed -i 's|proxy_pass http://app:8000;|proxy_pass http://app:8080;|' nginx/default.conf
docker compose restart web
sleep 3
curl -sS -i http://127.0.0.1:8080/ | head -1
docker compose logs web --tail 1
HTTP/1.1 502 Bad Gateway
web-1  | 2026/10/16 12:06:11 [error] 8#8: *1 connect() failed (111: Connection refused) while connecting to upstream, client: 172.18.0.1, server: _, request: "GET / HTTP/1.1", upstream: "http://172.18.0.2:8080/", host: "127.0.0.1:8080"

502 — ответил nginx, значит, он работает. В журнале: соединение с …:8080 отклонено. Приложение слушает 8000 (docker compose logs app → слушаю 0.0.0.0:8000). Возвращаем порт:

sed -i 's|proxy_pass http://app:8080;|proxy_pass http://app:8000;|' nginx/default.conf
docker compose restart web
sleep 3
curl -sS http://127.0.0.1:8080/

Заметки — восстановлено.

3. Приложение остановлено

docker compose stop app
curl -sS -i --max-time 10 http://127.0.0.1:8080/
docker compose ps -a --format 'table {{.Name}}\t{{.Status}}'
curl: (28) Operation timed out after 10003 milliseconds with 0 bytes received
NAME        STATUS
rpd-app-1   Exited (0) 10 seconds ago
rpd-web-1   Up 20 seconds

Запрос повис: nginx помнит адрес остановленного контейнера и ждёт соединения. Без --max-time через минуту пришёл бы 504 Gateway Time-out, а в журнале web — upstream timed out (110: Connection timed out). Состояние отвечает сразу: app — Exited (0), то есть остановлен штатно, не упал. Политика unless-stopped его не перезапустила — кто-то остановил вручную.

docker compose start app

Через несколько секунд (прогрев) — Заметки.

Чего не делать

  • docker compose down -v «чтобы начать с чистого листа» — удалит заметки и не найдёт причину: она в файлах проекта и вернётся при следующем запуске.
  • Править файлы внутри контейнера через docker compose exec — исправление пропадёт при пересоздании.
  • --privileged, монтирование сокета Docker или корня хоста — ни одна из учебных поломок не связана с правами контейнера на хост; такие флаги только открывают хост.
  • docker system prune — удаляет чужие остановленные контейнеры, образы и (с --volumes) данные на всей машине.

Практика: две связанные поломки

Поломку создаёт скрипт 08-diagnostics-break.md. Чтобы тренировка была честной, запустите его, не читая: там видно, что меняется. В будущей лабе платформа подготовит поломку сама.

После скрипта curl -sS http://127.0.0.1:8080/ не отдаёт Заметки. Найдите и исправьте все причины.

Ограничения: не удалять том rpd-notes-data, не использовать down -v, не менять server.py, Dockerfile и nginx/default.conf — поломки в другом месте.

Признаки успеха: curl -sS -i http://127.0.0.1:8080/ → 200, Server: nginx…, X-App: rpd-notes; docker compose ps — app (healthy), web Up; для каждой причины названа строка вывода, на которой основан диагноз; заметки, сохранённые до поломки, на месте.

Подсказка 1

Начните со состояния обоих сервисов. Если один из них не запустился вовсе, причина может быть в другом — от которого он зависит.

Подсказка 2

Журнал приложения называет путь, в который оно не может писать. Сравните его с тем, куда подключён том (docker compose config, раздел volumes сервиса app).

Подсказка 3

После первого исправления симптом сменится. Посмотрите столбец PORTS у web: в какой порт контейнера идёт пересылка, и какой порт слушает nginx?

Решение — в answers/08-diagnostics.md.

Если результат другой

  • После пересоздания app nginx отвечает 502 или висит до 504. nginx запомнил при запуске старый адрес контейнера. docker compose restart web заставит его найти адрес заново.
  • docker compose up -d пишет dependency failed to start: container rpd-app-1 is unhealthy. web не запустится, пока app не готов. Сначала чините app.
  • Изменили compose.yaml, а docker compose restart ничего не поменял. restart перезапускает те же контейнеры со старыми настройками. Новые настройки применяет docker compose up -d — он пересоздаёт изменённые сервисы.

Проверка понимания

  1. curl отвечает 502 Bad Gateway. Какой сервис точно работает и с чего начать поиск?
  2. Почему исправление в файле проекта лучше, чем «быстрая правка» внутри контейнера через exec?

Ответы — в answers/08-diagnostics.md.

Дальше: 9. Итоговое развёртывание и проверка.

Создать учебную поломку главы 8

Лист к главе 8. Скрипт меняет только compose.yaml в ~/docker-course/rpd-notes и перед этим сохраняет исправную копию в compose.yaml.before-break. Данные (том rpd-notes-data) не трогаются.

Исходное состояние: проект поднят с итоговым compose.yaml главы 7, сайт отвечает. Сохраните заметку, чтобы потом проверить, что она пережила ремонт: curl -sS -d 'До поломки' http://127.0.0.1:8080/notes.

Как устроен скрипт

  1. Проверки — ничего не меняют. Скрипт откажется (Отказ: …, Ничего не изменено.), если:
    • уже есть compose.yaml.before-break — значит, поломка уже создавалась, и сохранённую исправную копию нельзя перезаписать;
    • compose.yaml не похож на исправный итоговый файл главы 7: нет condition: service_healthy, уже есть строка DATA_DIR, или строки, которые скрипт меняет, не найдены ровно по одному разу.
  2. Подготовка. Копия создаётся без права перезаписи (set -o noclobber), затем меняется compose.yaml, результат сверяется, и проект перезапускается (docker compose up -d, не дольше 2 минут). При неожиданной ошибке скрипт останавливается и пишет, как вернуть исправный файл.

Чтобы тренировка была честной, запустите блок, не вчитываясь в него.

bash <<'BREAK'
rp_break() {
  fail() { echo "Отказ: $1"; echo "Ничего не изменено."; return 1; }
  cd ~/docker-course/rpd-notes 2>/dev/null || { fail "нет папки ~/docker-course/rpd-notes"; return 1; }
  [ -f compose.yaml ] || { fail "нет compose.yaml"; return 1; }
  [ ! -e compose.yaml.before-break ] || { fail "уже есть compose.yaml.before-break — поломка уже создавалась. Как вернуться или начать заново — ниже на странице"; return 1; }
  grep -q 'condition: service_healthy' compose.yaml || { fail "нужен итоговый compose.yaml главы 7"; return 1; }
  ! grep -q 'DATA_DIR' compose.yaml || { fail "в compose.yaml уже есть DATA_DIR — файл не исходный"; return 1; }
  [ "$(grep -c '^      STARTUP_DELAY: ${STARTUP_DELAY:-5}$' compose.yaml)" = 1 ] || { fail "строка STARTUP_DELAY не такая, как в главе 7"; return 1; }
  [ "$(grep -c '"127.0.0.1:${WEB_PORT:-8080}:80"' compose.yaml)" = 1 ] || { fail "строка порта web не такая, как в главе 7"; return 1; }
  command -v docker >/dev/null || { fail "не найден docker"; return 1; }

  (set -o noclobber; cat compose.yaml > compose.yaml.before-break) && cmp -s compose.yaml compose.yaml.before-break \
    || { echo "Ошибка: не удалось сохранить копию. compose.yaml не менялся."; return 1; }
  sed -i 's|^      STARTUP_DELAY: ${STARTUP_DELAY:-5}$|&\n      DATA_DIR: /srv/data|' compose.yaml \
    && sed -i 's|"127.0.0.1:${WEB_PORT:-8080}:80"|"127.0.0.1:${WEB_PORT:-8080}:8000"|' compose.yaml \
    && [ "$(grep -c 'DATA_DIR: /srv/data' compose.yaml)" = 1 ] && grep -q ':8000"' compose.yaml \
    || { echo "Ошибка после начала подготовки: compose.yaml изменён не так, как задумано."
         echo "Исправная копия — compose.yaml.before-break: cp compose.yaml.before-break compose.yaml"; return 1; }
  timeout 120 docker compose up -d 2>&1 | tail -1
  echo "Поломка готова. Проверьте: curl -sS http://127.0.0.1:8080/"
}
rp_break </dev/null
BREAK

Строка dependency failed to start: container rpd-app-1 is unhealthy перед «Поломка готова» — часть поломки, а не ошибка скрипта.

Как вернуться и как начать заново

  • Вернуть исправный проект (например, если запутались): cp compose.yaml.before-break compose.yaml && docker compose up -d.
  • Начать новую тренировку. Сначала верните исправный проект, затем переименуйте сохранённую копию — скрипт не перезаписывает её сам: mv compose.yaml.before-break "compose.yaml.before-break.$(date +%Y%m%d-%H%M%S)". После этого блок снова пройдёт проверки. Старые копии можно удалить вручную, когда они станут не нужны.
Разбор и проверка понимания

Лист ученика: 08-diagnostics.md.

Практика

Папка ~/docker-course/rpd-notes.

Симптом:

curl -sS http://127.0.0.1:8080/
curl: (7) Failed to connect to 127.0.0.1 port 8080 after 0 ms: Couldn't connect to server

Состояние:

docker compose ps -a --format 'table {{.Name}}\t{{.Status}}'
NAME        STATUS
rpd-app-1   Restarting (1) Less than a second ago
rpd-web-1   Created

web даже не запускался — он ждёт готовности app, а app падает. Начинать с app.

Журнал app:

docker compose logs app --tail 2
app-1  | notes: не могу писать в /srv/data: Permission denied

Приложение пишет в /srv/data, а том подключён в /data (docker compose config → у app target: /data). В environment появилась строка DATA_DIR: /srv/data. Причина 1 — переопределённый путь данных. Исправление — удалить эту строку из compose.yaml и применить:

docker compose up -d
curl -sS http://127.0.0.1:8080/
 Container rpd-web-1 Started
curl: (56) Recv failure: Connection reset by peer

Симптом сменился: web запущен, порт опубликован, но переслать некуда.

docker compose ps --format 'table {{.Name}}\t{{.Ports}}'
NAME        PORTS
rpd-app-1   8000/tcp
rpd-web-1   80/tcp, 127.0.0.1:8080->8000/tcp

Причина 2 — у web опубликован порт контейнера 8000, а nginx слушает 80 (80/tcp без стрелки). Исправление — в ports вернуть "127.0.0.1:${WEB_PORT:-8080}:80" и снова docker compose up -d.

Проверка:

curl -sS -i http://127.0.0.1:8080/ | grep -E 'HTTP|Server|X-App'
curl -sS http://127.0.0.1:8080/notes
HTTP/1.1 200 OK
Server: nginx/1.30.x
X-App: rpd-notes
До поломки

Заметка на месте: том не трогали. Точная версия в Server зависит от образа (у автора был заменитель с nginx 1.24).

Неверные пути: down -v (теряются заметки, причина остаётся), chmod 777 на что-либо внутри контейнера (путь /srv/data вообще не тот), docker compose restart вместо up -d (не применит изменения compose.yaml).

Воспроизведено: Docker Engine 29.8.2, Compose 5.5.1, образы-заменители.

Проверка понимания

  1. Ответ 502 выдал nginx — значит, web работает и принимает запросы. Искать между nginx и приложением: журнал web (какой upstream и какая ошибка соединения), затем состояние и журнал app.
  2. Контейнер — одноразовый экземпляр образа и настроек. Правка через exec живёт в его слое и пропадёт при следующем up -d с изменениями, обновлении образа или down. В файле проекта исправление сохраняется, попадает в Git и работает у всех, кто запустит проект.

Практика без терминала

Разобрать два варианта в браузере. Выберите наблюдения, проверьте ремонт и сохраните прогресс в аккаунте.

Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.