Deployment¶
How the application is packaged, configured, and deployed to Modal's serverless infrastructure — including principles, environment matrix, and operational runbook.
Deployment Principles¶
-
Serverless-first
No EC2, no VMs, no Kubernetes. Modal provisions containers on demand and scales to zero when idle. The
featanddevenvironments pay nothing when unused. -
Secret isolation
Secrets (
JWT_SECRET, etc.) are injected by Modal at runtime from named secret stores — never baked into the image, never in version control. -
Environment parity
All environments run identical container images. The only differences are hardware sizing, min-container count, and the secrets injected. No environment-specific code paths.
-
Declarative config
EnvConfigis a Python dataclass — environments are defined in code, reviewed in PRs, and versioned with the application. No YAML config sprawl. -
Test-gated deploys
No code reaches Modal without passing all 79 tests. The
needs: testgate in GitHub Actions is the single enforcement point. -
Persistent volumes
A Modal Volume is mounted at
/root/fastapi-volumefor durable storage across container restarts and deployments.
Environment Matrix¶
| Config | feat |
dev |
prod |
|---|---|---|---|
| Modal app name | modal-template-fastapi-feat |
modal-template-fastapi-dev |
modal-template-fastapi-prod |
| Custom domain | feat-app.modal.run |
dev-app.modal.run |
prod-app.modal.run |
| CPU cores | 1 | 1 | 1 |
| RAM (MiB) | 256 | 256 | 256 |
| Min containers | 0 | 0 | 1 |
| GPU | None | None | None |
| Timeout (s) | 150 | 150 | 150 |
| Deploy trigger | push feat/** |
PR merged → dev |
PR merged → production |
| Modal secrets | fastapi-auth-secrets |
fastapi-auth-secrets |
fastapi-auth-secrets |
Min containers
min_containers=1 in prod keeps one container always warm, eliminating cold start latency for real users. feat and dev scale to zero and incur a cold start (~1-3s) on the first request.
EnvConfig Reference¶
Defined in modal_common.py. Every field maps directly to a Modal function kwarg via build_fastapi_config().
| Field | Type | Default | Description |
|---|---|---|---|
env_name |
str |
— | Required. Short identifier: feat, dev, prod |
app_name |
str |
"modal-template-fastapi" |
Base Modal app name. Final name is {app_name}-{env_name} |
custom_domain |
str \| None |
None |
Modal subdomain for this environment |
cpu_core_count |
int |
1 |
vCPU count per container |
ram_memory_mib |
int |
256 |
Memory limit in MiB per container |
gpu_type |
str \| None |
None |
GPU SKU string (e.g. "A10G", "H100") — None for CPU-only |
server_hard_timeout_seconds |
int |
150 |
Maximum request duration before hard kill |
min_containers |
int |
0 |
Minimum always-running containers |
secrets |
list |
[] |
List of modal.Secret objects injected at runtime |
volumes |
Dict[str, Volume] |
FASTAPI_VOLUME |
Volume mounts as {path: volume} |
Adding GPU support¶
STAGING = EnvConfig(
env_name="staging",
gpu_type="A10G", # or "T4", "H100", "L4", etc.
ram_memory_mib=4096, # bump RAM for GPU workloads
secrets=[modal.Secret.from_name("fastapi-auth-secrets")],
)
Container Image¶
The image is defined once in modal_common.py and shared across all environments:
cpu_image = (
modal.Image.debian_slim(python_version="3.10") # (1)
.apt_install("curl", "jq") # (2)
.uv_pip_install( # (3)
"fastapi",
"uvicorn[standard]",
"websockets",
"pydantic",
"PyJWT",
)
.add_local_dir(".", remote_path="/root") # (4)
)
- Slim Debian base — minimal attack surface
- System packages:
curlfor health probes,jqfor JSON manipulation in scripts uv_pip_installuses the fastuvresolver — significantly faster than vanilla pip- Copies the entire repo into
/rootinside the container — your application code is available at/root/src/
Customising the image
Add Python packages by appending to the PIP_PACKAGES list. Add OS-level tools to APT_PACKAGES. Modal rebuilds and caches the image layer automatically on the next deploy.
Persistent Volume¶
volume = modal.Volume.from_name("fastapi-volume", create_if_missing=True)
FASTAPI_VOLUME = {
"/root/fastapi-volume": volume # mount path → Volume object
}
The volume is created once and reused across all container instances and all deployments. Data written to /root/fastapi-volume/ inside the container persists across restarts.
Running Locally¶
No cloud deployment needed for development:
# Uses the local_entrypoint — starts uvicorn on port 8000
MODAL_ENV=dev modal run modal_app.py
# Test the running server
curl http://localhost:8000/api/v1/health
The @app.local_entrypoint() in modal_app.py imports the FastAPI app and starts uvicorn directly — the same application code runs locally and in production.
Manual Deployment¶
Prefer CI/CD for production
Manual deploys bypass the test gate. Use modal deploy directly only for emergency hotfixes or local iteration on feat.
Operational Runbook¶
First-time setup¶
# 1. Install Modal
pip install modal
# 2. Authenticate
modal setup # opens browser for OAuth
# 3. Create the named secret store in Modal
modal secret create fastapi-auth-secrets \
JWT_SECRET="$(python -c "import secrets; print(secrets.token_hex(32))")"
# 4. Deploy to feat
MODAL_ENV=feat modal deploy modal_app.py
Viewing logs¶
# Stream logs from the feat environment
modal app logs modal-template-fastapi-feat
# Or open the Modal dashboard
modal app list
Checking deployment status¶
modal app list
# NAME STATE CREATED
# modal-template-fastapi-feat deployed 2026-01-01 12:00:00
# modal-template-fastapi-dev deployed 2026-01-01 11:00:00
# modal-template-fastapi-prod deployed 2026-01-01 10:00:00
Stopping an environment¶
Adding a New Environment¶
-
Define the config in
modal_common.py: -
Add a GitHub Variable
STAGING = stagingin repo Settings → Variables -
Add routing in
modal-deploy.ymlSet Modal environmentstep: -
Add the trigger to
modal-deploy.ymlon.pull_request.branches: