Деплой на сервер
Как развернуть проект на сервере и как выкатывать обновления. Этот документ — основной источник по деплою.
Живое демо проекта: https://demoplast.skerter.dev.
Как устроен прод
Один VPS (Selectel VDS, Ubuntu 24.04), Docker Compose. Стек проекта
(deploy/selectel/compose.yaml):
web— Django + gunicorn (внутри прогоняет миграции,collectstaticи demo-seed),db— PostgreSQL 16,media— nginx, отдаёт только/media/(загрузки админки).
Входной reverse-proxy — общий Traefik на :80/:443, который сам выдаёт и
продлевает TLS-сертификаты Let's Encrypt (resolver le). Traefik живёт отдельным
стеком и подключается к проекту через внешнюю docker-сеть proxy; CI его не
трогает. Подробнее об устройстве — Архитектура.
Перед сервером стоит Cloudflare в режиме proxy: DNS-запись домена оранжевая, поэтому посетитель соединяется с edge-узлом Cloudflare, а тот уже идёт на VPS. Это скрывает IP сервера и даёт CDN. Сертификат на origin всё равно выпускает Traefik — Cloudflare не заменяет Let's Encrypt, а работает поверх него.
Образ собирается в GitHub Actions и пушится в GHCR
(ghcr.io/skerter/django-plastic-landing); сервер только тянет готовый образ —
на VPS ничего не собирается (избегаем нагрузки/OOM).
Internet
│
▼
┌────────────┐
│ Cloudflare │ proxied, режим шифрования Full
└────────────┘
│ :80/:443
▼
┌──────────┐ docker network "proxy" (external)
│ Traefik │◄────────┬──────────────┐
└──────────┘ ┌─────▼────┐ ┌─────▼──────┐
│ web │ │ media │
│ gunicorn │ │ nginx │
└─────┬────┘ └────────────┘
network "demoplast_internal" (приватная)
┌─────▼──────┐
│ db │
└────────────┘
web и media — в сети proxy (их видит Traefik по docker-labels);
db — только во внутренней demoplast_internal. Статика —
WhiteNoise внутри web; загрузки админки (/media/) — сервис media.
Первый деплой на новый сервер
Делается один раз вручную: секретов, .env и docker-сети proxy на сервере
ещё нет. Дальнейшие деплои — автоматические (см. ниже).
Предполагается, что сервер уже подготовлен: пользователь deploy с sudo и
доступом по SSH-ключу, открыт firewall (22/80/443), установлен Docker + Compose,
поднят общий Traefik с внешней сетью proxy. Подключение и базовые команды —
Эксплуатация.
1. Клонировать репозиторий
sudo mkdir -p /opt/django-plastic-landing
sudo chown deploy:deploy /opt/django-plastic-landing
git clone https://github.com/Skerter/django-plastic-landing.git /opt/django-plastic-landing
cd /opt/django-plastic-landing
2. Войти в реестр образов (GHCR)
echo "<GHCR_TOKEN>" | docker login ghcr.io -u <github-пользователь> --password-stdin
GHCR_TOKEN — Personal Access Token (classic) с правом read:packages
(GitHub → Settings → Developer settings → Tokens (classic)).
3. Создать .env проекта
cp deploy/selectel/.env.example deploy/selectel/.env
nano deploy/selectel/.env
.env лежит рядом с compose.yaml — Compose читает его и для интерполяции
${...}, и как env_file: контейнеров. Заполнить минимум:
SECRET_KEY—python3 -c "import secrets; print(secrets.token_urlsafe(50))"POSTGRES_PASSWORD— сильный пароль; тот же вDATABASE_URLALLOWED_HOSTSиSITE_DOMAIN— домен сайтаDJANGO_SETTINGS_MODULE=config.settings.prod,DEBUG=False
Полный список переменных — Переменные окружения. Каналы уведомлений (SMTP/Telegram) можно заполнить позже — Каналы уведомлений.
4. Убедиться, что есть внешняя docker-сеть
docker network create proxy # если ещё не создана общим Traefik-стеком
Сеть proxy (external: true) общая с Traefik. Без неё up -d проекта упадёт.
5. Поднять проект
docker compose -f deploy/selectel/compose.yaml pull
docker compose -f deploy/selectel/compose.yaml up -d
web при старте сам прогонит миграции, demo-seed и collectstatic. Traefik по
домену из SITE_DOMAIN автоматически запросит боевой сертификат Let's Encrypt —
как только сайт поднимется, в браузере будет валидный замок.
Лимит боевого Let's Encrypt
5 сертификатов в неделю на домен. Не пересоздавайте том сертификатов Traefik без нужды — иначе можно упереться в лимит.
6. Создать администратора
docker compose -f deploy/selectel/compose.yaml exec web \
python manage.py createsuperuser
Дальше — наполнить каталог через админку и включить каналы уведомлений.
Обновить прод
Деплой автоматический: push в main запускает
GitHub Actions,
который собирает образ, пушит в GHCR и по SSH обновляет контейнеры на сервере.
git push origin main # ← запускает сборку и деплой
Прогресс — на вкладке Actions в GitHub. CI делает git pull + docker
compose pull + up -d проекта; миграции, demo-seed и collectstatic выполняет
сам контейнер web при старте.
Ручной деплой (если CI недоступен):
cd /opt/django-plastic-landing
git pull
docker compose -f deploy/selectel/compose.yaml pull
docker compose -f deploy/selectel/compose.yaml up -d
Go-live: переезд на боевой домен
Порядок ниже проверен на переезде демо с временного nip.io-адреса на
demoplast.skerter.dev. Он же годится для боевого домена заказчика.
Домен задан одной переменной SITE_DOMAIN, которую Compose подставляет в
Traefik-labels — compose.yaml править не нужно.
- DNS: A-запись домена → IP сервера. Если домен в Cloudflare, на время выпуска сертификата поставить запись в DNS only (серое облако) — см. предупреждение ниже. TTL заранее снизить до 300.
.envна сервере:SITE_DOMAIN=<домен>,ALLOWED_HOSTS=<домен>,CSRF_TRUSTED_ORIGINS=https://<домен>. Если хостов несколько — перечислить через запятую (<домен>,www.<домен>).- Применить:
docker compose -f deploy/selectel/compose.yaml up -d. Compose увидит изменившиеся labels и пересоздастwebиmedia, Traefik построит роутер на новыйHostи запросит сертификат. - Проверить:
curl -sI https://<домен>/→HTTP/2 200. Ответ400сserver: gunicornозначает, что контейнер не видит домен вALLOWED_HOSTS(частая причина — дубль переменной в.env, побеждает последняя строка). - Вернуть прокси Cloudflare (оранжевое облако), если снимали на шаге 1,
и убедиться, что сайт по-прежнему отдаёт
200. - Открыть индексацию в
robots_txt(apps/pages/views.py): на демо стоитDisallow: /, боевому сайту нуженAllow: /.
Cloudflare: сначала сертификат, потом прокси
В режимах Full и Full (Strict) Cloudflare идёт на origin по HTTPS и ждёт
там рабочий сертификат. Пока Traefik его не выпустил, получается замкнутый
круг, и посетитель видит 526 Invalid SSL certificate. Поэтому запись
сначала переводится в DNS only, сертификат выпускается напрямую, и только
потом включается прокси. Продлению сертификата прокси уже не мешает:
Cloudflare не редиректит путь /.well-known/acme-challenge/.
Режим Flexible несовместим с проектом
При Flexible Cloudflare ходит на origin по HTTP, а SECURE_SSL_REDIRECT
в config/settings/prod.py
отправляет обратно на HTTPS — получается бесконечный цикл редиректов.
Допустимы только Full и Full (Strict).