
How do I set up secrets (API keys) and environment variables in Modal for production deployments?
Most production workloads on Modal need secrets (API keys, tokens, DB credentials) and environment variables that are safe, auditable, and easy to rotate—without hard‑coding anything into your repo or baking credentials into container images. The good news is that Modal treats both as first‑class infrastructure you define in Python, so you can keep local iteration fast and production deployments predictable.
Quick Answer: In Modal, you store sensitive values like API keys in Secrets, and non-sensitive configuration in environment variables attached to Apps, Functions, and Classes. You create Secrets in the Modal dashboard (or via CLI), reference them in code with
modal.Secret.from_name(), and define env vars via@app.function/@app.clsdecorators orImage.env. When you deploy withmodal deploy, the same code runs in production with the right secrets and env injected into each container.
Why This Matters
If you’re calling external APIs (OpenAI, Stripe, Algolia), talking to a managed database, or splitting dev/staging/prod configs, secret and env management is not optional—it’s the difference between a reproducible system and “works on my laptop.” Hard‑coded keys in code or images are a compliance problem, a rotation nightmare, and an easy way to leak access if your repo is ever exposed.
With Modal, you keep all of that in the platform:
- Secrets live in Modal’s managed store, isolated using gVisor and governed by team controls, SOC2/HIPAA, and data residency rules.
- Environment stays code‑defined: you can see exactly what a production Function runs with, diff it in Git, and patch it via redeploy.
- You can run the exact same code path in development (
modal serve/modal run) and production (modal deploy) without manual wiring.
Key Benefits:
- No secrets in code or images: API keys, tokens, and passwords live in Modal Secrets, not in your repo or Dockerfile.
- Config by environment: Use env vars and different Secrets per Modal Environment to separate dev, staging, and prod cleanly.
- Safe rotation and debugging: Rotate keys centrally, redeploy quickly, and inspect running containers’ behavior via logs without exposing secrets.
Core Concepts & Key Points
| Concept | Definition | Why it's important |
|---|---|---|
| Secret | Encrypted, managed key/value store entry (e.g., OPENAI_API_KEY, DB_PASSWORD) you create in the Modal dashboard and inject into containers. | Keeps sensitive data out of code, images, and logs while still being available at runtime as env vars or JSON. |
| Environment variable | Non-secret configuration exposed to a container as os.environ (e.g., ENV, MODEL_NAME, feature flags). | Lets you parameterize behavior across dev/staging/prod without branching code or rebuilding images. |
| Code-defined wiring | The pattern of attaching Secrets and env vars directly in decorators (@app.function(secret=...), .env(...), Image.env(...)). | Ensures config is versioned with your app code and re-applied consistently on every modal deploy. |
How It Works (Step-by-Step)
At a high level, you:
- Create Secrets in the Modal dashboard (or via API/CLI).
- Wire Secrets and env vars into your Modal app in Python.
- Deploy to production with
modal deploy, and let the platform inject everything at runtime.
Let’s go through that with concrete code.
1. Create a Secret for your API keys
Say you’re integrating Algolia in production. You don’t want this in your code:
ALGOLIA_API_KEY = "xxxxxxxx"
APPLICATION_ID = "YYYYYYY"
Instead:
-
Go to the Modal dashboard → Secrets.
-
Create a new Secret, e.g.,
algolia-secret. -
Add keys/values (these map to environment variables or JSON fields):
API_KEY:xxxxxxxxAPPLICATION_ID:YYYYYYY
This lines up with the official pattern:
Set up a project and create an API key with write access… create a Secret on the Modal Secrets page with the
API_KEYandAPPLICATION_IDyou just created. You can name this anything you want, but we named italgolia-secretand so that’s what the code below expects.
You’ll repeat that for other providers: openai-secret, stripe-secret, prod-db-secret, etc.
2. Reference Secrets in your Modal app
Now wire the Secret into your app. First, get imports out of the way:
import os
import modal
app = modal.App("prod-config-example")
Attach a Secret to a Function
You can attach Secrets directly on a per-Function basis:
algolia_secret = modal.Secret.from_name("algolia-secret")
@app.function(
secrets=[algolia_secret],
)
def index_document(doc_id: str, content: str):
# These will be available as environment variables
api_key = os.environ["API_KEY"]
app_id = os.environ["APPLICATION_ID"]
# Initialize Algolia client here
# ...
return {"doc_id": doc_id, "indexed": True}
When this runs in production, the container will see API_KEY and APPLICATION_ID populated with the values from the algolia-secret Secret—no extra wiring.
You can define multiple Secrets on a single function:
openai_secret = modal.Secret.from_name("openai-secret")
db_secret = modal.Secret.from_name("prod-db-secret")
@app.function(
secrets=[openai_secret, db_secret],
)
def generate_and_persist(prompt: str):
openai_key = os.environ["OPENAI_API_KEY"]
db_password = os.environ["DB_PASSWORD"]
# ...
Attach a Secret to a Class (stateful servers)
For long-lived model servers (@app.cls with @modal.enter):
llm_secret = modal.Secret.from_name("openai-secret")
@app.cls(secrets=[llm_secret])
class LLMServer:
@modal.enter()
def setup(self):
self.openai_key = os.environ["OPENAI_API_KEY"]
# load large model weights once per container, etc.
@modal.method()
def complete(self, prompt: str) -> str:
# use self.openai_key to call OpenAI
...
Every container backing this class gets the same Secret injection.
3. Define environment variables (non-secret config)
Secrets are for things you’d never want in Git. Environment variables are great for:
ENV(dev,staging,prod)- Model/version selection (
MODEL_NAME,MODEL_REVISION) - Per‑environment feature toggles (
USE_NEW_PIPELINE)
There are two main ways to define them.
Env vars via decorators
You can attach env directly on Functions:
@app.function(
secrets=[algolia_secret],
env={
"ENV": "prod",
"ALGOLIA_INDEX": "docs_prod",
},
)
def index_document(doc_id: str, content: str):
env = os.environ["ENV"]
index_name = os.environ["ALGOLIA_INDEX"]
...
Same with Classes:
@app.cls(
env={
"ENV": "prod",
"MODEL_NAME": "my-llm-v2",
}
)
class LLMServer:
...
This keeps the definition next to the code that uses it.
Env vars on the Image (shared across many functions)
If multiple Functions share the same environment variables, push them down to the Image:
image = (
modal.Image.debian_slim()
.pip_install("requests")
.env(
{
"ENV": "prod",
"REGION": "us-east-1",
}
)
)
@app.function(image=image)
def do_something():
print(os.environ["ENV"])
You can override or add env on individual Functions if needed:
@app.function(image=image, env={"ENV": "staging"})
def do_something_staging():
...
4. Use different Secrets and env for different environments
Most teams want different configs for dev vs prod:
- Different OpenAI orgs or API keys
- Different Algolia indices (
docs_devvsdocs_prod) - Different DBs (sandbox vs production)
The pattern:
import os
import modal
ENV = os.getenv("APP_ENV", "dev") # read from local machine when running `modal run/serve`
app = modal.App(f"my-app-{ENV}")
if ENV == "prod":
algolia_secret = modal.Secret.from_name("algolia-secret-prod")
algolia_index = "docs_prod"
else:
algolia_secret = modal.Secret.from_name("algolia-secret-dev")
algolia_index = "docs_dev"
@app.function(
secrets=[algolia_secret],
env={"ALGOLIA_INDEX": algolia_index, "ENV": ENV},
)
def index_document(doc_id: str, content: str):
...
Then:
- For dev:
APP_ENV=dev modal serve app.py - For prod:
APP_ENV=prod modal deploy app.py
Same code, different wiring.
5. Deploy to production
Use the CLI:
# Iterate locally against cloud containers
modal serve app.py # hot‑reloads on code changes
# When ready for production
modal deploy app.py
The app that lands in production has the same Secret and env configuration expressed in your decorators. For logs and debugging:
# See your production logs
modal app list
modal app logs prod-config-example # or your app name
These CLI commands are the ones you’ll use to see what’s actually happening:
modal deploy path/to/your/app.py- Deploy your app (Functions, web endpoints, etc.) to Modal.modal app logs <app_name>- Stream logs for a deployed app.- Resources like
modal volume list, and similarly forsecret,dict,queue, etc.
You can also use modal secret list and related commands to inspect your Secret inventory.
Common Mistakes to Avoid
-
Hard‑coding secrets in code or Images:
Don’t doOPENAI_API_KEY = "..."in Python orENV OPENAI_API_KEY=...in yourImage. Use Modal Secrets instead so you can rotate keys, audit access, and avoid leaking credentials if the repo or image is exposed. -
Mixing dev and prod config in a single Secret:
Don’t putDEV_API_KEYandPROD_API_KEYin the same Secret; if your prod function accidentally uses the dev key, debugging becomes painful. Use separate Secrets (openai-secret-dev,openai-secret-prod) and select them based on environment or separate Apps. -
Forgetting that Modal always runs in the cloud:
Modal always executes code in the cloud—even when you’re usingmodal runormodal serve. Don’t assume local machine env vars are visible; explicitly define env viaenv={...},Image.env(...), or propagate selected variables withenv={"ENV": os.environ["ENV"]}if you really need to.
Real-World Example
Imagine you’re running a RAG (retrieval‑augmented generation) system in production. You have:
- OpenAI or another LLM provider
- Algolia as your vector/text search index
- Different indices for
devandprod
A minimal Modal app might look like this:
import os
import modal
ENV = os.getenv("APP_ENV", "dev")
app = modal.App(f"rag-qna-{ENV}")
openai_secret = modal.Secret.from_name(f"openai-secret-{ENV}")
algolia_secret = modal.Secret.from_name(f"algolia-secret-{ENV}")
image = (
modal.Image.debian_slim()
.pip_install("openai", "algoliasearch", "tiktoken")
.env({"ENV": ENV})
)
@app.function(
image=image,
secrets=[openai_secret, algolia_secret],
env={
"ALGOLIA_INDEX": f"potus_speeches_{ENV}",
},
)
def answer_question(question: str) -> str:
from openai import OpenAI
from algoliasearch.search_client import SearchClient
openai_key = os.environ["OPENAI_API_KEY"]
algolia_app_id = os.environ["APPLICATION_ID"]
algolia_api_key = os.environ["API_KEY"]
index_name = os.environ["ALGOLIA_INDEX"]
client = OpenAI(api_key=openai_key)
search_client = SearchClient.create(algolia_app_id, algolia_api_key)
index = search_client.init_index(index_name)
hits = index.search(question, {"hitsPerPage": 5})["hits"]
context = "\n\n".join(hit["content"] for hit in hits)
completion = client.responses.create(
model="gpt-4.1-mini",
input=f"Context:\n{context}\n\nQuestion: {question}\nAnswer:",
)
return completion.output[0].content[0].text
Deploy this:
APP_ENV=prod modal deploy app.py
Your production containers now:
- Load the prod OpenAI and Algolia credentials from Modal Secrets.
- Target the correct prod index via env vars.
- Keep all sensitive values out of Git and Images.
Pro Tip: When rotating an API key, create a new Secret (e.g.,
openai-secret-prod-v2), wire it in your code, deploy, and only then delete the old Secret. This lets you test the new credentials in production with a fast rollback path.
Summary
Setting up Secrets and environment variables correctly in Modal is how you keep production deployments safe and reproducible:
- Create Secrets in the Modal dashboard for API keys and credentials, then reference them via
modal.Secret.from_name(...)on Functions and Classes. - Use env vars for non-secret configuration (
ENV, indices, model names), defined on decorators or Images so that configuration lives in code, not tribal knowledge. - Separate dev/staging/prod with distinct Secrets and env sets, and drive it via a simple environment flag (
APP_ENV) when you callmodal serveandmodal deploy.
Once you’ve wired Secrets and env this way, you can iterate with modal serve, deploy with modal deploy, and trust that your production containers have exactly the configuration you intend—no shell scripts, no manual environment editing, and no credentials in Git.