Skip to content

Commit fe9d46c

Browse files
committed
fix: 포기
1 parent cef4db9 commit fe9d46c

13 files changed

Lines changed: 580 additions & 19 deletions

File tree

‎.dockerignore‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.git
2+
.github
3+
.venv
4+
venv
5+
__pycache__
6+
*.py[cod]
7+
8+
# 로컬 개발용 DB/미디어는 이미지에 넣지 않는다.
9+
# 서버에서는 named volume에 별도로 보관된다.
10+
db.sqlite3
11+
media/
12+
staticfiles/
13+
data/
14+
15+
# 시크릿은 이미지에 절대 굽지 않는다. EC2의 .env를 런타임에 주입한다.
16+
.env
17+
*.env
18+
!.env.example
19+
20+
.idea
21+
.vscode
22+
docs
23+
README.md

‎.env.example‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
# 로컬 개발용 예시. 이 파일을 .env로 복사해서 값을 채운다.
2+
# 아래 DJANGO_* 값들은 로컬에서는 비워둬도 된다(기본값이 개발용으로 잡혀 있음).
3+
# EC2의 ~/heartmark/.env 에는 반드시 채워야 한다.
4+
5+
# --- 배포 환경에서만 필요한 값 ---
6+
# 서버용 시크릿 키. 아래 명령으로 새로 만들어 쓸 것:
7+
# python -c "import secrets; print(secrets.token_urlsafe(64))"
8+
# Django의 get_random_secret_key()를 써도 되지만 #, $, ' 같은 문자가 섞일 수 있다.
9+
# 이 .env는 docker compose가 변수 치환에도 사용하므로 그런 문자는 값이 깨질 수 있어
10+
# 영숫자와 -_ 만 나오는 token_urlsafe를 권장한다.
11+
DJANGO_SECRET_KEY=
12+
# EC2 퍼블릭 IP나 도메인. 콤마로 여러 개 지정 가능.
13+
DJANGO_ALLOWED_HOSTS=
14+
# 스킴을 포함해야 한다. 예: http://12.34.56.78
15+
DJANGO_CSRF_TRUSTED_ORIGINS=
16+
17+
# --- 사이트/소셜 로그인 ---
18+
# 배포 시에는 EC2 주소로 바꿔야 소셜 로그인 redirect가 맞는다.
119
SITE_DOMAIN=127.0.0.1:8000
220
SITE_NAME=HeartMark Local
321
GOOGLE_CLIENT_ID=
@@ -7,5 +25,6 @@ NAVER_CLIENT_SECRET=
725
KAKAO_CLIENT_ID=
826
KAKAO_CLIENT_SECRET=
927

28+
# --- 카카오맵 ---
1029
KAKAO_REST_KEY=
11-
KAKAO_JS_KEY=
30+
KAKAO_JS_KEY=

‎.gitattributes‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Windows에서 작업해도 shell 스크립트는 항상 LF로 유지한다.
2+
# CRLF가 섞이면 리눅스 컨테이너에서 shebang이 깨져
3+
# "entrypoint.sh: no such file or directory" 같은 엉뚱한 에러가 난다.
4+
*.sh text eol=lf
5+
6+
# 컨테이너 안에서 읽히는 설정 파일들도 마찬가지.
7+
Dockerfile text eol=lf
8+
*.conf text eol=lf
9+
*.yml text eol=lf
10+
*.yaml text eol=lf

‎.github/workflows/deploy.yml‎

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,69 @@ on:
44
push:
55
branches:
66
- staging # staging 브랜치에 push할 때 실행
7+
workflow_dispatch: # Actions 탭에서 수동 실행도 가능하게 함
78

89
jobs:
910
deploy:
10-
runs-on: ubuntu-latest # GitHub Actions 실행 환경
11+
runs-on: ubuntu-latest
1112

1213
steps:
1314
- name: 코드 체크아웃
14-
uses: actions/checkout@v3
15+
uses: actions/checkout@v4
1516

1617
- name: Docker Hub 로그인
17-
uses: docker/login-action@v2
18+
uses: docker/login-action@v3
1819
with:
1920
username: ${{ secrets.DOCKER_USERNAME }}
2021
password: ${{ secrets.DOCKER_PASSWORD }}
2122

