devspoon-startup-web 은 스타트업에 필요한 서비스 — Plane, Jenkins, Gitea(사설 git 서버), Harbor(사설 Docker 레지스트리) — 를 쉽게 구축하는 통합 관리 솔루션입니다. Docker Compose 파일로 개발·백업·관리 서비스를 하나씩 또는 한 번에 설치할 수 있습니다. 이 저장소는 devspoon-web 프로젝트를 기반으로 합니다. devspoon-web 은 Docker Compose 로 php · python · django · nginx · redis 기반 웹/API 를 쉽게 구축하는 오픈소스입니다.
Docker Compose 로 Python · Django · PHP 등을 쉽게 서비스할 수 있는 오픈소스 인프라 통합 솔루션을 제공합니다. 상용 수준으로 커스터마이징 가능한 nginx 와 redis 를 한 번에 설치하고, 더 많은 서비스를 함께 설치·관리할 수 있습니다. 관심 있으시면 Devspoon-Projects 를 방문하세요.
- 준비 중입니다.
이 저장소가 설치할 수 있는 솔루션 4종입니다. 각 항목의 상세 설명 · 구성 · 설치 방법은 링크를 따라가세요.
| 솔루션 | 무엇을 하는가 | 컨테이너 | 설치 방식 | 상세 (원본 저장소) |
|---|---|---|---|---|
| Plane | 프로젝트 관리 — 이슈 · 사이클 · 모듈 | 13개 | compose (단독 / 통합) | makeplane/plane |
| Jenkins | CI — 빌드 · 테스트 · 배포 자동화 | 2개 | compose (단독 / 통합) | jenkinsci/jenkins |
| Gitea | 자체 호스팅 git 서비스 | 1개 | compose (단독 / 통합) | go-gitea/gitea |
| Harbor | 사설 Docker 레지스트리 | installer 가 생성 | 자체 installer (별도 호스트 권장) | goharbor/harbor |
Plane · Jenkins · Gitea 셋은 한 nginx 뒤에 함께 띄울 수도 있고(→ 통합 서비스 기동), 필요한 것만 하나씩 띄울 수도 있습니다(→ 단독 구축).
-
devspoon-web 에서 이식한 웹 스택 —
compose/web_service/에 5개 스택이 있습니다:nginx_gunicorn,nginx_uvicorn,nginx_uwsgi,nginx_daphne(Python / Django 6, uv),nginx_php(PHP 8.4 전용). 웹 스택 & CI 참고. -
필요한 것만 설치 — 모든 솔루션을 설치할 필요 없이
compose/project_mng_service/<solution>에서 원하는 것만 고릅니다. 단독 서비스는 한 번에 하나만 기동합니다(단독 서비스 동시 기동 규칙 참고). -
올인원 조합 — 웹 스택과 Plane · Jenkins · Gitea 를 함께 띄우려면
compose/master_service/의 5개 파일 중 하나를 사용하세요(통합 서비스 기동 참고). -
nginx 하나로 모두 리버스 프록시 — master_service 에서는 nginx 하나가 웹 앱을 서비스하고 Plane · Jenkins · Gitea 를 도메인별로 프록시합니다.
예시 test.com -> 회사 웹사이트 plane.test.com -> Plane jen.test.com -> Jenkins git.test.com -> Gitea -
비밀값 분리(
.env-example) — 모든 compose 폴더에 추적되는.env-example이 있고 실제.env는 gitignore 됩니다..env로 복사한 뒤script/lib/django_secrets.sh(ensure_env_secrets)로 빈 비밀값을 생성하고, 실제 파일은 커밋하지 마세요. 필수 비밀값이 없으면 compose 의${VAR:?}검사가 기동을 거부합니다. -
기타
- Gitea 의 git over SSH 는 2222 포트를 씁니다.
- Harbor 는 자체 installer 스크립트로 설치합니다(Harbor 참고).
-
개발 지향 docker 서비스 — 수정과 테스트가 잦은 스타트업이나 신규 서비스 개발팀에 적합합니다.
-
AWS · GCM 이 아닌 일반 서버 대상 — 직접 운영하는 서버와 일반 서버 호스팅을 대상으로 합니다. AWS · GCM 등 클라우드 연동은 계획 중입니다.
-
요구 사항 — Compose 플러그인이 포함된 Docker Engine. Python 앱 이미지 빌드에 2.17 이상, master_service 와 단독 Plane · Gitea 스택이 쓰는
include:에 2.20 이상이 필요합니다. 레거시docker-compose(v1) 명령은 번들된 Harbor installer 만 사용합니다.
웹 스택 서술은 정본 devspoon-web README 와 같습니다(이 저장소 구조에 맞춤). 모든 명령은 저장소 루트 기준입니다.
| 스택 | compose 폴더 | app 서비스 | nginx 설정 폴더 | 프로파일 | 호스트 포트 |
|---|---|---|---|---|---|
| gunicorn | compose/web_service/nginx_gunicorn |
gunicorn-app |
config/web-server/nginx/gunicorn |
celery |
80, 443, 127.0.0.1:5555(flower) |
| uvicorn | compose/web_service/nginx_uvicorn |
uvicorn-app |
config/web-server/nginx/uvicorn |
celery |
80, 443, 127.0.0.1:5555 |
| uwsgi | compose/web_service/nginx_uwsgi |
uwsgi-app |
config/web-server/nginx/uwsgi |
celery |
80, 443, 127.0.0.1:5555 |
| daphne | compose/web_service/nginx_daphne |
daphne-app |
config/web-server/nginx/gunicorn (공유) |
celery |
80, 443, 127.0.0.1:5555 |
| php 8.4 | compose/web_service/nginx_php |
php-app |
config/web-server/nginx/php |
redis |
80, 443 |
- Python 스택은
webserver· app ·redis가 항상 뜨고,--profile celery가celery·celery-beat·flower를 더합니다. php 스택은webserver·php-app이 뜨고--profile redis가redis를 더합니다. - 모든 스택이 80/443 을 쓰므로 한 호스트에서 한 스택만 기동합니다.
- 샘플 앱: Python 스택은
www/django_sample, php 스택은www/php_sample.
.env-example 의 비밀값은 빈 값입니다(Python 스택 DJANGO_SECRET_KEY · REDIS_PASSWORD · FLOWER_PWD, php 스택 REDIS_PASSWORD). compose 가 ${VAR:?} 로 요구하므로 비워 둔 채로는 기동이 거부됩니다. 저장소 루트에서 스택마다 한 번:
D=compose/web_service/nginx_gunicorn
cp "$D/.env-example" "$D/.env"
bash -c ". script/lib/django_secrets.sh && ensure_env_secrets $D/.env"-
값이 비었거나 옛
CHANGE_ME_*인 비밀 키만openssl rand -hex무작위 값으로 채웁니다(DJANGO_SECRET_KEY100 hex,*_KEY_BASE128 hex, 그 외 64 hex). 이미 값이 있는 키는 바꾸지 않습니다. -
같은 폴더의 임시 파일에 쓴 뒤 교체하며, 값을 생성했으면 권한을 600 으로 좁힙니다(더 엄격하면 유지). openssl 이 없거나 실패하면
FAIL로 끝나고.env내용은 바뀌지 않습니다. -
비밀이 아닌 자리표시자는 헬퍼가 채우지 않습니다. 운영 전에 직접 입력하세요.
키 어디에 값 FLOWER_ID웹 스택 · master flower 로그인 ID (기본값 CHANGE_ME_FLOWER_USER)DJANGO_ALLOWED_HOSTS웹 스택 · master 서비스할 도메인 추가 PLANE_DOMAIN·PLANE_WEB_URL·PLANE_CORS_ALLOWED_ORIGINSmaster · 단독 plane 셋 다 같은 도메인. proxy conf 의 server_name과도 같아야 합니다GITEA_DOMAIN·GITEA_ROOT_URLmaster · 단독 gitea 도메인과 전체 URL -
호스트에서
manage.py를 직접 실행할 때만www/django_sample의secrets.json이 필요합니다:bash -c '. script/lib/django_secrets.sh && ensure_django_secrets'(없을 때만 생성, 600). 컨테이너는DJANGO_SECRET_KEY환경변수를 씁니다.
업그레이드 노트 — 이전 버전에서 쓰던
.env를 유지하는 경우옛
.env에는DJANGO_SECRET_KEY줄이 아예 없거나CHANGE_ME_*값이 남아 있을 수 있습니다. 같은.env에 위 헬퍼를 한 번 실행하면 정리됩니다.헬퍼는 이렇게 동작합니다.
- 어디를 보는가: 같은 폴더의
docker-compose*.yml, 그리고 그 파일들이include:로 참조하는 조각(compose/common/*.yml).- 무엇을 찾는가: 그 파일들이
:?로 요구하는 키 중 이름에 SECRET · PASSWORD · PWD 가 들어가거나_KEY_BASE로 끝나는 것.- 무엇을 하는가: 없는 키는 파일 끝에 추가하고,
CHANGE_ME_*는 새 값으로 바꿉니다. 이미 값이 있는 키는 건드리지 않고, 파일 권한을 600 으로 맞춥니다.
KEY=""처럼 따옴표로 감싼 빈 값은 채우지 않습니다. 먼저KEY=로 고쳐 두세요.이전 버전은
compose/web_service/*·compose/master_service·compose/project_mng_service/*의.env를 git 으로 추적했습니다. 이 버전에서 추적이 해제되어git pull이 로컬.env를 지울 수 있으니 pull 전에.env를 다른 곳에 복사해 두세요. 이전에 저장소에 공개됐던secrets.json·.env의 키로 운영 중이라면 새 값으로 교체하세요(세션 무효화).
§1 의 .env 명령(저장소 루트) 뒤, 스택 폴더로 이동해 기동합니다. --build 는 업그레이드나 Dockerfile · uv.lock 변경 뒤 이미지를 다시 빌드합니다. 한 호스트에 한 스택만 띄웁니다.
# gunicorn (저장소 루트에서)
cd compose/web_service/nginx_gunicorn
docker compose up -d --build # webserver + gunicorn-app + redis
docker compose --profile celery up -d # + celery · celery-beat · floweruvicorn · uwsgi · daphne 는 gunicorn 과 같은 명령이며 폴더 이름만 compose/web_service/nginx_uvicorn · nginx_uwsgi · nginx_daphne 로 바꿉니다.
# php (저장소 루트에서)
cd compose/web_service/nginx_php
docker compose up -d --build # webserver + php-app
docker compose --profile redis up -d # + redis
⚠️ docker compose down -v는 named volume 을 삭제합니다. 웹 스택의 앱 데이터, master_service 의 Plane 데이터(plane-pgdata등), Gitea 저장소(gitea-data)가 모두 여기에 해당합니다. 컨테이너만 내릴 때는stop을 쓰세요.
- 기동 순서 — app 이 DB 를 초기화한 뒤 celery · beat 가 뜹니다.
DB 초기화는 app 서비스 하나만 수행합니다.
manage.py가 있으면python manage.py migrate --noinput, 없으면 프로젝트의prestart.sh를 돌립니다.celery·celery-beat는depends_on: <app>: condition: service_healthy로 묶여 app 이 healthy 가 된 뒤에 뜹니다. 여러 컨테이너가 동시에 migrate 하는 경쟁이 없습니다. - Flower 는
127.0.0.1:5555에만 바인드됩니다. 원격 접근은 SSH 터널을 쓰세요:ssh -L 5555:127.0.0.1:5555 <host>후 로컬 브라우저에서http://127.0.0.1:5555. DJANGO_DEBUG(기본0) ·DJANGO_ALLOWED_HOSTS를.env로 제어하며 app · celery · beat 에 전달됩니다.DJANGO_DEBUG=1은 로컬 개발에서만 쓰세요.- 컨테이너는
docker compose stop/start/restart로 운영합니다. 프로필 서비스까지 대상이면 기동과 같은 프로필을 붙입니다(docker compose --profile celery stop, php 는--profile redis stop) — 프로필 없는stop은 celery · celery-beat · flower(php 는 redis) 컨테이너를 남깁니다.
-
이미지 이름은
${IMAGE_NAMESPACE:-devspoon}-nginx:latest형식입니다(-py-app:latest·-uwsgi-app:latest·-php-app:8.4). 기본값이면devspoon-*태그가 됩니다.테스트는 운영 태그를 덮어쓰지 않도록 다른 네임스페이스를 씁니다 — 검증기와
verify-ngxblocker.sh는IMAGE_NAMESPACE=devspoon-it, run-ci 의 빌드 단계(s2_build.sh)는devspoon-test/*. -
앱 이미지(
py-app·uwsgi-app)의 사전 설치 패키지는www/django_sample/uv.lock에서 도출됩니다. compose 가build.additional_contexts: lock: ../../../www/django_sample로 자동 전달하므로 Docker Compose ≥ 2.17 이 필요합니다(docker compose version). -
Compose 가 2.17 미만이거나
docker build를 직접 쓸 때는 추가 빌드 컨텍스트를 명시합니다(저장소 루트, BuildKit):docker build --build-context lock=www/django_sample -t devspoon-py-app:latest docker/gunicorn/ docker build --build-context lock=www/django_sample -t devspoon-uwsgi-app:latest docker/uwsgi/
빠뜨리면 빌드가
"/pyproject.toml": not found로 실패합니다. -
uv —
www/django_sample의 의존성은pyproject.toml·uv.lock으로 관리합니다. 컨테이너는 가상환경 없이 시스템 Python 에 설치하고(UV_PROJECT_ENVIRONMENT=/usr/local), 기동 명령이uv sync --inexact --extra <stack> --extra celery를 실행합니다. 같은 스택의 app · celery · celery-beat 는 같은 extras 로 sync 합니다.의존성을 추가할 때는 순서가 있습니다.
cd www/django_sample && uv add <pkg> # 1. 호스트에서 추가 → uv.lock 커밋 cd compose/web_service/nginx_gunicorn # 2. 스택 폴더로 docker compose --profile celery stop docker compose up -d --build # 3. 앱 이미지 재빌드 docker compose --profile celery up -d # 4. celery · celery-beat 도 다시 올림
4번을 빠뜨리면 celery 쪽이 옛 의존성으로 계속 돕니다. celery 서비스에는
build:가 없어 app 과 같은 이미지를 참조만 하기 때문에, 프로필 없는up --build만으로는 교체되지 않습니다.
-
nginx conf 생성기 —
config/web-server/nginx/<gunicorn|uvicorn|uwsgi|php>/에 있는nginx_http_conf.sh·nginx_https_conf.sh가sample_nginx_http(s).conf를 바탕으로 도메인별 conf 를conf.d/에 만듭니다. 옵션은-h로 확인하세요. daphne 스택은 gunicorn 폴더를 그대로 씁니다. -
nginx 기동 훅 — 이미지의
/docker-entrypoint.d/30-wait-upstreams.sh가 conf 에 적힌 upstream(앱 컨테이너) 이름이 해석될 때까지 기다렸다가 nginx 를 띄웁니다. 대기 시간은NGINX_UPSTREAM_WAIT초(기본 30,0이면 비활성)이며 webserver 의environment로 조정합니다.재부팅이나
start처럼 app 보다 webserver 가 먼저 뜨는 상황에서[emerg] host not found in upstream으로 죽는 것을 막아 줍니다. 다만 전체docker compose restart는 모두가 동시에 재시작하므로 훅으로도 완전히 막히지 않습니다 — conf 반영은nginx -s reload, 전체 재기동은stop→start를 쓰세요. -
php-fpm pool 은
config/app-server/php/pool.d/www.conf를 직접 편집합니다. compose 가 읽기 전용으로 마운트하는 파일은 그www.conf와config/app-server/php/php_ini/php.ini둘뿐입니다. 그래서php_conf.sh가pool.d/에 만드는<DOMAIN>_php.conf는 로드되지 않습니다.
저장소를 고친 뒤 "이 저장소의 모든 스택이 여전히 실제로 뜨는가" 를 한 번에 확인하는 회귀 테스트 묶음입니다. 문법 검사에서 끝나지 않고 컨테이너를 실제로 띄워 응답까지 받아 봅니다.
GitHub Actions(.github/workflows/test.yml)가 push 마다 같은 스크립트를 호출하고, 개발자가 로컬에서 bash script/ci/run-ci.sh 로 똑같이 돌릴 수도 있습니다.
12개 단계를 순서대로 실행하고, 한 단계라도 실패하면 즉시 중단합니다. 단계마다 로그를 log/ci/ 에 남기고, 실패 시 "어느 단계에서 무슨 오류로" 실패했는지 로그 끝부분과 함께 알립니다.
| # | 단계 | 하는 일 |
|---|---|---|
| 1 | preflight | 필요한 도구·파일이 있는지, 설계 불변식이 지켜졌는지 확인 (읽기 전용) |
| 2 | 의존성 취약점 감사 | www/*/uv.lock 전수를 uv audit 로 검사 — script/test/audit-allow.txt 에 없는 알려진 취약점이 있으면 실패 (읽기 전용, 수 초) |
| 3 | prereq · 로그 디렉터리 | 테스트가 쓸 로그 폴더 생성 |
| 4 | nginx conf 생성기 | nginx_http_conf.sh · nginx_https_conf.sh 가 만든 conf 가 설정대로 나오는지 |
| 5 | compose 검증 | 모든 스택의 compose 문법과 마운트 경로 |
| 6 | 저장소 고유 검사 | script/ci/repo-steps.sh — 이 저장소에만 있는 규칙(Plane · Gitea · Jenkins · Harbor 관련 정적 단언) |
| 7 | 이미지 빌드 | 모든 Dockerfile 을 devspoon-test/* 태그로 빌드 (운영 태그를 덮어쓰지 않음) |
| 8 | 정적 회귀 | s6_regression.sh — 과거에 고친 결함이 되살아나지 않았는지 검사하는 불변식 모음 |
| 9 | healthcheck | 5개 스택의 healthcheck · depends_on 선언 검증 |
| 10 | 스택 매트릭스 | gunicorn · uvicorn · uwsgi · daphne · php 를 차례로 실제 기동 — 200 응답, 봇 차단, healthy, 재시작 0회, DEBUG off, 업로드 경로 403 |
| 11 | 샘플 프로젝트 | django · php 샘플이 동작하는지 |
| 12 | 스크립트 로그 | 스크립트들이 로그를 제대로 남기는지 |
- 실제 컨테이너를 띄우므로 호스트 80 · 443 · 5555 포트가 비어 있어야 합니다.
- 필요한 도구: docker(Compose ≥ 2.17), uv, jq, curl, openssl, php-cli.
- 알림은 선택입니다. 저장소 secrets 에
SLACK_WEBHOOK_URL·TELEGRAM_BOT_TOKEN·TELEGRAM_CHAT_ID가 없으면 해당 알림만 건너뛰고 테스트는 그대로 진행합니다. - 업로드하는 로그(
log/ci,log/test_run)는script/lib/mask_secrets.sh로 비밀값을 가린 뒤 올립니다.
CI 는 저장소 운영에만 쓰이고 서비스 기동과는 무관합니다. 필요 없으면 .github/workflows/test.yml 만 지우면 Actions 가 돌지 않고, script/ci/ · script/test_run/ 을 통째로 지워도 compose/ 아래 서비스 기동에는 영향이 없습니다.
Plane · Jenkins · Gitea 는 두 가지 방식으로 설치합니다. 어느 쪽이든 서비스 정의는 compose/common/ 의 같은 파일을 쓰므로 동작은 동일하고, 앞단 nginx 를 공유하느냐만 다릅니다.
통합 구축 (compose/master_service) |
단독 구축 (compose/project_mng_service) |
|
|---|---|---|
| 무엇이 뜨는가 | 웹 스택 + Plane + Jenkins + Gitea 를 한 번에 | 고른 서비스 하나만 |
| nginx | 웹 앱과 세 솔루션을 한 nginx 가 도메인별로 프록시 | 서비스 전용 nginx 하나 |
| 동시 기동 | 전부 함께 뜸 | 한 번에 하나만 (80/443 을 서로 뺏음) |
| TLS | 구성 가능 (HTTPS 절) | HTTP 전용 |
| 쓰는 경우 | 운영, 여러 솔루션을 같이 쓸 때 | 하나만 평가 · 시험할 때 |
Harbor 는 자체 installer 로 설치하며 두 방식 어디에도 포함되지 않습니다 — Harbor 참고.
통합 구축 예시 — nginx 하나가 도메인으로 갈라 보냄
test.com -> 웹 앱 (django / php)
plane.test.com -> Plane
jen.test.com -> Jenkins
git.test.com -> Gitea (+ ssh://…:2222 로 git clone)
compose/master_service/ 의 compose 파일 하나를 실행하면 웹 스택 · Plane · Jenkins · Gitea 가 한 번에 뜨고, nginx 하나가 도메인별로 갈라 보냅니다. 서비스마다 따로 기동할 필요가 없습니다.
웹 스택 종류만 다르고 Plane · Jenkins · Gitea 는 다섯 파일 모두에 들어 있습니다. 하나만 고르세요.
| 파일 | 웹 스택 | 붙일 프로파일 |
|---|---|---|
docker-compose-gunicorn.yml |
gunicorn | --profile celery |
docker-compose-uvicorn.yml |
uvicorn | --profile celery |
docker-compose-uwsgi.yml |
uwsgi | --profile celery |
docker-compose-daphne.yml |
daphne | --profile celery |
docker-compose-php.yml |
php 8.4 | --profile redis (celery 없음) |
한 파일이 띄우는 것 — php 조합 기준 컨테이너 17개:
| 묶음 | 컨테이너 |
|---|---|
| 웹 | nginx-webserver · php-app(또는 <stack>-app) · redis_db |
| Plane | plane-db · plane-redis · plane-mq · plane-minio · plane-migrator(1회성) · plane-api · plane-worker · plane-beat-worker · plane-web · plane-space · plane-admin · plane-live · plane-proxy |
| Jenkins | jenkins( + 1회성 jenkins-init) |
| Gitea | gitea |
Plane · Gitea 정의는 단독 스택과 같은 파일(compose/common/{plane-services,gitea-service}.yml)을 include: 로 가져옵니다 — Docker Compose ≥ 2.20 이 필요합니다.
아래 명령은 전부 저장소 루트에서 시작합니다. 사전 준비는 없습니다 — Plane · Gitea 관리자 계정은 기동한 뒤에 만듭니다.
1. .env 만들고 비밀값 채우기
D=compose/master_service
cp "$D/.env-example" "$D/.env"
bash -c ". script/lib/django_secrets.sh && ensure_env_secrets $D/.env"헬퍼가 include: 한 정의까지 훑어 Plane 비밀 키 5종과 REDIS_PASSWORD · DJANGO_SECRET_KEY 등을 채웁니다.
2. 자리표시자 직접 입력 — 헬퍼가 채우지 않는 값입니다. compose/master_service/.env 를 열어 실제 도메인으로 바꾸세요.
| 키 | 값 | 비고 |
|---|---|---|
DJANGO_ALLOWED_HOSTS |
웹 앱 도메인 추가 | 예: localhost,127.0.0.1,web.example.com |
PLANE_DOMAIN · PLANE_WEB_URL · PLANE_CORS_ALLOWED_ORIGINS |
Plane 도메인 (셋 다 같은 값) | WEB_URL · CORS 는 http:// 포함 |
GITEA_DOMAIN · GITEA_ROOT_URL |
Gitea 도메인 | ROOT_URL 은 끝에 / 포함한 전체 URL |
FLOWER_ID |
flower 로그인 ID | Python 스택만 해당 |
3. 웹 앱 도메인 conf 생성 — 웹 스택은 도메인별 conf 를 생성기로 만듭니다. 이 단계를 건너뛰면 저장소에 딸린 localhost conf 만 있어서, 웹 앱 도메인으로 들어온 요청은 catch-all 에 걸려 444 로 끊깁니다.
# php 조합 예 — 웹루트 php_sample, 백엔드 php-app:9000
bash config/web-server/nginx/php/nginx_http_conf.sh -w php_sample -p 80 -d web.example.com -a php-app -s 9000Python 조합은 스택 폴더와 인자가 다릅니다(예: gunicorn → config/web-server/nginx/gunicorn/nginx_http_conf.sh -w django_sample -p 80 -d <도메인> -a gunicorn-app -s 8000). 옵션은 -h 로 확인하세요.
4. proxy conf 3종 복사 — Plane · Jenkins · Gitea 용입니다. 복사본을 만들지 않으면 그 서비스만 비활성이고 나머지는 정상 기동합니다.
P=config/web-server/nginx/php/proxy
cp "$P/plane/plane_proxy.conf.example" "$P/plane/plane_proxy.conf" # server_name 을 Plane 도메인으로
cp "$P/jenkins/jenkins_proxy.conf.example" "$P/jenkins/jenkins_proxy.conf" # server_name 을 Jenkins 도메인으로
cp "$P/gitea/gitea_proxy.conf.example" "$P/gitea/gitea_proxy.conf" # server_name 을 Gitea 도메인으로webserver 는 이 폴더를 /etc/nginx/proxy.d/<svc>/ 로 읽기 전용 마운트하고, nginx.conf 가 include /etc/nginx/proxy.d/*/*.conf; 로 읽습니다. conf.d 로 복사하지 마세요. 샘플에는 HTTP(80) 서버 블록만 있습니다 — TLS 는 HTTPS 절에서 443 블록을 더합니다.
5. 기동 — compose 파일 하나로 전부 올립니다.
cd compose/master_service
docker compose -f docker-compose-php.yml --profile redis up -d --build # php 조합
# 또는
docker compose -f docker-compose-gunicorn.yml --profile celery up -d --build # gunicorn 조합--build 는 웹 스택 이미지(nginx · app)를 다시 만듭니다. 업그레이드나 Dockerfile · uv.lock 변경 뒤에 붙이세요. Plane · Jenkins · Gitea 는 공개 이미지를 받아 쓰므로 빌드하지 않습니다.
⚠️ 첫 기동은 수 분 걸립니다.plane-migrator가 DB 마이그레이션을 끝내야plane-api가 뜨고, 그 뒤에plane-proxy가 준비됩니다.--wait를 붙이면 전부 준비될 때까지 기다립니다(--wait --wait-timeout 900).
6. 확인
docker compose -f docker-compose-php.yml --profile redis ps # 컨테이너 상태
docker compose -f docker-compose-php.yml exec webserver nginx -t # proxy.d 포함 설정 검사7. 서비스별 첫 설정 — 기동이 끝난 뒤에 합니다.
| 서비스 | 할 일 |
|---|---|
| Plane | 브라우저로 접속해 가입 → 첫 사용자가 됩니다. 인스턴스 설정은 /god-mode/ (Plane 참고) |
| Jenkins | docker compose -f docker-compose-php.yml exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword 로 초기 비밀번호 확인 |
| Gitea | docker compose -f docker-compose-php.yml exec -u git gitea gitea admin user create --username <id> --password '<pw>' --email <mail> --admin |
-f docker-compose-<stack>.yml 과 기동할 때 쓴 프로파일을 항상 같이 붙입니다.
| 목적 | 명령 |
|---|---|
| conf 만 반영 | docker compose -f … exec webserver nginx -t && … exec webserver nginx -s reload |
| nginx 만 재시작 | docker compose -f … restart webserver |
| 전체 재기동 | docker compose -f … --profile redis stop → … start |
| 전체 정지 | docker compose -f … --profile redis stop |
⚠️ 전체docker compose restart는 피하세요. 모두 동시에 재시작하면서 app 이 내려간 순간 nginx 가 먼저 떠[emerg] host not found in upstream으로 한 번 죽을 수 있습니다.⚠️ docker compose down -v는 Plane 데이터와 Gitea 저장소를 삭제합니다.
- 단독 서비스는 한 번에 하나만 기동합니다.
nginx_plane·nginx_jenkins·gitea가 모두 호스트 80/443(또는 2222)을 쓰고,plane-*·jenkins·gitea컨테이너 이름이 master_service 와 같습니다. 웹 스택(compose/web_service) · master_service 와도 동시에 띄울 수 없습니다. - 여러 서비스를 동시에 운영하려면 master_service 를 쓰세요.
- 단독 스택은 HTTP 전용입니다(80 만, TLS 없음).
443 포트가 매핑돼 있긴 하지만 서비스용 TLS 서버 블록이 없습니다. catch-all
default.conf가 TLS 핸드셰이크를 거부할 뿐입니다(ssl_reject_handshake on). 즉 로그인 자격증명이 평문으로 오갑니다. 공개망에서 쓰려면 앞단에 TLS 종단(별도 리버스 프록시 · LB)을 두거나, TLS 를 구성할 수 있는 master_service 를 쓰세요.
단독 스택의 nginx 는 이렇게 구성됩니다.
- 서비스별 폴더
config/web-server/nginx/php/proxy/<svc>/를/etc/nginx/conf.d/로 읽기 전용 마운트합니다. - 그 폴더의 자리표시자
default.conf위에 catch-allconfig/web-server/nginx/php/conf.d/default.conf를 덮어 마운트합니다. - 복사본
<svc>_proxy.conf를 만들지 않으면 모든 Host 에 444 를 돌려주며 정상 기동합니다.
자리표시자
default.conf는 읽기 전용 마운트 지점입니다. 편집하거나 지우지 마세요.
공식 사이트: Plane
한 줄 요약: 이슈로 일을 쪼개고 주기로 묶어 굴리는 오픈소스 프로젝트 관리 도구입니다. Jira · Linear 와 같은 부류이며, 직접 설치해 쓰는 self-hosted 방식입니다.
개념 구조 — 위에서 아래로 포개집니다.
| 개념 | 설명 |
|---|---|
| 인스턴스 | 설치한 Plane 전체. 가입 정책 · 인증 · 메일을 God Mode(/god-mode/)에서 정합니다 |
| 워크스페이스 | 회사 · 팀 단위 공간. 멤버와 권한이 여기에 붙습니다 |
| 프로젝트 | 워크스페이스 안의 작업 단위. 고유 식별자(예: LIVE)를 가집니다 |
| 이슈 | 실제 할 일. 상태 · 담당자 · 우선순위 · 라벨 · 마감일을 가집니다 |
| 사이클(Cycle) | 기간으로 묶은 이슈 모음 — 스프린트에 해당합니다 |
| 모듈(Module) | 기능 · 목표로 묶은 이슈 모음 — 기간과 무관합니다 |
주요 기능
| 기능 | 설명 |
|---|---|
| 다중 뷰 | 같은 이슈 목록을 보드(칸반) · 리스트 · 캘린더 · 간트 · 스프레드시트로 전환해 봅니다 |
| 사이클 · 모듈 | 스프린트 운영과 기능 단위 묶음을 따로 관리하고, 번다운 차트로 진행률을 봅니다 |
| 페이지 | 프로젝트에 딸린 문서. 여러 명이 동시에 편집합니다(plane-live 가 담당) |
| 공개 공유 | 이슈 목록이나 페이지를 외부에 읽기 전용으로 공개합니다(plane-space 가 담당) |
| 첨부 · 이미지 | S3 호환 저장소(plane-minio)에 보관합니다 |
| API | REST API 로 워크스페이스 · 프로젝트 · 이슈를 생성 · 조회합니다 |
| God Mode | 인스턴스 관리자 화면 — 가입 허용, 인증 수단(비밀번호 · 매직링크 · OAuth), SMTP, 파일 크기 제한 |
이럴 때 씁니다
- 이슈 · 스프린트 관리를 SaaS 가 아니라 사내 서버에 두고 싶을 때
- Jenkins · Gitea 와 같은 도메인 아래 묶어 한 곳에서 운영하고 싶을 때
알아 둘 점
- 컨테이너 13개가 한 묶음이라 이 저장소의 서비스 중 가장 무겁습니다. 첫 기동에 수 분 걸립니다.
- 메일 발송(초대 · 알림)은 기본값이 꺼져 있습니다. 쓰려면 God Mode 에서 SMTP 를 설정하세요.
컨테이너 13개가 한 묶음으로 동작합니다. 정의는 compose/common/plane-services.yml 한 곳에 있고, 단독 스택과 master_service 5조합이 이 파일을 include: 로 함께 씁니다(Docker Compose ≥ 2.20 필요).
| 역할 | 컨테이너 | 이미지 |
|---|---|---|
| 웹 UI | plane-web |
makeplane/plane-frontend |
| 공개 페이지 | plane-space |
makeplane/plane-space |
| 관리 화면(God Mode) | plane-admin |
makeplane/plane-admin |
| API 서버 | plane-api |
makeplane/plane-backend |
| 실시간 협업(WebSocket) | plane-live |
makeplane/plane-live |
| 백그라운드 작업 | plane-worker · plane-beat-worker |
makeplane/plane-backend |
| DB 마이그레이션(1회성) | plane-migrator |
makeplane/plane-backend |
| 데이터베이스 | plane-db |
postgres:15.7-alpine |
| 캐시 | plane-redis |
valkey/valkey:7.2.11-alpine |
| 메시지 큐 | plane-mq |
rabbitmq:3.13.6-management-alpine |
| 첨부 파일 저장소 | plane-minio |
quay.io/minio/minio (S3 호환) |
| 스택 내부 프록시 | plane-proxy |
makeplane/plane-proxy (Caddy) |
- 호스트 포트를 열지 않습니다. 앞단 nginx 가
plane-proxy로 프록시합니다. - 데이터는 전부 named volume 입니다:
plane-pgdata(DB) ·plane-uploads(첨부) ·plane-rabbitmq·plane-redisdata·plane-proxy-*·plane-logs-*. 호스트 폴더를 미리 만들 필요가 없습니다. - 업스트림
deployments/cli/community/docker-compose.yml을 기준으로, 호스트 포트를 닫고 앞단 nginx 뒤에 두도록 조정했습니다.
-
.env생성 — 비밀 키 5종(PLANE_SECRET_KEY·PLANE_LIVE_SERVER_SECRET_KEY·PLANE_DB_PASSWORD·PLANE_MQ_PASSWORD·PLANE_MINIO_PASSWORD)이 비어 있으면 compose 가 기동을 거부합니다. 헬퍼가include:한 정의까지 훑어 채웁니다.D=compose/project_mng_service/nginx_plane cp "$D/.env-example" "$D/.env" bash -c ". script/lib/django_secrets.sh && ensure_env_secrets $D/.env"
-
도메인 입력 —
PLANE_DOMAIN·PLANE_WEB_URL·PLANE_CORS_ALLOWED_ORIGINS세 값을 같은 도메인으로 직접 적습니다(헬퍼가 채우지 않습니다). proxy conf 의server_name과도 같아야 로그인 · API · 실시간 협업이 모두 동작합니다. HTTPS 로 서비스하면 URL 두 개를https://로 적습니다. -
proxy conf 준비 (저장소 루트에서)
P=config/web-server/nginx/php/proxy/plane cp "$P/plane_proxy.conf.example" "$P/plane_proxy.conf" # server_name 을 실제 도메인으로 수정
-
기동
cd compose/project_mng_service/nginx_plane docker compose up -d --build첫 기동은 수 분 걸립니다.
plane-migrator가 DB 마이그레이션을 끝내야plane-api가 뜨고, 그 뒤에 나머지가 준비됩니다. -
첫 계정 — 브라우저로 도메인에 접속해 가입하면 그 계정이 인스턴스의 첫 사용자가 됩니다. 인스턴스 관리는 같은 도메인의
/god-mode/로 들어갑니다. 외부 가입을 막으려면 God Mode 의 Authentication 설정에서 sign-up 을 끕니다. -
백업
D=compose/project_mng_service/nginx_plane # master: D=compose/master_service + -f docker-compose-<stack>.yml (cd "$D" && docker compose exec -T plane-db pg_dump -U plane -d plane) | gzip > ~/plane-db-$(date +%F).sql.gz chmod 600 ~/plane-db-*.sql.gz # 사용자 · 워크스페이스 데이터가 들어 있습니다
첨부 파일은
plane-uploads볼륨에 있습니다:docker run --rm -v plane-uploads:/d -v "$HOME":/b alpine tar czf /b/plane-uploads.tgz -C /d .
⚠️ 비밀값을 바꾸면 기존 DB 와 어긋납니다.PLANE_DB_PASSWORD는plane-pgdata볼륨을 처음 만들 때의 PostgreSQL 비밀번호로 굳습니다..env를 새로 만들어 값이 달라지면plane-migrator가FATAL: password authentication failed for user "plane"으로 실패하고, 뒤따르는 컨테이너가 전부 못 뜹니다. 값을 바꾸려면 DB 쪽도 함께 바꾸거나(docker compose exec plane-db psql -U plane -c "ALTER USER plane PASSWORD '<새 값>';"), 데이터를 버려도 되면 볼륨을 새로 만드세요(docker compose down -v).
자세한 내용은 Plane 공식 문서를 참고하세요.
공식 사이트: Jenkins
한 줄 요약: 정해 둔 작업을 자동으로 실행해 주는 CI/CD 서버입니다. "코드가 올라오면 빌드하고 테스트하고 배포한다" 를 사람 손 없이 반복합니다.
개념 구조
| 개념 | 설명 |
|---|---|
| Job / Pipeline | 실행할 작업의 정의. 요즘은 저장소의 Jenkinsfile 에 파이프라인을 적는 방식을 씁니다 |
| Build | Job 을 한 번 실행한 결과. 번호 · 로그 · 산출물(artifact)이 남습니다 |
| Trigger | 실행 조건 — git push 웹훅, 주기 실행(cron), 수동 실행, 다른 Job 성공 시 |
| Agent / Node | 실제로 작업을 돌리는 실행기. 기본은 Jenkins 자신(built-in node)입니다 |
| Credentials | 저장소 접근 키 · 레지스트리 비밀번호 등을 암호화해 보관하고 Job 에 주입합니다 |
| Plugin | 기능 확장. Git · Docker · Slack 연동 등 대부분의 기능이 플러그인입니다 |
주요 기능
| 기능 | 설명 |
|---|---|
| 자동 빌드 · 테스트 | push 마다 빌드 · 테스트를 돌려 깨진 커밋을 바로 찾아냅니다 |
| 파이프라인 | 빌드 → 테스트 → 이미지 생성 → 배포를 단계로 나눠 실행하고, 단계별 성공 · 실패를 봅니다 |
| 병렬 · 분산 실행 | 여러 에이전트에 작업을 나눠 돌립니다 |
| 산출물 보관 | 빌드 결과물과 테스트 리포트를 빌드 번호별로 남깁니다 |
| 알림 | 실패 시 메일 · Slack 등으로 알립니다(플러그인) |
이 저장소와 묶어 쓰는 그림
Gitea 에 push → 웹훅 → Jenkins 빌드 · 테스트 → Harbor 에 이미지 push
알아 둘 점
- 설치 직후에는 플러그인이 거의 없습니다. 첫 로그인 뒤 마법사에서 권장 플러그인을 설치하세요.
- 데이터가 전부
jenkins_home한 폴더에 쌓입니다(설정 · Job · 빌드 기록 · 플러그인). 백업 대상은 이 폴더입니다.
| 역할 | 컨테이너 | 이미지 |
|---|---|---|
| Jenkins 본체 | jenkins |
jenkins/jenkins:lts-jdk21 |
| 소유권 보정(1회성) | jenkins-init |
같은 이미지를 root 로 실행 |
- 데이터는 compose 폴더 아래 호스트 디렉터리
jenkins_home에 쌓입니다(Plane · Gitea 와 달리 named volume 이 아닙니다). - jenkins 이미지는 컨테이너 안에서 uid 1000 으로 동작합니다. 그래서 기동할 때마다
jenkins-init이 먼저jenkins_home의 소유자를 1000 으로 맞춥니다. 저장소를 클론한 호스트 계정의 uid 가 1000 이 아니어도missing rw permissions on JENKINS_HOME재시작 루프가 생기지 않습니다. 이 때문에 호스트에서는jenkins_home이 uid 1000 소유로 보입니다(master_service 도 동일). - 호스트 포트를 열지 않고 컨테이너 8080 을 앞단 nginx 가 프록시합니다.
-
proxy conf 준비 (저장소 루트에서)
P=config/web-server/nginx/php/proxy/jenkins cp "$P/jenkins_proxy.conf.example" "$P/jenkins_proxy.conf" # server_name 수정
-
.env생성 — 로그 설정만 있고 비밀값은 없습니다.D=compose/project_mng_service/nginx_jenkins cp "$D/.env-example" "$D/.env"
-
기동 —
--build는 업그레이드나 Dockerfile 변경 뒤 nginx 이미지를 다시 만들 때 붙입니다.cd compose/project_mng_service/nginx_jenkins docker compose up -d --build -
첫 로그인 — 초기 관리자 비밀번호를 꺼내 웹 UI 에 입력합니다.
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
자세한 내용은 Jenkins 공식 문서를 참고하세요.
공식 사이트: Gitea
한 줄 요약: 사내에 두는 GitHub 입니다. 저장소 호스팅에 웹 UI · 이슈 · 풀 리퀘스트 · 권한 관리가 딸려 오고, 컨테이너 하나로 돕니다.
개념 구조
| 개념 | 설명 |
|---|---|
| 사용자 · 조직 | 저장소 소유자. 조직 아래 팀을 만들고 팀 단위로 권한을 줍니다 |
| 저장소 | git 저장소 + 이슈 · PR · 위키 · 릴리스 |
| 풀 리퀘스트 | 브랜치 병합 요청. 리뷰 · 승인 · 병합 방식(merge · squash · rebase)을 정합니다 |
| 웹훅 | 이벤트(push · PR 등)를 외부로 알립니다 — Jenkins 자동 빌드 연동이 여기서 나옵니다 |
| 접근 방식 | SSH(공개키 등록 후 ssh://git@<도메인>:2222/…) 와 HTTP(비밀번호 · 토큰) 둘 다 |
주요 기능
| 기능 | 설명 |
|---|---|
| 저장소 호스팅 | private · public, 조직/팀 권한, 브랜치 보호 규칙 |
| 이슈 · PR | 라벨 · 마일스톤 · 리뷰 · 코드 코멘트 |
| 웹훅 · API | push 시 CI 트리거, REST API 로 저장소 · 사용자 · 이슈 관리 |
| 미러링 | 외부 저장소를 주기적으로 끌어오거나 내보냅니다 |
| 릴리스 | 태그에 바이너리를 붙여 배포합니다 |
| Actions | GitHub Actions 호환 CI (별도 runner 필요 — 이 저장소 구성에는 포함하지 않았습니다) |
GitHub · GitLab 과 비교
| Gitea | |
|---|---|
| 무게 | 컨테이너 1개, 메모리 수백 MB — GitLab 보다 훨씬 가볍습니다 |
| DB | 기본 내장 SQLite. 규모가 커지면 PostgreSQL 등 외부 DB 로 바꿉니다 |
| 범위 | 저장소 · 이슈 · PR 중심. CI 는 Jenkins 같은 별도 도구와 묶는 구성이 이 저장소의 기본입니다 |
알아 둘 점
- 기본값이 가입 차단(
GITEA_DISABLE_REGISTRATION=true)입니다. 계정은 관리자가 만듭니다. - git over SSH 는 호스트
2222를 씁니다. 클라우드면 보안 그룹에도 열어야 합니다.
| 역할 | 컨테이너 | 이미지 |
|---|---|---|
| Gitea 본체 | gitea |
gitea/gitea |
- 컨테이너 하나로 끝납니다. DB 는 내장 SQLite 를 씁니다. 외부 DB(PostgreSQL 등)로 바꾸려면
.env에GITEA__database__*를 추가합니다. - 데이터는 named volume
gitea-data한 곳에 모입니다(저장소 · DB · SSH 호스트키). - HTTP 는 호스트 포트를 열지 않고 컨테이너 3000 을 앞단 nginx 가 프록시합니다.
- git over SSH 는 컨테이너가 호스트
2222(GITEA_SSH_PORT)를 직접 게시합니다. 컨테이너 안에서는 이미지에 포함된 OpenSSH 가 22 번을 서비스합니다. - 설치 마법사는
INSTALL_LOCK으로 건너뜁니다 —.env값으로 설정이 확정됩니다.
-
.env생성 후 도메인 입력 — 비밀값은 없습니다.D=compose/project_mng_service/gitea cp "$D/.env-example" "$D/.env" # GITEA_DOMAIN · GITEA_ROOT_URL 수정
GITEA_ROOT_URL은 끝에/를 포함한 전체 URL 입니다(예:http://git.example.com/). 웹훅 · 클론 URL · OAuth 리다이렉트가 이 값을 그대로 씁니다. HTTPS 로 서비스하면https://로 적습니다. -
proxy conf 준비 (저장소 루트에서)
P=config/web-server/nginx/php/proxy/gitea cp "$P/gitea_proxy.conf.example" "$P/gitea_proxy.conf" # server_name 수정
-
기동
cd compose/project_mng_service/gitea docker compose up -d --build -
관리자 계정 생성 (최초 1회)
docker compose exec -u git gitea gitea admin user create \ --username <id> --password '<pw>' --email <mail> --admin
master_service에서는cd compose/master_service로 옮긴 뒤 위 명령에-f docker-compose-<stack>.yml을 붙입니다.기본값이
GITEA_DISABLE_REGISTRATION=true라 관리자만 계정을 만들 수 있습니다. 자체 가입을 허용하려면.env에서false로 바꾸세요. -
사용 — 웹 UI 에서 저장소를 만들고 SSH 공개키를 등록한 뒤
git clone ssh://git@<도메인>:2222/<계정>/<저장소>.git # SSH git clone http://<도메인>/<계정>/<저장소>.git # HTTP (개인 저장소는 토큰 · 비밀번호 인증)
-
백업
docker compose exec -u git gitea gitea dump -c /data/gitea/conf/app.ini -f /tmp/gitea-dump.zip docker compose cp gitea:/tmp/gitea-dump.zip ~/
⚠️ SSH 2222 를 외부에 열려면 방화벽만으로는 부족합니다. Docker 가 게시한 포트는 호스트 iptables 의 INPUT 체인을 거치지 않습니다. 클라우드라면 보안 그룹(OCI VCN 보안 목록 · AWS 보안 그룹 등)에도 인바운드 2222 를 추가해야 합니다. 빠뜨리면 사설 IP 로는 접속되는데 공인 도메인으로는 timeout 이 납니다.
자세한 내용은 Gitea 공식 문서를 참고하세요.
공식 사이트: Harbor
한 줄 요약: 사내에 두는 Docker Hub 입니다. 컨테이너 이미지를 외부에 올리지 않고 직접 보관 · 배포하며, 권한 · 스캔 · 서명 같은 기업용 기능이 붙습니다.
개념 구조
| 개념 | 설명 |
|---|---|
| 프로젝트 | 이미지를 담는 단위. public / private 를 정하고 멤버에게 역할(개발자 · 마스터 · 게스트)을 줍니다 |
| 리포지터리 · 태그 | <harbor 도메인>/<프로젝트>/<이미지>:<태그> 형태로 push · pull 합니다 |
| 로봇 계정 | CI 가 쓰는 전용 자격증명. 사람 계정과 분리하고 권한을 좁힙니다 |
| 복제(Replication) | 다른 레지스트리(Docker Hub · 다른 Harbor)와 이미지를 주기적으로 동기화합니다 |
| 보존 · GC | 오래된 태그를 규칙으로 지우고, 참조 없는 레이어를 정리해 디스크를 회수합니다 |
주요 기능
| 기능 | 설명 |
|---|---|
| 접근 제어 | 프로젝트 단위 권한, LDAP · OIDC 연동 |
| 취약점 스캔 | push 된 이미지의 CVE 를 검사하고, 심각도 기준으로 pull 을 막을 수 있습니다 |
| 이미지 서명 | 서명된 이미지만 배포되도록 강제합니다 |
| 감사 로그 | 누가 무엇을 push · pull · 삭제했는지 남깁니다 |
| 차트 · 아티팩트 | 컨테이너 이미지 외 Helm 차트 등 OCI 아티팩트도 보관합니다 |
이 저장소와 묶어 쓰는 그림
Jenkins 빌드 → docker push <harbor 도메인>/<프로젝트>/<앱>:<태그> → 운영 서버에서 pull
알아 둘 점
- 설치 방식이 다른 세 서비스와 완전히 다릅니다 — compose 정의가 아니라 Harbor 공식 installer 를 씁니다(아래 구성 참고).
- 자체 nginx 로 80 포트를 잡으므로 다른 서비스와 같은 호스트에 두지 않는 편이 좋습니다.
harbor.yml의hostname에는 루프백 IP(127.0.0.1)를 쓸 수 없습니다. Harbor 자체 검증이127.0.0.1 can not be the hostname으로 설치를 거부합니다 — 도메인이나 실제 IP 를 쓰세요.
이 저장소의 다른 서비스와 방식이 다릅니다. compose 정의를 제공하지 않습니다.
대신 Harbor 가 공식 배포하는 installer 를 번들해 두었습니다(compose/project_mng_service/harbor-v2.0.0/). installer 가 자체 compose 파일을 만들어 여러 컨테이너(포털 · core · registry · DB · job service 등)를 띄웁니다.
- installer 는
docker compose플러그인이 아니라docker-compose라는 이름의 명령을 직접 호출합니다(Compose v1 시절 스크립트). - Harbor 는 자체 nginx 로 http 포트(기본 80)를 씁니다.
-
레거시
docker-compose명령 준비common.sh의check_dockercompose가docker-compose --version출력을 1.18.0 이상으로 파싱하지 못하면[Step 1]에서 중단합니다:Need to install docker-compose(1.18.0+) by yourself first and run this script again.Compose v1 바이너리를 PATH 에 두거나,
docker compose플러그인을 부르는 래퍼를 만들면 됩니다. 버전 판정은 이름이docker-compose인 명령의 출력만 보므로 래퍼로도 통과합니다.printf '#!/bin/sh\ncase "$1" in --version|version) exec docker compose version ;; esac\nexec docker compose "$@"\n' \ | sudo tee /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose
래퍼가
--version을version으로 바꿔 전달하는 이유는, 최근 Compose 플러그인이docker compose --version에 버전 대신 사용법을 출력하기 때문입니다. 이 래퍼로 http · https 설치와 기동(포털 · API · 레지스트리 토큰)을 검증했습니다. -
설치 — root 로 실행합니다(Harbor 공식 안내와 동일).
cd compose/project_mng_service/harbor-v2.0.0 sudo bash autoinstall.sh # 설정 생성 + 설치를 한 번에
나눠서 하려면
bash update_harbor_config.sh로harbor.yml을 만든 뒤sudo bash install.sh를 실행합니다. 일반 사용자로 돌리면prepare가 만든 root 소유 600 파일 때문에[Step 4]에서permission denied로 멈춥니다. 설치 후docker-compose down·ps같은 명령도 같은 폴더에서sudo로 실행합니다. -
입력값 — 도메인, http 포트, https 사용 여부를 물어봅니다. 경로(ssl · data volume · log)는
/data처럼 일반 경로 그대로 적습니다(이스케이프 불필요). -
https 를 쓰려면 설치 전에 인증서를 아래 구조로 둡니다.
compose/project_mng_service/harbor-v2.0.0/ssl/letsencrypt/live/<도메인>/{fullchain,privkey}.pemautoinstall.sh가ssl/내용을 입력한 ssl path 아래로 복사하고(<ssl path>/letsencrypt/live/<도메인>/…),harbor.yml의 인증서 경로에 그 도메인을 넣습니다. 기본 경로/etc는 root 권한이 필요합니다. -
실행 권한 —
prepare만 저장소에 실행 권한(100755)으로 추적됩니다. 나머지(install.sh·autoinstall.sh·update_harbor_config.sh·common.sh)는100644라 위 예시처럼bash <스크립트>로 실행합니다../install.sh형태로 쓰려면 그 파일에만chmod +x하세요.
⚠️ 다른 서비스와 같은 호스트에 두지 마세요. Harbor 는 자체 nginx 로 80 포트를 잡습니다. 이 저장소의 웹 스택 · master_service · 단독 proxy 와 충돌합니다. 같은 호스트에 둬야 한다면update_harbor_config.sh에서 다른 http 포트를 지정하고, 앞단 nginx(예: master_service proxy conf)에서 그 포트로 프록시하세요.
자세한 내용은 Harbor 2.0 공식 문서를 참고하세요.
HTTP 로 먼저 띄운 뒤 인증서를 발급받고 HTTPS conf 로 교체하는 순서입니다.
master_service 폴더에는
docker-compose.yml이 없습니다. 아래 모든docker compose명령에-f docker-compose-<stack>.yml을 붙이세요. 예:docker compose -f docker-compose-gunicorn.yml exec webserver bash /script/letsencrypt.sh
-
HTTP conf 생성 —
config/web-server/nginx/<service>/nginx_http_conf.sh를 실행합니다. 도메인마다 conf 가conf.d/아래 만들어지고, 파일명은 항상_http로 끝납니다. -
HTTP 로 기동 — compose 폴더에서
docker compose up -d --build. (--build는 업그레이드나 Dockerfile ·uv.lock변경 뒤에만 필요합니다.) -
인증서 발급 —
docker compose exec webserver bash /script/letsencrypt.sh를 실행하고 도메인과 이메일을 입력합니다. ACME webroot 는/www/certbot으로 고정이라(생성되는 conf 와 proxy 샘플이 모두 여기서/.well-known/acme-challenge/를 서비스합니다) 따로 입력하지 않습니다. -
HTTPS conf 로 교체 — 같은 폴더의
nginx_https_conf.sh를 실행한 뒤,conf.d/에서 3번까지 쓰던_httpconf 를 지웁니다. -
proxy conf 는 수동입니다 — Plane · Jenkins · Gitea 의 proxy conf 에는 생성기가 없습니다. 인증서를 받은 뒤
config/web-server/nginx/php/proxy/<svc>/<svc>_proxy.conf에listen 443 ssl서버 블록을 직접 추가하세요(ssl 지시어는config/web-server/nginx/php/sample_nginx_https.conf참고).URL 을 담는
.env값도 함께https://로 바꿉니다 — Plane 은PLANE_WEB_URL·PLANE_CORS_ALLOWED_ORIGINS, Gitea 는GITEA_ROOT_URL. -
반영 — conf 만 바꿨으면 nginx 만 다시 읽히면 됩니다.
docker compose exec webserver nginx -t && docker compose exec webserver nginx -s reload
5번에서
.env값을 바꿨다면 해당 컨테이너만 다시 만듭니다.docker compose up -d plane-api plane-web plane-live docker compose up -d gitea
⚠️ 전체docker compose restart는 쓰지 마세요. 모든 서비스가 동시에 재시작하면서 app 이 내려간 순간 nginx 가 먼저 떠[emerg] host not found in upstream으로 한 번 죽을 수 있습니다(자동 재기동은 됩니다). 전체 재기동은stop→start순서로 하세요.⚠️ docker compose down -v도 쓰지 마세요. named volume(Plane 데이터 · Gitea 저장소 등)이 삭제됩니다. -
갱신 cron — certbot 갱신은 nginx 이미지 안에 내장돼 있습니다(
docker/nginx/Dockerfile이 빌드할 때 crontab 에 등록). 호스트에 별도 cron 을 걸 필요가 없습니다. 확인:docker compose exec webserver crontab -l.
- Jenkins · Gitea · Plane 간 시스템 연동
- docker-swarm, kubernetes 지원
- docker 및 오케스트레이션 모니터링 시스템
- 백업 및 보안 시스템
- AWS, GCM 등 클라우드 지원
- 개인 웹사이트 : 운영자 개인 웹사이트는 devspoon.com 입니다.
- 임도현 Owner Developer/project Manager, bluebamus@gmail.com
개인 github.io : bluebamus.github.io