- 🐳 Running FastAPI with
main.pyin Docker can break relative imports due to Python's execution context. - 🔥 Uvicorn's module syntax (
app.main:app) is the recommended way to launch FastAPI in Docker environments. - 🧊 The
python:3.10-slimbase image is better for FastAPI Docker builds than Alpine due to compatibility issues. - 🔐 Managing secrets via
.envand environment variables prevents exposing credentials in Docker images. - 🚀 For production, use Gunicorn with UvicornWorker to ensure multiprocessing and scalability.
FastAPI's clean syntax and async capabilities make it a good backend framework. But deploying it correctly, especially with Docker, is important. This prevents issues like import errors, broken environment variables, or bad scalability. This article shows how to run FastAPI in Docker correctly. It covers using uvicorn and good methods for builds that scale, are secure, and ready for production.
Why FastAPI and Docker Work So Well Together
FastAPI is a modern Python backend. It offers features like automatic OpenAPI schema generation, asynchronous request handling, and strong typing with Pydantic. These features make it a good choice for building fast and reliable APIs.
Docker works very well with FastAPI. It provides containerized environments that make sure the software runs the same way in development, QA, and production. With Docker, all dependencies, from the Python version to OS packages, are put directly into the container image. This makes sure your FastAPI application works the same way if you are developing on macOS, deploying on Linux, or testing with a CI/CD pipeline. It gets rid of the "works on my machine" problem. It also makes it easy to bring new developers on board or set up test environments.
How Most People Start: uvicorn main.py
Beginners often start FastAPI using Uvicorn with a direct file name like this:
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Sample Dockerfile for a quick start:
FROM python:3.10
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
This approach seems simple, and it works, but only for very basic applications. It works if you are testing locally or if your app has a simple file setup. But when your app grows to include many modules and packages, this shortcut causes big problems.
Why Running main.py Can Break Your App
When you run main.py directly, Python sees it as the main script. It does not see it as part of your package. This turns off relative imports and goes against how Python finds modules.
For example:
# app/main.py
from .models import User # ❌ Raises ImportError if run directly
When main.py starts without context, like with python main.py or uvicorn main.py, Python turns off how it normally imports packages. This means from .models import User fails. And you will wonder why your imports are not working as they should.
Other consequences:
- Docker's CMD command sees
main.pyas a separate file when it runs. - Unit tests that import files relatively might break.
- Logging and config loading might not work if you use relative paths.
Rule: Do not run FastAPI apps using the filename. This causes builds to be unstable and creates problems with imports, especially in Docker.
Prefer Module Syntax: uvicorn app.main:app
Do not start your FastAPI app with a direct script filename. Instead, use Python’s module syntax. Here is the way to do it:
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Your project structure should look like this:
/project
/app
__init__.py
main.py
models/
routes/
Now Python and Uvicorn see the app directory as a real package. Relative imports now work correctly:
from .models import User # ✅ Works reliably
This approach works well as your project grows. This is because it follows good practices from both the FastAPI Docker documentation and Uvicorn's guidelines.
Dockerfile Tips for FastAPI + Uvicorn
To build a Docker container that is small but strong, start with the right base image. Also, keep your code clean. Here is a better Dockerfile:
FROM python:3.10-slim
WORKDIR /code
COPY ./app /code/app
COPY requirements.txt /code/
RUN pip install --no-cache-dir -r requirements.txt
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Why this works well:
- Python Slim makes the image smaller than standard images.
--no-cache-dirstops pip from saving extra cache files.- Files are copied in separate steps. This helps Docker reuse cached parts.
Do not use Alpine for most FastAPI apps. This is unless you know a lot about its problems with Python wheels and C extensions.
Managing Environment Variables Safely
Environment variables are important for production settings. They let you keep secrets or specific data, like API keys or database addresses, separate. Here is a safe way to do it:
- Create a
.envfile:
DATABASE_URL=postgresql://user:password@db:5432/mydb
DEBUG=False
- Run the container with
--env-file:
docker run --env-file=.env fastapi-image
- Access inside your FastAPI app:
import os
DEBUG = os.getenv("DEBUG", "False") == "True"
DATABASE_URL = os.getenv("DATABASE_URL")
You can use python-dotenv to load .env files automatically in development:
pip install python-dotenv
And in your code:
from dotenv import load_dotenv
load_dotenv()
Never put sensitive .env contents directly into your image with COPY. Always pass them securely when the app runs.
Debugging and Logs Inside Docker
To see what is happening inside your FastAPI app in Docker:
-
Use stdout for logs:
import logging logging.basicConfig(level=logging.INFO) -
Do not write logs directly to files. Only do this if you are managing disk space or sending logs with tools like Fluentd.
-
During development, you can use Uvicorn’s reload feature:
CMD ["uvicorn", "app.main:app", "--reload", "--host", "0.0.0.0"]
But remember, --reload is:
- Slower
- Not safe for production
- Only use it during local development.
Setting Up for Production Deployment
Uvicorn is great, but it runs with only one thread by default. For production, use Gunicorn with Uvicorn for multiple workers:
gunicorn app.main:app -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
Gunicorn works as an app server. It sends tasks to Uvicorn workers to handle ASGI processes. Benefits:
- Process management
- Better performance with multiple workers
- Handles signals and restarts smoothly.
Also consider:
- Add a reverse proxy like Nginx or Traefik for TLS.
- Use Kubernetes probes for
/healthand/ready. - Use
--configflags to keep server settings outside the code.
Hardening Your Docker Container
Secure your container before deploying to production:
-
Create a non-root user:
RUN adduser --disabled-password fastapi USER fastapi -
Use multi-stage Docker builds. This helps remove build dependencies and makes the final image smaller.
-
Turn off FastAPI debug mode in
.env. -
Clearly state the exposed port:
EXPOSE 8000
Making your container secure helps in the long run. It makes it harder for attacks and helps meet security rules.
Avoid These Common Mistakes
Avoid these common mistakes with FastAPI and Docker:
- Do not use
uvicorn main.pyinstead of module syntax. - Do not forget
__init__.py. Python will not see folders as packages without it. - Do not forget to rebuild the Docker image after you change code.
- Do not copy your
.envfile into the image. - Do not set
--reloadin production builds.
Check your Dockerfile and scripts for these issues before deploying.
CI/CD and Docker Registry Integration
Automate your builds and deployments using CI/CD pipelines. Here is what a basic GitHub Actions workflow might look like:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: docker build -t myfastapi:latest .
- run: docker tag myfastapi myregistry.com/my-fastapi:latest
- run: docker push myregistry.com/my-fastapi:latest
Extra tips:
- Tag images using Git commit SHA or version numbers.
- Use Docker's BuildKit for layer caching.
- Run vulnerability scans with
docker scan, Snyk, or Trivy. - Set up multi-stage builds to keep the final image secure and small.
Example Directory Layout for FastAPI
A clean project layout makes it much easier to test, read, and extend your code. A good structure is:
/project-root
/app
__init__.py
main.py
routes/
models/
services/
/config
settings.py
/tests
requirements.txt
Dockerfile
.env
Putting components into routes/, models/, and services/ helps keep different parts of the code separate. The config/settings.py file can put all environment-based config loading in one place.
Debugging Python Import Errors in Docker
If you get ImportError or ModuleNotFoundError, try this first:
Run the app with module syntax:
python -m app.main
Instead of:
python app/main.py
Check paths inside Docker using:
RUN pwd && ls -la
When you add folders or use COPY commands, it is easy for your working directories to be wrong.
More tips to find the problem:
- Print
sys.path. - Log the actual
os.getcwd(). - Use
docker execto check file paths inside a running container.
To finish, if you are building or deploying FastAPI in Docker, do not start it with main.py. This works for basic uses, but it breaks imports and fails without a warning in more complex builds. Instead, use module syntax (uvicorn app.main:app). It is simple, strong, and follows proven production standards.
Ready to do even more with your FastAPI deployment? In future articles, we will cover setting up autoscaling in Kubernetes, caching with Redis, and building dashboards for monitoring with Prometheus and Grafana.
Citations
- JetBrains & Python Software Foundation. (2022). Python Developers Survey 2022. https://www.jetbrains.com/lp/python-developers-survey-2022/
- Stack Overflow. (2023). ImportError when running FastAPI
main.pyin Docker. https://stackoverflow.com/q/66064744 - FastAPI Documentation. (n.d.). FastAPI in Docker. https://fastapi.tiangolo.com/deployment/docker/
- Uvicorn Documentation. (n.d.). Running from the Command Line. https://www.uvicorn.org/#running-from-the-command-line