2223
- name: Docker 이미지 빌드 및 push
24+
# docker-compose.yml의 ${DOCKER_USERNAME}는 runner의 환경변수를 읽는다.
25+
# login-action은 로그인만 할 뿐 이 값을 심어주지 않으므로 직접 넣어야 한다.
26+
env:
27+
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
2328
run: |
2429
docker compose build
2530
docker compose push
2631
32+
- name: 배포 파일 EC2로 전송
33+
# EC2에는 소스가 없으므로 compose 파일과 nginx 설정을 매 배포마다 보내준다.
34+
# .env는 서버에서 직접 관리하며 여기서 덮어쓰지 않는다.
35+
uses: appleboy/scp-action@v0.1.7
36+
with:
37+
host: ${{ secrets.EC2_HOST }}
38+
username: ubuntu
39+
key: ${{ secrets.EC2_KEY }}
40+
source: "docker-compose.prod.yml,docker/nginx/default.conf"
41+
target: "~/heartmark"
42+
2743
- name: EC2에 배포
28-
uses: appleboy/ssh-action@v0.1.6
44+
uses: appleboy/ssh-action@v1.2.0
2945
with:
3046
host: ${{ secrets.EC2_HOST }}
3147
username: ubuntu
3248
key: ${{ secrets.EC2_KEY }}
49+
envs: DOCKER_USERNAME,DOCKER_PASSWORD
3350
script: |
34-
cd ~
35-
docker compose pull
36-
docker compose up -d
51+
set -e
52+
cd ~/heartmark
53+
54+
# .env가 없으면 앱이 시크릿 없이 떠서 소셜 로그인 등이 조용히 깨진다.
55+
# 배포를 진행하기 전에 확실히 실패시킨다.
56+
if [ ! -f .env ]; then
57+
echo ".env가 없습니다. ~/heartmark/.env를 먼저 만들어주세요."
58+
exit 1
59+
fi
60+
61+
# private 이미지를 pull하려면 EC2에서도 로그인이 필요하다.
62+
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
63+
64+
docker compose -f docker-compose.prod.yml pull
65+
docker compose -f docker-compose.prod.yml up -d
66+
67+
# 이전 배포에서 남은 태그 없는 이미지를 정리한다.
68+
# EC2 디스크가 작으면 금방 가득 차기 때문에 매 배포마다 돌린다.
69+
docker image prune -f
70+
env:
71+
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
72+
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}

