셀프호스팅 배포
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.yml에 Caddy 서비스가 정식 포함되어 있어 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 /metrics | Prometheus 포맷 메트릭(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_ENABLED이 true이고 client id·authorize/token/userinfo URL이 모두 채워져야 활성화됩니다.
로그인 흐름은 GET /api/v1/auth/oauth/login → 프로바이더 → GET /api/v1/auth/oauth/callback입니다.
백업 / 복원
Postgres 볼륨(db_data)에 텔레메트리·설정이 전부 있습니다. 저장소에 백업/복원 스크립트가
포함되어 있습니다.
scripts/backup.sh—pg_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를 참고하세요.