Python dotenv 사용법: .env 파일 로드와 Django·Flask 설정
Python에서 `python-dotenv`로 `.env` 파일을 명시적으로 로드하고, `override=False`로 기존 환경변수 우선순위를 지키는 방법을 설명합니다. Django 설정, Flask CLI의 자동 로딩 범위, Git에 비밀값이 노출됐을 때의 대응도 함께 다룹니다.
python-dotenv를 사용할 때는 .env 파일을 만든 뒤 애플리케이션 시작 지점에서 load_dotenv()를 호출하면 됩니다. 경로를 명시하고 override=False를 유지하면, 로컬 .env는 개발용 기본값으로 쓰면서 운영 환경이나 CI가 이미 주입한 값은 그대로 우선할 수 있습니다.
python-dotenv는 .env의 키-값을 읽어 현재 프로세스의 os.environ에 설정합니다. 기본값으로 이미 존재하는 환경변수는 덮어쓰지 않습니다.
가장 작은 python-dotenv 예제
먼저 패키지를 설치합니다.
pip install python-dotenv
프로젝트 루트에 .env 파일을 만들고, 실제 비밀값 대신 개발용 값을 둡니다.
# .env DATABASE_URL=sqlite:///local.db APP_SECRET=replace-me-in-local-development
다음 코드는 .env 경로를 명시해 실행 위치가 달라져도 같은 파일을 읽도록 합니다.
# example.py from pathlib import Path import os from dotenv import load_dotenv BASE_DIR = Path(__file__).resolve().parent load_dotenv(BASE_DIR / ".env", override=False) DATABASE_URL = os.environ["DATABASE_URL"] APP_SECRET = os.environ["APP_SECRET"] # 비밀값 자체는 로그에 출력하지 않습니다. print(f"DATABASE_URL is configured: {bool(DATABASE_URL)}") print(f"APP_SECRET is configured: {bool(APP_SECRET)}")
필수 설정에는 os.environ["KEY"]가 적합합니다. 값이 없으면 프로그램 시작 단계에서 KeyError가 발생하므로, 잘못된 배포 설정을 요청 처리 이후까지 숨기지 않습니다. 반대로 선택 사항에는 os.getenv("KEY", "default")처럼 기본값을 둘 수 있습니다.
load_dotenv()에 경로를 주지 않으면 라이브러리는 .env를 탐색합니다. 간단한 스크립트에는 편리하지만, 작업 디렉터리·테스트 러너·배포 명령이 달라지는 프로젝트에서는 어느 파일을 읽는지 모호해질 수 있습니다. Django처럼 프로젝트 기준 경로를 계산할 수 있다면 dotenv_path를 명시하는 편이 안전합니다.
.env와 기존 환경변수의 우선순위
load_dotenv()의 override 기본값은 False입니다. 즉, 컨테이너·systemd·CI/CD가 이미 주입한 환경변수가 있으면 .env의 같은 이름 값은 적용하지 않습니다. 로컬 파일은 개발 편의용 기본값으로 두고, 배포 시스템이 주입한 값은 우선하게 만들 수 있습니다.
load_dotenv(BASE_DIR / ".env", override=False)
개인 로컬 개발에서는 .env와 override=False를 함께 두어 셸에서 임시로 설정한 값이 우선하도록 만들 수 있습니다. CI 테스트와 운영 배포에서는 CI 시크릿, 플랫폼 환경변수 또는 조직의 시크릿 저장소로 값을 주입하는 편이 적합합니다.
override=True은 기존 환경변수를 바꾸므로 일반적인 애플리케이션 시작 코드에는 사용하지 않는 편이 낫습니다. 테스트에서 꼭 필요하다면 테스트 프로세스를 격리하고, 어떤 값이 우선하는지 테스트 코드에서 명확히 선언하세요.
또한 python-dotenv는 # 주석과 ${NAME} 형태의 변수 확장을 지원합니다. $NAME처럼 중괄호가 없는 형식은 확장 대상이 아닙니다. .env 문법은 정식 표준이 아니라 라이브러리 문서의 지원 범위를 기준으로 확인해야 합니다.
Django 설정에서 .env 읽기
Django는 SECRET_KEY를 코드에 고정하는 대신 환경변수에서 읽는 방식을 배포 체크리스트에서 안내합니다. 운영에서 DEBUG=False라면 유효한 ALLOWED_HOSTS도 반드시 설정해야 합니다.
아래 예제는 settings.py에서 프로젝트 루트의 .env를 한 번 읽고, 필수 키가 누락되면 즉시 실패하도록 구성합니다.
# .env DJANGO_SECRET_KEY=replace-with-a-local-development-secret DJANGO_DEBUG=false DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
# config/settings.py from pathlib import Path import os from dotenv import load_dotenv BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(BASE_DIR / ".env", override=False) def env_bool(name: str, default: bool = False) -> bool: value = os.getenv(name) if value is None: return default normalized = value.strip().lower() if normalized in {"1", "true", "yes", "on"}: return True if normalized in {"0", "false", "no", "off"}: return False raise ValueError(f"{name} must be a boolean-like value") SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] DEBUG = env_bool("DJANGO_DEBUG", default=False) ALLOWED_HOSTS = [ host.strip() for host in os.getenv("DJANGO_ALLOWED_HOSTS", "").split(",") if host.strip() ]
DEBUG = os.getenv("DEBUG", "False") == "True"처럼 비교해도 동작은 하지만, true, 1 등 다른 표기를 모두 False로 처리합니다. 팀에서 입력 형식을 엄격히 고정하지 않았다면 위처럼 허용값과 오류 처리를 명시하는 편이 설정 실수를 더 빨리 찾게 합니다.
Flask CLI와 애플리케이션 설정의 역할 나누기
Flask CLI로 flask --app ... run을 실행하고 python-dotenv가 설치되어 있으면 Flask가 .env와 .flaskenv를 로드합니다. 이때 우선순위는 **명령줄 환경변수 > .env > .flaskenv**입니다. .flaskenv에는 FLASK_APP처럼 공개 가능한 CLI 설정을, .env에는 커밋하지 않을 비공개 값을 두는 방식으로 구분할 수 있습니다.
따라서 Flask CLI 개발 서버만 사용한다면 앱 코드에서 다시 load_dotenv()를 호출할 필요가 없습니다. 반면 Gunicorn 등으로 앱을 실행하는 운영 배포에서는 Flask CLI의 dotenv 로딩에 의존하지 말고, 배포 환경이 변수를 주입하도록 하거나 애플리케이션 시작 지점에서 명시적으로 로드해야 합니다. Flask 문서는 dotenv 파일이 flask 명령 또는 run() 호출에서만 자동 로드된다고 설명합니다.
Flask의 접두사 기반 설정 로더를 쓰면 환경변수 이름과 설정 키의 관계를 일정하게 유지할 수 있습니다.
# .env — Flask CLI가 개발 시 읽을 수 있는 비공개 설정 FLASK_SECRET_KEY=replace-with-a-local-development-secret
# app.py from flask import Flask def create_app() -> Flask: app = Flask(__name__) app.config.from_prefixed_env() if not app.config.get("SECRET_KEY"): raise RuntimeError("FLASK_SECRET_KEY must be configured") return app
app.config.from_prefixed_env()는 기본적으로 FLASK_ 접두사를 제거해 설정 키를 만듭니다. 예를 들어 FLASK_SECRET_KEY는 app.config["SECRET_KEY"]가 됩니다. 값은 기본적으로 JSON 파싱을 시도하므로 불리언은 true 또는 false처럼 소문자 JSON 값으로 작성해야 합니다. Flask의 디버그 모드는 설정 파일보다 flask --debug run 같은 CLI 옵션으로 제어하는 방식이 권장됩니다.
.env를 저장소와 운영 이미지에서 분리하기
.env는 암호화 파일이나 시크릿 관리 기능이 아닙니다. 비밀값을 담았다면 저장소에 포함하지 않고, 로컬 파일 접근 권한과 백업·공유 경로도 별도로 관리해야 합니다.
# .gitignore .env .env.* !.env.example
.env.example에는 키 이름과 안전한 예시 값만 넣고 실제 토큰·비밀번호·연결 문자열은 넣지 않습니다. 이미 Git이 추적 중인 .env는 .gitignore에 추가해도 계속 추적되므로, git rm --cached .env로 추적만 해제한 뒤 커밋해야 합니다.
운영 환경에서는 .env 파일을 이미지에 복사하는 방식보다 배포 플랫폼의 환경변수·시크릿 주입 기능 또는 조직의 중앙 시크릿 관리 체계를 우선 검토합니다. 여러 인스턴스에서 비밀값 교체, 접근 권한 분리, 감사 기록이 필요하다면 .env만으로 운영 절차를 해결하려 하지 말고 조직의 시크릿 관리 정책에 맞는 저장소를 사용합니다.
이미 .env를 커밋했다면
먼저 노출된 비밀값을 폐기하거나 교체해야 합니다. 파일을 삭제하거나 새 커밋으로 값을 지우는 것만으로는 기존 Git 기록의 값이 사라지지 않습니다. GitHub는 민감 데이터 제거 전에 자격 증명 교체를 우선하고, 필요 시 git-filter-repo로 이력을 재작성한 뒤 협업자의 기존 클론과 포크까지 조율해야 한다고 안내합니다. 이력 재작성과 강제 푸시는 협업 브랜치에 영향을 주므로 영향 범위를 확인한 뒤 진행하세요.
테스트에서는 전역 환경을 바꾸지 않고 값 읽기
테스트에서 단지 .env.test의 값을 확인하려는 목적이라면 load_dotenv(..., override=True)로 전역 환경을 바꾸기보다 dotenv_values()를 사용할 수 있습니다. 이 함수는 파일을 파싱한 딕셔너리를 반환하고 os.environ은 수정하지 않습니다.
from dotenv import dotenv_values config = dotenv_values(".env.test") assert config["DATABASE_URL"] == "sqlite:///:memory:"
애플리케이션이 os.environ을 직접 읽는 구조라면 테스트 프레임워크의 환경변수 패치 기능으로 필요한 키만 주입하고, 테스트가 끝난 뒤 원래 상태로 복원하세요. 이렇게 하면 개발자의 셸 환경이나 CI가 제공한 값을 테스트 파일이 바꾸는 일을 피할 수 있습니다.