generated from ttipo/project-base
93147b88b7
- ClamAV 1.4.6 LTS 를 둘로 나눴다: clamd 는 백엔드와만 닿는 internal 네트워크(바깥 길 없음 · 서명 볼륨 읽기 전용), freshclam 은 자기 바깥 길에만. 비-root · cap_drop ALL. 운영은 늘 뜨고 개발은 profile clamav. clamd.conf 는 한 파일 한도 2047M 까지 끝까지 검사하고 암호 · 한도 초과를 판정으로 알린다(실측표는 mail/README). 검사 중에 죽으면 남던 임시 파일을 띄울 때 지운다. - nginx 를 포털 · 공개 내려받기(files.) 두 server 로 가른다(SHARE_PUBLIC_URL 과 CORS_ORIGIN 의 호스트 비교 — 같으면 포털이 함께 연다). 조각 PUT 17m, 받기는 버퍼링 · 압축 끔 + X-Accel-Buffering 넘김 + 머리글 버퍼 16k, limit_conn 은 X-Forwarded-For 마지막 칸으로 IP 마다 공개 10 · 앱 안 30, 전체 공개 400 · 앱 안 200. 받기 location 은 /api/ 블록 안의 대소문자를 가리지 않는 정규식이다(리뷰 — /FILES/ 같은 변형이 상한을 비켜 갔다). 접근 로그는 경로 모양과 43자 토큰 모양으로 토큰을 가린다. - 파일은 named volume <PROJECT_NAME>_files(개발도 — builder 단계가 /app/files 를 node 소유로 만든다), 백업하지 않는다. - CI infra 잡: nginx 두 꼴 · 받기 경로 변형 시험(가짜 백엔드) · clamconf(-n 없이 기본값까지) · 틀린 SHARE_PUBLIC_URL 거부. - mail/README: 악성 검사 · 공개 호스트 · NPM 칸(Advanced 다섯 줄) · 메모리 예산 · 문제 해결. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
214 lines
14 KiB
Bash
214 lines
14 KiB
Bash
# ==========================================
|
|
# 프로젝트 전역 설정
|
|
# ==========================================
|
|
PROJECT_NAME=project
|
|
APP_ENV=development
|
|
BUILD_MODE=development
|
|
FRONTEND_DIR=frontend
|
|
BACKEND_DIR=backend
|
|
|
|
# ==========================================
|
|
# 포트 설정
|
|
#
|
|
# 컨테이너 포트 매핑뿐 아니라 **호스트에서 띄우는 vite dev 서버**도 이 값을 따른다.
|
|
# (frontend/vite.config.ts 가 이 파일을 직접 읽는다 — FRONTEND_PORT 로 열고
|
|
# /api 프록시는 BACKEND_PORT 로 보낸다. 포트를 바꾸면 CORS_ORIGIN 도 함께 고칠 것)
|
|
# ==========================================
|
|
FRONTEND_PORT=5173
|
|
BACKEND_PORT=3000
|
|
WEBSOCKET_PORT=3001
|
|
DB_PORT=5432
|
|
REDIS_PORT=6379
|
|
|
|
# ==========================================
|
|
# 내부 네트워크 (compose app-network · edge)
|
|
#
|
|
# 서브넷을 고정한다 — 메일 엔진은 app-network 대역만 신뢰한다 (게이트웨이 로그인·자동 차단 예외, mail/README.md).
|
|
# APP_NETWORK_SUBNET : backend·db·redis·mail(·mailpit) — 사설 대역, /24 ~ /28 (mail setup 이 검사한다)
|
|
# EDGE_NETWORK_SUBNET : frontend ↔ backend 전용 (frontend 는 app-network 에 붙지 않는다)
|
|
# MAIL_RESOLVER_SUBNET : mail ↔ 메일 전용 DNS 리졸버(mail-resolver) 전용 네트워크 mail-dns — 바깥 길이 없는 internal (사설 대역, /24 ~ /29)
|
|
# MAIL_RESOLVER_IP : 그 안의 리졸버 주소(고정) — 엔진의 DNS 와 mail 컨테이너의 dns 가 이 주소다. 서브넷의 끝 쪽에 둔다
|
|
# (도커는 mail 에 앞쪽부터 차례로 준다). 둘은 함께 바꾸고 setup 을 다시 실행한다 (mail/README.md "DNS 리졸버와 스팸 차단 목록")
|
|
# 도커 기본 주소 풀(172.17~31.x, 192.168.x) 밖의 값이라 자동으로 배정되는 네트워크와 겹치지 않는다.
|
|
# 같은 호스트의 다른 프로젝트와 겹치면 up 이 "Pool overlaps" 로 실패한다 → 다른 /24 로 바꾸고 setup 을 다시 실행한다.
|
|
# ==========================================
|
|
APP_NETWORK_SUBNET=172.16.71.0/24
|
|
EDGE_NETWORK_SUBNET=172.16.72.0/24
|
|
MAIL_RESOLVER_SUBNET=172.16.73.0/24
|
|
MAIL_RESOLVER_IP=172.16.73.254
|
|
|
|
# ==========================================
|
|
# Database 설정
|
|
# ==========================================
|
|
DB_IMAGE=postgres:15-alpine
|
|
DB_USER=app_user
|
|
DB_PASSWORD=change-me-in-local-env
|
|
DB_NAME=project_db
|
|
DB_DATA_DIR=./data/postgres
|
|
|
|
# ==========================================
|
|
# Redis 설정
|
|
# ==========================================
|
|
REDIS_IMAGE=redis:7-alpine
|
|
REDIS_PASSWORD=change-me-in-local-env
|
|
REDIS_DATA_DIR=./data/redis
|
|
|
|
# ==========================================
|
|
# 화면·API 주소 — 같은 주소(origin) 배포 (docs/mail/api-contract.md 머리말)
|
|
#
|
|
# 브라우저는 화면과 같은 주소의 /api 를 부른다 (개발은 vite 프록시, 운영은 프론트 nginx 가 백엔드로 넘긴다).
|
|
# - CORS_ORIGIN : 화면의 주소(origin). 상태 변경 요청의 출처 검사에 쓴다 — FRONTEND_PORT 를 바꾸면 함께 고칠 것
|
|
# - BACKEND_API_URL : 화면 주소 + /api. 앱은 쓰지 않고, 운영 기동 때 공개 주소인지 검사하는 데만 쓴다
|
|
# - TRUST_PROXY : 백엔드 앞 프록시 홉 수. 개발은 vite 프록시 하나라 1 (비우면 1)
|
|
# ==========================================
|
|
BACKEND_API_URL=http://localhost:5173/api
|
|
CORS_ORIGIN=http://localhost:5173
|
|
TRUST_PROXY=1
|
|
|
|
# ==========================================
|
|
# Rate Limiting (API 남용 방지)
|
|
# 미설정 시 백엔드 기본값(60초 / 100회) 사용
|
|
# ==========================================
|
|
THROTTLE_TTL_MS=60000
|
|
THROTTLE_LIMIT=100
|
|
|
|
# ==========================================
|
|
# DB 스키마 동기화 (개발 전용, 기본 false)
|
|
# true 로 켜면 엔티티 변경이 DB 에 자동 반영되어 빠르게 실험할 수 있지만,
|
|
# 그 상태에서는 migration:generate 가 "변경 없음"만 내놓는다.
|
|
# 운영으로 갈 코드라면 꺼둔 채 마이그레이션으로 관리할 것.
|
|
# (운영 환경에서는 이 값과 무관하게 항상 비활성화된다)
|
|
# ==========================================
|
|
DB_SYNCHRONIZE=false
|
|
|
|
# ==========================================
|
|
# 선택적 서비스 (Compose profiles)
|
|
#
|
|
# 기본값 = mail — 메일 엔진(Stalwart)과 개발용 Mailpit 을 띄운다.
|
|
# 직원 로그인이 메일 엔진에 의존하므로 개발에서도 켜 두는 것을 권장한다.
|
|
# (redis 와 frontend 컨테이너는 기동하지 않는다)
|
|
#
|
|
# mail 메일 엔진 + Mailpit (기본) → COMPOSE_PROFILES=mail
|
|
# redis 캐시·세션 공유가 필요해질 때 → COMPOSE_PROFILES=mail,redis
|
|
# frontend vite 를 컨테이너로 띄워야 할 때 → COMPOSE_PROFILES=mail,frontend
|
|
# clamav 대용량 첨부의 악성 검사기(ClamAV — 메모리 1GB 남짓 · 처음에 서명 110MB 를 받는다)
|
|
# → COMPOSE_PROFILES=mail,clamav 와 함께 아래 "대용량 첨부" 의 CLAMAV_HOST=clamav (운영은 profile 없이 늘 뜬다)
|
|
#
|
|
# 일회성으로 켜려면: docker compose --profile redis ... up -d
|
|
#
|
|
# ⚠️ 개발 환경의 프론트엔드는 호스트에서 띄우는 것이 기본이다 (`cd frontend && npm run dev`).
|
|
# 컨테이너 vite 는 바인드 마운트 때문에 첫 서빙까지 수 분이 걸린다.
|
|
# ==========================================
|
|
COMPOSE_PROFILES=mail
|
|
|
|
# ==========================================
|
|
# 파일 업로드 (로컬 디스크)
|
|
#
|
|
# UPLOAD_DATA_DIR 는 개발 전용 — 호스트 디렉터리에 바로 쌓여 열어 볼 수 있다 (운영은 named volume <PROJECT_NAME>_uploads).
|
|
# 리눅스 호스트는 처음 한 번 mkdir -p data/uploads (없으면 도커가 root 소유로 만들어 backend 가 쓰지 못한다)
|
|
# ==========================================
|
|
UPLOAD_DATA_DIR=./data/uploads
|
|
UPLOAD_MAX_SIZE_MB=5
|
|
|
|
# ==========================================
|
|
# 대용량 첨부 (11단계 — docs/mail/api-contract.md 4절, mail/README.md "악성 검사 (ClamAV)")
|
|
#
|
|
# 일반 첨부 한도(아래 MAIL_ATTACHMENT_*)를 넘는 파일은 메일 엔진이 아니라 백엔드의 볼륨(<PROJECT_NAME>_files — 개발도 named volume)에 올리고,
|
|
# 메일에는 받는 사람이 로그인 없이 여는 내려받기 링크를 넣는다. 아래 값은 백엔드 기본값과 같다 — 비워 두어도 그 값이다.
|
|
# SHARE_PUBLIC_URL : 공개 내려받기 화면의 출처(스킴 · 호스트[· 포트] — 경로 · 끝의 / 없이). 개발은 비운다 = CORS_ORIGIN 의 첫 주소(포털과 같은 호스트).
|
|
# 운영은 포털과 다른 호스트다(.env.production.example)
|
|
# SHARE_REPORT_ADDRESS : 공개 화면의 신고 · 문의 주소 (비우면 postmaster@<MAIL_DOMAIN>)
|
|
# SHARE_DOWNLOAD_LIMIT : 파일마다 내려받기 상한(받기 시작한 수, 1~100000)
|
|
# MAIL_LARGE_FILE_MAX_SIZE_MB : 한 파일(MB, 1~2047 — 악성 검사기가 끝까지 검사할 수 있는 2GiB 아래, MAIL_ATTACHMENT_MAX_SIZE_MB 이상)
|
|
# MAIL_LARGE_FILE_MAX_COUNT : 메일 한 통에 새로 싣는 대용량 파일 수(1~20)
|
|
# MAIL_LARGE_FILE_USER_QUOTA_GB : 직원마다 동시에 보관하는 양(GB, 1~1000 — 올리는 중 · 임시 저장 포함)
|
|
# MAIL_LARGE_FILE_RETENTION_DAYS : 보낸 날(한국)부터 받을 수 있는 날 수(1~90)
|
|
# FILE_STORAGE_MAX_TOTAL_GB : 서버 전체 보관량(GB). 비우면 한도 없음 — 디스크 여유만 본다
|
|
# FILE_STORAGE_MIN_FREE_GB : 올리기를 만든 뒤에도 남길 디스크 여유(GB) — 개발은 Docker Desktop 가상 디스크의 여유를 본다
|
|
# CLAMAV_HOST · CLAMAV_PORT : 악성 검사기(clamd). 개발은 비워 둔다 = 검사하지 않고 다 올리자마자 "검사하지 못함".
|
|
# 켜려면 COMPOSE_PROFILES=mail,clamav 와 함께 CLAMAV_HOST=clamav (compose 의 clamav 서비스, 포트 3310)
|
|
# ==========================================
|
|
SHARE_PUBLIC_URL=
|
|
SHARE_REPORT_ADDRESS=
|
|
SHARE_DOWNLOAD_LIMIT=500
|
|
MAIL_LARGE_FILE_MAX_SIZE_MB=2047
|
|
MAIL_LARGE_FILE_MAX_COUNT=10
|
|
MAIL_LARGE_FILE_USER_QUOTA_GB=10
|
|
MAIL_LARGE_FILE_RETENTION_DAYS=30
|
|
FILE_STORAGE_MAX_TOTAL_GB=
|
|
FILE_STORAGE_MIN_FREE_GB=10
|
|
CLAMAV_HOST=
|
|
CLAMAV_PORT=3310
|
|
|
|
# ==========================================
|
|
# 메일 엔진 (Stalwart) — profile `mail`
|
|
#
|
|
# 직원 로그인 = 메일 계정. 백엔드는 내부 JMAP 으로 메일 엔진에 붙는다.
|
|
# 처음 띄운 뒤 한 번 실행한다 (여러 번 실행해도 안전하다):
|
|
# node --env-file=.env.development mail/scripts/setup.mjs
|
|
# 자세한 내용: mail/README.md
|
|
#
|
|
# 개발 도메인은 외부와 섞이지 않는 예약 TLD(.test)를 쓴다.
|
|
# ⚠️ 약한 비밀번호는 Stalwart 의 강도 정책이 거부한다. 복사 후 무작위 값으로 교체할 것.
|
|
# ⚠️ 비밀번호는 영문·숫자로만 만든다. # · 따옴표 · $ · 공백은 compose 와 Node(--env-file)가
|
|
# 서로 다르게 읽어(Node 는 # 부터 주석으로 자른다) 컨테이너와 setup 스크립트의 값이 어긋난다.
|
|
# ==========================================
|
|
# 패치까지 고정한다 — setup 스크립트가 0.16 의 동작(관리 API·bootstrap)에 맞춰져 있다 (docker 스킬 "이미지 버전 고정")
|
|
MAIL_IMAGE=stalwartlabs/stalwart:v0.16.27
|
|
MAIL_DOMAIN=arasystem.test
|
|
# 서버 호스트명 — 최초 bootstrap 때만 반영된다 (이후 변경은 관리 화면에서)
|
|
MAIL_HOSTNAME=mail.arasystem.test
|
|
# 관리 화면·setup 스크립트용 HTTP 포트 (127.0.0.1 로만 열린다)
|
|
MAIL_ADMIN_PORT=8180
|
|
# JMAP·OAuth 디스커버리 기준 주소. 개발은 관리 화면 주소 — MAIL_ADMIN_PORT 를 바꾸면 함께 고칠 것
|
|
MAIL_PUBLIC_URL=http://localhost:8180
|
|
# 영구 관리자 아이디 (주소는 <이 값>@MAIL_DOMAIN, 미설정 시 admin) — setup 스크립트가 이 이름으로 관리자를 만들고,
|
|
# 백엔드는 이 계정의 웹메일 로그인·대리 접근을 막는다 (두 쪽이 이 값 하나를 읽는다).
|
|
# ⚠️ bootstrap 전에만 정할 수 있다. bootstrap 뒤에 이 값만 바꾸면 엔진 관리자는 그대로 남고 백엔드는 새 이름만 막게 되므로,
|
|
# setup 이 중단한다 (되돌리는 절차: mail/README.md "문제 해결"). 옮겨 올 메일함에 같은 주소가 있으면 다른 이름으로 정한다.
|
|
# 영문·숫자·. _ - 만, 앞뒤는 영문·숫자, 64자 이하 (백엔드와 같은 규칙)
|
|
MAIL_ADMIN_LOCAL_PART=admin
|
|
# 영구 관리자 비밀번호 — setup 스크립트가 이 값으로 맞춘다
|
|
MAIL_ADMIN_PASSWORD=change-me-in-local-env
|
|
# 최초 기동(bootstrap)용 임시 관리자 "아이디:비밀번호". ⚠️ 값이 있는 동안은 정상 모드에서도 관리자로 로그인된다
|
|
# (개발은 127.0.0.1 에만 열려 있어 이 파일에 둔다 — 운영은 파일에 두지 않고 일회성 override 로만 쓴다)
|
|
MAIL_RECOVERY_ADMIN=admin:change-me-in-local-env
|
|
# 백엔드가 직원 메일함을 대리 접근(impersonate)할 때 쓰는 서비스 계정 (주소 전체).
|
|
# ⚠️ 이 비밀번호는 전 직원 메일함의 열쇠다
|
|
MAIL_GATEWAY_USER=mail-gateway@arasystem.test
|
|
MAIL_GATEWAY_PASSWORD=change-me-in-local-env
|
|
# 사내 목록 계정 아이디 (주소는 <이 값>@MAIL_DOMAIN, 미설정 시 mail-directory) — 백엔드가 엔진의 계정 목록에서 직원 이름·주소만 읽을 때
|
|
# 게이트웨이 대리 접근으로 쓰는 전용 계정이다. 비밀번호가 없고, setup 스크립트가 복구 관리자(위 MAIL_RECOVERY_ADMIN)로 만든다.
|
|
# 관리자·게이트웨이 아이디와 달라야 한다. 바꾸면 setup 을 다시 실행하고 backend 를 다시 띄운다 (mail/README.md "사내 목록 계정").
|
|
# 영문·숫자·. _ - 만, 앞뒤는 영문·숫자, 64자 이하 (MAIL_ADMIN_LOCAL_PART 와 같은 규칙)
|
|
MAIL_DIRECTORY_LOCAL_PART=mail-directory
|
|
# 백엔드가 쓰는 내부 JMAP 주소 (compose 내부망). 보통 바꿀 일이 없다
|
|
MAIL_JMAP_URL=http://mail:8080
|
|
# 첨부 1개 최대 크기(MB, 미설정 시 20). 프론트 nginx 의 본문 한도가 이 값 + 1MB 로 따라간다
|
|
# (그 앞에 외부 리버스 프록시를 두면 그쪽 한도도 맞춘다 — docker 스킬 "운영 환경")
|
|
MAIL_ATTACHMENT_MAX_SIZE_MB=20
|
|
# 메일 한 통의 첨부 합계(MB, 미설정 시 25) — 파일당 한도 이상이어야 한다.
|
|
# 바꾸면 mail setup 도 다시 실행한다 — 엔진의 임시 업로드 한도를 이 값의 20배로 맞춘다 (mail/README.md "엔진 한도")
|
|
MAIL_ATTACHMENT_TOTAL_MAX_SIZE_MB=25
|
|
# 게이트웨이 계정을 쓸 수 있고 자동 차단에서 빠지는 대역 (콤마 구분).
|
|
# 비우면 APP_NETWORK_SUBNET 과 같다. 그 안의 더 좁은 대역만 쓸 수 있다 — 더 넓으면 setup 이 실패한다
|
|
MAIL_TRUSTED_NETWORKS=
|
|
# (선택) 웹메일 로그인·대리 접근을 막을 계정 — 주소 전체를 콤마로. 비어 있어도 위 관리자(MAIL_ADMIN_LOCAL_PART)는 항상 막힌다.
|
|
# 관리자를 더 만들었다면 여기에 넣는다 (setup 이 빠진 관리자 계정을 [주의] 로 알려 준다)
|
|
# MAIL_BLOCKED_ACCOUNTS=ops@arasystem.test
|
|
# 시범 공존 — 한 사람만 새 서버를 먼저 쓰고 나머지 직원은 기존 서버(mailcow)에 남을 때 (mail/README.md "파일럿 공존"). 비우면 꺼짐.
|
|
# 개발은 비워 두고, 시험할 때만 셸에서 일회성으로 준다 (기존 서버 역 = Mailpit — 개발에서는 셸 값을 일부러 받는다):
|
|
# MAIL_PILOT_LEGACY_HOST=mailpit:1025 node --env-file=.env.development mail/scripts/setup.mjs
|
|
# 값 없이 다시 실행하면 setup 이 시범 설정을 지운다. 기존 서버 주소 — host · host:port · [host]:port (포트 기본 25)
|
|
MAIL_PILOT_LEGACY_HOST=
|
|
# 기존 서버가 이 서버에 붙을 때의 IPv4 · CIDR (콤마). 개발은 비우면 백엔드 컨테이너 주소(개발의 시험 SMTP 가 나가는 곳)
|
|
MAIL_PILOT_LEGACY_IPS=
|
|
# Spamhaus 조회 키 (선택 — mail/README.md "DNS 리졸버와 스팸 차단 목록"). 키는 운영 서버의 .env.production 에만 둔다 — 개발은 비워 둔다.
|
|
# 비어 있으면 setup 이 스팸 필터의 Spamhaus 두 항목(ZEN · DBL)을 꺼 둔다 — 개발도 운영과 같다(공개 미러는 소규모 · 비상업 용도다)
|
|
MAIL_SPAMHAUS_DQS_KEY=
|
|
# 개발용 테스트 직원 계정(ara.kim@ 등)의 공통 비밀번호 — 개발 환경에서만 만들어진다
|
|
MAIL_DEV_USER_PASSWORD=change-me-in-local-env
|
|
# 개발 전용 메일 수집기 — 사내 도메인 밖으로 보낸 메일이 여기에만 쌓인다
|
|
MAILPIT_IMAGE=axllent/mailpit:v1.32.0
|
|
MAILPIT_UI_PORT=8025
|