[TIL] FastAPI 서비스를 Oracle Always Free VM에 배포하기(2)

RE_BROTHER·2026년 7월 28일

FastAPI-migration

목록 보기
5/7

FastAPI 마이그레이션을 진행한 뒤에는 애플리케이션을 실제 서버에서 실행해봐야 했다.

처음에는 Cloudflare에서 FastAPI 애플리케이션까지 실행하는 구조를 검토했다. 하지만 Cloudflare Container는 Workers Paid 플랜이 필요했고, 애플리케이션 실행 환경을 Cloudflare에 종속시키지 않으려는 기존 방향과도 완전히 맞지는 않았다.

그래서 실행 서버는 Oracle Cloud의 Always Free VM으로 분리하고, 데이터와 파일 저장소는 기존처럼 Cloudflare D1/R2를 사용하는 구조를 선택했다.

1. 최종적으로 선택한 구조

사용자 브라우저
    -> Cloudflare DNS / Proxy
    -> Oracle Always Free VM
    -> Docker Compose
    -> Uvicorn
    -> FastAPI
    -> Cloudflare D1 / R2

각 서비스의 역할은 다음처럼 분리했다.

  • Oracle VM: FastAPI 애플리케이션 실행
  • Docker: 실행 환경 패키징
  • Uvicorn: ASGI 서버
  • Cloudflare D1: 운영 데이터 저장
  • Cloudflare R2: 에셋과 문서 이미지 저장
  • Cloudflare DNS/Proxy: 도메인, HTTPS, 외부 접근 제어

FastAPI는 표준 Docker 이미지로 실행하므로 다른 VM이나 다른 클라우드로 옮겨도 같은 방식으로 배포할 수 있다.

2. Cloudflare Container 대신 Oracle VM을 선택한 이유

Cloudflare Container는 무료 Workers 플랜에서 사용할 수 없고 Workers Paid 플랜이 필요하다.

무료 티어를 우선하는 상황에서 Oracle Always Free VM은 다음 장점이 있었다.

  • FastAPI를 일반 Docker 애플리케이션으로 실행할 수 있음
  • Cloudflare Worker 전용 런타임에 종속되지 않음
  • 기존 D1/R2 연결을 유지할 수 있음
  • 나중에 다른 VM이나 클라우드로 옮기기 쉬움

최종 구조는 Oracle을 애플리케이션 실행 서버로만 사용하고, D1/R2는 저장소 서비스로 유지하는 방식이다.

3. VCN과 Subnet 구성

VM 생성 화면에서 네트워크를 함께 만들 수도 있지만, Public Subnet이 정확히 선택되지 않으면 공인 IPv4를 할당할 수 없다. 그래서 VCN을 먼저 만들었다.

Oracle Console에서 다음 메뉴로 이동했다.

Networking
  -> Virtual cloud networks
  -> Start VCN Wizard
  -> Create VCN with Internet Connectivity

사용한 CIDR 구성은 다음과 같다.

VCN CIDR Block:             10.0.0.0/16
Public Subnet CIDR Block:   10.0.1.0/24
Private Subnet CIDR Block:  10.0.2.0/24

처음에는 Private Subnet 기본값이 Public Subnet과 같은 10.0.1.0/24로 들어가 있어서 CIDR이 겹쳤다. 같은 VCN 안의 Subnet은 주소 범위가 겹치면 안 되므로 Private Subnet을 10.0.2.0/24로 변경했다.

최종 네트워크는 다음과 같다.

fastapi-vcn
  -> Public Subnet   10.0.1.0/24
  -> Private Subnet  10.0.2.0/24

VM은 외부 SSH와 웹 요청을 받아야 하므로 Public Subnet에 배치한다.

4. A1 인스턴스 생성 시도와 용량 부족

처음에는 Oracle Always Free의 Ampere A1 인스턴스를 사용하려고 했다.

Shape: VM.Standard.A1.Flex
OCPU: 1
Memory: 6GB
Image: Ubuntu

그러나 다음 오류가 발생했다.

Out of capacity for shape VM.Standard.A1.Flex in availability domain AD-1.