‎Dockerfile‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
FROM python:3.11-slim
2+
3+
# Python 로그가 버퍼링 없이 바로 docker logs에 찍히도록 함.
4+
ENV PYTHONUNBUFFERED=1 \
5+
PYTHONDONTWRITEBYTECODE=1
6+
7+
WORKDIR /app
8+
9+
# Pillow(이미지 처리)와 cryptography 빌드에 필요한 시스템 패키지.
10+
# 설치 후 apt 캐시를 지워 이미지 용량을 줄인다.
11+
RUN apt-get update \
12+
&& apt-get install -y --no-install-recommends \
13+
build-essential \
14+
libjpeg-dev \
15+
zlib1g-dev \
16+
&& rm -rf /var/lib/apt/lists/*
17+
18+
# requirements를 먼저 복사해서 의존성 레이어를 캐시한다.
19+
# 코드만 바뀐 경우 이 레이어는 재사용되어 빌드가 빨라진다.
20+
COPY requirements.txt .
21+
RUN pip install --no-cache-dir --upgrade pip \
22+
&& pip install --no-cache-dir -r requirements.txt
23+
24+
COPY . .
25+
26+
# SQLite 파일과 업로드 미디어가 저장될 디렉토리.
27+
# compose에서 named volume을 여기에 마운트해 재배포 시에도 데이터가 유지된다.
28+
RUN mkdir -p /app/data /app/media /app/staticfiles
29+
30+
RUN chmod +x /app/docker/entrypoint.sh
31+
32+
EXPOSE 8000
33+
34+
ENTRYPOINT ["/app/docker/entrypoint.sh"]
35+
CMD ["gunicorn", "config.wsgi:application", \
36+
"--bind", "0.0.0.0:8000", \
37+
"--workers", "3", \
38+
"--timeout", "60", \
39+
"--access-logfile", "-", \
40+
"--error-logfile", "-"]
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from django.db import migrations
2+
3+
# bgm/static/bgm/audio/ 에 들어 있는 음원 파일과 짝이 되는 DB 레코드.
4+
# 이 행이 없으면 파일이 있어도 BGM이 재생되지 않으므로(services.get_music_for_record가
5+
# mood_group으로 조회한다) 새 DB에 배포할 때마다 자동으로 생성되도록 마이그레이션으로 넣는다.
6+
#
7+
# 주의: mood_group 배정과 artist/라이선스 정보는 파일명에서 추정한 값이다.
8+
# 실제 출처(Pixabay 등)를 확인해서 source_url / license_name / license_url /
9+
# attribution_required 를 채워 넣을 것.
10+
SEED_MUSIC = [
11+
{
12+
"file_name": "alex-morgan-sad-piano-emotional-rain-story-575883.mp3",
13+
"mood_group": "grief",
14+
"title": "Sad Piano Emotional Rain Story",
15+
"artist": "Alex Morgan",
16+
},
17+
{
18+
"file_name": "nastelbom-love-466198.mp3",
19+
"mood_group": "love",
20+
"title": "Love",
21+
"artist": "Nastelbom",
22+
},
23+
{
24+
"file_name": "the_mountain-love-481753.mp3",
25+
"mood_group": "love",
26+
"title": "Love",
27+
"artist": "The Mountain",
28+
},
29+
{
30+
"file_name": "viacheslavstarostin-cute-pets-animals-music-382060.mp3",
31+
"mood_group": "joy",
32+
"title": "Cute Pets Animals Music",
33+
"artist": "Viacheslav Starostin",
34+
},
35+
{
36+
"file_name": "andriig-warm-warm-acoustic-music-573245.mp3",
37+
"mood_group": "moody",
38+
"title": "Warm Warm Acoustic Music",
39+
"artist": "AndriiG",
40+
},
41+
]
42+
43+
44+
def seed_background_music(apps, schema_editor):
45+
BackgroundMusic = apps.get_model("bgm", "BackgroundMusic")
46+
47+
for entry in SEED_MUSIC:
48+
# file_name을 기준으로 삼아 이미 넣어둔 음원은 건드리지 않는다.
49+
BackgroundMusic.objects.update_or_create(
50+
file_name=entry["file_name"],
51+
defaults={
52+
"mood_group": entry["mood_group"],
53+
"title": entry["title"],
54+
"artist": entry["artist"],
55+
"is_active": True,
56+
},
57+
)
58+
59+
60+
def unseed_background_music(apps, schema_editor):
61+
BackgroundMusic = apps.get_model("bgm", "BackgroundMusic")
62+
63+
# 기록에 연결된 음원은 RecordBackgroundMusic이 PROTECT로 잡고 있어 삭제되지 않는다.
64+
# 되돌리기는 아직 아무 기록도 붙지 않은 경우에만 깔끔하게 동작한다.
65+
BackgroundMusic.objects.filter(
66+
file_name__in=[entry["file_name"] for entry in SEED_MUSIC]
67+
).delete()
68+
69+
70+
class Migration(migrations.Migration):
71+
72+
dependencies = [
73+
("bgm", "0002_recordbackgroundmusic"),
74+
]
75+
76+
operations = [
77+
migrations.RunPython(seed_background_music, unseed_background_music),
78+
]

‎config/settings.py‎

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,28 @@ def load_local_env():
2525

2626
load_local_env()
2727

28-
SECRET_KEY = "development-only"
29-
DEBUG = True
30-
ALLOWED_HOSTS = []
28+
# 배포 환경에서는 아래 값들을 모두 환경변수로 주입한다.
29+
# 기본값은 로컬 개발 기준이라, .env 없이도 로컬 실행은 그대로 동작한다.
30+
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "development-only")
31+
32+
# 서버용 compose 파일에 DJANGO_DEBUG=False가 박혀 있으므로
33+
# 누가 .env를 빠뜨려도 프로덕션에서 DEBUG가 켜지지 않는다.
34+
DEBUG = os.environ.get("DJANGO_DEBUG", "True").lower() not in ("false", "0", "no")
35+
36+
# "1.2.3.4,example.com" 형태의 콤마 구분 문자열을 받는다.
37+
ALLOWED_HOSTS = [
38+
host.strip()
39+
for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")
40+
if host.strip()
41+
]
42+
43+
# nginx 뒤에 있을 때 POST 요청의 CSRF origin 검사를 통과시키기 위한 값.
44+
# "http://1.2.3.4" 처럼 스킴을 반드시 포함해야 한다.
45+
CSRF_TRUSTED_ORIGINS = [
46+
origin.strip()
47+
for origin in os.environ.get("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",")
48+
if origin.strip()
49+
]
3150

3251
INSTALLED_APPS = [
3352
"django.contrib.admin",
@@ -84,10 +103,23 @@ def load_local_env():
84103
]
85104
WSGI_APPLICATION = "config.wsgi.application"
86105

106+
# 컨테이너에서는 DB 파일을 /app/data 아래에 둔다.
107+
# BASE_DIR(=/app)에 볼륨을 마운트하면 코드가 덮여버리므로 경로를 분리했다.
108+
DB_PATH = os.environ.get("DJANGO_DB_PATH") or (BASE_DIR / "db.sqlite3")
109+
87110
DATABASES = {
88111
"default": {
89112
"ENGINE": "django.db.backends.sqlite3",
90-
"NAME": BASE_DIR / "db.sqlite3",
113+
"NAME": DB_PATH,
114+
"OPTIONS": {
115+
# WAL 모드는 읽기와 쓰기가 서로를 막지 않게 해준다.
116+
# gunicorn 워커가 여러 개인 환경에서 "database is locked"를 크게 줄여준다.
117+
"init_command": "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;",
118+
# 잠금이 걸렸을 때 바로 실패하지 않고 최대 20초까지 기다린다.
119+
"timeout": 20,
120+
# 쓰기 트랜잭션의 잠금을 시작 시점에 잡아 갱신 충돌을 방지한다.
121+
"transaction_mode": "IMMEDIATE",
122+
},
91123
}
92124
}
93125

@@ -96,9 +128,16 @@ def load_local_env():
96128
USE_I18N = True
97129
USE_TZ = True
98130
STATIC_URL = "static/"
99-
STATICFILES_DIRS = [BASE_DIR / "static"]
131+
132+
# collectstatic이 각 앱의 static/을 모아두는 위치. nginx가 이 디렉토리를 서빙한다.
133+
STATIC_ROOT = os.environ.get("DJANGO_STATIC_ROOT") or (BASE_DIR / "staticfiles")
134+
135+
# 프로젝트 루트의 static/은 현재 존재하지 않는다.
136+
# 없는 경로를 그대로 두면 collectstatic이 W004 경고를 내므로 있을 때만 넣는다.
137+
STATICFILES_DIRS = [BASE_DIR / "static"] if (BASE_DIR / "static").exists() else []
138+
100139
MEDIA_URL = "media/"
101-
MEDIA_ROOT = BASE_DIR / "media"
140+
MEDIA_ROOT = os.environ.get("DJANGO_MEDIA_ROOT") or (BASE_DIR / "media")
102141

103142
LOGIN_URL = "/accounts/login/"
104143
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

‎docker-compose.prod.yml‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# EC2에서 실행되는 배포용 compose 파일.
2+
# build 섹션이 없다. 이미지는 CI가 Docker Hub에 올린 것을 pull해서 쓴다.
3+
#
4+
# EC2의 ~/heartmark/ 에 이 파일과 docker/nginx/default.conf, .env 가 있어야 한다.
5+
# .env는 CI가 건드리지 않으니 서버에서 한 번 만들어두고 계속 재사용한다.
6+
7+
services:
8+
web:
9+
image: ${DOCKER_USERNAME}/heartmark:staging
10+
env_file:
11+
- .env
12+
environment:
13+
# 서버에서는 DEBUG를 코드로 못 켜게 여기에 못박는다.
14+
# .env를 실수로 빠뜨려도 프로덕션에서 DEBUG가 켜지지 않는다.
15+
DJANGO_DEBUG: "False"
16+
DJANGO_DB_PATH: /app/data/db.sqlite3
17+
DJANGO_MEDIA_ROOT: /app/media
18+
DJANGO_STATIC_ROOT: /app/staticfiles
19+
volumes:
20+
# SQLite 파일, 업로드 미디어, 정적 파일을 볼륨에 둔다.
21+
# 이미지가 새로 배포돼도 이 세 개는 그대로 남는다.
22+
- db_data:/app/data
23+
- media_data:/app/media
24+
- static_data:/app/staticfiles
25+
# 외부로 포트를 열지 않는다. nginx만 이 컨테이너에 접근한다.
26+
expose:
27+
- "8000"
28+
restart: unless-stopped
29+
30+
nginx:
31+
image: nginx:1.27-alpine
32+
ports:
33+
- "80:80"
34+
volumes:
35+
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
36+
# web이 collectstatic으로 채운 정적 파일을 같은 볼륨으로 읽는다.
37+
- static_data:/app/staticfiles:ro
38+
- media_data:/app/media:ro
39+
depends_on:
40+
- web
41+
restart: unless-stopped
42+
43+
volumes:
44+
db_data:
45+
media_data:
46+
static_data:

0 commit comments

Comments
 (0)