20 Commits

Author SHA1 Message Date
7f96932a40 fix: directory with proper permissions 1000:1000 in Dockerfile
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 45s
2026-03-20 17:31:24 +03:00
d59c867010 fix: increase SQLite timeout to 60s and enable WAL mode for better concurrency with multiple Gunicorn workers
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 36s
2026-03-20 03:05:38 +03:00
9ea851ad95 fix: add /app/database directory creation with app:app ownership in Dockerfile
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 34s
2026-03-20 02:28:58 +03:00
5146f88c7d mod: fix: add sqlite3 utility to final stage Dockerfile for manage.py dbshell support
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m3s
2026-03-20 01:52:03 +03:00
20ecb9cc4c fix: set X-Forwarded-Proto to https explicitly for internal proxying to Docker 2026-03-20 01:09:08 +03:00
ea2d352cae fix: copy /usr/local/bin from builder stage to include gunicorn executable
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 36s
2026-03-20 00:51:53 +03:00
d459eedf41 fix: create media/errors directory with proper permissions in Dockerfile
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 33s
2026-03-20 00:43:42 +03:00
9d31429e14 fix: uncomment chown for nginx_configs_host and correct volume path to ./config
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 33s
2026-03-20 00:29:54 +03:00
c5963d1d30 mod: Dockerfile .
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 33s
2026-03-20 00:19:39 +03:00
12001bc749 mod: Dockerfile
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 22s
2026-03-20 00:17:31 +03:00
99a2ace43f fix: arm64 bild & create nginx config directory with proper permissions before USER app
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m36s
2026-03-19 23:57:07 +03:00
81efaf1ba5 fix: correct volume path in docker-compose.prod.yml and add model migration
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 28s
2026-03-19 23:34:55 +03:00
42b378fcbc fix: correct sed command and paths in docker-compose.prod.yml for proper nginx config generation 2026-03-19 19:33:12 +03:00
ba4175dfdb mod: больше времени на сборку
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m20s
2026-03-19 17:24:35 +03:00
4194b351d2 mod: собираем образы только linux/amd64 2026-03-19 17:22:31 +03:00
be68a82927 fix: dockerfile - add AS keywords and create staticfiles directory (root)
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 3m5s
2026-03-19 17:12:11 +03:00
53b127a966 fix: dockerfile - add AS keywords and create staticfiles directory
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m57s
2026-03-19 17:06:05 +03:00
746c50a988 fix: rebuild with new docker configuration and healthcheck support
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 2m5s
2026-03-19 17:00:00 +03:00
2520362ad5 mod: Новая версия типографа etpgrf и стили для висячей пунктуации
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 2m27s
2026-03-19 15:37:38 +03:00
a857101c3f mod: улучшения для видимости ИИ 2026-03-19 13:39:44 +03:00
11 changed files with 153 additions and 75 deletions

View File

@@ -63,3 +63,8 @@ jobs:
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# ДОБАВЛЕНО:
cache-from: type=gha
cache-to: type=gha,mode=max
# И это для медленного интернета:
timeout: 900 # 15 минут на всю сборку

View File

