commit 52280c721f4b6e21430808ea84668a9fb2114c01 Author: chase Date: Mon May 4 11:07:31 2026 +0300 push diff --git a/docs/1. Что такое Docker.md b/docs/1. Что такое Docker.md new file mode 100644 index 0000000..e69c7e5 --- /dev/null +++ b/docs/1. Что такое Docker.md @@ -0,0 +1,34 @@ +## Что такое Docker + +Docker - это платформа для упаковки приложения вместе со всеми зависимостями в **изолированную среду** (контейнер), которая запускается одинаково на любом устройстве. + +Коротко по терминам: + +1. **Образ (image)** - неизменяемый «снимок» файловой системы и метаданных запуска (команда по умолчанию, переменные окружения, порты). Образ строится из слоёв; его можно скачать из реестра (например, Docker Hub) или собрать сами из `Dockerfile`. +2. **Контейнер** - запущенный экземпляр образа. У контейнера есть ID, имя, сеть, опционально тома с данными. Один образ может породить много контейнеров. +3. **Docker Engine** - демон и CLI (`docker`), которые создают и управляют контейнерами на хосте. +4. **Docker Compose** - инструмент (плагин к CLI: `docker compose`) для описания **нескольких** сервисов в одном YAML-файле: сборка образов, порты, тома, сети, зависимости запуска, переменные окружения. + + +## Зачем это нужно + +1. **Стабильность** - «у меня не работает» больше не актуально, докер образ можно запускать на любом устройстве, где установлен докер. +2. **Изоляция** - приложения не мешают друг другу версиями библиотек и не требуют полноценной виртуальной машины на каждый сервис. +3. **Масштаб** - образы легко переносить, дальше их поднимают Kubernetes, Nomad и т.д., но база всё равно Docker-образ. +4. **Compose для стека** - фронт, бэкенд, БД и кэш поднимаются одной командой и общаются по внутренней сети. + +## Docker и виртуальная машина + +Виртуальная машина эмулирует железо и поднимает полную ОС. Контейнер делится ядром хоста и изолируется, он легче и быстрее стартует, но это не замена полной изоляции уровня ВМ для всех сценариев безопасности. + +## Когда достаточно `docker run`, а когда Compose + +- **`docker run`** - один контейнер, эксперимент, отладка, скрипты. +- `docker compose` - несколько связанных сервисов, общая сеть, тома, единая точка входа для разработки и демо. + + + +## Документация + +- [Официальная документация Docker](https://docs.docker.com/) +- [Compose](https://docs.docker.com/compose/compose-file/) diff --git a/docs/2. Docker Engine — установка и основные команды.md b/docs/2. Docker Engine — установка и основные команды.md new file mode 100644 index 0000000..dbb94f9 --- /dev/null +++ b/docs/2. Docker Engine — установка и основные команды.md @@ -0,0 +1,219 @@ +## Установка Docker Engine на Debian/Ubuntu + +Tиповой путь через официальный репозиторий Docker. +Лучше проверить актуальную версию в [официальном гайде](https://docs.docker.com/engine/install/ubuntu/) + +``` +sudo apt-get update +sudo apt-get install ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc + +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "${VERSION_CODENAME:-$VERSION_CODENAME}") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +sudo apt-get update +sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y +``` + +Проверяем: +``` +sudo docker run --rm hello-world +``` + + + +Опционально можно запускать без `sudo`,: + +``` +sudo usermod -aG docker $USER +# выйти из сессии и зайти снова +docker run --rm hello-world +``` + + + +Проверка версии: + +``` +docker version +docker info +``` + + + +## Базовый жизненный цикл контейнера + +Скачать образ и запустить интерактивную оболочку, образ автоматически скачивается с Docker Registry: + +``` +docker run -it --rm debian:bookworm-slim bash +``` + +Установка образа: + + +Мы указали `bash` в команде, по этому зашли в интерактивный терминал: + + + +В контейнере, так же как и в линуксе, можно управлять файлами, директориями, сервисами, но ограничено. +Выйти из bash - `exit`, контейнер остановится и `--rm` удалит его. + +Коротко про флаги: + +| Флаг | Смысл | +| -------------------------------------- | --------------------------------- | +| `-it` | интерактив + псевдо-TTY | +| `--rm` | удалить контейнер после остановки | +| `-d` | запустить контейнер в фон | +| `--name` | имя контейнера | +| `-p 8080:80` | проброс порта хост:контейнер | +| `-v /путь/на/хосте:/путь/в/контейнере` | том (данные на хосте) | + +Запуск веб-сервера в фоне: +``` +docker run -d --name demo-nginx -p 8080:80 nginx:alpine +``` + + + +Проверим работу на порту 8080: + + + +Список контейнеров и логи: + +`docker ps` - посмотреть список АКТИВНЫХ контейнеров +`docker ps -a` - посмотреть список ВСЕХ контейнеров +`docker logs demo-nginx` - вывод логов контейнера +`docker logs -f demo-nginx` - смотреть логи контейнера в реальном времени + + + +Остановка и удаление: + +`docker stop demo-nginx` - остановка контейнера +`docker rm demo-nginx` - удалить контейнер + + + +## Вход в уже запущенный контейнер: `docker exec` + +``` +docker run -d --name my-nginx -p 8080:80 nginx:alpine +docker exec -it my-nginx sh +``` + +Внутри можно смотреть файлы конфигурации, например `cat /etc/nginx/nginx.conf`, ставить утилиты в отладочных образах и т.д. + + + +Так же интересный факт: мы уже устанавливали образ `nginx:alpine`, по этому нам его повторно скачивать не нужно, он хранится в docker cache. Единственный минус - занимает место на диске. +## Копирование файлов **в** контейнер и **из** контейнера: `docker cp` + +На хосте создадим простейший html файл: +`echo '

