As you package your FastAPI application into a Docker container, you create 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 seamlessly 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 robust 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 beyond 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.
© 2025 ApX Machine Learning