Docker — пакування та поставка ESWF / DOP¶
Все про Docker у цьому проекті: які образи бувають, як їх збирати, запускати, налаштовувати, дебажити, і як зробити нову поставку.
Зміст¶
- Огляд
- Dockerfile.demo — мінімальний демо-образ
- Структура образу
- Команди для роботи з образом
- Де фізично лежать образи
- Порти і конфлікти з dev-середовищем
- Налаштування процесів усередині контейнера
- Додавання плагінів
- Розширена поставка (full image)
- Перенесення образу на іншу машину
- Дебаг
- Поширені проблеми
Огляд¶
У репозиторії є один Docker-образ — Dockerfile.demo у корені.
Це самодостатній односкладовий образ для оцінки продукту колегою / клієнтом:
один контейнер, один порт, попередньо засіяна SQLite-база. Не призначений
для production — для production використовується render.yaml (Render.com)
та власні Postgres/CDN.
| Файл | Призначення |
|---|---|
Dockerfile.demo |
Multi-stage білд: frontend (node:20) + runtime (python:3.13-slim) |
.dockerignore |
Виключає mobile/, інші фронтенди, node_modules, venv, db.sqlite3 |
docker/nginx.conf |
nginx як reverse proxy на Daphne + статика |
docker/supervisord.conf |
Менеджер процесів: запускає Daphne + nginx |
backend/eswf/settings/demo.py |
Settings для контейнера: SQLite, ALLOWED_HOSTS=*, CORS відкритий |
README.colleague.md |
Колезі-faced quick-start |
Dockerfile.demo — мінімальний демо-образ¶
Що в ньому є¶
- Frontend: тільки ERP (React 19 + Mantine 8). Portal / News / Shop не в образі.
- Backend: усі core Django apps + плагін
essentials_quality. - Плагіни не входять:
logistic(немає в backend/),containerhub(хардово залежить відlogistic.models.ContainerType, тому виключений у демо — інакше падаєModuleNotFoundError). - База: SQLite, файл
/app/backend/db.sqlite3, попередньо засіяна черезseed_demo --resetпід часdocker build. Перший запуск контейнера миттєвий — нічого не мігрується. - Дані: 6 модельних організацій, ~10 водіїв, ~11 ТЗ, 30 ПЛ, 12 замовлень,
3 cross-org sales scenarios + 1 purchase scenario з PartyLedger проводками,
App Store з активаційними кодами і fake-розсилкою email. Усе з
descriptionукраїнською (див.seed-methodology.md§2-quinquies). - Логін:
admin/admin.
Ключові ENV¶
| Змінна | Default | Призначення |
|---|---|---|
DJANGO_SETTINGS_MODULE |
eswf.settings.demo |
Settings-модуль |
DJANGO_SECRET_KEY |
demo-not-a-secret-... |
Не для production! |
ESWF_PLUGINS |
essentials_quality |
Перелік активних плагінів (CSV або *) |
CSRF_TRUSTED_ORIGINS |
(порожньо) | За потреби — якщо колега виставляє образ за зовнішнім доменом |
Структура образу¶
Stage 1: frontend-build (node:20-alpine)¶
WORKDIR /build/erp
COPY frontend/erp/package*.json → npm ci
COPY frontend/erp/ → npx vite build
└─→ /build/erp/dist/
Чому npx vite build, а не npm run build:
у package.json сценарій build = tsc -b && vite build, а в репо є
pre-existing TS-помилки. npx vite build пропускає type-check і збирає
завжди (це задокументовано в CLAUDE.md, розділ Launchers).
Stage 2: runtime (python:3.13-slim)¶
apt-get → nginx + supervisor + libpq5 + curl
python -m venv /opt/venv ← deps в окремий venv (predictable PATH)
pip install -r requirements.txt
WORKDIR /app/backend
COPY backend/ → /app/backend/
COPY --from=frontend-build /build/erp/dist/ → /var/www/erp/
COPY docker/nginx.conf → /etc/nginx/nginx.conf
COPY docker/supervisord.conf → /etc/supervisor/conf.d/eswf.conf
ENV ESWF_PLUGINS="essentials_quality"
RUN: migrate core → bootstrap-tenant → migrate → drop bootstrap → seed_demo --reset → collectstatic
EXPOSE 80
CMD supervisord
Bootstrap-tenant dance¶
У RUN-кроці є нетривіальний фінт:
python manage.py migrate core --noinput # тільки core (Tenant table)
python manage.py shell -c "...create _bootstrap_..." # тимчасовий тенант
python manage.py migrate --noinput # решта
python manage.py shell -c "...delete _bootstrap_..." # прибрати
python manage.py seed_demo --reset # реальні дані
Навіщо. Міграція essentials/0024_taxrate_predefined_item_tax_rate.py
створює predefined TaxRate. На свіжій БД вона тригерить гілку
"no tenants exist" і пише tenant=None, але поле tenant стане nullable
лише в міграції 0025. На fresh DB → IntegrityError: NOT NULL constraint failed.
Bootstrap-тенант форсує міграцію 0024 у per-tenant гілку, а 0025 потім
переплавляє ці записи в глобальний (tenant=NULL).
Виправляти саму міграцію 0024 ризиковано (історія міграцій), тому фінт у Dockerfile є чистим обхідним шляхом для одноразового білду.
Команди для роботи з образом¶
Збірка¶
# Перший раз — повний білд (5–8 хв)
docker build -f Dockerfile.demo -t eswf-demo .
# Повторний — BuildKit перевикористає кеш до зміненого кроку (1–2 хв)
docker build -f Dockerfile.demo -t eswf-demo .
# Без кешу (форсовано все з нуля)
docker build --no-cache -f Dockerfile.demo -t eswf-demo .
# З додатковими ENV (наприклад, інший секретний ключ)
docker build --build-arg DJANGO_SECRET_KEY=mysecret -f Dockerfile.demo -t eswf-demo .
Запуск¶
# Foreground — Ctrl+C зупиняє і видаляє
docker run --rm -p 8080:80 eswf-demo
# Background, з ім'ям
docker run -d --name eswf -p 8080:80 eswf-demo
# Інший порт хоста
docker run --rm -p 9000:80 eswf-demo
# З персистентним томом (зберігає зміни в БД між запусками)
docker run -d --name eswf -p 8080:80 -v eswf-data:/app/backend eswf-demo
Увага про том: при першому запуску з томом том перекриває /app/backend з образу — БД буде ПУСТА. Треба зайти всередину і запустити seed:
docker exec -it eswf python manage.py seed_demo --reset.
Зупинка / видалення¶
docker stop eswf # зупинити
docker rm eswf # видалити контейнер
docker rmi eswf-demo # видалити образ
docker system prune -a # видалити ВСІ невикористані образи + кеш (звільнити vhdx)
Де фізично лежать образи¶
Docker Desktop на Windows з WSL2-бекендом тримає всі образи всередині одного віртуального диска:
Цей файл росте з кожним новим образом. Звільнити місце —
docker system prune -a. Сам vhdx Windows не зменшує автоматично; для
ручного стискання:
wsl --shutdown
diskpart
> select vdisk file="C:\Users\<USER>\AppData\Local\Docker\wsl\disk\docker_data.vhdx"
> attach vdisk readonly
> compact vdisk
> detach vdisk
> exit
WSL distro Docker Desktop:
wsl -l -v показує docker-desktop distro (на новіших Docker Desktop —
один distro замість двох).
Порти і конфлікти з dev-середовищем¶
| Сервіс | Порт | Звідки слухає |
|---|---|---|
| Демо-контейнер | 8080 → 80 (container) | nginx у контейнері |
| Backend dev | 8000 | Daphne (host) |
| ERP dev | 5173 | Vite (host) |
| Portal dev | 3000 | Next.js (host) |
| News dev | 5174 | Vite (host) |
| Shop dev | 5175 | Next.js (host) |
Контейнер слухає тільки мапнутий порт хоста (за замовчуванням 8080).
Решта dev-портів вільні. Можна паралельно тримати docker run і
launcher-dev.js без конфліктів.
Якщо потрібен інший порт хоста: docker run -p 9000:80 eswf-demo →
http://localhost:9000/.
Налаштування процесів усередині контейнера¶
Менеджер процесів — supervisord (docker/supervisord.conf):
[program:daphne]
command=/opt/venv/bin/daphne -b 127.0.0.1 -p 8000 eswf.asgi:application
directory=/app/backend
environment=DJANGO_SETTINGS_MODULE="eswf.settings.demo",PYTHONUNBUFFERED="1"
priority=10
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
priority=20
- Daphne слухає
127.0.0.1:8000(тільки всередині контейнера, не експортується назовні). - nginx на 80 — приймає публічний трафік, проксує
/api/,/ws/,/admin/на Daphne, віддає/static/(Django collectstatic),/media/(uploads) і ERP-SPA (try_files ... /index.html). - WebSocket — у
nginx.confє upgrade-map і окремаlocation /ws/.
Логи обох процесів пишуться в /dev/stdout і /dev/stderr, тож
docker logs <container> показує все злито.
Зайти в контейнер живий:
docker exec -it eswf bash
supervisorctl status # daphne RUNNING, nginx RUNNING
supervisorctl restart daphne # рестарт без перезапуску контейнера
tail -f /var/log/nginx/error.log
Додавання плагінів¶
Плагіни ESWF — це окремі Django apps, які додаються в INSTALLED_APPS
динамічно через ESWF_PLUGINS env (див. plugin-instruction.md).
Плагіни що вже в backend/¶
essentials_quality— самостійний, працює зі стандартним демо.containerhub— залежить відlogistic(хардовийfrom logistic.models import ContainerTypeуcontainerhub/views.py). У демо без logistic — крашить URL resolution, тому виключений.
Плагіни в plugins/¶
plugins/eswf-logistic/logistic/— Django applogistic, не на Python path. Маєpyproject.toml, можна встановити черезpip install ./plugins/eswf-logistic.
Як підключити logistic у образі¶
Додати в Dockerfile.demo після pip install -r requirements.txt:
COPY plugins/eswf-logistic /tmp/logistic-plugin
RUN pip install /tmp/logistic-plugin && rm -rf /tmp/logistic-plugin
І змінити:
Перевірити: docker exec eswf python -c "import logistic; print(logistic.__file__)".
Розширена поставка (full image)¶
Поточний Dockerfile.demo ставить тільки ERP. Щоб додати усі фронтенди
+ logistic, треба окремий Dockerfile.full з такими додатковими стадіями:
1. Frontend збірки (3 нові stage)¶
FROM node:20-alpine AS portal-build
WORKDIR /build/portal
COPY frontend/portal/package*.json ./
RUN npm ci
COPY frontend/portal/ ./
RUN npm run build # → out/ (Next.js static export)
FROM node:20-alpine AS news-build
WORKDIR /build/news
COPY frontend/news/package*.json ./
RUN npm ci
COPY frontend/news/ ./
RUN npx vite build # → dist/
FROM node:20-alpine AS shop-build
WORKDIR /build/shop
COPY frontend/shop/package*.json ./
RUN npm ci
COPY frontend/shop/ ./
RUN npm run build # → .next/ (Next.js SSR)
2. Runtime — додати Node для shop SSR¶
FROM python:3.13-slim AS runtime
RUN apt-get install -y nodejs npm # для запуску `next start` або `node`
COPY --from=portal-build /build/portal/out/ /var/www/portal/
COPY --from=news-build /build/news/dist/ /var/www/news/
COPY --from=shop-build /build/shop/ /app/shop/
COPY plugins/eswf-logistic /tmp/logistic-plugin
RUN pip install /tmp/logistic-plugin
ENV ESWF_PLUGINS="essentials_quality,containerhub,logistic"
3. nginx — multi-server для віртуальних хостів¶
server { server_name eswf.dev; root /var/www/portal/; index index.html; ... }
server { server_name news.eswf.dev; root /var/www/news/; index index.html; ... }
server { server_name erp.eswf.dev; root /var/www/erp/; index index.html; try_files $uri /index.html; }
server { server_name shop.eswf.dev; location / { proxy_pass http://127.0.0.1:3001; } }
server { server_name api.eswf.dev; location / { proxy_pass http://127.0.0.1:8000; } }
4. supervisord — додати shop process¶
[program:shop]
command=/usr/bin/node /app/shop/node_modules/next/dist/bin/next start -p 3001
directory=/app/shop
priority=15
5. Стратегія URL — як колега відкриває різні фронтенди¶
Тут є три підходи, від найпростішого до найскладнішого:
A. Path-based (рекомендовано для демо)¶
Усе на одному порту, різні фронтенди — за різними префіксами шляху:
http://localhost:8080/ → Portal
http://localhost:8080/news/ → News
http://localhost:8080/shop/ → Shop
http://localhost:8080/erp/ → ERP
http://localhost:8080/api/ → Backend
nginx роздає за префіксом (location /erp/ { root /var/www; } тощо).
Колезі — один URL, нуль налаштувань. Ціна: треба перезібрати фронтенди
з відповідним base path (Vite base: '/erp/', Next.js basePath: '/shop'),
і поправити будь-які абсолютні /api/ посилання у фронтендах на
<base>/api/.
B. Port-based (нульова доробка фронтендів)¶
Кожен фронтенд на окремому порту хоста:
docker run -p 8080:80 -p 8081:81 -p 8082:82 -p 8083:83 eswf-full
http://localhost:8080/ → Portal
http://localhost:8081/ → Shop
http://localhost:8082/ → News
http://localhost:8083/ → ERP
nginx має кілька server { listen N; }. Швидко зробити, але колега бачить
кілька різних URL — не дуже зручно.
C. Host-based (production-like, тільки за потреби)¶
Колега додає в C:\Windows\System32\drivers\etc\hosts (як адмін):
nginx розрізняє за server_name. Сенс тільки якщо у фронтенді хардкодом
зашиті ці домени (fetch("https://api.eswf.dev/...")) і ти хочеш повністю
production-like демо. Для оцінки продукту — overkill: колезі некомфортно
правити hosts.
Рекомендація — варіант C з nip.io¶
Архітектурно правильний — host-based (C). У production воно вже саме
так працює (eswf.dev, shop.eswf.dev, api.eswf.dev — окремі домени).
Path-based (A) ламає Service Worker scope, Next.js Image, природний
cookie/CSRF flow і генерує "другу версію" фронтендів, що розходиться з
production. Port-based (B) виглядає некрасиво для колеги.
Бар'єр з hosts-файлом обходимо через публічні wildcard-DNS сервіси
типу nip.io або sslip.io. Вони резолвлять *.127.0.0.1.nip.io →
127.0.0.1 без жодного налаштування на стороні колеги:
http://eswf.127.0.0.1.nip.io:8080/ → Portal
http://shop.127.0.0.1.nip.io:8080/ → Shop
http://news.127.0.0.1.nip.io:8080/ → News
http://erp.127.0.0.1.nip.io:8080/ → ERP
http://api.127.0.0.1.nip.io:8080/ → API
nginx розрізняє за server_name eswf.127.0.0.1.nip.io; тощо. Колега:
- відкриває одну стартову адресу (наприклад, eswf.127.0.0.1.nip.io:8080);
- по посиланнях у Portal переходить на ERP/Shop/News — все працює як у production;
- нічого не править у hosts;
- працює offline (DNS-резолюція nip.io відбулась один раз і кешується ОС).
Поточний репозиторій не містить
Dockerfile.full— реалізація відкладена до Wave 2. Архітектурне рішення зафіксовано: варіант C з nip.io (host-based з публічним wildcard-DNS), щоб не ламати production-конфігурацію фронтендів.
Перенесення образу на іншу машину¶
Через .tar файл¶
# На збиральній машині
docker save eswf-demo:latest -o eswf-demo.tar # ~600 MB
# На цільовій
docker load -i eswf-demo.tar
docker run -p 8080:80 eswf-demo
Через registry (Docker Hub / GitHub Container Registry)¶
docker tag eswf-demo:latest ghcr.io/eswf/demo:wave1
docker push ghcr.io/eswf/demo:wave1
# На цільовій
docker pull ghcr.io/eswf/demo:wave1
docker run -p 8080:80 ghcr.io/eswf/demo:wave1
Дебаг¶
Контейнер не стартує¶
Перші ~30 рядків — Daphne, далі — nginx. Якщо Daphne крашиться при старті (зазвичай через відсутній модуль або міграцію) — лог покаже traceback.
502 Bad Gateway від nginx¶
Daphne впав. Зайди всередину:
docker exec -it <container-id> bash
supervisorctl status # очікувано: daphne RUNNING; якщо FATAL — див. логи
supervisorctl tail daphne stderr
Перевірити стан БД¶
docker exec -it <container-id> bash
sqlite3 /app/backend/db.sqlite3 ".tables"
sqlite3 /app/backend/db.sqlite3 "SELECT COUNT(*) FROM essentials_organization;"
Reset бази без перезбірки образу¶
Подивитися що зашите в образі¶
Поширені проблеми¶
For security reasons C:\ProgramData\DockerDesktop must be owned by elevated account¶
Інсталер Docker Desktop знайшов залишок від попередньої спроби установки. Видали теку як адмін:
Запусти інсталер Run as administrator.
ModuleNotFoundError: No module named 'logistic'¶
ESWF_PLUGINS=* намагається підключити плагін logistic, але його нема
в backend/. Або встанови його з plugins/eswf-logistic/ (див.
"Додавання плагінів"), або виключи з ESWF_PLUGINS.
containerhub/views.py теж хардово імпортує logistic.models → у full
image сидять разом, у demo image — обидва виключені.
IntegrityError: NOT NULL constraint failed: essentials_taxrate.tenant_id¶
Міграція 0024 на свіжій БД хоче tenant=None, але в схемі цієї міграції
поле NOT NULL (стане nullable у 0025). Рішення вшито в Dockerfile.demo як
"bootstrap-tenant dance" (див. вище). Поза контейнером — створи
Tenant(code='_bootstrap_') через manage.py shell перед migrate.
vhdx росте і не зменшується¶
Docker Desktop не зменшує docker_data.vhdx після видалення образів.
Періодично:
docker system prune -a # видалити невикористані образи/шари/кеш
wsl --shutdown
# далі diskpart compact vdisk (див. "Де фізично лежать образи")
Посилання¶
- README.colleague.md — quick-start для колеги
- desktop-installer.md — 📋 план Windows-інсталятора з ярликом для end-users (tarball-based, поверх цього Docker-образу)
- Dockerfile.demo — сам Dockerfile
- docker/nginx.conf — nginx config
- docker/supervisord.conf — supervisord config
- backend/eswf/settings/demo.py — settings контейнера
- seed-methodology.md — як побудовані демо-дані
- plugin-instruction.md — як писати плагіни