Hallo from DOCKER

' > /tmp/index.html` + +Затем копирует его прямо в наш docker контейнер: +`docker cp /tmp/index.html my-nginx:/usr/share/nginx/html/index.html` + + +Обновим страницу в браузере - должен отобразиться новый html. + + +Скопировать **из** контейнера на хост: +``` +docker cp my-nginx:/etc/nginx/nginx.conf /tmp/nginx.conf.from-container +``` + + + +Важно: `docker cp` удобен для отладки и разовых правок. Для поставки контента, файлов в образ используйте функцию `COPY` в `Dockerfile` или тома в Compose (смотреть дальше по гайду). + +## Тома (volumes): данные переживают пересоздание контейнера + +Именованный том: +``` +docker volume create webdata +docker run -d --name nginx-vol -p 8081:80 -v webdata:/usr/share/nginx/html nginx:alpine +docker exec nginx-vol sh -c 'echo vol > /usr/share/nginx/html/index.html' +``` + +Про каждую команду: +`docker volume create webdata` - создаем именованный том `webdata` на нашем хосте. + +`docker run -d --name nginx-vol -p 8081:80 -v webdata:/usr/share/nginx/html nginx:alpine` - создем контейнер `nginx-vol` на порту 8081 и монитируем ему том `webdata` в каталог контейнера `/usr/share/nginx/html`. В этом каталоге nginx подхватывает index.html файл. + +`docker exec nginx-vol sh -c 'echo vol > /usr/share/nginx/html/index.html'` - выполняем команду ВНУТРИ контейнера `nginx-vol` через "sh -c" и передать ему команду `echo`. + + + + +Коротко, что мы сделали: +Мы создали том `webdata` - это физическое место на нашем хосте. Мы к контейнеру примонтировали этот том `-v webdata:/usr/share/nginx/html`. То есть у контейнера теперь есть доступ к этому физическому месту(тому). Затем мы создали файл `index.html` в каталоге `/usr/share/nginx/html`, это означает, что мы фактически создали файл у себя на хосте. Файлы в томах никуда не пропадают при удалении контейнеров. + +Проверим на деле: +``` +docker stop nginx-vol +docker rm nginx-vol +docker run -d --name nginx-vol -p 8081:80 -v webdata:/usr/share/nginx/html nginx:alpine +``` + + + +Новый контейнер взял файл с тома. Если мы зайдем в контейнер и удалим файл index.html, то он удалится и в томе(на хосте). + +Bind-mount, привязка папки с хоста: + +``` +mkdir -p ~/docker-html && echo '

Bind mount edited

' > ~/docker-html/index.html +docker run -d --name nginx-bind -p 8082:80 -v ~/docker-html:/usr/share/nginx/html:ro nginx:alpine +``` + +Про каждую команду: + +`mkdir -p ~/docker-html && echo '

Bind mount

