Packaging a FastAPI application into a Docker container creates a self-contained unit. However, applications rarely operate identically in every environment. You might need different database connection strings for development versus production, varying API keys for external services, or perhaps just a different logging level for debugging. Hardcoding these configuration values directly into your application code is inflexible and poses security risks, especially for sensitive information.
A standard and effective practice, particularly in containerized environments, is to manage configuration through environment variables. These variables exist outside your application code, within the operating system or container environment where your application runs. This approach decouples configuration from the application logic, making your Docker images more portable and adaptable.
Using environment variables offers several significant advantages:
Python's standard library provides the os module to interact with the operating system, including accessing environment variables. The primary way to read them is using os.environ, which behaves like a dictionary.
import os
# Accessing an environment variable
# Using os.environ['VAR_NAME'] will raise a KeyError if the variable is not set.
# api_key = os.environ['MY_API_KEY']
# A safer way: using .get() with an optional default value
api_key = os.environ.get('MY_API_KEY', 'default_key_if_not_set')
model_path = os.environ.get('MODEL_PATH', './models/default_model.joblib') # Example for ML model path
log_level = os.environ.get('LOG_LEVEL', 'INFO')
print(f"API Key: {api_key}")
print(f"Model Path: {model_path}")
print(f"Log Level: {log_level}")
Using os.environ.get('VAR_NAME', default_value) or os.getenv('VAR_NAME', default_value) is generally preferred because it allows you to provide a default value if the environment variable isn't set, preventing your application from crashing due to missing configuration.
While os.environ.get works well for simple cases, managing numerous configuration variables can become cumbersome. FastAPI integrates with Pydantic, which offers a powerful way to manage application settings, including reading from environment variables automatically.
Pydantic's settings management (now often used via the pydantic-settings library) allows you to define a configuration schema using a Pydantic model. Pydantic will then attempt to load values for the fields in your model from environment variables, automatically performing type casting and validation.
First, ensure you have pydantic-settings installed:
pip install pydantic-settings
Now, you can define a settings class:
# config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Optional
class AppSettings(BaseSettings):
# Pydantic will automatically try to load these from environment variables
# Example: APP_TITLE will be loaded from the environment variable APP_TITLE
app_title: str = "ML Model API"
log_level: str = "INFO"
model_path: str = "./models/default_model.joblib"
api_key: Optional[str] = None # Example for an optional secret
# Configure Pydantic settings
model_config = SettingsConfigDict(
# Environment variables are typically uppercase, but Pydantic is case-insensitive by default
case_sensitive=False,
# You can optionally load from a .env file during development (requires python-dotenv)
# env_file = '.env'
)
# Create a single instance to be used throughout the application
settings = AppSettings()
In this example:
AppSettings inherits from pydantic_settings.BaseSettings.app_title, log_level, model_path, api_key) represent your application's configuration settings.APP_TITLE, LOG_LEVEL, MODEL_PATH, and API_KEY.str, Optional[str]) ensure that the loaded values are correctly parsed and validated. If an environment variable exists but cannot be cast to the specified type (e.g., providing "not-an-integer" for an int field), Pydantic will raise a validation error.You can then import and use the settings instance wherever you need configuration values in your FastAPI application, often using dependency injection:
# main.py (or your relevant router file)
from fastapi import FastAPI, Depends
from .config import AppSettings, settings # Import the instance
# Dependency function to get settings
def get_settings() -> AppSettings:
return settings
app = FastAPI()
@app.get("/info")
async def info(current_settings: AppSettings = Depends(get_settings)):
# Access settings through the dependency
return {
"app_title": current_settings.app_title,
"log_level": current_settings.log_level,
"model_path_configured": current_settings.model_path
}
# You can also use the settings instance directly if not using Depends
print(f"Starting application: {settings.app_title}")
# ... rest of your application setup
Using Pydantic BaseSettings provides a structured, validated, and type-safe way to handle configuration loaded from the environment.
Now that your application knows how to read environment variables, how do you provide them to the Docker container? There are several common methods:
Using the ENV instruction in the Dockerfile:
You can set default environment variables directly within your Dockerfile. These values are baked into the image. This is suitable for non-sensitive defaults or variables unlikely to change often between environments.
# Dockerfile excerpt
FROM python:3.9-slim
WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY ./app /app/app
COPY ./models /app/models # Assuming models are copied in
# Set default environment variables
ENV LOG_LEVEL="INFO"
ENV MODEL_PATH="/app/models/iris_model.joblib"
ENV APP_TITLE="Default ML API Title"
# Expose the port FastAPI will run on
EXPOSE 8000
# Command to run the application using Uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Variables set with ENV are available during the image build process and as defaults when a container starts from the image.
Using the docker run -e or --env flag:
You can override ENV defaults or provide additional environment variables when starting a container using the -e or --env flag. This is the most common way to provide environment-specific configuration or secrets at runtime.
# Run the container, overriding LOG_LEVEL and setting a specific API_KEY
docker run -d -p 8000:8000 \
-e LOG_LEVEL="DEBUG" \
-e API_KEY="secret-prod-api-key-12345" \
-e APP_TITLE="Production ML Service" \
your-fastapi-image-name:latest
Each -e flag sets one environment variable (VAR_NAME=value). This method keeps sensitive data out of the image itself.
Using an Environment File (--env-file):
For managing a larger number of variables, especially during development or with tools like Docker Compose, you can place them in a file (e.g., .env) and pass the file path to the docker run command.
# Example .env file (e.g., config.env)
LOG_LEVEL=DEBUG
API_KEY=local-dev-key-abcdef
MODEL_PATH=/app/models/dev_model.joblib
APP_TITLE=Development ML API
# Run the container using the environment file
docker run -d -p 8000:8000 --env-file ./config.env your-fastapi-image-name:latest
Docker reads each line in the file as a KEY=VALUE pair and sets the corresponding environment variable inside the container. Variables set via -e typically override those from an --env-file.
While environment variables are a significant improvement over hardcoding secrets, be aware that they might still be visible through container inspection tools or logs in some environments. For highly sensitive production secrets, consider using dedicated secret management systems (like HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, Azure Key Vault). These systems provide more security features like access control, auditing, and secret rotation. Integrating these often involves fetching secrets at application startup or using sidecar containers, which is a more advanced deployment pattern outside the scope of this immediate section but important to know for production hardening. For many applications, however, environment variables managed carefully offer a good balance of security and simplicity.
By leveraging environment variables, especially when combined with Pydantic's settings management, you can create flexible, portable, and more secure containerized FastAPI applications ready for different deployment stages. This practice is fundamental to building applications that can easily adapt to the environments they run in.
Was this section helpful?
os module, detailing how to interact with the operating system and access environment variables.pydantic-settings, explaining structured configuration management, including loading settings from environment variables with type validation.docker run -e and --env-file, essential for containerized application configuration.© 2026 ApX Machine LearningEngineered with