Deployment
What to set, what to check, and the two things people get wrong behind a proxy.
Available since v0.1.0
The environment
Every setting can come from the environment: FASTFORT_ prefix, double
underscore for nesting.
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" on every group, so a misspelled key is a start-up error rather
than a setting that silently does nothing.
Gate the deploy on the check
uv run fastfort check --app main:fort --deploy
Exits non-zero. It reports debug=True, an insecure cookie, a
JavaScript-readable cookie, CSRF disabled, security headers disabled, an access
token lifetime over an hour, and refresh rotation turned off.
- 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"
Or make start-up refuse:
settings.require_production_ready()
The two things people get wrong behind a proxy
trust_forwarded_for
FASTFORT_SECURITY__TRUST_FORWARDED_FOR=true
Only when your proxy overwrites X-Forwarded-For. Behind one that appends,
or behind none, a client can forge its own address — and lockout and audit
records become attributable to whoever the attacker chose.
Do not compress HTML
Static assets are already negotiated Brotli → gzip → identity by FastFort, cached per encoding per process. HTML is deliberately excluded, because a page holds a CSRF token and request-chosen text — the BREACH pattern.
So: no GZipMiddleware, and turn HTML compression off at the proxy for the
admin’s paths.
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;
}
Debug mode and asset caching
With debug=False, CSS and JavaScript are read from disk once and cached per
process. That is what you want in production, and it means an edit to a
stylesheet needs a restart.
With debug=True they are re-read on every request — which is why the setting
exists, and why it must not be on in production.
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"]
Migrations
FastFort never runs DDL. Your schema is yours — Alembic, or whatever you already use. The models are ordinary declarative models with no FastFort base class in the MRO, which is exactly what makes that possible.
Database extensions are the same: postgis, hstore, citext and vector
each add a type, so a CREATE TABLE naming one fails on the DDL. Enabling them
is your project’s job — an admin framework that quietly installs database
extensions is one nobody can review.
The first account
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, not --password: an argument is visible in the process list.
Sizing
| Setting | Why |
|---|---|
admin.count_strategy = "capped" | An exact count dominates query time past a few hundred thousand rows |
admin.max_page_size | A ceiling on ?ps=; without it one request can ask for the whole table |
admin.export_limit | An export has no pagination |
admin.autocomplete_limit | Per keystroke, per picker |
admin.dashboard_days | Each day is one indexed count, so this is also the dashboard’s query count |
What is not in the box
Rate limiting, an audit-log backend and soft delete are planned for
fastfort.contrib and are not there yet. Rate limiting in particular is
something to put at your proxy or gateway today — lockout_* protects the login
form, and nothing else.