이 오류는 설정 오류나 결제 오류가 아니라 해당 Availability Domain의 호스트 용량 부족이다. 현재 계정의 Home Region에서는 AD-1만 표시되어 다른 Availability Domain으로 옮길 수도 없었다.

A1 용량이 언제 확보될지 알 수 없는 상태에서 기다리는 것보다 우선 서비스를 실행하기 위해 VM.Standard.E2.1.Micro를 생성했다.

Shape: VM.Standard.E2.1.Micro
Memory: 1GB

E2.1.Micro는 테스트와 소규모 실행용으로는 사용할 수 있지만 메모리와 CPU가 작다. 또한 E2.1.Micro는 나중에 A1.Flex로 단순 변경할 수 없다.

A1 용량이 확보되면 새 VM을 만들고 같은 Docker 이미지와 환경변수로 재배포해야 한다. D1/R2를 외부 저장소로 사용하고 있기 때문에 실행 VM을 교체해도 핵심 데이터는 유지된다.

5. Public IP와 네트워크 확인

VM 생성 시 다음을 선택했다.

Virtual cloud network: fastapi-vcn
Subnet: Public Subnet
Public IPv4 address: Automatically assign

VM 상세의 Detail 탭에는 IP 정보가 요약되어 보이지 않을 수 있다. 실제 IP는 다음 위치에서 확인할 수 있었다.

VM 상세 화면
  -> Networking 탭
  -> Primary VNIC
  -> Private IPs

공인 IP가 할당된 상태라면 Primary VNIC에 Private IP도 존재한다.

외부 접속에 필요한 보안 규칙도 추가했다.

TCP 22  SSH
TCP 80  HTTP
TCP 443 HTTPS

6. Windows에서 SSH 연결

Oracle에서 SSH Private Key를 다운로드한 뒤 Windows에서 접속하려고 하자 다음 오류가 발생했다.

WARNING: UNPROTECTED PRIVATE KEY FILE!
Permissions for 'ssh-key.key' are too open.

Windows 파일 권한에 Authenticated Users가 포함되어 있어서 OpenSSH가 개인 키를 거부한 것이다.

PowerShell에서 상속 권한을 제거하고 현재 사용자에게만 읽기 권한을 부여했다.

$key = "G:\Oracle_key\ssh-key.key"
icacls $key /inheritance:r
icacls $key /remove "Authenticated Users" "Users" "Everyone"
icacls $key /grant:r "$($env:USERNAME):(R)"

그 후 다음 명령으로 접속할 수 있었다.

ssh -i "G:\Oracle_key\ssh-key.key" ubuntu@<oracle-public-ip>

Windows에서는 별도의 SSH 프로그램보다 Windows Terminal의 기본 OpenSSH 클라이언트를 사용하는 편이 간단했다. 이후 원격 파일 편집이 필요하면 VS Code Remote-SSH를 사용할 수 있다.

7. E2.1.Micro에 Swap 추가

E2.1.Micro는 메모리가 1GB이므로 Docker 설치나 이미지 빌드 중 메모리 부족이 발생할 수 있다. 그래서 2GB Swap을 추가했다.

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h

Swap은 RAM을 대체하지 않지만 설치 과정에서 프로세스가 즉시 종료되는 상황을 완화할 수 있다.

8. Docker와 FastAPI 소스 설치

Ubuntu VM에 Docker, Compose plugin, Git을 설치했다.

sudo apt update
sudo apt install -y docker.io docker-compose-v2 git
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"

Docker 그룹 권한을 적용하기 위해 SSH 세션을 종료한 뒤 다시 접속했다.

exit

이후 마이그레이션 브랜치의 소스를 내려받았다.

mkdir -p ~/apps
cd ~/apps
git clone --branch feature/FastAPI-migration https://github.com/jhpark-jarvis/maple-life-docs.git
cd maple-life-docs

9. 운영 환경변수 설정

운영 환경변수는 저장소에 커밋하지 않고 서버에서 직접 작성한다.

cp .env.example .env
chmod 600 .env
nano .env

주요 설정은 다음과 같다.

