You're using Docker to deploy your FastAPI application with Aiven PostgreSQL, when all of a sudden you notice this traceback:
ValueError: invalid literal for int() with base 10: 'None'
It refers to SQLAlchemy's URL parser and complains that the string "-None" is the port portion of your database URL. Your URL appears as postgresql+asyncpg://user:pass@host:None/dbname. What caused that to occur? And above all, how do you fix it?
In this tutorial, I'll walk you through the cause of this error and provide a clean solution. We'll also discuss best practices for managing environment variables in containerized FastAPI applications.
Understanding the Error
The error occurs when SQLAlchemy's make_url() function tries to parse the database URL and convert the port component to an integer, but receives the literal string "None". This happens because somewhere in your code, you're building the URL with a variable that evaluates to None, and when interpolated into an f-string, None becomes the string "None".
The Code That Breaks
Consider this common pattern in database.py:
import os
DB_USER = os.getenv("DB_USER")
DB_PASSWORD = os.getenv("DB_PASSWORD")
DB_HOST = os.getenv("DB_HOST")
DB_PORT = os.getenv("DB_PORT")
DB_NAME = os.getenv("DB_NAME")
ASYNC_DB_URL = f"postgresql+asyncpg://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
ASYNC_DB_URL = os.getenv("DATABASE_URL") # attempt to override with full URL
engine = create_async_engine(ASYNC_DB_URL)
When DB_PORT is not set in your environment (such as a Docker container), it becomes None. A URL with :None in the port position is generated by the f-string. The engine creation still uses the final value even if you later reassign ASYNC_DB_URL with the correct DATABASE_URL. Why, then, does it still have "None"? Because the assignment order is deceptive or because DATABASE_URL may also be missing.
In my instance, DATABASE_URL was set, but a string containing "-None" was already created in the first line and used. No, the engine actually makes use of the final value. Since the second line sets ASYNC_DB_URL to None but the engine attempts to use None, the issue must be that DATABASE_URL is not set. That would result in a different error. The port "None" error we observe indicates that the f-string URL is being used.
This only occurs in the event that DATABASE_URL is not set, in which case the code reverts to the constructed URL (possibly through a or clause). For example:
ASYNC_DB_URL = os.getenv("DATABASE_URL") or f"postgresql+asyncpg://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
If DATABASE_URL is empty, it uses the constructed URL, which has None for port if any component is missing.
Step-by-Step Debugging
- Check your environment variables inside the container:
docker exec <container_name> env
Verify that DATABASE_URL is present and correct, and that individual component variables are not set (unless you intentionally set them).
Inspect the code in
database.py. Look for any fallback logic that builds the URL from components. If you see something like the above, that's your culprit.Simplify: Remove the component-based construction entirely. Use only the full
DATABASE_URL:
import os
DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL:
raise ValueError("DATABASE_URL environment variable is not set")
engine = create_async_engine(DATABASE_URL)
This ensures you're not accidentally creating a malformed URL.
- Test by running your app again. The error should disappear.
Why the Single-Env-Var Approach is Better
-
Consistency: Most cloud database providers (Aiven, Heroku, etc.) give you a single
DATABASE_URL. Using it directly avoids mismatches. - Simplicity: Fewer variables to manage in your .env and Docker configuration.
- Portability: Your app can be deployed anywhere without changing how the URL is assembled.
- Security: You avoid accidentally logging or exposing partial credentials.
Handling Async with asyncpg
If you're using asyncpg with SQLAlchemy, your DATABASE_URL must use the postgresql+asyncpg:// scheme. Aiven provides a URL like:
postgres://avnadmin:password@host:port/defaultdb?sslmode=require
You need to change the scheme to postgresql+asyncpg://. You can do this programmatically:
raw_url = os.getenv("DATABASE_URL")
if raw_url and raw_url.startswith("postgres://"):
raw_url = raw_url.replace("postgres://", "postgresql+asyncpg://", 1)
engine = create_async_engine(raw_url)
Full Working Example
Here's a robust database.py for FastAPI with Aiven:
import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, declarative_base
DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL:
raise ValueError("DATABASE_URL environment variable is required")
# Ensure asyncpg scheme
if DATABASE_URL.startswith("postgres://"):
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql+asyncpg://", 1)
engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
Base = declarative_base()
Environment Variables in Docker Compose
In your docker-compose.yml, pass the DATABASE_URL via environment or an env_file:
services:
app:
build: .
env_file:
- .env
And in your .env file:
DATABASE_URL=postgresql+asyncpg://avnadmin:password@host:port/defaultdb?sslmode=require
Conclusion
The ValueError: invalid literal for int() with base 10: 'None' is a symptom of a configuration mismatch. By adopting a single DATABASE_URL environment variable and eliminating component-based construction, you can avoid this and similar pitfalls. Remember to always validate required environment variables early and use defensive programming.
If you found this helpful, please share it with others who might be struggling with the same issue. And if you have questions, kindly drop them in the comments below!
Top comments (0)