' > ~/docker-html/index.html` - в домашней директории юзера создаем папку `docker-html`, в которой создаем index.html файл. + +`docker run -d --name nginx-bind -p 8082:80 -v ~/docker-html:/usr/share/nginx/html:ro nginx:alpine` - поднимает контейнер `nginx-bind` на порту 8082, к которому мы примонтировали нашу ЛОКАЛЬНУЮ папку `~/docker-html`. Это не именованный том из зоны docker, а непосредственно наша папка. + +`:ro` - только чтение из контейнера, не дает перезаписывать наши файлы в папке `~/docker-html`. + + + +Теперь поменяем в нашей локальной папке файл и посмотрим что будет: + + +Всё работает. +## Сеть: как контейнеры видят друг друга + +По умолчанию контейнеры в **bridge** могут общаться по IP. Удобнее создать **пользовательскую сеть** и обращаться по **имени контейнера**: + +``` +docker network create app-net +docker run -d --name backend --network app-net nginx:alpine +docker run -it --rm --network app-net alpine wget -qO- http://backend +``` + + + +Мы создали виртуальную докер сеть `app-net`, создали первый контейнер с nginx с сетью `app-net`, затем второй контейнер с такой же сетью. Со второго контейнера мы обратились к первому по его названию. + + + +## Очистка среды + +``` +docker stop my-nginx nginx-vol nginx-bind backend +docker rm my-nginx nginx-vol nginx-bind backend +docker volume rm webdata +docker network rm app-net +``` + +## Что дальше + +- Часть **3** — сборка своего образа, `Dockerfile`, многоэтапные сборки, `COPY` vs монтирование. +- Часть **4** — полный пример **Docker Compose** из репозитория `docker/examples/multi-service`. diff --git a/docs/3. Dockerfile — сборка образа и COPY.md b/docs/3. Dockerfile — сборка образа и COPY.md new file mode 100644 index 0000000..835942e --- /dev/null +++ b/docs/3. Dockerfile — сборка образа и COPY.md @@ -0,0 +1,162 @@ +## Зачем нужен Dockerfile + +`Dockerfile` — это **рецепт** сборки образа: от какого базового образа начать, какие пакеты установить, какие файлы скопировать, какую команду запускать по умолчанию. Команда сборки: + +``` +docker build -t my-app:1.0 -f Dockerfile /path/to/context +``` + +**Контекст сборки** — каталог, который вы передаёте последним аргументом (часто `.`). В образ попадают только файлы из контекста (по правилам `.dockerignore`). + +--- + +## Минимальный Dockerfile и сборка + +Готовый вариант каталога **web** лежит в репозитории: `docker/examples/multi-service/web/` — можно собирать оттуда (`docker build -t my-web:1.0 ./web` из корня `multi-service`). Ниже — пошагово «с нуля»: + +``` +mkdir -p ~/learn-docker/web && cd ~/learn-docker/web +``` + +Содержимое `Dockerfile` (ниже — максимально «говорящий» вариант с комментариями; в реальном файле комментарии допустимы): + +```dockerfile +# Синтаксис Dockerfile (опционально, для BuildKit features) +# syntax=docker/dockerfile:1 + +# Базовый образ: лучше фиксировать мажорную версию и по возможности slim/alpine +FROM nginx:1.27-alpine + +# Метаданные (удобно в docker inspect / реестрах) +LABEL org.opencontainers.image.title="Demo static site" +LABEL org.opencontainers.image.description="Nginx with baked-in HTML from COPY" + +# Переменные на этапе сборки (не попадают в runtime автоматически как ENV) +ARG BUILD_DATE=unknown +LABEL build-date="${BUILD_DATE}" + +# Переменные окружения внутри контейнера при запуске +ENV NGINX_ENTRYPOINT_QUIET_LOGS=1 + +# Рабочая директория для последующих инструкций +WORKDIR /usr/share/nginx/html + +# Копирование файлов ИЗ контекста сборки В образ (ключевой способ «положить файлы в образ») +COPY ./html/ /usr/share/nginx/html/ + +# Права (nginx в alpine часто от uid 101 — для продакшена уточняйте образ) +RUN chown -R nginx:nginx /usr/share/nginx/html + +# Документируем порт (не открывает его само по себе на хосте — это метаданные) +EXPOSE 80 + +# Healthcheck — Docker помечает контейнер unhealthy при ошибках (оркестраторы могут рестартовать) +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://127.0.0.1/ || exit 1 + +# Команда по умолчанию наследуется от nginx; при необходимости переопределяем: +# CMD ["nginx", "-g", "daemon off;"] +``` + +Папка `html/index.html`: + +```html + + +Docker COPY demo +

Файл попал в образ через COPY

