
How do I set up secrets (API keys) and environment variables in Modal for production deployments?
Most teams hit the same wall the moment a Modal prototype becomes “real”: you suddenly need to store API keys, auth tokens, and configuration in a way that won’t leak in logs, break across environments, or require a redeploy every time marketing spins up a new key. On Modal, that line is very explicit: you use Secrets for sensitive values (API keys, tokens, credentials) and environment variables for non‑secret configuration that your code reads at runtime.
Quick Answer: In Modal, you store API keys and other sensitive config in Secrets managed by the platform, then mount those secrets into Functions and Classes via the
secrets=[...]parameter. For regular environment variables, you define them in your Python app (per Function/Class or globally) using theenvparameter or@app.function()arguments, and deploy to production withmodal deployso the same definitions apply in your production containers.
Why This Matters
Hard‑coding keys or relying on local .env files works until you deploy to production, add a second engineer, or need to rotate credentials without breaking a model server running on 100s of GPUs. Modal is explicitly designed for code-defined infrastructure, which means your compute, secrets, and environment all live in one place: Python.
Done right:
- You avoid leaking API keys in Git, logs, or container images.
- You can change configuration per environment (dev vs prod) without touching the code path that runs your models.
- You can scale to thousands of containers and know that each one gets the same, correctly scoped secret and env config.
Key Benefits:
- Security by default: Secrets never live in your source repo; they’re stored encrypted by Modal and injected at runtime only where needed.
- Code-defined config: Environment variables and secret usage live next to your Functions, so infra changes are readable and reviewable in code review.
- Production-ready behavior: You can promote the same app from
modal serveduring development tomodal deployin production without changing how secrets and env vars are wired.
Core Concepts & Key Points
| Concept | Definition | Why it's important |
|---|---|---|
| Secret | Encrypted key–value storage managed by Modal, referenced by name (e.g., "algolia-secret") and injected into containers at runtime. | Keeps API keys and credentials out of code, images, and logs while giving your app controlled access. |
| Environment variables | Non-secret configuration exposed to your code as os.environ[...], defined in Python or via app-level config. | Lets you switch behavior between dev/prod, tweak timeouts, or set flags without redeploying new code. |
| Environment separation | The practice of distinguishing dev, staging, and prod behavior using different secrets and env configs. | Prevents accidents like pointing your prod app at a dev DB or using test API keys for real users; simplifies safe rollout. |
How It Works (Step-by-Step)
Let’s walk through a concrete workload: calling a third‑party API (say Algolia, OpenAI, or Stripe) from a Modal Function that runs as a production endpoint.
At a high level:
- Create the secret in the Modal UI/CLI (e.g.,
algolia-secretwithAPI_KEYandAPPLICATION_ID). - Reference that secret in your Modal app code so it gets mounted into the right Functions/Classes.
- Define environment variables (e.g.,
APP_ENV=production,QUERY_TIMEOUT=2.5) in code and use them from your Python logic. - Deploy with
modal deploy, and manage updates (key rotation, config changes) via updating Secrets/config rather than rewriting code.
1. Create a Secret in Modal
First, you put your API keys into a Modal Secret. Using the Algolia example from Modal’s docs:
-
Sign up for Algolia and create an API key with the right ACLs (
write,addObject,editSettings,deleteIndex). -
Go to the Secrets page in the Modal dashboard.
-
Create a new Secret, say
algolia-secret, with fields:API_KEY=<your_algolia_api_key>APPLICATION_ID=<your_algolia_app_id>
You can name the Secret whatever you like; your code just needs to use the same string:
import modal
app = modal.App("search-service")
algolia_secret = modal.Secret.from_name("algolia-secret")
Under the hood, Modal stores those key–value pairs encrypted. They’re only decrypted inside the sandboxed container (gVisor) when your Function runs.
2. Attach Secrets to Functions and Classes
Secrets are opt-in per Function/Class. Nothing gets access to a secret unless you declare it.
Here’s a simple Function that uses the Algolia secret:
import os
import modal
app = modal.App("search-service")
algolia_secret = modal.Secret.from_name("algolia-secret")
@app.function(secrets=[algolia_secret])
def index_document(doc_id: str, payload: dict):
from algoliasearch.search_client import SearchClient
# Secrets are now available as environment variables
api_key = os.environ["API_KEY"]
app_id = os.environ["APPLICATION_ID"]
client = SearchClient.create(app_id, api_key)
index = client.init_index("docs")
index.save_object({"objectID": doc_id, **payload})
Key points:
secrets=[algolia_secret]tells Modal: mount this secret into the container.- Inside the container, each key in the secret (
API_KEY,APPLICATION_ID) is exposed as an environment variable. - You pull them with
os.environ[...]like any other env var, but they never live in your git repo.
For stateful model servers using @app.cls, it’s the same pattern:
@app.cls(secrets=[algolia_secret])
class SearchClientWrapper:
def __init__(self):
self._client = None
@modal.enter()
def init(self):
from algoliasearch.search_client import SearchClient
self._client = SearchClient.create(
os.environ["APPLICATION_ID"],
os.environ["API_KEY"],
)
@modal.method()
def query(self, text: str):
index = self._client.init_index("docs")
return index.search(text)
Here, you pay the cost of initializing the client once per container at @modal.enter(), while still keeping the credentials in a Secret.
3. Define Environment Variables for Configuration
Secrets are for sensitive data. Many toggles aren’t sensitive: e.g., LOG_LEVEL, APP_ENV, QUERY_TIMEOUT_SECONDS. These are best handled as environment variables defined in your app.
You can set env vars per Function:
@app.function(
secrets=[algolia_secret],
env={
"APP_ENV": "production",
"QUERY_TIMEOUT_SECONDS": "2.5",
},
)
def search(text: str):
timeout = float(os.environ.get("QUERY_TIMEOUT_SECONDS", "2.0"))
app_env = os.environ.get("APP_ENV", "development")
# Use app_env to change behavior, e.g. logging verbosity
...
If you want a shared config across several Functions, define a helper:
COMMON_ENV = {
"APP_ENV": "production",
"QUERY_TIMEOUT_SECONDS": "2.5",
}
@app.function(secrets=[algolia_secret], env=COMMON_ENV)
def search(...):
...
@app.function(env=COMMON_ENV)
def stats(...):
...
You can even branch on environment in the code:
IS_PROD = os.environ.get("APP_ENV") == "production"
During development you may want APP_ENV=development and different defaults. You can:
- Override via
envon another Function in the same app. - Or use separate Apps (e.g.
search-service-devvssearch-service-prod) with differentCOMMON_ENVand different Secret names.
4. Wire It Into a Web Endpoint for Production
Let’s expose that search Function as a production web endpoint using FastAPI:
from fastapi import FastAPI
import modal
import os
app = modal.App("search-service")
algolia_secret = modal.Secret.from_name("algolia-secret")
web = FastAPI()
@web.get("/search")
async def search_route(q: str):
return await search.remote(q)
@app.function(
secrets=[algolia_secret],
env={"APP_ENV": "production"},
)
@modal.asgi_app()
def fastapi_app():
return web
When you run:
modal deploy search_service.py
Modal deploys:
- The ASGI endpoint defined by
fastapi_app. - The
searchFunction. - The Secret and env wiring you defined.
Every container serving this endpoint will:
- Mount the
algolia-secretcontents as env vars. - See
APP_ENV=productionset.
Logs for this app will show up in the Modal dashboard, and you can stream them with:
modal app logs search-service
Common Mistakes to Avoid
-
Hard-coding API keys in code or Images:
This is the classic “it works in dev” trap. Never writeOPENAI_API_KEY="sk-..."in your Python files or bake it into your Modal Image. Always create a Secret and reference it withmodal.Secret.from_name(...). This avoids accidental git leaks and lets you rotate keys without rebuilding images. -
Using one Secret for everything across environments:
If dev, staging, and prod share a single Secret, you’ll eventually mix test and production credentials. Instead, create separate Secrets likeopenai-dev,openai-prod,algolia-dev,algolia-prodand wire them into different Apps or Functions. Use anAPP_ENVenv var to make it explicit in the code which environment you’re in.
Real-World Example
Imagine you’re running a RAG search API on Modal: a user hits your /search endpoint, you call an LLM (OpenAI) and an external search index (Algolia), and you want this to scale elastically with traffic.
You’d set up:
-
openai-prodsecret withOPENAI_API_KEY. -
algolia-prodsecret withAPI_KEYandAPPLICATION_ID. -
A
search-apiModal App where:- The request handler Function mounts both secrets plus
env={"APP_ENV": "production", "MAX_TOKENS": "2048"}. - A separate
indexerFunction uses only the Algolia secret and runs as a batch job (.map()or.spawn()) when you ingest new documents.
- The request handler Function mounts both secrets plus
Code sketch:
import modal
import os
import openai
app = modal.App("search-api")
openai_secret = modal.Secret.from_name("openai-prod")
algolia_secret = modal.Secret.from_name("algolia-prod")
@app.function(
secrets=[openai_secret, algolia_secret],
env={"APP_ENV": "production", "MAX_TOKENS": "2048"},
)
def rag_search(query: str):
from algoliasearch.search_client import SearchClient
# External search
algolia = SearchClient.create(
os.environ["APPLICATION_ID"],
os.environ["API_KEY"],
)
docs = algolia.init_index("docs").search(query)["hits"]
# LLM call
openai.api_key = os.environ["OPENAI_API_KEY"]
max_tokens = int(os.environ["MAX_TOKENS"])
# ...craft prompt from docs...
completion = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": query}],
max_tokens=max_tokens,
)
return {
"answer": completion.choices[0].message.content,
"documents": docs,
"env": os.environ["APP_ENV"],
}
Deploy with:
modal deploy rag_search_app.py
Now you have:
- A production endpoint that scales with traffic.
- All keys in Secrets, not code.
- Configuration (
MAX_TOKENS,APP_ENV) as environment variables you can change in code and redeploy in seconds.
Pro Tip: When you iterate with
modal serveduring development, use a separate App name, Secrets, and env values (e.g.,openai-dev,APP_ENV=development). When it’s stable, promote the same code with production Secrets and env values viamodal deployso you don’t accidentally test against production services.
Summary
Setting up secrets (API keys) and environment variables correctly in Modal is what turns a weekend prototype into something you’re comfortable running for users:
- Secrets: store API keys, tokens, and credentials in Modal’s Secret manager, then attach them explicitly to Functions and Classes using
secrets=[...]. Access them viaos.environinside the container. - Environment variables: define non-secret config using
env={...}on Functions/Classes so you can change behavior across dev/staging/prod without touching core logic. - Production deployment: develop with
modal serve, then deploy the same app withmodal deploy. The Secret and env wiring you defined in Python is what matters—no extra YAML, no hidden config.
Once you treat secrets and env vars as first‑class, code‑defined infra, you get repeatable, auditable deployments that can scale to thousands of GPU-backed containers without turning key management into a fire drill.