FastAPI에서 .env와 pydantic-settings를 사용해 DB 주소, SECRET_KEY, 디버그 설정을 코드와 분리하는 방법을 예제로 정리합니다.
FastAPI 환경변수 설정을 제대로 해두면 DB 주소, JWT 비밀키, 디버그 옵션을 코드에서 분리할 수 있다. 처음에는 main.py에 직접 적는 게 편해 보이지만, GitHub에 올리거나 서버에 배포하는 순간 문제가 될 수 있다.
이 글은 2026년 6월 기준 FastAPI 최신 버전과 Pydantic v2 흐름을 기준으로 작성했다. 설정 관리는 pydantic-settings를 사용하고, 로컬 개발 환경에서는 .env 파일을 함께 사용하는 방식으로 설명한다.
환경변수 설정은 단순히 .env 파일을 하나 만드는 작업이 아니다.
개발 환경과 운영 환경의 설정을 분리하고, 실수로 비밀값이 코드 저장소에 올라가지 않게 막는 구조에 가깝다.
왜 FastAPI에서 환경변수를 분리해야 할까?
예를 들어 아래처럼 DB 주소와 비밀키를 코드에 직접 넣었다고 해보자.
# main.py
DATABASE_URL = "sqlite:///./app.db"
SECRET_KEY = "my-secret-key"
로컬 테스트만 할 때는 문제없어 보인다.
하지만 실제 프로젝트에서는 다음 문제가 생긴다.
- 개발 DB와 운영 DB 주소를 바꾸기 어렵다.
- JWT 비밀키가 코드에 그대로 노출된다.
- GitHub에 올렸을 때 민감한 값이 함께 올라갈 수 있다.
- Docker, 배포 서버, CI/CD 환경에서 설정을 바꾸기 불편하다.
- 팀원이 프로젝트를 실행할 때 필요한 설정값을 파악하기 어렵다.
여기서 중요한 점은 .env가 보안을 완성해 주는 도구는 아니라는 것이다.
.env는 주로 로컬 개발 환경에서 설정값을 분리하기 위한 파일이다.
운영 환경에서는 서버 환경변수, Docker secret, AWS Secrets Manager, GCP Secret Manager 같은 별도 비밀값 관리 방식을 쓰는 경우가 많다.
이 글에서 만들 구조
예시는 FastAPI 프로젝트에서 자주 쓰는 형태로 구성한다.
fastapi-env-example/
├── app/
│ ├── main.py
│ ├── config.py
│ └── database.py
├── .env
├── .env.example
├── .gitignore
└── requirements.txt
역할은 다음과 같다.
| 파일 | 역할 |
|---|---|
.env |
실제 로컬 환경변수 값 저장 |
.env.example |
필요한 환경변수 이름만 공유 |
.gitignore |
.env가 Git에 올라가지 않도록 차단 |
app/config.py |
환경변수를 읽는 설정 클래스 |
app/database.py |
DB 연결 설정 |
app/main.py |
FastAPI 앱에서 설정값 사용 |
실제로는 auth.py, routers/, schemas/, models/ 같은 파일이 더 생길 수 있다.
하지만 환경변수 흐름을 이해하는 데는 위 구조가 가장 단순하다.
1. 필요한 패키지 설치하기
먼저 FastAPI와 설정 관리에 필요한 패키지를 설치한다.
pip install fastapi uvicorn pydantic-settings python-dotenv
각 패키지의 역할은 다음과 같다.
| 패키지 | 역할 |
|---|---|
fastapi |
FastAPI 웹 프레임워크 |
uvicorn |
FastAPI 앱 실행 서버 |
pydantic-settings |
환경변수 기반 설정 클래스 관리 |
python-dotenv |
.env 파일 로드 지원 |
pydantic-settings는 Pydantic v2 흐름에서 설정 관리를 담당한다.
과거 Pydantic v1 예제를 보면 pydantic.BaseSettings를 사용하는 경우가 있는데, 최신 흐름에서는 pydantic_settings.BaseSettings를 사용하는 방식으로 이해하면 된다.
requirements.txt로 관리한다면 아래처럼 적을 수 있다.
fastapi
uvicorn
pydantic-settings
python-dotenv
JWT 예시 코드까지 직접 실행할 계획이라면 pyjwt도 추가한다.
fastapi
uvicorn
pydantic-settings
python-dotenv
pyjwt
주의할 점이 있다.
파이썬에서 JWT를 사용할 때는 보통 PyJWT 패키지를 설치한다.
설치 명령어는 아래처럼 작성한다.
pip install pyjwt
초보자들이 실수로 pip install jwt를 실행하는 경우가 있다.
그러면 예제에서 import jwt는 되는 것처럼 보여도, 실제로 jwt.encode()를 호출할 때 문제가 생길 수 있다.
2. .env 파일 만들기
프로젝트 루트에 .env 파일을 만든다.
DATABASE_URL=sqlite:///./app.db
SECRET_KEY=local-dev-secret-key
ACCESS_TOKEN_EXPIRE_MINUTES=30
DEBUG=true
여기서는 설명을 위해 SQLite 주소를 넣었다.
PostgreSQL을 사용한다면 이런 형태가 될 수 있다.
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
SECRET_KEY=local-dev-secret-key
ACCESS_TOKEN_EXPIRE_MINUTES=30
DEBUG=true
주의할 점이 있다.
.env 파일 안의 값은 기본적으로 문자열 형태로 읽힌다.
그래서 30, true처럼 적어도 코드에서 숫자나 불리언으로 안전하게 쓰려면 변환 과정이 필요하다.
Pydantic Settings를 쓰면 이 변환과 검증을 설정 클래스에서 처리할 수 있다.
3. .env.example 파일 만들기
.env는 Git에 올리면 안 된다.
대신 어떤 환경변수가 필요한지 알려주는 .env.example 파일을 만든다.
DATABASE_URL=
SECRET_KEY=
ACCESS_TOKEN_EXPIRE_MINUTES=30
DEBUG=false
이 파일에는 실제 비밀번호나 비밀키를 넣지 않는다.
팀 프로젝트라면 새로 합류한 사람이 .env.example을 보고 .env를 직접 만들 수 있다.
cp .env.example .env
Windows PowerShell에서는 아래처럼 복사할 수 있다.
Copy-Item .env.example .env
이 방식은 블로그 예제뿐 아니라 실제 프로젝트에서도 자주 쓰인다.
4. .gitignore에 .env 추가하기
.env 파일이 Git에 올라가지 않도록 .gitignore에 추가한다.
.env
.venv/
__pycache__/
*.pyc
여기서 가장 중요한 줄은 .env다.
이미 .env를 Git에 커밋한 적이 있다면 .gitignore에 추가하는 것만으로는 부족하다.
Git 추적 대상에서 제거해야 한다.
git rm --cached .env
그다음 커밋한다.
git commit -m "Remove .env from tracking"
단, 이미 GitHub 같은 원격 저장소에 비밀키가 올라갔다면 단순 삭제로 끝내면 안 된다.
노출된 키는 폐기하고 새 키로 교체해야 한다.
예를 들어 JWT SECRET_KEY가 올라갔다면 새 키를 만들어 교체해야 한다.
외부 API Key나 DB 비밀번호가 올라갔다면 해당 서비스에서 키를 재발급하거나 비밀번호를 바꾸는 게 안전하다.
5. config.py에서 환경변수 읽기
이제 app/config.py 파일을 만든다.
# app/config.py
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
database_url: str
secret_key: str
access_token_expire_minutes: int = 30
debug: bool = False
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
)
@lru_cache
def get_settings() -> Settings:
return Settings()
여기서 핵심은 Settings 클래스다.
class Settings(BaseSettings):
database_url: str
secret_key: str
이렇게 적으면 Pydantic Settings가 환경변수에서 값을 읽는다.
.env 파일에는 대문자로 적었다.
DATABASE_URL=sqlite:///./app.db
SECRET_KEY=local-dev-secret-key
코드에서는 소문자 필드로 썼다.
database_url: str
secret_key: str
일반적으로 .env에는 대문자, Python 코드에는 소문자 필드를 쓰면 읽기 좋다.
6. @lru_cache는 왜 사용할까?
get_settings() 함수 위에 @lru_cache를 붙였다.
@lru_cache
def get_settings() -> Settings:
return Settings()
이렇게 하면 설정 객체를 매번 새로 만들지 않고 재사용한다.
FastAPI 앱은 요청이 들어올 때마다 여러 의존성을 실행할 수 있다.
설정 파일을 매 요청마다 다시 읽을 필요는 없다.
@lru_cache를 붙이면 처음 한 번 생성한 설정 객체를 캐시해서 사용한다.
개발자가 보기에도 get_settings()를 통해 설정을 가져오는 흐름이 명확해진다.
7. main.py에서 설정값 사용하기
이제 FastAPI 앱에서 설정값을 사용해보자.
# app/main.py
from fastapi import Depends, FastAPI
from app.config import Settings, get_settings
app = FastAPI()
@app.get("/")
def read_root(settings: Settings = Depends(get_settings)):
return {
"message": "FastAPI environment settings example",
"database_url": settings.database_url,
"debug": settings.debug,
}
서버를 실행한다.
uvicorn app.main:app --reload
브라우저에서 아래 주소로 접속한다.
http://127.0.0.1:8000
응답 예시는 다음과 비슷하다.
{
"message": "FastAPI environment settings example",
"database_url": "sqlite:///./app.db",
"debug": true
}
여기서는 동작 확인을 위해 database_url을 응답에 포함했다.
하지만 실제 서비스에서는 DB 주소도 내부 정보에 가까우므로 외부 API 응답에 노출하지 않는 편이 좋다.
secret_key는 절대 응답으로 반환하면 안 된다.
8. SECRET_KEY는 어디에 사용할까?
SECRET_KEY는 보통 JWT 토큰 서명에 사용한다.
JWT 예시를 실행하려면 PyJWT를 설치해야 한다.
pip install pyjwt
pip install jwt가 아니라 pip install pyjwt다.
패키지 이름은 pyjwt지만, 코드에서는 아래처럼 import jwt로 사용한다.
# 예시 코드
from datetime import datetime, timedelta, timezone
import jwt
from app.config import get_settings
def create_access_token(data: dict) -> str:
settings = get_settings()
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.access_token_expire_minutes
)
payload = data.copy()
payload.update({"exp": expire})
token = jwt.encode(
payload,
settings.secret_key,
algorithm="HS256",
)
return token
이 코드는 JWT 흐름을 보여주기 위한 예시다.
실제 운영에서는 토큰 만료 시간, 알고리즘, refresh token, 비밀번호 해싱, HTTPS 적용까지 함께 고려해야 한다.
여기서 중요한 점은 settings.secret_key를 코드에 직접 적지 않는다는 것이다.
나쁜 예시는 아래와 같다.
token = jwt.encode(payload, "my-secret-key", algorithm="HS256")
수정한 예시는 아래와 같다.
settings = get_settings()
token = jwt.encode(
payload,
settings.secret_key,
algorithm="HS256",
)
작은 차이처럼 보이지만, 배포 환경에서는 큰 차이가 된다.
9. DB 연결 코드에서 DATABASE_URL 사용하기
SQLAlchemy나 SQLModel을 사용할 때도 DB 주소를 환경변수에서 가져올 수 있다.
예시는 SQLAlchemy 기준이다.
# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.config import get_settings
settings = get_settings()
engine = create_engine(
settings.database_url,
connect_args={"check_same_thread": False},
)
SessionLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=engine,
)
SQLite를 사용할 때는 connect_args={"check_same_thread": False}를 자주 사용한다.
하지만 PostgreSQL 같은 DB에서는 이 옵션이 필요하지 않다.
PostgreSQL로 바꾸면 예시는 이렇게 달라질 수 있다.
engine = create_engine(settings.database_url)
즉, 환경변수로 DB 주소를 분리하면 코드 전체를 크게 바꾸지 않고 DB 연결 대상을 바꿀 수 있다.
10. 흔한 오류 1: .env 파일을 찾지 못하는 경우
가장 자주 만나는 문제는 .env 파일 위치다.
예를 들어 프로젝트 구조가 아래와 같다고 해보자.
fastapi-env-example/
├── app/
│ ├── main.py
│ └── config.py
└── .env
env_file=".env"는 보통 서버를 실행하는 현재 작업 디렉터리를 기준으로 찾는다.
프로젝트 루트에서 실행하면 정상이다.
uvicorn app.main:app --reload
하지만 다른 위치에서 실행하면 .env를 못 찾을 수 있다.
이럴 때는 먼저 현재 위치를 확인한다.
pwd
Windows PowerShell이라면 다음 명령어를 쓸 수 있다.
Get-Location
가장 단순한 해결 방법은 프로젝트 루트에서 서버를 실행하는 것이다.
cd fastapi-env-example
uvicorn app.main:app --reload
Tip. 실행 위치와 상관없이 루트의 .env를 읽고 싶다면
프로젝트가 커지면 실행 위치가 바뀌어도 항상 프로젝트 루트의 .env를 읽고 싶을 수 있다.
이럴 때는 config.py에서 절대 경로를 계산해 넣을 수 있다.
# app/config.py
from functools import lru_cache
from pathlib import Path
from pydantic_settings import BaseSettings, SettingsConfigDict
BASE_DIR = Path(__file__).resolve().parent.parent
class Settings(BaseSettings):
database_url: str
secret_key: str
access_token_expire_minutes: int = 30
debug: bool = False
model_config = SettingsConfigDict(
env_file=BASE_DIR / ".env",
env_file_encoding="utf-8",
)
@lru_cache
def get_settings() -> Settings:
return Settings()
여기서 Path(__file__).resolve()는 현재 파일의 실제 경로를 찾는다.parent.parent는 app/config.py에서 두 단계 위, 즉 프로젝트 루트를 가리킨다.
이 방식은 편리하지만, 프로젝트 구조가 바뀌면 BASE_DIR 계산도 함께 확인해야 한다.
11. 흔한 오류 2: 필수 환경변수가 없을 때
.env에서 SECRET_KEY를 빼고 실행하면 Pydantic 검증 오류가 날 수 있다.
DATABASE_URL=sqlite:///./app.db
ACCESS_TOKEN_EXPIRE_MINUTES=30
DEBUG=true
이 상태에서 Settings를 만들면 secret_key 값이 없기 때문이다.
오류 메시지는 버전에 따라 조금 다를 수 있지만, 대략 이런 형태다.
Field required [type=missing, input_value=..., input_type=dict]
이 오류는 오히려 좋은 신호다.
앱이 잘못된 설정으로 조용히 실행되는 것보다, 시작 단계에서 필요한 값이 없다고 알려주는 편이 안전하다.
해결 방법은 .env에 값을 추가하는 것이다.
SECRET_KEY=local-dev-secret-key
12. 흔한 오류 3: bool 값이 예상과 다르게 동작하는 경우
.env에는 모든 값이 문자열로 들어간다.
DEBUG=false
직접 os.getenv()만 사용하면 "false"라는 문자열이 반환된다.
문자열 "false"는 Python 조건문에서 True처럼 동작할 수 있다.
import os
debug = os.getenv("DEBUG")
if debug:
print("debug mode")
이 코드는 DEBUG=false여도 "false" 문자열이 비어 있지 않기 때문에 조건문이 실행될 수 있다.
Pydantic Settings를 쓰면 debug: bool 타입으로 선언했을 때 적절히 bool 값으로 변환된다.
class Settings(BaseSettings):
debug: bool = False
환경변수를 직접 읽는 방식보다 설정 클래스를 쓰는 이유가 여기에 있다.
단순히 값을 가져오는 것이 아니라, 타입 변환과 검증까지 함께 처리할 수 있다.
13. 흔한 오류 4: jwt 패키지를 잘못 설치한 경우
JWT 예제를 따라 하다가 아래 같은 오류를 만날 수 있다.
AttributeError: module 'jwt' has no attribute 'encode'
이 경우 먼저 설치한 패키지를 확인해보는 것이 좋다.
pip list | grep -i jwt
Windows PowerShell에서는 아래처럼 확인할 수 있다.
pip list | findstr /i jwt
jwt라는 다른 패키지를 설치했다면 제거하고 PyJWT를 다시 설치한다.
pip uninstall jwt
pip install pyjwt
코드에서는 그대로 import jwt를 사용한다.
import jwt
헷갈리기 쉬운 부분은 패키지 설치 이름과 import 이름이 다르다는 점이다.
| 구분 | 값 |
|---|---|
| 설치 명령어 | pip install pyjwt |
| 코드 import | import jwt |
| 주요 사용 | jwt.encode(), jwt.decode() |
이 부분은 FastAPI 문제가 아니라 Python 패키지 이름 때문에 생기는 혼동에 가깝다.
14. 운영 환경에서는 .env를 그대로 써도 될까?
로컬 개발에서는 .env를 써도 괜찮다.
하지만 운영 환경에서는 상황에 따라 다르게 접근해야 한다.
| 환경 | 권장 방식 |
|---|---|
| 로컬 개발 | .env 파일 |
| 개인 테스트 서버 | 서버 환경변수 또는 제한된 .env |
| Docker | --env-file, Docker Compose 환경변수, secret 관리 |
| 클라우드 배포 | 플랫폼 환경변수 또는 Secret Manager |
| 팀/회사 운영 | AWS Secrets Manager, GCP Secret Manager, Vault, CI/CD Secret |
Secret Manager는 비밀번호, API Key, 토큰 같은 민감한 값을 안전하게 저장하고 접근 권한을 관리하는 서비스다.
AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault 같은 도구가 여기에 해당한다.
운영 서버에 .env를 둘 수는 있다.
다만 권한 관리, 백업, 배포 로그 노출, 서버 접근 권한까지 함께 봐야 한다.
특히 아래 값은 더 조심해야 한다.
- DB 비밀번호
- JWT
SECRET_KEY - OAuth Client Secret
- 외부 API Key
- SMTP 비밀번호
- 관리자 초기 비밀번호
.env를 쓴다고 해서 비밀값이 자동으로 안전해지는 것은 아니다.
Git에 올리지 않고, 로그에 찍지 않고, 노출되면 즉시 교체할 수 있어야 한다.
15. 환경변수 이름은 어떻게 정하면 좋을까?
작은 프로젝트라면 아래처럼 단순하게 써도 된다.
DATABASE_URL=
SECRET_KEY=
DEBUG=
프로젝트가 커지면 접두사를 붙이는 것도 좋다.
APP_DATABASE_URL=
APP_SECRET_KEY=
APP_DEBUG=
이 경우 설정 클래스도 접두사를 인식하도록 만들 수 있다.
# app/config.py
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
database_url: str
secret_key: str
debug: bool = False
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="APP_",
)
@lru_cache
def get_settings() -> Settings:
return Settings()
이렇게 하면 .env의 APP_DATABASE_URL 값이 database_url 필드로 들어간다.
프로젝트 하나만 운영한다면 필수는 아니다.
하지만 여러 앱을 같은 서버에서 실행하거나, 환경변수 이름이 겹칠 가능성이 있다면 접두사를 붙이는 편이 안전하다.
16. 초보자가 자주 하는 실수 정리
FastAPI에서 .env를 쓸 때 자주 하는 실수는 비슷하다.
| 실수 | 문제 | 해결 |
|---|---|---|
.env를 Git에 올림 |
비밀값 노출 | .gitignore 추가, 노출된 키 교체 |
SECRET_KEY를 코드에 직접 작성 |
배포 후 교체 어려움 | 환경변수로 분리 |
.env.example을 만들지 않음 |
협업 시 필요한 값 파악 어려움 | 변수 이름만 담은 예시 파일 작성 |
os.getenv()만 사용 |
타입 변환 실수 가능 | Pydantic Settings 사용 |
.env 위치를 잘못 둠 |
설정값 로드 실패 | 프로젝트 루트 실행 또는 절대 경로 지정 |
pip install jwt 실행 |
jwt.encode() 오류 가능 |
pip install pyjwt 사용 |
운영 서버에서 로컬 .env 그대로 사용 |
보안/운영 위험 | 서버 환경변수 또는 Secret Manager 검토 |
실제로는 .env 파일 하나보다 이 체크리스트가 더 중요하다.
설정값을 어디에 두는지보다, 노출되면 어떤 영향을 주는지 먼저 판단해야 한다.
전체 코드 예시
마지막으로 최소 실행 예시를 한 번에 정리하면 다음과 같다.
.env
DATABASE_URL=sqlite:///./app.db
SECRET_KEY=local-dev-secret-key
ACCESS_TOKEN_EXPIRE_MINUTES=30
DEBUG=true
.env.example
DATABASE_URL=
SECRET_KEY=
ACCESS_TOKEN_EXPIRE_MINUTES=30
DEBUG=false
.gitignore
.env
.venv/
__pycache__/
*.pyc
requirements.txt
fastapi
uvicorn
pydantic-settings
python-dotenv
pyjwt
app/config.py
from functools import lru_cache
from pathlib import Path
from pydantic_settings import BaseSettings, SettingsConfigDict
BASE_DIR = Path(__file__).resolve().parent.parent
class Settings(BaseSettings):
database_url: str
secret_key: str
access_token_expire_minutes: int = 30
debug: bool = False
model_config = SettingsConfigDict(
env_file=BASE_DIR / ".env",
env_file_encoding="utf-8",
)
@lru_cache
def get_settings() -> Settings:
return Settings()
app/main.py
from fastapi import Depends, FastAPI
from app.config import Settings, get_settings
app = FastAPI()
@app.get("/")
def read_root(settings: Settings = Depends(get_settings)):
return {
"message": "FastAPI environment settings example",
"database_url": settings.database_url,
"debug": settings.debug,
}
실행 명령어
uvicorn app.main:app --reload
브라우저에서 아래 주소로 접속한다.
http://127.0.0.1:8000
마무리
FastAPI에서 .env를 쓰는 목적은 단순히 파일을 하나 더 만드는 것이 아니다.
DB 주소, 비밀키, 토큰 만료 시간처럼 환경마다 달라지는 값을 코드에서 분리하는 것이다.
입문 단계에서는 아래 기준만 지켜도 충분하다.
- 실제 값은
.env에 둔다. .env는 Git에 올리지 않는다.- 공유용으로
.env.example을 만든다. - 설정값은
pydantic-settings로 읽고 검증한다. - JWT 예제에서는
pip install pyjwt를 사용한다. - 비밀키는 API 응답이나 로그에 출력하지 않는다.
- 운영 환경에서는 서버 환경변수나 Secret Manager 사용을 검토한다.
처음에는 os.getenv()로도 충분해 보일 수 있다.
하지만 DB 주소, JWT 비밀키, 디버그 모드, 토큰 만료 시간처럼 설정값이 늘어나면 Pydantic Settings로 관리하는 편이 훨씬 안정적이다.
'개발 > FastAPI' 카테고리의 다른 글
| FastAPI CORS 설정 방법: 프론트엔드 연동할 때 막히는 이유 (0) | 2026.05.29 |
|---|---|
| FastAPI JWT 로그인 구현 기초: 토큰 인증 흐름 이해하기 (0) | 2026.05.29 |
| FastAPI 프로젝트 구조 잡는 법: main.py 하나에서 벗어나기 (0) | 2026.05.27 |
| FastAPI CRUD API 만들기: GET, POST, PUT, DELETE 한 번에 이해하기 (0) | 2026.05.27 |
| FastAPI SQLite 연결하기: SQLModel로 DB 저장 API 만들기 (0) | 2026.05.26 |