개발/FastAPI

FastAPI 환경변수 설정: .env로 DB 주소와 비밀키 분리하기

notebase 2026. 6. 4. 13:45

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.parentapp/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()

 

이렇게 하면 .envAPP_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로 관리하는 편이 훨씬 안정적이다.