SECRET_KEY=<long-random-secret>
DATABASE=/app/instance/app.db
UPLOAD_FOLDER=/app/uploads
MAX_CONTENT_LENGTH=20971520
DISPLAY_TIMEZONE=Asia/Seoul

REPOSITORY_BACKEND=d1
STORAGE_BACKEND=r2

CLOUDFLARE_ACCOUNT_ID=<cloudflare-account-id>
D1_DATABASE_ID=<d1-database-id>
CLOUDFLARE_API_TOKEN=<d1-runtime-api-token>

R2_BUCKET_NAME=<r2-bucket-name>
R2_ACCOUNT_ID=<cloudflare-account-id>
R2_ACCESS_KEY_ID=<r2-access-key-id>
R2_SECRET_ACCESS_KEY=<r2-secret-access-key>
R2_PUBLIC_BASE_URL=<r2-public-base-url>

FLASK_ENV=production
FLASK_DEBUG=0

처음 .env.example에는 다음과 같은 placeholder가 있었다.

DATABASE={}
UPLOAD_FOLDER={}

이 값은 실제 경로가 아니므로 컨테이너에서는 중괄호를 제거해야 한다. 잘못 입력하면 {/app/uploads} 전체가 경로로 해석되어 FastAPI가 시작되지 않는다.

10. Cloudflare D1 API Token

로컬 .env에는 CLOUDFLARE_API_TOKEN이 없었지만 로컬 D1 연결은 정상 동작했다. 로컬에서는 Wrangler 로그인 정보가 fallback으로 사용되고 있었기 때문이다.

하지만 Oracle VM에는 로컬 PC의 Wrangler 인증 파일이 없으므로 D1 REST API를 호출하려면 별도의 API Token이 필요하다.

Cloudflare에서 다음 권한으로 런타임 토큰을 만들었다.

Token name: oracle-fastapi-d1-runtime

Account
  -> D1
      -> Edit

Account Resources는 사용하는 Cloudflare Account 하나로 제한했다. Zone, DNS, Worker, R2 관리 권한은 추가하지 않았다.

R2는 Cloudflare API Token이 아니라 S3 호환 API용 Access Key와 Secret Access Key로 접근한다.

R2_ACCESS_KEY_ID=<r2-access-key-id>
R2_SECRET_ACCESS_KEY=<r2-secret-access-key>

따라서 D1 API Token과 R2 S3 인증키는 서로 다른 용도다.

11. Oracle Docker Compose 구성

Oracle 배포용 Compose 설정은 다음과 같다.

services:
  fastapi:
    build:
      context: ../..
      dockerfile: Dockerfile
    container_name: personal-service-fastapi
    restart: unless-stopped
    env_file:
      - ../../.env
    ports:
      - "127.0.0.1:8000:8000"
    volumes:
      - fastapi-instance:/app/instance

volumes:
  fastapi-instance:

외부 포트를 직접 공개하지 않고 127.0.0.1:8000에만 바인딩했다. 이후 Nginx가 80/443 요청을 받고 내부 FastAPI로 전달하게 된다.

/app/instance는 named volume으로 보존한다. D1이 운영 데이터의 원본이지만, SQLite shadow DB와 페이지뷰 로그가 컨테이너 재생성 때 사라지지 않도록 하기 위해서다.

컨테이너는 다음 명령으로 실행했다.

cp deployment/oracle/docker-compose.yml.example deployment/oracle/docker-compose.yml
docker compose -f deployment/oracle/docker-compose.yml up -d --build

12. 첫 실행 오류와 설정 로더 수정

처음 컨테이너를 실행했을 때 다음 오류가 발생했다.

RuntimeError: Directory '{/app/uploads}' does not exist

원인은 .env에 경로를 다음처럼 입력했기 때문이다.

UPLOAD_FOLDER={/app/uploads}

또한 기존 설정 로더는 기본 uploads/ 디렉터리는 만들고 있었지만 환경변수로 지정된 UPLOAD_FOLDER는 생성하지 않고 있었다.

이 문제를 해결하기 위해 설정 로더를 수정했다.

database = os.environ.get("DATABASE", str(instance_path / "app.db"))
upload_folder = os.environ.get("UPLOAD_FOLDER", str(default_upload_dir))

