Skip to main content
Version: LinkBoard v1.0.110

셀프호스팅 배포

LinkBoard는 처음부터 Docker Compose로 설계되어 있고 Kubernetes/클라우드 전용 매니페스트는 없습니다. 단일 서버(VM)에 Docker Compose + 리버스 프록시로 배포하는 것이 현재 구조에 가장 맞는 경로입니다.

현재 상태 요약

아직 구현되지 않은 부분은 있는 척 설명하지 않습니다. 실제 서버에 올리기 전 아래 표를 먼저 확인하세요.

구성요소상태
로컬 개발(Docker Compose)✅ 완비
백엔드 프로덕션 실행형태✅ 완비
프론트엔드 프로덕션 실행형태✅ 완비 (Nginx 정적 서빙 + /api,/ws 리버스프록시)
DB 마이그레이션 자동화🟡 CI에서 검증만 함(up/down 왕복) — 실배포 시 수동 실행 필요
CI(테스트)✅ 완비 — ruff + 마이그레이션 왕복 검증 + pytest
CD(자동 배포)🟡 부분 — main 푸시 + 테스트 통과 시 백엔드/프론트 이미지를 GHCR로 자동 빌드·푸시. 서버 반영은 수동(update.sh 또는 레지스트리 pull)
리버스 프록시 / TLS✅ 완비 — docker-compose.prod.yml에 Caddy 서비스 포함(자동 HTTPS/Let's Encrypt)
EMQX 인증🟡 옵트인 — 기본은 익명 허용, 프로덕션에서는 반드시 활성화 필요
헬스체크·메트릭/health·/health/ready·/metrics(Prometheus) 엔드포인트 노출
모니터링 스택(Prometheus/Grafana)✅ 옵트인 — docker-compose.monitoring.yml로 제공(대시보드·알람 룰 프로비저닝 포함). Alertmanager는 후속
수평 확장(워커 분리)✅ 지원 — API/워커 분리 + MQTT 공유 구독 + 스케줄러 분산 락
백업✅ 완비 — scripts/backup.sh(pg_dump + 보관 회전 + 오프사이트) · scripts/restore.sh(복원 전 안전 백업)

사전 준비

  • Docker Engine + Docker Compose v2가 설치된 서버 (Linux 권장)
  • 도메인 + DNS A 레코드 (리버스 프록시/TLS용)
  • .env 파일을 서버에서 직접 생성 (git에 커밋하지 않습니다)

.env.example 대비 프로덕션에서 반드시 바꿔야 하는 값:

APP_ENV=production
SECRET_KEY=<32자 이상 랜덤값> # 기본값 그대로 쓰면 안 됩니다
POSTGRES_PASSWORD=<강력한 값>
DATABASE_URL=postgresql+asyncpg://linkboard:<위 값>@db:5432/linkboard
EMQX_DASHBOARD_PASSWORD=<기본값에서 반드시 변경>
MQTT_PASSWORD=<백엔드용 MQTT 슈퍼유저 비밀번호>
API_DOCS_ENABLED=false # 프로덕션에서 Swagger/ReDoc 노출 여부(기본 비노출)

배포 절차

git pull origin main
docker compose -f docker-compose.prod.yml build backend frontend
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml exec backend alembic upgrade head # 반드시 기동 후 실행

최초 1회만 seed_admin.py로 SystemAdmin 계정을 생성하고, 평문 비밀번호는 시딩 직후 바로 변경하세요.

이후 업데이트는 scripts/update.sh 한 번으로 처리할 수 있습니다 — 내부적으로 ① DB 백업 → ② git pull → ③ 이미지 재빌드·재기동 → ④ 마이그레이션 적용 순으로 진행합니다. CI가 GHCR에 푸시한 이미지를 pull 받아 배포하는 방식(즉시 롤백 가능)도 저장소의 docs/DEPLOYMENT.md에 안내되어 있습니다.

리버스 프록시 + TLS (Caddy 통합)

docker-compose.prod.ymlCaddy 서비스가 정식 포함되어 있어 80/443 TLS 종단과 자동 HTTPS(Let's Encrypt)를 기본으로 제공합니다. frontend는 호스트 포트를 열지 않고 내부망으로만 노출되며, Caddy가 정적 서빙 프록시와 /api, /ws 백엔드 프록시를 담당합니다.

  • infra/caddy/Caddyfile에서 도메인만 바꾸고 DNS A 레코드 + 방화벽 80·443만 열면 됩니다.
  • 발급된 인증서는 caddy_data 볼륨에 영속되어 재시작 시 재발급되지 않습니다.
  • 별도 리버스 프록시(Nginx 등)를 직접 쓰려면 Caddy 서비스를 제거하고 frontend를 직접 노출하도록 바꿀 수 있습니다.
  • EMQX 대시보드(18083)·MQTT(1883)는 필요할 때만 외부에 노출하세요 (가급적 방화벽으로 내부망 한정).

EMQX 인증 활성화 (프로덕션 필수)

개발 기본값은 익명 MQTT 연결을 허용합니다. 프로덕션에서는 반드시 HTTP auth 콜백을 켜야 합니다 — 그렇지 않으면 누구나 인증 없이 MQTT 브로커에 연결해 임의 장치로 텔레메트리를 주입할 수 있습니다. 활성화 절차는 저장소의 infra/emqx/README.md를 참고하세요.

헬스체크 & 관측성

백엔드는 아래 엔드포인트를 앱 루트(/api/v1 하위 아님)에 노출합니다.

엔드포인트용도
GET /health라이브니스. 의존성 확인 없이 항상 빠르게 200 + {status, app, env, version} 반환.
GET /health/ready레디니스. DB·Redis·EMQX를 병렬 핑. 준비되면 200, 아니면 503 + 항목별 up/down. (DB·Redis 필수, EMQX는 정보용)
GET /metricsPrometheus 포맷 메트릭(linkboard_http_requests_total, linkboard_http_request_duration_seconds).

모든 응답에 X-Request-ID 헤더가 실리며(요청에 있으면 그대로 사용), 로그에도 함께 기록되어 요청 추적이 가능합니다.

Prometheus + Grafana 스택은 옵트인 compose 파일(docker-compose.monitoring.yml)로 제공됩니다. 스크레이프 설정·Grafana 대시보드·알람 룰이 미리 프로비저닝되어 있으며, 포트는 루프백에만 바인드되므로 원격에서 볼 때는 SSH 터널 또는 인증이 붙은 리버스 프록시를 사용하세요.

docker compose -f docker-compose.prod.yml -f docker-compose.monitoring.yml up -d

로드밸런서 헬스체크는 /health(라이브니스) 또는 /health/ready(레디니스)를 사용하세요.

수평 확장 (워커 분리)

백엔드는 HTTP/WS 요청 처리(API)와 백그라운드 처리(MQTT 구독·스케줄러·Rule 스트림)를 함께 수행합니다. 단일 컨테이너에서는 RUN_BACKGROUND_WORKERS=true(기본값)로 한 프로세스가 둘 다 맡습니다.

부하가 늘면 API를 무상태로 확장하고 백그라운드 처리를 전용 워커로 분리합니다.

# API 컨테이너: 백그라운드 비활성화 → 무상태, 수평 확장 대상
RUN_BACKGROUND_WORKERS=false

# 전용 워커 컨테이너
python -m app.worker

# API 컨테이너 스케일아웃
docker compose -f docker-compose.prod.yml up -d --scale backend=3

중복 처리는 다음으로 방지합니다.

  • MQTT 공유 구독 ($share/{group}/…, MQTT_SHARED_SUBSCRIPTION=true) — 한 메시지를 워커 하나만 처리
  • 스케줄러 분산 락 (lock:scheduler:tick, Redis) — 매 tick을 한 인스턴스만 실행
  • Rule 스트림 소비자 그룹 (Redis Stream) — 룰 처리 부하 분산

단일 API 프로세스를 멀티 워커로 돌리려면 uvicorn 대신 gunicorn을 고려할 수 있습니다.

gunicorn -k uvicorn.workers.UvicornWorker -w 4 app.main:app -b 0.0.0.0:8000

API Rate Limit

/api/v1/ 요청에는 Redis 슬라이딩 윈도우 rate limit이 적용됩니다(Redis 장애 시 fail-open). 기본값:

RATE_LIMIT_ENABLED=true
RATE_LIMIT_REQUESTS=600 # 윈도우당 허용 요청 수
RATE_LIMIT_WINDOW_SECONDS=60 # 윈도우 길이(초)

초과 시 429와 함께 Retry-After·X-RateLimit-Limit·X-RateLimit-Remaining 헤더가 반환됩니다.

OAuth2 / SSO 로그인

범용 OAuth2 Authorization Code 프로바이더로 SSO 로그인을 붙일 수 있습니다(단일 제너릭 프로바이더; SAML은 미지원). 프로바이더 정보는 DB가 아니라 **환경변수(Settings)**로 설정합니다.

OAUTH_ENABLED=true
OAUTH_CLIENT_ID=<발급받은 client id>
OAUTH_CLIENT_SECRET=<client secret>
OAUTH_AUTHORIZE_URL=<프로바이더 authorize 엔드포인트>
OAUTH_TOKEN_URL=<프로바이더 token 엔드포인트>
OAUTH_USERINFO_URL=<프로바이더 userinfo 엔드포인트>
OAUTH_REDIRECT_URI=<https://your-domain/api/v1/auth/oauth/callback>
OAUTH_SCOPE=openid email profile # 기본값
OAUTH_AUTO_PROVISION=false # true면 최초 로그인 시 사용자 자동 생성

OAUTH_ENABLEDtrue이고 client id·authorize/token/userinfo URL이 모두 채워져야 활성화됩니다. 로그인 흐름은 GET /api/v1/auth/oauth/login → 프로바이더 → GET /api/v1/auth/oauth/callback입니다.

백업 / 복원

Postgres 볼륨(db_data)에 텔레메트리·설정이 전부 있습니다. 저장소에 백업/복원 스크립트가 포함되어 있습니다.

  • scripts/backup.shpg_dump | gzip으로 타임스탬프 백업을 만들고 무결성·최소 크기를 검증합니다. 보관 개수 회전과 오프사이트 업로드(BACKUP_REMOTE)를 지원하며, cron에 걸어 자동화할 수 있습니다.
  • scripts/restore.sh — 복원 전 현재 상태를 자동으로 안전 백업한 뒤, 앱 정지 → 스키마 재생성 → 복원 순으로 진행합니다 (⚠️ 파괴적 작업, 확인 프롬프트 있음).
./scripts/backup.sh
# cron 예시: 매일 03:10 백업 + S3 오프사이트
10 3 * * * cd /home/ubuntu/linkboard && BACKUP_REMOTE=s3:bkt/linkboard ./scripts/backup.sh >> /var/log/linkboard-backup.log 2>&1

db_data · emqx_data · redis_data는 named volume이므로 서버 디스크 자체의 백업(또는 볼륨 스냅샷)도 함께 고려하세요. 시점 복구(PITR)가 필요하면 WAL 아카이빙 구성은 저장소의 docs/DEPLOYMENT.md를 참고하세요.