How do I set up secrets (API keys) and environment variables in Modal for production deployments?
Platform as a Service (PaaS)

How do I set up secrets (API keys) and environment variables in Modal for production deployments?

9 min read

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.cls decorators or Image.env. When you deploy with modal 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

ConceptDefinitionWhy it's important
SecretEncrypted, 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 variableNon-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 wiringThe 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:

  1. Create Secrets in the Modal dashboard (or via API/CLI).
  2. Wire Secrets and env vars into your Modal app in Python.
  3. 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:

  1. Go to the Modal dashboard → Secrets.

  2. Create a new Secret, e.g., algolia-secret.

  3. Add keys/values (these map to environment variables or JSON fields):

    • API_KEY: xxxxxxxx
    • APPLICATION_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_KEY and APPLICATION_ID you just created. You can name this anything you want, but we named it algolia-secret and 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_dev vs docs_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 for secret, 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 do OPENAI_API_KEY = "..." in Python or ENV OPENAI_API_KEY=... in your Image. 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 put DEV_API_KEY and PROD_API_KEY in 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 using modal run or modal serve. Don’t assume local machine env vars are visible; explicitly define env via env={...}, Image.env(...), or propagate selected variables with env={"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 dev and prod

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 call modal serve and modal 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.

Next Step

Get Started