Path(database).parent.mkdir(parents=True, exist_ok=True)
Path(upload_folder).mkdir(parents=True, exist_ok=True)

서버의 .env도 다음처럼 수정했다.

sed -i 's|^DATABASE=.*|DATABASE=/app/instance/app.db|' .env
sed -i 's|^UPLOAD_FOLDER=.*|UPLOAD_FOLDER=/app/uploads|' .env

이후 최신 코드를 반영하고 컨테이너를 재생성했다.

git pull origin feature/FastAPI-migration
docker compose -f deployment/oracle/docker-compose.yml up -d --build --force-recreate

13. FastAPI와 D1 연결 확인

VM 내부에서 health endpoint를 호출했다.

curl http://127.0.0.1:8000/health

응답은 다음과 같았다.

{
  "ok": true,
  "service": "personal-service-fastapi",
  "repository_backend": "d1",
  "storage_backend": "r2",
  "database_configured": true,
  "database_role": "shadow"
}

database_roleshadow인 것은 오류가 아니다.

D1 = 운영 데이터 원본
SQLite = 공용 repository helper가 사용하는 로컬 shadow DB

D1 API가 실제로 동작하는지 문서 목록 API도 호출했다.

curl "http://127.0.0.1:8000/api/documents?per_page=1"

이 단계에서 다음 항목을 확인했다.

  • Docker 컨테이너 실행
  • Uvicorn 프로세스 시작
  • FastAPI ASGI 엔트리포인트 로딩
  • 환경변수 기반 설정 로딩
  • 로컬 shadow SQLite 생성
  • Cloudflare D1 API 연결
  • FastAPI health endpoint 응답

14. 현재 남은 작업

현재 컨테이너는 127.0.0.1:8000에만 열려 있어 외부 브라우저에서 직접 접근할 수 없다.

다음 순서로 운영 연결을 진행한다.

  1. Oracle VM에 Nginx 설치
  2. Nginx에서 80/443 -> 127.0.0.1:8000 reverse proxy 설정
  3. 가비아에서 구매한 도메인을 Cloudflare에 추가
  4. 가비아 네임서버를 Cloudflare 네임서버로 변경
  5. Cloudflare DNS에 Oracle VM 공인 IP를 A 레코드로 등록
  6. Cloudflare Origin Certificate 또는 공개 인증서 설정
  7. Cloudflare SSL/TLS를 Full (strict)로 설정
  8. Cloudflare Access 또는 별도 인증으로 서비스 보호

현재 FastAPI API에는 별도 로그인 인증이 없으므로 도메인을 바로 공개하기 전에 접근 제어를 추가해야 한다.

마무리

이번 작업을 통해 FastAPI 애플리케이션을 Oracle VM에서 Docker로 실행하고, Cloudflare D1 연결까지 확인했다.

A1 인스턴스 용량 부족으로 E2.1.Micro를 임시 실행 서버로 선택했지만, 애플리케이션은 Docker와 외부 저장소에 분리되어 있다. 나중에 A1 VM을 새로 만들더라도 같은 이미지와 환경변수로 재배포할 수 있다.

이번 단계에서 확인한 핵심은 다음과 같다.

  • FastAPI는 Cloudflare Worker 없이 일반 Docker 서버에서 실행 가능하다.
  • Oracle Always Free VM을 애플리케이션 실행 서버로 사용할 수 있다.
  • 로컬 Wrangler 인증과 서버용 D1 API Token은 구분해야 한다.
  • R2 업로드에는 Cloudflare API Token이 아니라 R2 S3 Access Key가 필요하다.
  • 컨테이너 환경에서는 환경변수 경로를 실제 디렉터리로 지정하고 생성해야 한다.
  • 실행 서버를 교체해도 D1/R2를 유지하면 애플리케이션 재배포가 단순해진다.

다음 글에서는 Nginx reverse proxy와 가비아 도메인, Cloudflare DNS, HTTPS 연결 과정을 정리할 예정이다.

profile
@github https://github.com/jhpark-jarvis

0개의 댓글