Развёртывание
Что настроить, что проверить и две ошибки, которые чаще всего допускают при работе за прокси.
Доступно с версии v0.1.0
Окружение
Любую настройку можно задать через переменные окружения: префикс FASTFORT_,
двойное подчёркивание для вложенности.
FASTFORT_SECRET_KEY=… # from `fastfort generate-secret`
FASTFORT_DEBUG=false
FASTFORT_SECURITY__COOKIE_SECURE=true
FASTFORT_SECURITY__HSTS_SECONDS=31536000
FASTFORT_SECURITY__CSRF_ENABLED=true
FASTFORT_ADMIN__COUNT_STRATEGY=capped # on a large table
FASTFORT_ADMIN__COUNT_CAP=10000
FASTFORT_ADMIN__EXPORT_LIMIT=50000
FASTFORT_UI__ENVIRONMENT_LABEL=PRODUCTION
FASTFORT_UI__ENVIRONMENT_TONE=danger
extra="forbid" установлен для каждой группы, поэтому опечатка в ключе — это
ошибка при запуске, а не настройка, которая молча ничего не делает.
Блокируйте развёртывание проверкой
uv run fastfort check --app main:fort --deploy
Команда завершается с ненулевым кодом. Она сообщает о debug=True,
небезопасном cookie, cookie, доступном из JavaScript, отключённой защите CSRF,
отключённых заголовках безопасности, времени жизни access-токена более часа и
отключённой ротации refresh-токена.
- run: uv run fastfort check --app main:fort --deploy
env:
FASTFORT_SECRET_KEY: ${{ secrets.FASTFORT_SECRET_KEY }}
FASTFORT_DEBUG: "false"
FASTFORT_SECURITY__COOKIE_SECURE: "true"
Либо заставьте само приложение отказываться от запуска:
settings.require_production_ready()
Две ошибки, которые допускают за прокси
trust_forwarded_for
FASTFORT_SECURITY__TRUST_FORWARDED_FOR=true
Включайте это только если ваш прокси перезаписывает X-Forwarded-For. За
прокси, который дописывает значение, или вообще без прокси клиент может
подделать собственный адрес — и записи о блокировках (lockout) и аудите будут
указывать на того, кого выбрал злоумышленник.
Не сжимайте HTML
Статические файлы уже согласуются FastFort по схеме Brotli → gzip → identity и кешируются по кодировке в рамках процесса. HTML намеренно исключён из этого, потому что страница содержит одновременно CSRF-токен и выбранный по запросу текст — классический паттерн атаки BREACH.
Поэтому: никакого GZipMiddleware, а сжатие HTML на прокси для путей админки
нужно отключить.
location /admin {
gzip off;
proxy_pass http://app;
proxy_set_header X-Forwarded-For $remote_addr; # overwrite, not append
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
}
У Caddy нет способа сказать «всё, кроме HTML» — в его матчере ответов нет
not, — поэтому список составляется по принципу «что сжимать»:
encode {
zstd
gzip
match {
header Content-Type application/json*
header Content-Type text/csv*
}
}
И это в любом случае более правильный подход: тип контента, о котором ещё никто не подумал, приходит несжатым и привлекает внимание, а не сжимается только потому, что его забыли добавить в чёрный список.
Считайте CDN ещё одним прокси
Это как раз то, что чаще всего упускают, и об этом стоит сказать отдельно,
потому что демо самого этого проекта было изначально настроено неправильно.
На origin-сервере всё поправили, чтобы HTML не трогали, развёртывание прошло
успешно, а /admin/login всё равно возвращал:
content-encoding: zstd
server: cloudflare
Cloudflare, Fastly, CloudFront и любой другой CDN по умолчанию сжимают HTML на edge-узлах и сами терминируют TLS — поэтому до клиента всё равно доходит сжатая длина, а BREACH не важно, какая именно машина выполнила сжатие. Исправить только origin-сервер и остановиться на этом — не исправить ничего.
В Cloudflare это Rules → Compression Rules: указываете путь админки и отключаете сжатие. Аналог есть у любого CDN, которым стоит пользоваться; главное — помнить, что у вас два прокси, а исправили вы только один.
Проверить это можно одним запросом — и это единственный надёжный способ убедиться:
curl -sI -H 'Accept-Encoding: gzip, br, zstd' https://example.com/admin/login \
| grep -i content-encoding
Отсутствие вывода — правильный результат.
Режим отладки и кеширование ресурсов
При debug=False CSS и JavaScript читаются с диска один раз и кешируются в
рамках процесса. Именно это нужно в продакшене, и это означает, что изменение
стиля требует перезапуска.
При debug=True они перечитываются при каждом запросе — именно для этого
существует данная настройка, и именно поэтому она не должна быть включена в
продакшене.
Docker
FROM python:3.13-slim
# uv, then dependencies, then source — so a source change does not re-resolve.
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
COPY . .
# No `npm install`, no node_modules, no build step. The admin's CSS and
# JavaScript are files inside the wheel.
ENV FASTFORT_DEBUG=false
EXPOSE 8000
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Миграции
FastFort никогда не выполняет DDL. Схема — это ваша зона ответственности: Alembic или что вы уже используете. Модели — обычные декларативные модели без базового класса FastFort в MRO, и именно это делает такой подход возможным.
С расширениями базы данных то же самое: postgis, hstore, citext и
vector каждое добавляет свой тип, поэтому CREATE TABLE, ссылающийся на
такой тип, падает на уровне DDL, если расширение не включено. Включать их —
задача вашего проекта: фреймворк админки, который тихо устанавливает
расширения базы данных, никто не сможет проверить.
Первая учётная запись
FF_PASSWORD="$(openssl rand -base64 24)" \
uv run fastfort createsuperuser \
--app app.main:fort \
--identity ops@example.com \
--password-env FF_PASSWORD \
--no-input
--password-env, а не --password: аргумент командной строки виден в списке
процессов.
Настройки под нагрузку
| Настройка | Почему |
|---|---|
admin.count_strategy = "capped" | Точный подсчёт строк начинает доминировать во времени выполнения запроса уже после нескольких сотен тысяч строк |
admin.max_page_size | Верхний предел для ?ps=; без него один запрос может затребовать всю таблицу целиком |
admin.export_limit | Экспорт не имеет постраничной разбивки |
admin.autocomplete_limit | На каждое нажатие клавиши, для каждого поля выбора |
admin.dashboard_days | Каждый день — это один индексированный подсчёт, поэтому это же число определяет и количество запросов дашборда |
Чего нет «из коробки»
Ограничение частоты запросов (rate limiting), бэкенд журнала аудита и мягкое
удаление (soft delete) запланированы для fastfort.contrib, но пока их там
нет. Ограничение частоты запросов в первую очередь стоит уже сегодня настроить
на вашем прокси или шлюзе — lockout_* защищает только форму входа, и больше
ничего.