diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..f32a9ed
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,37 @@
+# VCS
+.git
+.github
+.gitignore
+
+# Python artifacts
+__pycache__
+*.py[oc]
+*.egg-info
+build
+dist
+
+# Virtual environments and caches
+.venv
+.cache
+.pytest_cache
+.ruff_cache
+.uv-cache
+
+# Secrets and local data
+.env
+*.sqlite3
+staticfiles
+
+# Docker
+Dockerfile
+docker-compose*.yml
+.dockerignore
+
+# Tooling and docs
+.pre-commit-config.yaml
+Makefile
+README.md
+
+# IDE
+.idea
+.vscode
diff --git a/.gitignore b/.gitignore
index 37c188f..297bb87 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ wheels/
# Logs and databases
*.log
*.sqlite3
+staticfiles/
.cache/
# Virtual environments
diff --git a/Dockerfile b/Dockerfile
index a5db3bf..cea9739 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,64 +1,81 @@
-ARG UID=1000
+# syntax=docker/dockerfile:1
+
ARG PYTHON_VERSION=3.13
+FROM ghcr.io/astral-sh/uv:0.11 AS uv
+
+
+# ============================== builder ==============================
FROM python:${PYTHON_VERSION}-slim-trixie AS builder
-RUN apt-get update && apt-get install -y \
- build-essential \
- libpq-dev \
- curl \
- && rm -rf /var/lib/apt/lists/*
+ENV UV_COMPILE_BYTECODE=1 \
+ UV_LINK_MODE=copy \
+ UV_PYTHON_DOWNLOADS=never \
+ UV_PROJECT_ENVIRONMENT=/opt/venv
-RUN useradd --create-home --shell /bin/bash builder
-USER builder
-WORKDIR /home/builder
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential \
+ libpq-dev \
+ && rm -rf /var/lib/apt/lists/*
-COPY --chown=builder:builder --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
+COPY --from=uv /uv /uvx /usr/local/bin/
-RUN uv venv .venv
-ENV PATH="/home/builder/.venv/bin:$PATH"
+WORKDIR /app
-COPY --chown=builder:builder pyproject.toml uv.lock* ./
+COPY pyproject.toml uv.lock ./
+# --locked гарантирует точное соответствие uv.lock (сборка падает при рассинхроне)
ARG INSTALL_DEV=false
-RUN if [ "$INSTALL_DEV" = "true" ] ; then \
- uv sync --group dev ; \
+RUN --mount=type=cache,target=/root/.cache/uv \
+ if [ "$INSTALL_DEV" = "true" ]; then \
+ uv sync --locked --group dev; \
else \
- uv sync --no-group dev ; \
+ uv sync --locked --no-group dev; \
fi
+# ============================== runtime ==============================
FROM python:${PYTHON_VERSION}-slim-trixie AS runtime
-RUN apt-get update && apt-get install -y \
- libpq5 \
- netcat-openbsd \
- && rm -rf /var/lib/apt/lists/*
-
-ARG UID
-RUN useradd --create-home --shell /bin/bash --uid ${UID:-1000} django
-USER django
-WORKDIR /home/django/app
+LABEL org.opencontainers.image.title="django-boilerplate" \
+ org.opencontainers.image.description="Production-ready Django API boilerplate" \
+ org.opencontainers.image.source="https://github.com/ak4code/django_boilerplate"
-COPY --from=builder --chown=django:django /home/builder/.venv /home/django/.venv
-
-ENV VIRTUAL_ENV="/home/django/.venv"
-ENV PATH="$VIRTUAL_ENV/bin:$PATH"
+ARG UID=1000
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ libpq5 \
+ netcat-openbsd \
+ && rm -rf /var/lib/apt/lists/* \
+ && useradd --create-home --shell /usr/sbin/nologin --uid "${UID}" django \
+ && install -d -o django -g django /app /app/staticfiles
+
+ENV VIRTUAL_ENV=/opt/venv \
+ PATH=/opt/venv/bin:$PATH \
+ PYTHONPATH=/app \
+ PYTHONUNBUFFERED=1 \
+ PYTHONDONTWRITEBYTECODE=1
-COPY --chown=django:django . .
+# venv принадлежит root: приложение не может модифицировать свои зависимости
+COPY --from=builder /opt/venv /opt/venv
+COPY --chmod=755 entrypoint.sh /usr/local/bin/entrypoint.sh
-RUN mkdir -p /home/django/logs /home/django/www
+WORKDIR /app
-ENV PYTHONPATH=/home/django/app \
- PYTHONUNBUFFERED=1 \
- PYTHONDONTWRITEBYTECODE=1
+USER django
EXPOSE 8000
-# Entrypoint
-COPY --chown=django:django entrypoint.sh /home/django/entrypoint.sh
-RUN chmod +x /home/django/entrypoint.sh
+HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
+ CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/', timeout=4)"]
-ENTRYPOINT ["/home/django/entrypoint.sh"]
+ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["dev"]
+
+
+# ============================= production ============================
+FROM runtime AS production
+
+COPY --chown=django:django . .
+
+CMD ["prod"]
diff --git a/Makefile b/Makefile
index 2658280..3de9965 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: env install run migrate makemigrations superuser test lint format up down logs
+.PHONY: env install run migrate makemigrations superuser test lint format up down logs up-prod down-prod logs-prod
env:
@if [ -f .env ]; then \
@@ -42,3 +42,12 @@ down:
logs:
docker compose logs -f django
+
+up-prod:
+ docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
+
+down-prod:
+ docker compose -f docker-compose.yml -f docker-compose.prod.yml down
+
+logs-prod:
+ docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f django
diff --git a/README.md b/README.md
index eeb3354..8a28995 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@
| 👤 **Кастомный пользователь** | Email-логин + `external_id` (UUID) для интеграций |
| 💓 **Health-check** | `/health/` с проверкой БД и Redis — готов для k8s-проб |
| 🧹 **Качество кода** | ruff · bandit · ty · pre-commit · pytest · GitHub Actions |
-| 🐳 **Docker** | Multi-stage образ (non-root) + compose с PostgreSQL и Redis |
+| 🐳 **Docker** | Multi-stage образ (non-root, HEALTHCHECK) + compose dev/prod |
## 🚀 Быстрый старт
@@ -196,13 +196,43 @@ make up / down # 🐳 docker compose up/down
`ENVIRONMENT=prod` включает: `DEBUG=False`, SSL redirect, HSTS, secure cookies,
-JSON-only рендеринг DRF, манифестную статику (whitenoise) и запуск через gunicorn
-(`MODE=prod` в docker-compose).
+JSON-only рендеринг DRF, манифестную статику (whitenoise) и запуск через gunicorn.
```bash
-ENVIRONMENT=prod MODE=prod INSTALL_DEV=false docker compose up -d --build
+make up-prod
+# или явно:
+docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
```
+В prod-режиме код запекается в образ (таргет `production`, без bind-mount),
+dev-зависимости не устанавливаются, порты PostgreSQL/Redis не публикуются,
+на сервисы наложены лимиты CPU/памяти.
+
+
+
+
+🐳 Устройство Docker-окружения
+
+
+
+**Dockerfile** (multi-stage):
+
+| Стадия | Назначение |
+| --- | --- |
+| `builder` | Сборка venv через uv (`--locked`, BuildKit-кэш, прекомпиляция байткода) |
+| `runtime` | Минимальный рантайм для разработки: non-root, venv принадлежит root |
+| `production` | `runtime` + код, запечённый в образ |
+
+Образ содержит `HEALTHCHECK`, который опрашивает `/health/` (БД + Redis).
+
+**Compose** (три файла):
+
+| Файл | Когда применяется |
+| --- | --- |
+| `docker-compose.yml` | Общая база: сервисы, healthchecks, лимиты логов |
+| `docker-compose.override.yml` | Dev: подхватывается автоматически — bind-mount кода, порты БД на localhost |
+| `docker-compose.prod.yml` | Prod: подключается явно — таргет `production`, лимиты ресурсов |
+
## 🧹 Качество кода
diff --git a/config/settings/prod.py b/config/settings/prod.py
index 5224290..121c64f 100644
--- a/config/settings/prod.py
+++ b/config/settings/prod.py
@@ -1,6 +1,11 @@
# Production settings
DEBUG = False
+# Healthcheck Docker/k8s ходит изнутри контейнера на 127.0.0.1 по HTTP
+if '127.0.0.1' not in ALLOWED_HOSTS: # noqa: F821
+ ALLOWED_HOSTS = [*ALLOWED_HOSTS, '127.0.0.1'] # noqa: F821
+SECURE_REDIRECT_EXEMPT = [r'^health/$']
+
# Приложение работает за обратным прокси (nginx/traefik), который терминирует TLS
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = env.bool('DJANGO_SECURE_SSL_REDIRECT', default=True) # noqa: F821
diff --git a/docker-compose.override.yml b/docker-compose.override.yml
new file mode 100644
index 0000000..3d6311b
--- /dev/null
+++ b/docker-compose.override.yml
@@ -0,0 +1,21 @@
+# Dev-оверрайд: подхватывается `docker compose up` автоматически.
+# Исходники монтируются в контейнер, порты БД доступны только с localhost.
+services:
+ postgres:
+ ports:
+ - "127.0.0.1:5432:5432"
+
+ redis:
+ ports:
+ - "127.0.0.1:6379:6379"
+
+ django:
+ build:
+ target: runtime
+ args:
+ INSTALL_DEV: "true"
+ environment:
+ ENVIRONMENT: dev
+ volumes:
+ - .:/app
+ command: dev
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
new file mode 100644
index 0000000..c2983f1
--- /dev/null
+++ b/docker-compose.prod.yml
@@ -0,0 +1,32 @@
+# Production-оверрайд:
+# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
+# Код запекается в образ (без bind-mount), порты БД не публикуются.
+services:
+ django:
+ build:
+ target: production
+ args:
+ INSTALL_DEV: "false"
+ environment:
+ ENVIRONMENT: prod
+ command: prod
+ deploy:
+ resources:
+ limits:
+ cpus: "${DJANGO_CPU_LIMIT:-2}"
+ memory: ${DJANGO_MEMORY_LIMIT:-1g}
+ reservations:
+ cpus: "0.5"
+ memory: 256m
+
+ postgres:
+ deploy:
+ resources:
+ limits:
+ memory: ${POSTGRES_MEMORY_LIMIT:-1g}
+
+ redis:
+ deploy:
+ resources:
+ limits:
+ memory: 384m
diff --git a/docker-compose.yml b/docker-compose.yml
index e86aae9..7c98cf0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,9 +1,16 @@
+# Общая конфигурация. Для разработки автоматически подхватывается
+# docker-compose.override.yml, для production:
+# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
+x-logging: &default-logging
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "3"
+
services:
postgres:
image: postgres:18-alpine
hostname: postgres
- ports:
- - "5432:5432"
environment:
POSTGRES_DB: ${POSTGRES_DB:-app}
POSTGRES_USER: ${POSTGRES_USER:-app}
@@ -17,14 +24,13 @@ services:
retries: 5
start_period: 10s
restart: unless-stopped
+ logging: *default-logging
networks:
- backend
redis:
image: redis:8-alpine
hostname: redis
- ports:
- - "6379:6379"
volumes:
- redis_data:/data
command: >
@@ -39,6 +45,7 @@ services:
retries: 5
start_period: 10s
restart: unless-stopped
+ logging: *default-logging
networks:
- backend
@@ -46,24 +53,18 @@ services:
build:
context: .
dockerfile: Dockerfile
- target: runtime
args:
PYTHON_VERSION: ${PYTHON_VERSION:-3.13}
UID: ${UID:-1000}
- INSTALL_DEV: ${INSTALL_DEV:-true}
hostname: django
+ init: true
ports:
- "8000:8000"
- volumes:
- - .:/home/django/app
- - ./.cache/logs:/home/django/logs
- - ./.cache/www:/home/django/www
- - uv_cache:/home/django/.cache/uv
environment:
- ENVIRONMENT: ${ENVIRONMENT:-dev}
DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE:-config.settings}
DATABASE_URL: postgres://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-app}@postgres:5432/${POSTGRES_DB:-app}
REDIS_URL: redis://redis:6379/1
+ REDIS_HOST: redis
POSTGRES_HOST: postgres
env_file:
- ./.env
@@ -72,15 +73,14 @@ services:
condition: service_healthy
redis:
condition: service_healthy
- command: ${MODE:-dev}
restart: unless-stopped
+ logging: *default-logging
networks:
- backend
volumes:
pg_data:
redis_data:
- uv_cache:
networks:
backend: