Deployment
A Caspian app is a FastAPI process behind a reverse proxy, so the unit you ship is a container. Anything that runs one will host it. This page covers the runtime contract the image has to satisfy, the environment that must be set before the first request, and the two failure modes that are silent rather than loud.
Caspian does not scaffold a Dockerfile.
create-caspian-app generates the application,
not its delivery. You either add the Dockerfile below to your repository, or
deploy to a host that can infer one —
Dilnaka Cloud
recognizes Caspian and generates a matching Dockerfile when your repo has none.
Deployment is an application concern rather than a framework feature, so
there is no caspian.config.json flag for it and
no packaged doc behind it. Treat the reference image here as a starting point
you own and adapt.
The Runtime Contract
Five things have to be true of the image, whoever builds it. Everything else is preference.
| Requirement | Why |
|---|---|
| Python and Node in the image |
The app runs on Python, but the build step is Node:
npm run build compiles
globals.css and regenerates the
route and component index. Node is a build-time dependency, not a
runtime one.
|
| npm run build at image build |
public/css/styles.css,
settings/files-list.json, and
settings/component-map.json are
generated artifacts. Build them into the image — never at runtime,
and never by committing them.
|
| Bind 0.0.0.0 on $PORT |
The entrypoint reads PORT. Bind
every interface inside the container — the isolation comes from the
host, which is why the process itself must not bind loopback only.
|
| --proxy-headers |
Traffic arrives through a proxy, so without this Uvicorn reports the
proxy's address and http for
every request, and URL generation and redirects come back wrong.
|
| /health |
The app answers GET /health with
{"status": "ok"}. It is
deliberately exempt from the page rate limiter, so a frequent probe can
never exhaust a bucket. Point the platform's health check here.
|
FROM python:3.14-slim ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ UV_LINK_MODE=copy \ UV_PROJECT_ENVIRONMENT=.venv WORKDIR /app # Node is needed to build assets, not to serve them. RUN apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates gnupg \ && mkdir -p /etc/apt/keyrings \ && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \ > /etc/apt/sources.list.d/nodesource.list \ && apt-get update \ && apt-get install -y --no-install-recommends nodejs \ && rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir uv # Dependencies first, so a source edit does not reinstall them. COPY pyproject.toml uv.lock ./ RUN uv sync --frozen --no-dev COPY package.json package-lock.json ./ RUN npm ci COPY caspian.config.json postcss.config.js main.py ./ COPY settings ./settings COPY public ./public COPY src ./src # Compiles the stylesheet and regenerates the route/component index. RUN npm run build ENV APP_ENV=production \ PORT=8080 EXPOSE 8080 CMD ["sh", "-c", "exec .venv/bin/python -m uvicorn main:app \ --host 0.0.0.0 --port ${PORT:-8080} \ --proxy-headers --forwarded-allow-ips='*'"]
src/lib/prisma/** must exist in the image.
Either commit it, or run npx ppy generate as a build
step before npm run build. It is not produced by
either build script — see
Database & ORM.
APP_ENV resolves fail-closed — read this before anything else
One variable gates the session secret, HTTPS-only cookies, HSTS, how much
error detail reaches the browser, the localhost origin bypass, and whether a
WebSocket handshake with no Origin header is
accepted. Deriving that from
APP_ENV == "production" meant a typo silently
selected every development fallback on a real deployment, so the question is
inverted.
Only an explicit, recognized development value turns the relaxations
on: dev, development,
local, staging,
test, testing.
Unset, empty, or misspelled counts as
production — and a production boot with no
AUTH_SECRET raises at import rather than
serving traffic under a known session secret.
The practical consequence is a good one: forgetting to set
APP_ENV on a host is safe. Misspelling
developement on your laptop is what breaks,
and it breaks visibly.
Production Environment
Set these on the platform, not in a committed
.env. Every value is
read from the process environment, and
load_dotenv() does not
override what is already there. Start with the generated values documented in
Environment Variables.
| Variable | Notes |
|---|---|
| AUTH_SECRET |
Required Signs the session cookie. Absent in production, the app refuses to start. Generate a long random value per environment and never reuse the development one. |
| APP_ENV |
Leave unset or set production. See the
fail-closed rule above.
|
| APP_BASE_URL | The public origin. Feeds RPC origin validation, the CORS allow-list, and OAuth redirect resolution. Set it to the domain users actually reach. |
| PORT | The port the container listens on. Most platforms inject it; the entrypoint reads it. |
| DATABASE_URL | When Prisma is enabled. A managed database usually injects this for you. |
| TRUST_FORWARDED_HEADERS | Enable only when a trusted proxy is genuinely in front. See the callout below — this one is easy to get wrong in both directions. |
| APP_TIMEZONE |
IANA name, default UTC. Sets the
application calendar. A slim image may need the
tzdata package, or every zone but
UTC raises.
|
|
CORS_ALLOWED_ORIGINS CORS_ALLOWED_METHODS CORS_ALLOWED_HEADERS |
Comma-separated. Only needed for cross-origin browser clients;
APP_BASE_URL is already included.
|
|
RATE_LIMIT_PAGES RATE_LIMIT_RPC RATE_LIMIT_AUTH |
Defaults 200/minute for pages and
60/minute for RPC and auth actions.
Public files and /health are exempt.
|
| CONTENT_SECURITY_POLICY | Replaces the default policy wholesale. Set it only when you mean to own the whole header. |
|
SESSION_LIFETIME_HOURS MAX_CONTENT_LENGTH_MB CACHE_ENABLED / CACHE_TTL |
Session length (7), upload ceiling in MB (16), and page caching (off, 600s). Caching is public-HTML only — an authenticated render is never cached. |
| UVICORN_WORKERS | Default 1. Raise it only with the container's CPU and memory limits in view; on shared capacity more workers usually buys nothing. |
| MCP_AUTH_TOKEN |
When MCP is enabled. Without a token, /mcp
returns 503 in production — it is mounted outside the routing tree, so
route auth does not protect it.
|
| WEBSOCKET_ALLOWED_ORIGINS | Required in production when WebSockets are enabled. The same-origin fallback is derived from the client-supplied Host header and is development-only. |
|
GOOGLE_CLIENT_ID / _SECRET GITHUB_CLIENT_ID / _SECRET |
OAuth providers are already registered. A provider with no client id is skipped, so the route simply falls through instead of failing. See Authentication. |
One rate-limit bucket for everybody
Behind a proxy every request arrives from the proxy's address, so the limiter
sees one caller and either blocks everyone or nobody. The forwarded chain is
consulted only when
TRUST_FORWARDED_HEADERS is set.
Do not set it without a proxy: a direct client can then supply the header itself and mint a fresh bucket per request, which is strictly worse than no limit at all. The same flag decides whether forwarded host and protocol are trusted for RPC origin validation.
The container filesystem is ephemeral
Two directories are written at runtime and neither survives a redeploy:
caches/ from page caching, and
public/uploads/** from file uploads.
A cold cache is only a slow first request. Uploads are data loss. Put user files in object storage and their metadata in the database — see Production Storage. With more than one replica, a disk cache is also per-container rather than shared.
Recommended host
Dilnaka Cloud
Dilnaka Cloud runs apps and databases as isolated Docker services on AWS, and recognizes Caspian directly: connect a repository with no Dockerfile and it generates a matching one from your project files. A Dockerfile you commit always wins, so the reference image above stays the escape hatch whenever you want the build to be yours.
Push to deploy
Every push to the tracked branch builds and redeploys. Pick the branch and
root directory, limit which paths trigger a build, or turn auto-deploy off and
release each change yourself. Build history gives one-click rollback to a past
commit, and the start command, health check path, and restart policy are yours
to set — point the health check at
/health.
Managed databases
PostgreSQL, MySQL, MariaDB, Redis, or MongoDB, provisioned with their own
credentials injected into the app's environment — which is where
DATABASE_URL comes from. Snapshots on
demand or on a schedule, and a restore boots an isolated copy rather than
overwriting the live database.
Domains, TLS, and the proxy
Every app gets a managed subdomain and can take your own on top, with a
certificate issued automatically once DNS resolves. The container binds the
host's loopback interface and is reached only through the managed proxy
— which is exactly why
--proxy-headers and
TRUST_FORWARDED_HEADERS matter here.
Logs, metrics, and rollback
Build logs, runtime output, and HTTP request logs per service, plus CPU and memory pressure and usage tracked against the plan before it becomes an overage. Environment variables are encrypted at rest and injected at run time.
$ npm install -g dilnaka-cli $ dilnaka login # approve the printed code in your browser $ dilnaka link --create # new project, named after this folder $ dilnaka up # deploy this directory
-
The CLI needs Node.js 18.17 or newer and packages source using Git's ignore
rules, leaving local
.envfiles out — so production values stay platform-side where they belong. -
For CI, create a token under Account → CLI Tokens and expose it as
DILNAKA_CLOUD_TOKEN. - Start on shared capacity, then move a workload to a dedicated instance once it needs guaranteed resources. Accounts begin with a 7-day trial on shared capacity.
connect,
variables,
logs,
redeploy) are not shipped yet. Check the
platform docs
for what is live today.
When You Do Not Need A Server
A content site — docs, marketing, a landing page — may not need the
Python process at all. npm run static
renders every static route through the real app and writes
static/, which any CDN
or static host will serve.
pp.rpc() actions,
auth and sessions, WebSockets, streaming, and per-request server data are inert in
that output. Pages still render their first paint, and the exporter flags routes
that appear to use pp.rpc() so nothing ships silently
broken. Read
Static Export
before choosing this path.
Static Export (SSG)
Pre-render routes to plain HTML for a CDN.
Authentication
Secrets, sessions, RBAC, and OAuth credentials.