Диагностика: найти и починить
Ситуация
Коллега поменял что-то в проекте «Заметки» и ушёл. Сайт не открывается. Хочется сделать docker compose down -v
и поднять всё заново — но это удалит заметки и не объяснит, что сломалось. Если причина в файлах проекта,
после пересоздания всё сломается снова.
Главная мысль
Тот же цикл, что в курсе «Агент чинит сервер»: симптом → факты → гипотеза → минимальное исправление → проверка. Для Compose у фактов три источника, и все три ничего не меняют:
- Симптом снаружи —
curl -sS -i http://127.0.0.1:8080/: какая ошибка и кто её выдал. - Состояние —
docker compose ps -a: какой сервис неUp, есть лиunhealthy,Restarting, код завершения. - Журнал —
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 от nginx | nginx работает, приложение отказало в соединении | журнал 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.
Если результат другой
- После пересоздания
appnginx отвечает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— он пересоздаёт изменённые сервисы.
Проверка понимания
curlотвечает502 Bad Gateway. Какой сервис точно работает и с чего начать поиск?- Почему исправление в файле проекта лучше, чем «быстрая правка» внутри контейнера через
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.
Как устроен скрипт
- Проверки — ничего не меняют. Скрипт откажется (
Отказ: …,Ничего не изменено.), если:- уже есть
compose.yaml.before-break— значит, поломка уже создавалась, и сохранённую исправную копию нельзя перезаписать; compose.yamlне похож на исправный итоговый файл главы 7: нетcondition: service_healthy, уже есть строкаDATA_DIR, или строки, которые скрипт меняет, не найдены ровно по одному разу.
- уже есть
- Подготовка. Копия создаётся без права перезаписи (
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, образы-заменители.
Проверка понимания
- Ответ
502выдал nginx — значит,webработает и принимает запросы. Искать между nginx и приложением: журналweb(какой upstream и какая ошибка соединения), затем состояние и журналapp. - Контейнер — одноразовый экземпляр образа и настроек. Правка через
execживёт в его слое и пропадёт при следующемup -dс изменениями, обновлении образа илиdown. В файле проекта исправление сохраняется, попадает в Git и работает у всех, кто запустит проект.
Практика без терминала
Разобрать два варианта в браузере. Выберите наблюдения, проверьте ремонт и сохраните прогресс в аккаунте.
Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.