@@ -1,63 +1,105 @@
# ==========================================
# Dockerfile для Django + Gunicorn + WhiteNoise
# ==========================================
# =================================================
# STAGE 1: Builder - Установка зависимостей
# =================================================
FROM python:3.12-slim AS builder
# 1. Базовый образ: Python 3.12 (Slim версия для меньшего размера)
FROM python:3.12-slim
# 2. Переменные окружения для Python
# PYTHONDONTWRITEBYTECODE: Запрещает Python писать .pyc файлы
# PYTHONUNBUFFERED: Гарантирует, что вывод консоли (logs) виден сразу (не буферизуется)
# Устанавливаем переменные окружения
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Poetry настройки: не создавать виртуальное окружение внутри контейнера (ставим системно).
# Дублирует `poetry config virtualenvs.create false` в пп.7 (на всякий случай).
# Говорим Poetry, чтобы он не создавал venv, а ставил пакеты в системный site-packages
ENV POETRY_VIRTUALENVS_CREATE=false
# Путь настройки Django (по умолчанию для production) на случай если контейнер будет запущен не через docker-compose.
ENV DJANGO_SETTINGS_MODULE=dicquo.settings
# 3. Рабочая директория внутри контейнера
WORKDIR /app
# 4. Установка системных зависимостей
# - libjpeg-dev zlib1g-dev: библиотеки для работы с изображениями (Pillow)
# Устанавливаем системные зависимости, необходимые для СБОРКИ пакетов (например, Pillow)
# build-essential нужен для компиляции, -dev пакеты для сборки Pillow
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libjpeg-dev \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
# 5. Установка Poetry через pip (быстро и надежно)
# Устанавливаем Poetry
RUN pip install --no-cache-dir poetry
# 6. Копируем файлы зависимостей (pyproject.toml и poetry.lock)
# Делаем это ДО копирования всего кода, чтобы использовать кэш Docker layers.
# Создаем рабочую директорию
WORKDIR /app
# Копируем только файлы зависимостей для кэширования этого слоя
COPY pyproject.toml poetry.lock /app/
# 7. Установка зависимостей проекта
# --no-interaction: не будет спрашивать подтверждения
# --no-ansi: уберваем цветные символы из логов сборки (они иногда мусорят)
# --no-root: не устанавливать сам проект как пакет (мы просто копируем код)
# --only main: не ставить dev-зависимости (тесты, линтеры и т.п.) для продакшена
# RUN poetry install --no-root --only main
# Настройка Poetry: не создавать venv и установка зависимостей (без dev-зависимостей для продакшена)
RUN poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi --no-root --only main
# Устанавливаем зависимости проекта. Poetry установит их в /usr/local/lib/python3.12/site-packages
RUN poetry install --no-interaction --no-ansi --no-root --only main
# 8. Копируем весь исходный код проекта в контейнер
COPY . /app/
# 9. Сборка статики (CSS, JS)
# Важно: Запускаем collectstatic с фейковым SECRET_KEY, так как на этапе сборки env файла может не быть.
RUN SECRET_KEY=dummy_build_key python dicquo/manage.py collectstatic --noinput --clear
# =================================================
# STAGE 2: Final - Создание чистого и безопасного образа
# =================================================
FROM python:3.12-slim AS stage-final
# 10. Открываем порт 8000
# Устанавливаем переменные окружения
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DJANGO_SETTINGS_MODULE=dicquo.settings
# Устанавливаем только RUNTIME системные зависимости.
# Пакеты -dev и build-essential здесь не нужны.
RUN apt-get update && apt-get install -y --no-install-recommends \
libjpeg62-turbo \
sqlite3 \
&& rm -rf /var/lib/apt/lists/*
# Создаем пользователя без прав root для безопасности
# RUN addgroup --system app && adduser --system --ingroup app app
# Создаем рабочую директорию
WORKDIR /home/app/web
# Копируем установленные Python-пакеты из builder-стадии
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
# Копируем исполняемые файлы (gunicorn, pip и т.д.)
COPY --from=builder /usr/local/bin /usr/local/bin
# Копируем исходный код проекта и устанавливаем правильного владельца
# ИЗМЕНЕНИЕ: app:app -> 1000:1000
COPY --chown=1000:1000 . .
# Создаём директорию для конфигов nginx и даём права пользователю app
# Это выполняется ещё от root, поэтому проблем с permissions не будет.
RUN mkdir -p /nginx_configs_host/nginx && chown -R 1000:1000 /nginx_configs_host
# Создаём директорию для собранной статики и даём права пользователю app
RUN mkdir -p /home/app/web/staticfiles && chown -R 1000:1000 /home/app/web/staticfiles
# Создаём директорию для ошибок (404, 500) и даём права пользователю app
RUN mkdir -p /app/public/media/errors && chown -R 1000:1000 /app/public/media
# Создаём директорию для БД и даём права пользователю app
# Это важно когда БД монтируется как том с хоста
RUN mkdir -p /app/database && chown -R 1000:1000 /app/database
# Переключаемся на пользователя без прав root
USER 1000
# Собираем статику
# Используем dummy ключ, так как .env файла нет на этапе сборки
RUN SECRET_KEY=dummy python dicquo/manage.py collectstatic --noinput --clear
# Открываем порт
EXPOSE 8000
# 11. Команда запуска
# Переходим в подпапку dicquo, где лежит код Django проекта
WORKDIR /app/dicquo
# Проверка здоровья контейнера
# Docker будет периодически проверять, жив ли контейнер, отправляя GET запрос к главной странице.
# Параметры:
# --interval=30s - проверка каждые 30 секунд
# --timeout=3s - ожидаем ответ максимум 3 секунды
# --start-period=10s - даем контейнеру 10 секунд на запуск перед первой проверкой
# --retries=3 - объявляем контейнер unhealthy после 3 неудачных попыток
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()" || exit 1
# Запускаем Gunicorn (по умолчанию, если не переопределено в docker-compose) на три воркера, привязывая его к
# порту 8000 и указывая на точку входа приложения (wsgi.py).
# Переходим в директорию с manage.py для корректного запуска gunicorn
WORKDIR /home/app/web/dicquo
# Команда запуска
CMD ["gunicorn", "--workers", "3", "--bind", "0.0.0.0:8000", "dicquo.wsgi:application"]

View File

@@ -108,7 +108,9 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# ВАЖНО: Явно указываем https, потому что клиент всегда приходит по HTTPS к Nginx
# Даже если внутри контейнера это HTTP на 127.0.0.1:8010, для Django это должно быть HTTPS
proxy_set_header X-Forwarded-Proto https;
# Тайм-ауты (важно для долгих операций, если они есть)
proxy_read_timeout 180s;

View File

@@ -81,8 +81,10 @@ DATABASES = {
'NAME': BASE_DIR.parent / 'database/db.sqlite3',
'OPTIONS': {
# Таймаут ожидания блокировки SQLite (в секундах)
# При сложных операциях (например, каскадное удаление тегов) нужно больше времени
'timeout': 20,
# ВАЖНО: Увеличен до 60 сек для работы с несколькими воркерами Gunicorn
'timeout': 60,
# Дополнительные опции для лучшей работы SQLite при concurrent доступе
'init_command': "PRAGMA journal_mode=WAL;", # Write-Ahead Logging для лучшей concurrency
},
}
}

View File

@@ -19,6 +19,7 @@
{# Favicons #}<link rel="shortcut icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}"/>
<link rel="icon" type="image/png" href="{% static 'img/favicon.png' %}"/>
{# Technical Meta #}<meta http-equiv="Last-Modified" content="{% block Last4Meta %}{% endblock %}"/>
{# Для ИИ #}<link rel="help" type="text/markdown" href="/llms.txt"/>
{# CSS #}<link rel="stylesheet" href="{% static 'css/dicquo.css' %}"/>
<noscript><style>body { opacity: 1; }</style></noscript>{# Показать все если JS не поддерживатся #}
{% block ExtraHead %}{# Если нужно что=то добавить в `<head>` #}{% endblock %}

View File

@@ -0,0 +1,17 @@
# Generated by Django 6.0.2 on 2026-03-19 20:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('web', '0003_alter_tbdictumandquotes_btypograph_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='tbdictumandquotes',
options={'ordering': ['-id'], 'verbose_name': 'ВЫСКАЗЫВАНИЕ', 'verbose_name_plural': 'ВЫСКАЗЫВАНИЯ'},
),
]

View File

@@ -38,14 +38,14 @@ services:
sh -c "python manage.py migrate --noinput &&
python manage.py collectstatic --noinput &&
mkdir -p /nginx_configs_host/nginx &&
sed \"s|/home/user/app/dq-site|${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}|g\" /app/configs/nginx/dq-app--external-nginx.conf > /nginx_configs_host/nginx/nginx_dq.conf.example &&
sed \"s|/home/user/app/dq-site|${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}|g\" /nginx_configs_host/nginx/dq-app--external-nginx.conf > /nginx_configs_host/nginx/nginx_dq.conf.example &&
if [ ! -f /nginx_configs_host/nginx/dq-app--external-nginx.conf ]; then
cp /nginx_configs_host/nginx/nginx_dq.conf.example /nginx_configs_host/nginx/dq-app--external-nginx.conf;
echo 'INIT: Created new nginx config with correct paths';
fi &&
mkdir -p /app/public/media/errors &&
cp /app/dicquo/templates/static_404.html /app/public/media/errors/404.html &&
cp /app/dicquo/templates/static_500.html /app/public/media/errors/500.html &&
cp /home/app/web/dicquo/templates/static_404.html /app/public/media/errors/404.html &&
cp /home/app/web/dicquo/templates/static_500.html /app/public/media/errors/500.html &&
gunicorn --workers 3 --bind 0.0.0.0:8000 dicquo.wsgi:application"
# 4. Проброс портов (Внешний Nginx -> localhost:8010)
@@ -67,7 +67,10 @@ services:
# Это нужно, чтобы скрипт запуска мог положить туда .example конфиг и прочитать боевой конфиг.
- ./config:/nginx_configs_host
# 6. Переменные окружения
# 6. Запускать от имени пользователя с UID 1000 и GID 1000
user: "1000:1000"
# 7. Переменные окружения
env_file:
- .env
environment:
@@ -76,14 +79,24 @@ services:
# Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать
- HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}
# 7. Логирование (Ротация)
# 8. Проверка здоровья контейнера (Healthcheck)
# Docker будет периодически проверять статус контейнера. Это критично для Watchtower!
# Если контейнер объявлен "unhealthy", Watchtower сначала остановит старый образ, потом запустит новый.
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()"]
interval: 30s # Проверка каждые 30 секунд
timeout: 3s # Таймаут ответа - 3 секунды
start_period: 10s # Даем 10 секунд на стартап перед первой проверкой
retries: 3 # Unhealthy после 3 неудачных попыток
# 9. Логирование (Ротация)
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# 8. Ресурсы
# 10. Ресурсы
deploy:
resources:
limits:
@@ -107,6 +120,9 @@ services:
- WATCHTOWER_SCOPE=dq-scope
- WATCHTOWER_CLEANUP=true # Удалять старые образы после обновления
- DOCKER_API_VERSION=1.44
# Дополнительные опции для правильной работы с healthcheck
- WATCHTOWER_WAIT_ON_TIMEOUT=60 # Ждем 60 сек пока контейнер станет healthy перед финализацией
- WATCHTOWER_LIFECYCLE_HOOKS=true # Включаем lifecycle hooks для graceful shutdown
command: --interval 1800 --cleanup # Проверять каждые 30 минут
logging:
driver: "json-file"

8
poetry.lock generated
View File

@@ -128,13 +128,13 @@ Django = ">=4.1"
[[package]]
name = "etpgrf"
version = "0.1.4"
version = "0.1.6"
description = "Electro-Typographer: Python library for advanced web typography (non-breaking spaces, hyphenation, hanging punctuation and ."
optional = false
python-versions = ">=3.10"
files = [
{file = "etpgrf-0.1.4-py3-none-any.whl", hash = "sha256:62d4371e1b5fab06b99f79bd351767aed8baf7d041cae7e5d4eb63f7c9545114"},
{file = "etpgrf-0.1.4.tar.gz", hash = "sha256:c699382c292e3110915331dd5539e7dde0c961e4f4ca65cf8db0e01e84dab72f"},
{file = "etpgrf-0.1.6-py3-none-any.whl", hash = "sha256:a2d2a67048f094e1d30fe42f05f420afd19babe66ec7daa35b517ca23306d5cc"},
{file = "etpgrf-0.1.6.tar.gz", hash = "sha256:a050c400a30be1c2379c892fc5fa398a79d15f0169094f00023a75dec01864af"},
]
[package.dependencies]
@@ -675,4 +675,4 @@ brotli = ["brotli"]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "b5fca935982220439294d6b37caaf1d893492df96d65abd6389dfd3c9464b992"
content-hash = "64c804553b6314e8f8f5830637781a3179fd70f14cceb6730bfcb2cf24c91a31"

View File

@@ -1,26 +1,14 @@
# DicQuo
User-Agent: *
Allow: /
Disallow: /admin/
Disallow: /*?tag=
Disallow: /*?
# Optimize for Yandex
Clean-param: tag /
# AI and LLM bots settings
# OpenAI GPT
# User-agent: GPTBot
# Disallow:
# Common Crawl (used by many AI models)
# User-agent: CCBot
# Disallow:
# Google Bard/Gemini
# User-agent: Google-Extended
# Disallow:
Host: dq.cube2.ru
Sitemap: https://dq.cube2.ru/sitemap.xml
# Ссылка на файл для ИИ-моделей
Link: /llms.txt

View File

@@ -306,12 +306,17 @@ footer button:hover {
/* --- ВЫРАВНИВАНИЕ СИМВОЛОВ ВИСЯЧЕЙ ПУНКТУАЦИИ (Hanging Punctuation) ТИПОГРАФА ETPGRF --- */
/* --- В ПРОЕКТЕ ТОЛЬКО ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по левому краю) --- */
.etp-laquo {margin-left: -0.44em;} /* « */
.etp-ldquo, .etp-bdquo { margin-left: -0.4em;} /* “ */
.etp-lsquo {margin-left: -0.22em;} /* */
.etp-lpar, .etp-lsqb, .etp-lcub {margin-left: -0.25em;}/* ( [ { */
.etp-laquo { margin-left: -0.49em; } /* « */
.etp-ldquo, .etp-bdquo { margin-left: -0.4em; } /* “ */
.etp-lsquo { margin-left: -0.22em; } /* */
.etp-lpar, .etp-lsqb, .etp-lcub { margin-left: -0.23em; } /* ( [ { */
/* компенсирующие пробелы для левых висячих символов */
.etp-sp-laquo { padding-right: 0.49em; }
.etp-sp-ldquo, .etp-sp-bdquo { padding-right: 0.4em; }
.etp-sp-lsquo { padding-right: 0.22em; }
.etp-sp-lpar, .etp-sp-lsqb, .etp-sp-lcub { padding-right: 0.23em; }
/* --- СЧЕТЧИКИ (СКРЫТЫЙ ПИКСЕЛЬ) --- */
/* --- СЧЕТЧИКИ (СКРЫТЫЙ ПИКСЕЛЬ) top.mail.ru и Яндекс.Метрика --- */
.counter-pixel {
border: 0;
position: absolute;

View File

@@ -12,7 +12,7 @@ django = "^6.0.2"
django-taggit = "^6.1.0"
pillow = "^12.1.1"
pytils = "^0.4.4"
etpgrf = "^0.1.4"
etpgrf = "0.1.6"
django-environ = "^0.12.1"
whitenoise = "^6.11.0"
gunicorn = "^25.1.0"