+ +``` + +Сборка и запуск: + +``` +docker build -t my-web:1.0 . +docker run -d --name from-dockerfile -p 8090:80 my-web:1.0 +``` + +**Скриншот:** этапы сборки `Step ...` и успешное `Successfully tagged my-web:1.0`. + + + +**Скриншот:** браузер на порту 8090 с вашим заголовком. + + + +--- + +## `.dockerignore` — что не отправлять в демон при сборке + +В корне контекста создайте `.dockerignore`, чтобы не копировать мусор и секреты: + +``` +.git +.env +*.log +node_modules +__pycache__ +``` + +**Скриншот:** сравнение размера контекста в логе сборки до/после добавления `.dockerignore` (если делали большую папку). + + + +--- + +## COPY vs ADD + +- **`COPY`** — только копирование из контекста (предсказуемо, предпочтительно). +- **`ADD`** — то же плюс распаковка локальных tar и загрузка по URL (URL почти не используют; для tar — редко). + +--- + +## Многоэтапная сборка (multi-stage) + +Первый этап — сборка (тяжёлые компиляторы), финальный образ — только артефакт. Пример идеи: этап `builder` с Node, финальный — `nginx` со статикой. + +```dockerfile +# syntax=docker/dockerfile:1 +FROM node:22-alpine AS builder +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY src ./src +RUN npm run build + +FROM nginx:1.27-alpine +COPY --from=builder /app/dist /usr/share/nginx/html +EXPOSE 80 +``` + +**Скриншот:** `docker images` — финальный образ заметно меньше, чем образ с полным Node (сравните REPOSITORY/TAG и SIZE). + + + +--- + +## Передача секретов при сборке (обзорно) + +Не кладите пароли в `Dockerfile` в открытом виде. Варианты: + +- BuildKit **secret mount**: `RUN --mount=type=secret ...` и `docker build --secret id=mysecret,src=.env`. +- **ARG** для некритичных параметров; помните, что значения ARG могут остаться в истории слоёв, если не обнулить. + +Подробности — в [Dockerfile reference](https://docs.docker.com/reference/dockerfile/). + +--- + +## Прослойка образов: `docker history` + +``` +docker history my-web:1.0 +``` + +**Скриншот:** список слоёв и размеров. + + + +--- + +## Связь с Compose + +В `docker-compose.yml` можно указать `build: .` для сервиса — Compose вызовет `docker build` с нужным контекстом. Полный стек — в части **4** и в каталоге `docker/examples/multi-service/`. diff --git a/docs/4. Docker Compose — несколько сервисов, сеть, тома.md b/docs/4. Docker Compose — несколько сервисов, сеть, тома.md new file mode 100644 index 0000000..4d2cb13 --- /dev/null +++ b/docs/4. Docker Compose — несколько сервисов, сеть, тома.md @@ -0,0 +1,224 @@ +## Зачем Docker Compose + +Когда сервисов несколько (БД, бэкенд, фронт, прокси), писать длинные `docker run` неудобно. **Compose** описывает весь стек в одном файле (`compose.yaml` или `docker-compose.yml`), поднимает общую сеть и тома, учитывает порядок старта через `depends_on` и проверки `healthcheck`. + +Команды: + +``` +docker compose up -d --build +docker compose ps +docker compose logs -f api +docker compose down +docker compose down -v +``` + +Последняя команда с **`-v`** удалит именованные тома — данные БД пропадут (удобно для сброса демо, опасно если это прод). + +--- + +## Где лежит учебный проект + +В репозитории подготовлен полный стек (четыре сервиса): + +| Путь | Содержимое | +|------|------------| +| `docker/examples/multi-service/compose.yaml` | Описание всех сервисов, сетей, томов | +| `docker/examples/multi-service/.env.example` | Пример переменных окружения для Compose | +| `docker/examples/multi-service/web/` | Nginx + статика через `COPY` | +| `docker/examples/multi-service/api/` | Flask + Postgres + загрузка файлов в том | +| `docker/examples/multi-service/proxy/` | Входной nginx: `/` → web, `/api/` → api | + +Скопируйте переменные окружения: + +``` +cd docker/examples/multi-service +cp .env.example .env +``` + +**Скриншот:** файловый менеджер или IDE с деревом папок `multi-service` (видны `compose.yaml`, `web`, `api`, `proxy`). + + + +Запуск: + +``` +docker compose up -d --build +``` + +**Скриншот:** вывод `docker compose up` — этапы build, Creating..., Started. + + + +**Скриншот:** `docker compose ps` — все сервисы `running` (или `healthy` где настроено). + + + +Откройте в браузере `http://127.0.0.1:8080` (если в `.env` не меняли `HTTP_PORT`). + +**Скриншот:** главная страница демо со ссылками на API. + + + +Проверка API через прокси: + +``` +curl -s http://127.0.0.1:8080/api/health | jq . +curl -s http://127.0.0.1:8080/api/notes | jq . +curl -s -X POST http://127.0.0.1:8080/api/notes \ + -H 'Content-Type: application/json' \ + -d '{"body":"Первая заметка из curl"}' | jq . + +echo demo > /tmp/upload-demo.txt +curl -s -F "file=@/tmp/upload-demo.txt" http://127.0.0.1:8080/api/upload | jq . +curl -s http://127.0.0.1:8080/api/uploads | jq . +``` + +**Скриншот:** терминал с ответами JSON. + + + +--- + +## Логическая схема стека + +1. **Пользователь хоста** подключается только к **proxy** (порт `HTTP_PORT`). +2. **proxy** по пути `/` отдаёт статику с **web**; по `/api/` — **api** (внутренний URL без префикса `/api`). +3. **api** ходит в **db** по hostname `db` и порту `5432` (стандарт Postgres) — резолвится DNS-ом Docker в той же сети `backend`. +4. Данные БД — в томе **`pgdata`**; загруженные файлы API — в томе **`api_uploads`**. + +**Скриншот:** нарисованная схема стрелок browser → proxy → web/api → db; подписать тома. + + + +--- + +## Разбор ключевых фрагментов `compose.yaml` + +### Имена и сеть + +Поле `name: multi-service-demo` задаёт **префикс** для имён контейнеров/ресурсов по умолчанию (удобно не конфликтовать с другими проектами). + +Сеть `backend` с драйвером `bridge` — все перечисленные сервисы в одной L2-сети, видят друг друга по **DNS-имени сервиса** (`db`, `api`, `web`, `proxy`). + +### Зависимости и здоровье + +``` +depends_on: + db: + condition: service_healthy +``` + +Без `condition` Compose лишь **упорядочивает** старт, но не ждёт готовности БД. С `healthcheck` у `db` сервис `api` не начнёт считаться поднятым для зависимых, пока Postgres не пройдёт проверку. + +**Скриншот:** `docker compose logs db` в момент `database system is ready to accept connections`. + + + +### `expose` vs `ports` + +- **`ports`** — публикация на хост (нужно для proxy). +- **`expose`** — документация + открытие порта **между** контейнерами сети; на сетевой интерфейс хоста не выводится. + +Так мы не публикуем Postgres наружу — к БД можно попасть только из контейнеров в той же сети (или если вы явно добавите `ports` для отладки). + +### Тома + +```yaml +volumes: + pgdata: + api_uploads: +``` + +Именованные тома хранятся в области Docker на диске. Пример «скопировать/сохранить файл в Docker» в runtime: + +``` +echo hello > /tmp/up.txt +# имя контейнера смотрите в выводе: docker compose ps -q api +docker cp /tmp/up.txt "$(docker compose ps -q api)":/data/uploads/host-demo.txt +docker compose exec api ls -la /data/uploads +``` + +**Скриншот:** список файлов в `/data/uploads` внутри контейнера `api`. + + + +Альтернатива для разработки — **bind-mount** каталога с хоста в сервис (в учебном файле не включено, чтобы не привязывать пути к вашему `$HOME`): + +```yaml +services: + api: + volumes: + - ./local_uploads:/data/uploads +``` + +--- + +## Копирование файлов в контейнер в контексте Compose + +| Способ | Когда использовать | +|--------|---------------------| +| `COPY` в `Dockerfile` | Статика, код, конфиги, которые должны быть **в образе** на момент деплоя | +| Именованный / bind том | Данные, логи, загрузки пользователей, меняющиеся без пересборки | +| `docker cp` | Разовая отладка, экстренная подмена файла | + +--- + +## Обновление одного сервиса после правки кода + +``` +# изменили только api/app.py +docker compose build api +docker compose up -d api +``` + +**Скриншот:** пересборка только слоя `COPY app.py` (кэш BuildKit для предыдущих шагов). + + + +--- + +## Просмотр конфигурации, которую «увидел» Compose + +``` +docker compose config +``` + +**Скриншот:** полный YAML после подстановки `.env` (проверьте порты и пароли перед скрином — размыть секреты). + + + +--- + +## Остановка и очистка + +Мягко (тома сохраняются): + +``` +docker compose down +``` + +Полный сброс демо: + +``` +docker compose down -v --rmi local +``` + +**Скриншот:** `docker volume ls` без томов проекта после `down -v`. + + + +--- + +## Идеи для дополнительных скриншотов (по желанию) + +- **Portainer** или Docker Desktop — список контейнеров и томов в GUI. +- **`docker compose top`** — процессы внутри сервисов. +- **Ошибка** неверного `depends_on` / healthcheck — учебный кадр «как выглядит», если api стартует раньше БД без `condition`. + +--- + +## Документация + +- [Compose file reference](https://docs.docker.com/compose/compose-file/) +- [Networking in Compose](https://docs.docker.com/compose/networking/) +- [Use Compose Watch](https://docs.docker.com/compose/file-watch/) — live reload при разработке (отдельная тема) diff --git a/docs/assets/.gitkeep b/docs/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/assets/image_1777876640274.png b/docs/assets/image_1777876640274.png new file mode 100644 index 0000000..0d93b75 Binary files /dev/null and b/docs/assets/image_1777876640274.png differ diff --git a/docs/assets/image_1777876646608.png b/docs/assets/image_1777876646608.png new file mode 100644 index 0000000..0d93b75 Binary files /dev/null and b/docs/assets/image_1777876646608.png differ diff --git a/docs/assets/image_1777876655709.png b/docs/assets/image_1777876655709.png new file mode 100644 index 0000000..0d93b75 Binary files /dev/null and b/docs/assets/image_1777876655709.png differ diff --git a/docs/assets/image_1777876699941.png b/docs/assets/image_1777876699941.png new file mode 100644 index 0000000..b6cdbc9 Binary files /dev/null and b/docs/assets/image_1777876699941.png differ diff --git a/docs/assets/image_1777876930686.jpg b/docs/assets/image_1777876930686.jpg new file mode 100644 index 0000000..65b43f4 Binary files /dev/null and b/docs/assets/image_1777876930686.jpg differ diff --git a/docs/assets/image_1777876998936.jpg b/docs/assets/image_1777876998936.jpg new file mode 100644 index 0000000..fd31e55 Binary files /dev/null and b/docs/assets/image_1777876998936.jpg differ diff --git a/docs/assets/image_1777877040047.jpg b/docs/assets/image_1777877040047.jpg new file mode 100644 index 0000000..d02144a Binary files /dev/null and b/docs/assets/image_1777877040047.jpg differ diff --git a/docs/assets/image_1777877076386.jpg b/docs/assets/image_1777877076386.jpg new file mode 100644 index 0000000..1f3c570 Binary files /dev/null and b/docs/assets/image_1777877076386.jpg differ diff --git a/docs/assets/image_1777877203794.jpg b/docs/assets/image_1777877203794.jpg new file mode 100644 index 0000000..1c9a3b6 Binary files /dev/null and b/docs/assets/image_1777877203794.jpg differ diff --git a/docs/assets/image_1777877288015.jpg b/docs/assets/image_1777877288015.jpg new file mode 100644 index 0000000..ff17224 Binary files /dev/null and b/docs/assets/image_1777877288015.jpg differ diff --git a/docs/assets/image_1777877487366.jpg b/docs/assets/image_1777877487366.jpg new file mode 100644 index 0000000..3515e32 Binary files /dev/null and b/docs/assets/image_1777877487366.jpg differ diff --git a/docs/assets/image_1777877573260.jpg b/docs/assets/image_1777877573260.jpg new file mode 100644 index 0000000..fcab004 Binary files /dev/null and b/docs/assets/image_1777877573260.jpg differ diff --git a/docs/assets/image_1777877711970.jpg b/docs/assets/image_1777877711970.jpg new file mode 100644 index 0000000..191d070 Binary files /dev/null and b/docs/assets/image_1777877711970.jpg differ diff --git a/docs/assets/image_1777877795229.jpg b/docs/assets/image_1777877795229.jpg new file mode 100644 index 0000000..38c1913 Binary files /dev/null and b/docs/assets/image_1777877795229.jpg differ diff --git a/docs/assets/image_1777877943446.jpg b/docs/assets/image_1777877943446.jpg new file mode 100644 index 0000000..bcd0dac Binary files /dev/null and b/docs/assets/image_1777877943446.jpg differ diff --git a/docs/assets/image_1777878241650.jpg b/docs/assets/image_1777878241650.jpg new file mode 100644 index 0000000..014d673 Binary files /dev/null and b/docs/assets/image_1777878241650.jpg differ diff --git a/docs/assets/image_1777878325863.jpg b/docs/assets/image_1777878325863.jpg new file mode 100644 index 0000000..1baf0ce Binary files /dev/null and b/docs/assets/image_1777878325863.jpg differ diff --git a/docs/assets/image_1777878347690.jpg b/docs/assets/image_1777878347690.jpg new file mode 100644 index 0000000..862bcb2 Binary files /dev/null and b/docs/assets/image_1777878347690.jpg differ diff --git a/docs/assets/image_1777878360584.jpg b/docs/assets/image_1777878360584.jpg new file mode 100644 index 0000000..862bcb2 Binary files /dev/null and b/docs/assets/image_1777878360584.jpg differ diff --git a/docs/assets/image_1777878436523.jpg b/docs/assets/image_1777878436523.jpg new file mode 100644 index 0000000..1816878 Binary files /dev/null and b/docs/assets/image_1777878436523.jpg differ diff --git a/docs/assets/image_1777879323365.jpg b/docs/assets/image_1777879323365.jpg new file mode 100644 index 0000000..18e9087 Binary files /dev/null and b/docs/assets/image_1777879323365.jpg differ diff --git a/docs/assets/image_1777879337998.jpg b/docs/assets/image_1777879337998.jpg new file mode 100644 index 0000000..63c63ed Binary files /dev/null and b/docs/assets/image_1777879337998.jpg differ diff --git a/docs/assets/image_1777879905425.jpg b/docs/assets/image_1777879905425.jpg new file mode 100644 index 0000000..9f89648 Binary files /dev/null and b/docs/assets/image_1777879905425.jpg differ diff --git a/docs/assets/image_1777880623434.jpg b/docs/assets/image_1777880623434.jpg new file mode 100644 index 0000000..5c23e86 Binary files /dev/null and b/docs/assets/image_1777880623434.jpg differ diff --git a/docs/assets/image_1777880680918.jpg b/docs/assets/image_1777880680918.jpg new file mode 100644 index 0000000..7c51bd3 Binary files /dev/null and b/docs/assets/image_1777880680918.jpg differ diff --git a/docs/assets/image_1777881422205.png b/docs/assets/image_1777881422205.png new file mode 100644 index 0000000..65c3b48 Binary files /dev/null and b/docs/assets/image_1777881422205.png differ diff --git a/docs/assets/image_1777881503149.jpg b/docs/assets/image_1777881503149.jpg new file mode 100644 index 0000000..578650e Binary files /dev/null and b/docs/assets/image_1777881503149.jpg differ diff --git a/examples/multi-service/.env.example b/examples/multi-service/.env.example new file mode 100644 index 0000000..c298f06 --- /dev/null +++ b/examples/multi-service/.env.example @@ -0,0 +1,10 @@ +# Скопируйте в .env и при необходимости измените: +# cp .env.example .env + +# Порт на хосте, куда пробрасывается входной nginx (proxy) +HTTP_PORT=8080 + +# Пароль БД (в продакшене — длинный случайный; не коммитьте .env в git) +POSTGRES_USER=app +POSTGRES_PASSWORD=app +POSTGRES_DB=appdb diff --git a/examples/multi-service/.gitignore b/examples/multi-service/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/examples/multi-service/.gitignore @@ -0,0 +1 @@ +.env diff --git a/examples/multi-service/api/.dockerignore b/examples/multi-service/api/.dockerignore new file mode 100644 index 0000000..647516a --- /dev/null +++ b/examples/multi-service/api/.dockerignore @@ -0,0 +1,5 @@ +__pycache__ +*.pyc +.env +.git +*.md diff --git a/examples/multi-service/api/Dockerfile b/examples/multi-service/api/Dockerfile new file mode 100644 index 0000000..45b1397 --- /dev/null +++ b/examples/multi-service/api/Dockerfile @@ -0,0 +1,34 @@ +# syntax=docker/dockerfile:1 +FROM python:3.12-slim-bookworm + +LABEL service="api" description="Flask API + Postgres + upload volume" + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +# Системные зависимости для psycopg2-binary и healthcheck +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpq5 \ + wget \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --upgrade pip && pip install -r requirements.txt + +# Копируем код приложения в образ +COPY app.py . + +# Не root в runtime (uid в образе python обычно есть) +RUN useradd --create-home --uid 10001 appuser \ + && mkdir -p /data/uploads \ + && chown -R appuser:appuser /data /app + +USER appuser + +EXPOSE 5000 + +# Продакшен-сервер (не встроенный dev-сервер Flask) +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"] diff --git a/examples/multi-service/api/app.py b/examples/multi-service/api/app.py new file mode 100644 index 0000000..2575b9b --- /dev/null +++ b/examples/multi-service/api/app.py @@ -0,0 +1,115 @@ +""" +Минимальное API для демонстрации Compose: +- подключение к Postgres по DATABASE_URL +- таблица notes при старте +- загрузка файла в каталог UPLOAD_DIR (том) +""" +import os +import uuid +from pathlib import Path + +from flask import Flask, jsonify, request +import psycopg2 +from psycopg2.extras import RealDictCursor + +app = Flask(__name__) + +UPLOAD_DIR = Path(os.environ.get("UPLOAD_DIR", "/data/uploads")) +DATABASE_URL = os.environ.get( + "DATABASE_URL", + "postgresql://app:app@localhost:5432/appdb", +) + + +def get_conn(): + return psycopg2.connect(DATABASE_URL, cursor_factory=RealDictCursor) + + +def init_db(): + UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + with get_conn() as conn: + with conn.cursor() as cur: + cur.execute( + """ + CREATE TABLE IF NOT EXISTS notes ( + id SERIAL PRIMARY KEY, + body TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + """ + ) + conn.commit() + + +@app.route("/health") +def health(): + try: + with get_conn() as conn: + with conn.cursor() as cur: + cur.execute("SELECT 1 AS ok;") + row = cur.fetchone() + return jsonify({"status": "ok", "db": bool(row)}), 200 + except Exception as exc: # noqa: BLE001 — демо + return jsonify({"status": "error", "detail": str(exc)}), 503 + + +def _note_row(r): + d = dict(r) + if d.get("created_at") is not None: + d["created_at"] = d["created_at"].isoformat() + return d + + +@app.route("/notes", methods=["GET"]) +def list_notes(): + with get_conn() as conn: + with conn.cursor() as cur: + cur.execute( + "SELECT id, body, created_at FROM notes ORDER BY id DESC LIMIT 50;" + ) + rows = cur.fetchall() + return jsonify([_note_row(r) for r in rows]) + + +@app.route("/notes", methods=["POST"]) +def create_note(): + data = request.get_json(silent=True) or {} + body = (data.get("body") or "").strip() + if not body: + return jsonify({"error": "body required"}), 400 + with get_conn() as conn: + with conn.cursor() as cur: + cur.execute( + "INSERT INTO notes (body) VALUES (%s) RETURNING id, body, created_at;", + (body,), + ) + row = cur.fetchone() + conn.commit() + return jsonify(_note_row(row)), 201 + + +@app.route("/upload", methods=["POST"]) +def upload(): + if "file" not in request.files: + return jsonify({"error": "file field required"}), 400 + f = request.files["file"] + if not f.filename: + return jsonify({"error": "empty filename"}), 400 + safe_name = f"{uuid.uuid4().hex}_{f.filename}" + dest = UPLOAD_DIR / safe_name + f.save(dest) + return jsonify({"saved_as": safe_name, "path_in_container": str(dest)}), 201 + + +@app.route("/uploads", methods=["GET"]) +def list_uploads(): + files = sorted(p.name for p in UPLOAD_DIR.iterdir() if p.is_file()) + return jsonify({"files": files}) + + +with app.app_context(): + init_db() + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) diff --git a/examples/multi-service/api/requirements.txt b/examples/multi-service/api/requirements.txt new file mode 100644 index 0000000..6c7e02a --- /dev/null +++ b/examples/multi-service/api/requirements.txt @@ -0,0 +1,3 @@ +flask==3.0.3 +gunicorn==22.0.0 +psycopg2-binary==2.9.9 diff --git a/examples/multi-service/compose.yaml b/examples/multi-service/compose.yaml new file mode 100644 index 0000000..5306cb3 --- /dev/null +++ b/examples/multi-service/compose.yaml @@ -0,0 +1,89 @@ +# Docker Compose — один файл описывает весь стек. +# Запуск из этой папки: docker compose up -d --build +# Версия схемы compose больше не обязательна в поле `version:` (Compose V2). + +name: multi-service-demo + +services: + # --- Сервис 1: PostgreSQL (готовый образ из Docker Hub) --- + db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-app} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-app} + POSTGRES_DB: ${POSTGRES_DB:-appdb} + volumes: + # Именованный том: данные БД на диске хоста, переживают пересоздание контейнера + - pgdata:/var/lib/postgresql/data + networks: + - backend + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-app} -d ${POSTGRES_DB:-appdb}"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 10s + + # --- Сервис 2: API (собирается из ./api/Dockerfile) --- + api: + build: + context: ./api + dockerfile: Dockerfile + restart: unless-stopped + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-app}@db:5432/${POSTGRES_DB:-appdb} + FLASK_ENV: production + UPLOAD_DIR: /data/uploads + volumes: + # Том для загрузок: файлы копируются в API в папку тома (пример «данные вне образа») + - api_uploads:/data/uploads + depends_on: + db: + condition: service_healthy + networks: + - backend + expose: + - "5000" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5000/health || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 20s + + # --- Сервис 3: статический фронт (nginx + COPY html в образе web) --- + web: + build: + context: ./web + dockerfile: Dockerfile + restart: unless-stopped + networks: + - backend + expose: + - "80" + + # --- Сервис 4: reverse proxy — единая точка входа с хоста --- + proxy: + build: + context: ./proxy + dockerfile: Dockerfile + restart: unless-stopped + ports: + # Порт хоста берём из .env (см. .env.example) + - "${HTTP_PORT:-8080}:80" + depends_on: + web: + condition: service_started + api: + condition: service_healthy + networks: + - backend + +networks: + backend: + driver: bridge + +volumes: + pgdata: + api_uploads: diff --git a/examples/multi-service/proxy/Dockerfile b/examples/multi-service/proxy/Dockerfile new file mode 100644 index 0000000..b3fc252 --- /dev/null +++ b/examples/multi-service/proxy/Dockerfile @@ -0,0 +1,12 @@ +# syntax=docker/dockerfile:1 +FROM nginx:1.27-alpine + +LABEL service="proxy" description="Reverse proxy to web + api" + +# Заменяем дефолтный конфиг на свой (в образе — воспроизводимо) +COPY nginx.conf /etc/nginx/nginx.conf + +EXPOSE 80 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1 diff --git a/examples/multi-service/proxy/nginx.conf b/examples/multi-service/proxy/nginx.conf new file mode 100644 index 0000000..14e51a5 --- /dev/null +++ b/examples/multi-service/proxy/nginx.conf @@ -0,0 +1,48 @@ +# Единая точка входа: снаружи открыт только proxy. +# Внутренние сервисы web и api не публикуют порты на хост — только expose в compose. + +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent"'; + access_log /var/log/nginx/access.log main; + sendfile on; + keepalive_timeout 65; + + # DNS резолвится внутри Docker-сети по именам сервисов + upstream web_upstream { + server web:80; + } + + upstream api_upstream { + server api:5000; + } + + server { + listen 80; + server_name _; + + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass http://web_upstream; + } + + # Запросы /api/* уходят на api без префикса /api (trailing slash у proxy_pass) + location /api/ { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass http://api_upstream/; + } + } +} diff --git a/examples/multi-service/web/.dockerignore b/examples/multi-service/web/.dockerignore new file mode 100644 index 0000000..de951f4 --- /dev/null +++ b/examples/multi-service/web/.dockerignore @@ -0,0 +1,2 @@ +*.md +.git diff --git a/examples/multi-service/web/Dockerfile b/examples/multi-service/web/Dockerfile new file mode 100644 index 0000000..79c66b2 --- /dev/null +++ b/examples/multi-service/web/Dockerfile @@ -0,0 +1,16 @@ +# syntax=docker/dockerfile:1 +FROM nginx:1.27-alpine + +LABEL service="web" description="Static HTML served by nginx" + +WORKDIR /usr/share/nginx/html + +# Все файлы из ./html попадают в образ при сборке (воспроизводимо, без docker cp) +COPY html/ /usr/share/nginx/html/ + +RUN chown -R nginx:nginx /usr/share/nginx/html + +EXPOSE 80 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1 diff --git a/examples/multi-service/web/html/index.html b/examples/multi-service/web/html/index.html new file mode 100644 index 0000000..87e63bc --- /dev/null +++ b/examples/multi-service/web/html/index.html @@ -0,0 +1,17 @@ + + + + + + Demo Web + + + +

Сервис web

+

Эта страница попала в контейнер при сборке образа (COPY в Dockerfile).

+

API стека: /api/health, список заметок: /api/notes.

+ +