함께하개의 로컬 개발 환경입니다. DB · 메시지 큐 · 플랫폼 서비스를 컨테이너로 한 번에 띄웁니다.
먼저 전체 그림을 보고, 이 레포가 그 안 어디에 있는지 본 뒤 읽습니다.
① 전체 구조 — 층으로 본 것. 위에서 아래로 요청이 내려가고, 어느 층에 무엇이 있는지.
② 전체 구조 — 서비스끼리 무엇을 주고받는지. 초록 실선이 /internal 호출, Kafka 표가 이벤트, 하늘색 점선이 VPC 경계.
③ 이 레포를 중심으로. 직접 연결된 것만 남긴 그림.
①② 는
service-template/docs에 있는 것을 가리킵니다. 서비스가 늘어도 그쪽 한 곳만 고칩니다.
[ 내 컴퓨터 ]
IntelliJ 로 띄우는 것 지금 고치고 있는 서비스
│ localhost 로 아래에 붙음
▼
Docker 네트워크 (pawtrail)
│
├── db postgres DB 10개 · 계정 10개
│
├── infra kafka · redis *거의 항상 켜 둠
│
├── platform config-server 8888 설정을 내려 줌
│ eureka-server 8761 주소 장부
│ gateway-server 8080 모든 요청의 입구
│
├── app auth-service 8081 개발이 끝난 도메인 서비스
│ user-service 8082 (app 에 들어갈 12개 중 넷)
│ pet-service 8083
│ place-service 8084
│
├── pipeline ingest-service 8088 수집 배치
│ *상시 기동이 아님
│
├── tools kafka-ui 9000 토픽·메시지 보기
│
└── observability prometheus 9090 · loki 3100 필요할 때만
zipkin 9411 · grafana 3000
서비스를 하나 개발하려면 그 뒤에 여러 개가 필요합니다.
| 만들려는 것 | 필요한 것 |
|---|---|
user-service |
PostgreSQL · Kafka · Redis · config-server · eureka-server · gateway-server |
| 그것들을 각자 설치하면 | 버전이 갈리고 "제 컴퓨터에서는 되는데" 가 생김 |
| 여기서 띄우면 | 명령 한 줄. 누구 컴퓨터에서든 같은 버전 |
숫자로 보면 이렇습니다.
| 값 | 어디에 | |
|---|---|---|
| 컨테이너 | 13개 | 1-1 |
| 프로파일 | 7개 | 3장 |
| DB | 10개 | 4-2 |
| Kafka 토픽 | 12개 | 4-3 |
| 코드 | 없음 | compose · 셸 스크립트 · 설정 파일만 |
파일이 이게 전부입니다.
paw-trail/infra
│
├── docker-compose.yml 컨테이너 13개
├── .env ⛔커밋 안 됨. 각자 만듦
├── .env.example 그 목록과 설명
│
├── init-db/
│ ├── 01-databases.sh DB 10개 · 계정 10개 · 권한
│ └── 02-extensions.sh PostGIS · pg_trgm
│
├── kafka/create-topics.sh 토픽 12개
├── prometheus/prometheus.yml 수집 대상
└── grafana/provisioning/ 데이터소스 자동 등록
Docker 를 써 본 적이 없어도 이 네 가지만 알면 됩니다.
① 컨테이너란 — 설치 없이 프로그램을 띄우는 방법
직접 설치하면 컨테이너로 띄우면
PostgreSQL 설치 프로그램 받기 docker compose up -d
설치 · 설정 · 서비스 등록 │
Kafka 도 · Redis 도 · ... └── 정해진 이미지를 받아 그대로 띄움
│ 내 컴퓨터에 아무것도 설치 안 됨
└── 사람마다 버전이 갈림 지우면 흔적 없이 사라짐
"제 컴퓨터에서는 되는데"
이미지는 설치가 끝난 상태를 통째로 찍어 둔 것이고, 컨테이너는 그 이미지를 띄운 것입니다.
postgis/postgis:17-3.5 처럼 버전이 이름에 박혀 있어 누구 컴퓨터든 같습니다.
② docker compose 란 — 여러 컨테이너를 한 파일로
docker run postgres ... 컨테이너 하나를 손으로
docker run kafka ...
docker run redis ...
│
▼
docker-compose.yml 13개를 파일 하나에 적어 두고
docker compose up -d 한 번에 띄움
▲
└── -d : 백그라운드로. 안 붙이면 터미널이 로그에 묶임
| 명령 | 뜻 |
|---|---|
up -d |
띄움 (백그라운드) |
down |
내림 |
ps |
상태 보기 |
logs -f <이름> |
로그 보기 (-f 는 계속 따라감) |
exec <이름> <명령> |
컨테이너 안에서 명령 실행 |
③ "프로파일" 이 둘 있습니다 — 서로 다른 것입니다
| Compose 프로파일 | 스프링 프로파일 | |
|---|---|---|
| 무엇 | 어느 컨테이너를 띄울지 고르는 태그 | 서비스가 어디서 도는지 알리는 이름 |
| 값 | db · infra · platform · app … |
local · dev · prod |
| 어디에 | .env 의 COMPOSE_PROFILES |
컨테이너의 SPRING_PROFILES_ACTIVE |
| 이 문서에서 | 3장 전체 | 컨테이너는 전부 dev |
이 문서에서 "프로파일" 이라고만 쓰면 Compose 프로파일입니다.
④ 여기 뜨는 것들이 무엇인가
| 컨테이너 | 한 줄로 | 우리 서비스가 쓰는 곳 |
|---|---|---|
| PostgreSQL | 데이터베이스 | 계정 · 장소 · 후기 … 전부 |
| Kafka | 우체통. 서비스끼리 직접 부르지 않고 편지를 남김 | account.created 같은 이벤트 |
| Redis | 빠른 메모장. 잠깐 두는 값 | 리프레시 토큰 · 인증 코드 |
| config-server | 설정을 나눠 주는 곳 | 모든 서비스가 기동할 때 |
| eureka-server | 주소 장부 | 게이트웨이가 서비스를 찾을 때 |
| gateway-server | 모든 요청의 입구 | 브라우저가 부르는 유일한 포트 |
| Kafka UI | Kafka 안을 들여다보는 화면 | 토픽에 메시지가 실렸는지 |
| Prometheus · Grafana | 지표 수집 · 화면 | 요청 수 · 응답 시간 |
| Loki | 로그 모아 두는 곳 | 서비스 로그 검색 |
| Zipkin | 요청 하나가 서비스 몇 개를 지났나 | 느린 화면 원인 찾기 |
| 지금 하려는 일 | 볼 곳 |
|---|---|
| 처음 세팅한다 | 1장 → 2장 |
| 무엇을 띄울지 고르고 싶다 | 3장 |
| 포트·DB·토픽이 뭐가 있는지 | 4장 |
| 자주 쓰는 명령 | 5장 |
| IntelliJ 로 서비스를 띄우려 한다 | 6장 |
| 플랫폼 이미지를 다시 굽는다 | 7장 |
| 뭔가 안 된다 | 8장 |
| Windows · macOS 차이 | 9장 |
| 모르는 말이 나온다 | 11장 |
| 프로파일 | 컨테이너 | 포트 | 메모리 | 언제 켜나 |
|---|---|---|---|---|
db |
postgres | 5432 | 1g | 항상 |
infra |
kafka | 9092 · 29092 | 1g | 항상 |
| redis | 6379 | 256m | 항상 | |
platform |
config-server | 8888 | 512m | 항상 |
| eureka-server | 8761 | 512m | 항상 | |
| gateway-server | 8080 | 512m | 항상 | |
app |
auth-service · user-service · pet-service · place-service | 8081 · 8082 · 8083 · 8084 | 각 640m | 그 서비스를 안 고칠 때 |
pipeline |
ingest-service | 8088 | 640m | ⛔수집을 돌릴 때만 |
tools |
kafka-ui | 9000 | 512m | 토픽을 볼 때 |
observability |
prometheus | 9090 | 512m | 지표를 볼 때 |
| loki | 3100 | 512m | ||
| zipkin | 9411 | 512m | ||
| grafana | 3000 | 512m |
Kafka UI 가 9000 인 이유 — 원래 8080 인데 게이트웨이와 겹칩니다.
메모리를 4GB 이상 줍니다.
Settings → Resources → Memory
| 조합 | 대략 |
|---|---|
infra,platform,db,tools |
약 4.2G |
+ app (auth) |
약 4.8G |
+ observability |
약 6.8G |
16GB 머신이면 관측 스택은 필요할 때만 켭니다. macOS 4.5G + 컨테이너 + Docker VM 0.5G 를 대략 잡으면 그렇습니다.
git clone https://github.com/paw-trail/infra.git
cd infra셸 스크립트가 있어 줄바꿈이 중요합니다.
.gitattributes가.sh를 LF 로 강제하므로 Windows 에서 클론해도 정상입니다. 직접 만든.sh를 넣을 때는 편집기 우하단이 LF 인지 확인합니다 — CRLF 면 컨테이너 안에서\r: command not found가 납니다.
① .env.example 을 복사해 .env 를 만들고 값을 채움
│ POSTGRES_PASSWORD · SERVICE_DB_PASSWORD · GRAFANA_PASSWORD
│ app 을 켤 거면 AUTH_ 값 3개도
▼
② docker compose config --services 오타 검사. 서비스 목록이 나오면 정상
│
▼
③ docker compose up -d .env 의 COMPOSE_PROFILES 조합대로
│
├──▶ postgres 가 처음 뜰 때 init-db 스크립트가 돎
│ DB 10개 · 계정 10개 · PostGIS · pg_trgm
│
▼
④ docker compose ps 전부 (healthy) 인지
│
▼
⑤ kafka/create-topics.sh 토픽 12개. 멱등이라 여러 번 실행해도 됨
│
▼
⑥ 확인 localhost:8761 · localhost:9000
macOS
cp .env.example .envWindows (PowerShell)
copy .env.example .env채워야 하는 값입니다.
| 이름 | 값 | 필수 |
|---|---|---|
COMPOSE_PROFILES |
infra,platform,db,tools |
✓ |
POSTGRES_USER |
pawtrail |
✓ |
POSTGRES_PASSWORD |
아무 값 | ✓ |
SERVICE_DB_PASSWORD |
아무 값 — 서비스 계정 10개가 공유 | ✓ |
GRAFANA_USER · GRAFANA_PASSWORD |
아무 값 | ✓ |
AUTH_JWT_PRIVATE_KEY_B64 |
팀장에게 받음 | app 켤 때만 |
AUTH_MAIL_PASSWORD |
팀장에게 받음 | app 켤 때만 |
AUTH_OAUTH_GOOGLE_CLIENT_SECRET |
팀장에게 받음 | app 켤 때만 |
AWS_ACCESS_KEY_ID |
팀장에게 받음 | app 켤 때만 |
AWS_SECRET_ACCESS_KEY |
팀장에게 받음 | app 켤 때만 |
OPENAI_API_KEY |
팀장에게 받음 | app 켤 때만 |
⛔
.env는 커밋되지 않습니다..gitignore에 있습니다. 비밀값을.env.example에 적지 않습니다.
SERVICE_DB_PASSWORD를 나중에 바꾸려면 DB 를 다시 만들어야 합니다. init 스크립트는 볼륨이 비어 있을 때만 돌기 때문입니다.
docker compose config --services서비스 목록이 나오면 정상입니다.
| 결과 | 뜻 |
|---|---|
| 목록이 나옴 | YAML 과 .env 가 정상 |
services: {} |
COMPOSE_PROFILES 가 비었거나 오타 |
| YAML 오류 | 들여쓰기 문제 |
docker compose pull
docker compose up -d
docker compose ps
pull을 먼저 합니다.up -d는 이미지가 없을 때만 내려받고, 이미 갖고 있으면 낡았더라도 그대로 씁니다. 플랫폼과 도메인 서비스 이미지는 다른 사람이 고쳐 다시 올리므로 내 컴퓨터의 사본이 조용히 뒤처집니다.처음 받을 때는 없던 이미지라
up -d만으로도 같지만, 습관으로 붙여 둡니다.
STATUS 가 전부 (healthy) 가 될 때까지 40초쯤 걸립니다.
NAME STATUS PORTS
pawtrail-postgres Up 40 seconds (healthy) 0.0.0.0:5432->5432/tcp
pawtrail-kafka Up 40 seconds (healthy) 0.0.0.0:9092->9092/tcp, 0.0.0.0:29092->29092/tcp
pawtrail-redis Up 40 seconds (healthy) 0.0.0.0:6379->6379/tcp
...
(health: starting)은 아직 확인 중이라는 뜻입니다. 기다립니다.
postgres 가 처음 뜰 때 init 스크립트가 돕니다.
[init-db] creating auth_db / auth_svc
[init-db] creating user_db / user_svc
...
[init-db] done: 10 databases
docker compose logs postgres | grep init-db컨테이너 안에서 실행합니다.
docker compose exec kafka bash /opt/scripts/create-topics.sh토픽 12개가 만들어집니다. 스크립트가 멱등이라 여러 번 실행해도 안전합니다.
자동 생성을 꺼 두었습니다. 켜 두면 컨슈머의 토픽명 오타가 조용히 빈 토픽을 만들어 "발행은 되는데 소비만 안 되는" 상태가 됩니다.
| 주소 | 무엇 |
|---|---|
http://localhost:8761 |
유레카 대시보드 — 플랫폼이 등록됐는지 |
http://localhost:9000 |
Kafka UI — 토픽 12개 |
http://localhost:8888/place-service/local |
설정이 내려오는지 |
http://localhost:3000 |
Grafana (observability 를 켰다면) |
모든 컨테이너에 profiles: 가 붙어 있습니다. 켠 프로파일에 해당하는 것만 뜹니다.
.env
COMPOSE_PROFILES=infra,platform,db,tools
docker compose up -d
│
└──▶ .env 의 조합이 뜸 ✅ 평소에 이렇게 씀
docker compose --profile app up -d
│
└──▶ ⛔ .env 값이 통째로 대체됨
활성 프로파일이 app 하나
│
└── auth 의 depends_on 이 가리키는
config-server · postgres 가 프로젝트에 없음
service "auth-service" depends on undefined service
→ invalid compose project
한 번만 다르게 띄우려면 전부 나열
docker compose --profile infra --profile platform --profile db --profile tools --profile app up -d
| 상황 | COMPOSE_PROFILES |
|---|---|
| auth 를 고치는 중 | infra,platform,db,tools |
| auth 를 컨테이너로 띄움 | infra,platform,db,tools,app |
| 대시보드·추적을 볼 때 | 위 조합 + ,observability |
| 수집 배치를 돌릴 때 | 위 조합 + ,pipeline |
.env는 사람마다 다른 파일입니다. 각자 자기 방식대로 두면 됩니다. auth 를 고치지 않는 팀원은app을 넣어 두는 편이 편합니다.⚠
app을 켤 때는 IntelliJ 의 auth 를 먼저 멈춥니다. 8081 이 겹칩니다.
docker compose down활성 프로파일에 없는 컨테이너는 안 내려갑니다.
.env 에 db 가 없는데 --profile db 로 postgres 를 띄웠다면
│
└── docker compose down 이 그것을 대상으로 안 잡음
Resource is still in use 로 네트워크 삭제도 실패
해결
docker compose --profile db down
# 또는
docker rm -f pawtrail-postgres8080 gateway-server *모든 API 요청의 입구
8081 auth-service 직접 확인할 때만
8088 ingest-service ⛔경로가 전부 /internal 이라 이쪽으로만 부를 수 있음
8761 eureka-server 대시보드
8888 config-server 설정
9000 kafka-ui *8080 이 아님 (게이트웨이와 겹침)
9090 prometheus
3000 grafana
3100 loki
9411 zipkin
5432 postgres
6379 redis
9092 kafka 컨테이너끼리
29092 kafka *호스트에서 붙을 때
Kafka 포트가 둘인 이유입니다.
컨테이너 안에서 kafka:9092 같은 도커 네트워크
호스트에서 localhost:29092 IntelliJ 로 띄운 서비스
한 브로커인데 리스너가 둘입니다. config 의 application-local.yml 은 29092 를,
application-dev.yml 은 9092 를 씁니다.
도메인 서비스 포트 배정입니다.
| 포트 | 서비스 |
|---|---|
| 8081 | auth |
| 8082 | user |
| 8083 | pet |
| 8084 | place |
| 8085 | policy |
| 8086 | verdict |
| 8087 | search |
| 8088 | ingest |
| 8089 | extract |
| 8090 | congestion |
| 8091 | route |
| 8092 | report |
| 8093 | notification |
| 8094 | review |
| 8095 | template (검증용) |
postgres 컨테이너 하나
│
├── auth_db auth_svc user_db user_svc
├── pet_db pet_svc place_db place_svc
├── policy_db policy_svc search_db search_svc
├── raw_db ingest_svc report_db report_svc
└── review_db review_svc notif_db notif_svc
계정이 자기 DB 에만 붙을 수 있습니다.
REVOKE CONNECT ON DATABASE ${db} FROM PUBLIC;
GRANT ALL PRIVILEGES ON DATABASE ${db} TO ${role};PostgreSQL 은 기본적으로 PUBLIC 에 CONNECT 를 줌
│
└── 이 줄이 없으면 auth_svc 가 place_db 에 그냥 접속됨
*Database per Service 를 문서가 아니라 권한으로 강제하는 자리
확인
docker compose exec postgres psql -U pawtrail -c "\l"DB 가 없는 서비스도 있습니다.
verdict · congestion · route 무상태
extract raw_db 를 읽고 policy 에 넘김 (자기 DB 없음)
PostGIS 와 pg_trgm 이 깔립니다.
| 확장 | 왜 |
|---|---|
postgis |
좌표 · 거리 검색 (place · search · route) |
pg_trgm |
오타·부분일치 검색 |
PostGIS 는 신뢰 확장이 아니라
CREATE EXTENSION에 슈퍼유저가 필요합니다. 그래서 init 스크립트가 만듭니다.
도메인 이벤트 6개 + 각각의 .dlq
place.updated place ──▶ search
policy.changed policy ──▶ notification · verdict
pet.profile.updated pet ──▶ verdict
account.created auth ──▶ user
account.withdrawn auth ──▶ user · pet · report · review · notification
report.resolved report ──▶ notification
.dlq 는 재시도가 끝난 메시지가 가는 곳입니다.
컨슈머가 실패
│
└── 1초 → 2초 → 4초 로 3회 재시도
│
└── 그래도 실패하면 {원본토픽}.dlq 로 보내고 넘어감
*그 메시지 하나 때문에 뒤가 막히지 않게
DLQ 조회 API 를 만들지 않았습니다. Kafka UI(9000)로 봅니다.
토픽 자동 생성을 껐습니다.
켜 두면
│
└── 컨슈머의 토픽명 오타가 조용히 빈 토픽을 만듦
발행은 되는데 소비만 안 되는 상태
서비스 ──▶ Loki 3100 로그
──▶ Zipkin 9411 추적 (요청 하나가 서비스 몇 개를 지났나)
◀── Prometheus 9090 지표 (긁어 감)
│
▼
Grafana 3000 셋을 한 화면에
Grafana 데이터소스는 자동 등록됩니다.
grafana/provisioning/datasources/datasources.yml
│
└── Prometheus · Loki · Zipkin 셋이 기동할 때 붙음
* Connections 메뉴에서 손으로 추가할 필요 없음
Prometheus 타깃을 추가할 때입니다.
# prometheus/prometheus.yml
- targets: ["host.docker.internal:8084"]
labels:
application: place-service # *블록째 추가⚠
metrics_path를 빠뜨리면 조용히 아무것도 안 모입니다. 기존 블록을 복사해 값만 바꾸는 편이 안전합니다.추적은 게이트웨이에서 시작됩니다. 서비스를 8084 로 직접 부르면 Zipkin 에서 전체 경로를 못 봅니다.
| 볼륨 | down 하면 |
|
|---|---|---|
| postgres | postgres_data |
남습니다 |
| kafka | 없음 | 사라집니다 |
| redis | 없음 | 사라집니다 |
| prometheus · loki · grafana | 없음 | 사라집니다 |
Kafka 는 일부러 볼륨을 안 잡음
│
└── apache/kafka 이미지가 비루트로 도는데
이미지에 없는 경로에 빈 볼륨을 마운트하면
그 디렉터리가 root 소유로 생겨 브로커가 못 씀
│
└── 토픽 생성 스크립트가 멱등이라 재실행 한 번이면 복구됨
DB 를 완전히 비우려면
docker compose --profile db down -v
docker compose up -d-v 가 볼륨을 지웁니다. 그러면 init 스크립트가 다시 돌아 DB 10개가 새로
만들어집니다.
# 기동 · 중지
docker compose up -d
docker compose down # 볼륨은 남음
docker compose --profile db down -v # ⚠DB 까지 지움
# 상태
docker compose ps
docker compose logs -f auth-service
docker compose logs postgres | grep init-db
# 이미지 갱신 — up -d 만으로는 안 받음
docker compose pull auth-service # 하나만
docker compose pull # 켜 둔 것 전부
docker compose up -d
# 토픽 재생성 (멱등)
docker compose exec kafka bash /opt/scripts/create-topics.sh
# DB 접속
docker compose exec postgres psql -U auth_svc -d auth_db -P pager=off
# Redis
docker compose exec redis redis-cli KEYS '*'
-P pager=off가 없으면 결과가 넓을 때 멈춘 것처럼 보입니다.
docker compose restart <서비스>는 컨테이너만 다시 시작합니다. 이미지를 바꾸려면pull+up -d입니다.
IntelliJ 로 서비스를 띄우려면 세 가지가 필요합니다.
① 컨테이너가 떠 있음 docker compose up -d
│
② IntelliJ 실행 구성에 환경변수
│
③ config 저장소에 그 서비스 파일 <서비스명>.yml
│
▼
기동
│
└──▶ *게이트웨이로 부르려면 라우트도 열어야 함
안 열면 서비스는 정상인데 404
Run → Edit Configurations → Environment variables
| 이름 | 값 | 누가 |
|---|---|---|
DB_HOST |
localhost |
DB 를 쓰는 서비스 |
SERVICE_DB_PASSWORD |
.env 와 같은 값 |
같음 |
AUTH_JWT_PRIVATE_KEY_B64 |
팀장에게 받음 | auth 만 |
AUTH_MAIL_PASSWORD |
팀장에게 받음 | auth 만 |
AUTH_OAUTH_GOOGLE_CLIENT_SECRET |
팀장에게 받음 | auth 만 |
DB_HOST=localhost;SERVICE_DB_PASSWORD=...
⚠
.env는 Docker Compose 가 읽는 파일이라 IntelliJ 와 무관합니다. 같은 값을 두 곳에 넣게 되는데, 한 곳만 관리하고 싶으면 OS 환경변수에 둡니다. 대신 터미널·IntelliJ 재시작이 필요합니다.
DB_HOST 가 컨테이너와 다릅니다.
IntelliJ (호스트에서 돎) localhost:5432
컨테이너 (도커 안에서 돎) postgres:5432
│
└── *같은 postgres 컨테이너인데 부르는 이름만 다름
컨테이너 안에서 localhost 는 자기 자신이라 DB 가 없음
컨테이너 쪽은 compose 에 박혀 있어 아무도 안 건드려도 됩니다.
빠뜨렸을 때 나오는 오류입니다.
| 빠진 것 | 증상 |
|---|---|
DB_HOST |
UnknownHostException: ${DB_HOST} — 치환이 안 돼 문자열 그대로 들어감 |
SERVICE_DB_PASSWORD |
password authentication failed for user "auth_svc" |
계정이 없으면 role does not exist 이므로 이 메시지는 비밀번호 문제 |
빌드는 환경변수 없이도 통과합니다. Testcontainers 가 자기 DB 를 띄우기 때문에 실제로 띄울 때가 되어서야 드러납니다.
세 가지가 다 되어야 합니다.
① 서비스가 유레카에 등록됐나 localhost:8761 에 보이는지
② config 에 <서비스명>.yml 이 있나 포트가 8080 으로 뜨면 없는 것
③ *게이트웨이 라우트가 열려 있나 ← 제일 빠지기 쉬움
③을 빠뜨리면 서비스는 정상인데 404 입니다.
curl http://localhost:8080/actuator/gateway/routes여는 법은 config README 5-2 에 있습니다.
IntelliJ 로 띄운 서비스도 게이트웨이가 찾아냅니다.
local 프로파일 → host.docker.internal 로 유레카에 등록
│
└── 그 이름이 컨테이너 안에서도 호스트를 가리킴
게이트웨이가 IntelliJ 서비스를 호출할 수 있음
EC2 PostgreSQL 을 아직 세우지 않았습니다.
auth · user · pet 은 공유할 데이터가 없음
│
└── 오히려 공유하면 서로의 테스트 계정이 섞여 성가심
EC2 가 필요해지는 것은 ingest 착수 시점
│
└── 수집 데이터 4,600건을 공유해야 하고
공공 API 쿼터가 하루 1,000건이라 각자 채울 수 없음
⚠ DB 를 공유하면 위험합니다. 한쪽이 Flyway 스크립트를 추가하면 공유 DB 에 즉시 적용되어 다른 쪽 스키마가 모르는 사이에 바뀝니다.
플랫폼·도메인 서비스의 코드를 고쳤을 때만 합니다.
① 서비스 저장소에서 빌드
./gradlew clean build
│
▼
② ghcr 로그인 기기당 한 번
│
▼
③ docker buildx build --platform linux/amd64,linux/arm64 --push
│
▼
④ 팀원은 docker compose pull && docker compose up -d
Windows (PowerShell)
$env:GPR_TOKEN | docker login ghcr.io -u <GitHub 아이디> --password-stdinmacOS
echo $GPR_TOKEN | docker login ghcr.io -u <GitHub 아이디> --password-stdin환경변수에 토큰이 없으면 값을 직접 넣어도 됩니다. 다만 셸 이력에 남습니다.
| 걸리는 것 | 증상 |
|---|---|
| 윈도우 계정명을 씀 | denied: denied — GitHub 아이디여야 함 |
토큰에 write:packages 가 없음 |
같음 |
자격증명은 Docker Desktop 이 기억하므로 기기당 한 번입니다.
조직명이
pawtrail이 아니라paw-trail입니다. 자바 패키지와 갈려 있습니다.
Windows (PowerShell)
cd C:\Tour_Prj\<서비스>
.\gradlew clean build
docker buildx build --platform linux/amd64,linux/arm64 `
-t ghcr.io/paw-trail/<서비스>:latest --push .macOS
cd ~/<서비스>
./gradlew clean build
docker buildx build --platform linux/amd64,linux/arm64 \
-t ghcr.io/paw-trail/<서비스>:latest --push .줄 이어 쓰기 문자가 다릅니다. PowerShell 은 백틱, macOS 는 역슬래시입니다. 한 줄로 쓰면 둘 다 그 문자가 필요 없습니다.
buildx 로 두 아키텍처를 함께 굽습니다. 굽는 기기가 무엇이든 결과가 같습니다.
| 걸리는 것 | 왜 |
|---|---|
--push 를 빼면 아무것도 안 남음 |
여러 아키텍처를 담은 이미지는 로컬 저장소에 넣을 수 없습니다. --load 는 한 아키텍처만 가능하고, 둘 다 빼면 굽기만 하고 버립니다 |
multiple platforms feature is currently not supported |
빌더를 한 번 만들어야 합니다 (아래) |
docker buildx create --name multiarch --driver docker-container --use --bootstrap왜 두 아키텍처를 다 굽는가
배포 서버는 x86 이고 팀원 중에 Apple Silicon 맥이 있습니다. 한쪽만 담으면 다른 쪽에서
no matching manifest for linux/arm64/v8로 컨테이너가 아예 뜨지 않습니다.부담은 거의 없습니다. Dockerfile 에
RUN이 하나도 없고 jar 를 복사하는 것뿐이라 다른 아키텍처를 흉내내어 명령을 실행할 일이 없습니다. 두 레이어가 각각 1초 안에 끝납니다.
확인 — 아키텍처
docker buildx imagetools inspect ghcr.io/paw-trail/<서비스>:latestlinux/amd64 와 linux/arm64 가 둘 다 나와야 합니다.
MediaType: application/vnd.oci.image.index.v1+json
Platform: linux/amd64
Platform: linux/arm64
Platform: unknown/unknown ← 빌드 증명, 정상입니다
Platform: unknown/unknown
MediaType이image.index여야 합니다.image.manifest하나만 나오면 아키텍처가 하나뿐인 이미지이며, 다른 아키텍처에서는 내려받지 못합니다.
unknown/unknown두 줄은 각 아키텍처의 빌드 증명입니다. 실행에 관여하지 않습니다.
확인 — 내용물
docker run --rm --entrypoint sh ghcr.io/paw-trail/<서비스>:latest -c "ls -lh /app"app.jar 가 수십 MB 면 정상입니다. 몇 KB 면 -plain.jar 가 담긴 것인데
build.gradle 에 tasks.named('jar') { enabled = false } 가 있어 지금은 안 생깁니다.
⚠
docker run <이미지> ls는 동작하지 않습니다.ENTRYPOINT가 있으면 뒤 명령이 인자로 붙습니다.--entrypoint sh가 필요합니다.
컨테이너 이미지는 기본이 비공개라 팀원이 못 받습니다.
GitHub 조직 → Packages → 해당 패키지 → Package settings
│
└── Change visibility → Public
⚠ 회색으로 막혀 있으면 조직 정책입니다. 조직 Settings → Packages 에서
Public을 허용하면 풀립니다.Maven 패키지(공통 모듈)는 레포 공개 여부를 따라가고 컨테이너 이미지만 이 정책을 따로 받습니다.
=> [linux/arm64 3/3] COPY build/libs/*.jar app.jar 0.8s
=> [linux/amd64 3/3] COPY build/libs/*.jar app.jar 0.8s
=> exporting to image 16.6s
=> => exporting manifest list sha256:de2f7230... 0.0s
=> => pushing layers 9.4s
아키텍처별 레이어가 각각 1초 안에 끝납니다. Dockerfile 이 jar 를 복사하는
것뿐이라 다른 아키텍처를 흉내내어 명령을 실행할 일이 없습니다.
exporting manifest list가 나와야 멀티아치입니다. 이 줄이 없으면 아키텍처가 하나뿐인 이미지입니다.
베이스 이미지가 같아 실제로 올라가는 것은 우리 jar 하나뿐입니다. 같은 층은 다시 올리지 않고 다른 저장소의 것을 가져다 씁니다.
| 증상 | 원인 |
|---|---|
docker compose config 가 services: {} |
COMPOSE_PROFILES 가 비었거나 오타 |
depends on undefined service |
--profile 이 .env 를 대체함. 3장 |
variable is not set 경고 |
.env 에 그 값이 없음 |
network ... not found |
이전 down 이 덜 끝남 → docker compose down 후 다시 |
up -d 가 Starting 에서 멈춤 |
Docker Desktop 이 먹통 — 아래 |
ports are not available ... forbidden |
윈도우 예약 포트 — 아래 |
no matching manifest for linux/arm64/v8 |
이미지에 그 아키텍처가 없음 — 아래 |
Docker Desktop 이 먹통일 때 (Windows)
# 1. Ctrl+C 로 중단
# 2. WSL 을 내림
wsl --shutdown
# 3. 그래도 멈춰 있으면
Get-Process "*docker*" | Stop-Process -Force
# 4. Docker Desktop 다시 실행윈도우 예약 포트 (Windows)
Hyper-V 가 동적 포트 범위를 잡아 두면 그 안의 포트를 못 씀
# 확인
netsh int ipv4 show excludedportrange protocol=tcp
# 범위를 위로 옮김 (관리자 권한, 재부팅 필요)
netsh int ipv4 set dynamicport tcp start=49152 num=16384no matching manifest for linux/arm64/v8 (주로 macOS)
이미지에 그 컴퓨터의 아키텍처가 담겨 있지 않다는 뜻입니다.
[+] Running 7/8
✘ auth-service Error context canceled
⠹ config-server Pulling
✘ postgres Error context canceled
✘ kafka Error context canceled
...
no matching manifest for linux/arm64/v8 in the manifest list entries
⚠
Error context canceled에 속지 않습니다. 하나가 실패하면 도커가 나머지를 전부 취소하면서 붙는 표시입니다. 실제 원인은 마지막 한 줄뿐입니다.
.env나 compose 문제가 아닙니다. 파일은 정상인데 받을 이미지가 없는 것입니다.
| 확인 | |
|---|---|
| 어느 이미지인지 | docker buildx imagetools inspect ghcr.io/paw-trail/<서비스>:latest |
| 정상 | MediaType 이 image.index 이고 linux/amd64 · linux/arm64 가 둘 다 |
| 문제 | image.manifest 하나뿐이거나 아키텍처가 하나만 |
고치는 사람은 그 이미지를 올린 사람입니다. 7-2 의 buildx
명령으로 다시 굽습니다. 옛 docker build 로 구우면 구운 기기의 아키텍처 하나만
담깁니다.
다시 올라온 뒤에는 받는 쪽에서 이렇게 합니다.
docker compose pull
docker compose up -d
pull을 먼저 합니다.up -d만으로는 이미 가진 이미지를 그대로 쓰고, 실패한 상태가 남아 있으면 고쳐지지 않습니다.
먼저 init 이 성공했는지 봅니다. 아래 대부분의 증상이 여기서 갈립니다.
docker compose logs postgres | grep "init-db"[init-db] done: 10 databases 와 [init-db] extensions done 이 둘 다 보여야 정상입니다.
⚠ PostgreSQL 은 init 스크립트가 실패해도 컨테이너를 그대로 띄웁니다. 그래서 계정이 하나도 없는 상태로 기동되고, 문제는 한참 뒤 서비스를 붙일 때
password authentication failed로 나타납니다. 원인과 증상이 다른 자리에서 드러나므로 비밀번호부터 의심하지 말고 위 로그를 먼저 확인하십시오.
| 증상 | 원인 | 조치 |
|---|---|---|
init 로그에 bad interpreter: Permission denied |
★스크립트에 실행 권한이 없음 | 아래 별도 항목 |
| DB 가 안 만들어짐 | 볼륨이 이미 있음. init 은 처음 한 번만 돎 | down -v 후 다시 |
role does not exist |
계정이 없음 — 위와 같음 | 같음 |
password authentication failed + init 로그에 done 이 없음 |
★계정이 아예 안 만들어짐. 비밀번호 문제가 아님 | init 실패 원인을 먼저 해결한 뒤 down -v |
password authentication failed + init 로그에 done 이 있음 |
계정은 있고 비밀번호만 다름 | SERVICE_DB_PASSWORD 확인 |
| 비밀번호를 바꿨는데 안 먹음 | 계정은 처음 만들 때의 값을 유지함. .env 를 고쳐도 안 바뀜 |
down -v 또는 ALTER USER |
Flyway Detected applied migration not resolved locally |
볼륨에 옛 이력이 남음 | down -v |
docker compose --profile db down -v
docker compose up -d계정과 DB 가 실제로 있는지는 이렇게 봅니다.
docker exec -it pawtrail-postgres psql -U pawtrail -c "\du" # 계정 10개
docker exec -it pawtrail-postgres psql -U pawtrail -c "\l" # DB 10개init 로그가 이렇게 끝나 있으면 스크립트 파일에 실행 권한이 없는 것입니다.
/docker-entrypoint-initdb.d/01-databases.sh: /bin/bash: bad interpreter: Permission denied
윈도우에서 커밋했을 때 생깁니다. 윈도우 파일시스템에는 실행 비트라는 개념이 없어
Git 이 파일을 100644 로 스테이징하고, 커밋한 사람은 아무 이상을 못 봅니다.
반면 macOS 에서는 그 파일을 실행하지 못해 init 이 통째로 실패합니다.
확인 — 전부 100755 여야 합니다.
git ls-files -s init-db/ kafka/100644 가 하나라도 있으면 저장소를 고쳐야 합니다. 각자 로컬에서 chmod 하는 것은
임시방편이며, 다음 사람이 클론하면 그대로 재발합니다.
# Windows — chmod 가 없으므로 Git 에 직접 지정합니다
git update-index --chmod=+x init-db/01-databases.sh
git update-index --chmod=+x init-db/02-extensions.sh
git update-index --chmod=+x kafka/create-topics.sh
git commit -m "fix: mark container entrypoint scripts as executable"# macOS
chmod +x init-db/*.sh kafka/*.sh
git add init-db kafka
git commit -m "fix: mark container entrypoint scripts as executable"고친 뒤에는 볼륨을 지우고 다시 띄워야 init 이 다시 돕니다.
git pull
docker compose down -v && docker compose up -d
.gitattributes가 줄바꿈(CRLF)은 막아주지만 실행 비트는 막지 못합니다. 컨테이너가 실행하는 스크립트를 새로 추가할 때마다 위 확인을 거치십시오.
| 증상 | 원인 |
|---|---|
UNKNOWN_TOPIC_OR_PARTITION 반복 |
토픽을 안 만들었음 → create-topics.sh |
| 토픽이 사라짐 | 볼륨이 없어 down 하면 사라짐. 재실행하면 복구 |
| 호스트에서 접속이 안 됨 | 29092 를 씀. 9092 는 컨테이너끼리 |
| Kafka UI 에서 오프셋 리셋이 안 됨 | 그 컨슈머 그룹이 활성 상태. 서비스를 먼저 내릴 것 |
| 증상 | 원인 |
|---|---|
| Grafana 에 Connections 메뉴가 없음 | 프로비저닝으로 이미 등록됨. Explore 에서 바로 고르면 됨 |
| Prometheus 타깃이 DOWN | 그 서비스가 꺼져 있음. connection refused 면 이름은 풀린 것 |
타깃이 no such host |
주소 해석 실패 — 이름이 잘못됨 |
| 지표가 안 모임 | metrics_path 를 빠뜨렸음 |
| Loki 에 로그가 없음 | 서비스가 local 프로파일이면 안 보냅니다 (의도) |
# Loki 가 받고 있는지
curl http://localhost:3100/loki/api/v1/labels응답에 data 필드가 있어야 합니다.
| 증상 | 원인 |
|---|---|
WRONGTYPE Operation against a key... |
그 키에 예전 자료형이 남아 있음. 아래 참고 |
| 키가 통째로 사라짐 | 볼륨이 없어 컨테이너를 내리면 사라집니다 (의도) |
| 호스트에서 접속이 안 됨 | 6379 를 씁니다. 컨테이너 이름은 pawtrail-redis |
자료형을 바꾼 키가 있습니다.
recent:places:{accountId} 는 처음에 목록이었다가 정렬 집합으로 바뀌었습니다.
같은 키 이름을 그대로 쓰므로, 그 이전 코드로 값을 넣어 둔 Redis 에서는
새 코드가 그 키를 건드리는 순간 WRONGTYPE 이 납니다.
# 자료형 확인 — zset 이어야 합니다
docker exec -it pawtrail-redis redis-cli TYPE "recent:places:{accountId}"
# list 로 나오면 지웁니다. 최근 본 장소 목록이라 지워도 기능에 지장이 없습니다
docker exec -it pawtrail-redis redis-cli DEL "recent:places:{accountId}"옮기는 코드를 두지 않은 것은 의도입니다. 그 값이 남아 있는 곳이 없기 때문입니다 — 배포한 적이 없고, 로컬에 있던 검증용 몇 건은 바꾸면서 지웠습니다. 없는 데이터를 위해 상시 코드를 두면 그것이 영영 안 쓰이는 채로 남습니다.
PowerShell curl |
Invoke-WebRequest 별칭 → curl.exe |
| 한글 바디 전송 | -d '{"..."}' 의 따옴표를 먹음 → Set-Content 로 파일에 쓰고 -d "@파일" |
| 줄바꿈 | .sh 는 반드시 LF. CRLF 면 \r: command not found |
| 예약 포트 | 8-1 |
| WSL 메모리 | Docker Desktop 이 WSL2 위에서 돎 |
⚠ 셸 스크립트를 추가하거나 수정하면 실행 권한을 함께 커밋하십시오. 윈도우에는 실행 비트가 없어 Git 이
100644로 스테이징하며, 커밋한 쪽에서는 아무 이상이 보이지 않습니다. macOS 에서만bad interpreter: Permission denied로 init 이 실패하고, 증상은 한참 뒤password authentication failed로 나타납니다.git update-index --chmod=+x <파일경로> git ls-files -s init-db/ kafka/ # 전부 100755 인지 확인
| 이미지 아키텍처 | 신경 쓰지 않아도 됩니다. 7-2 절의 buildx 명령이 amd64 와 arm64 를 함께 굽습니다 |
| 어느 기기에서 굽든 결과가 같으므로 맥에서 올려도 배포 서버(x86)에서 돕니다 | |
| 메모리 | Docker Desktop 기본이 낮을 수 있음 → 4GB 이상 |
⚠
postgis/postgis는 amd64 로 돌아갑니다. compose 에platform: linux/amd64를 명시해 두었으므로 "platform does not match" 경고가 떠도 의도된 동작입니다. 나머지 이미지는 모두 arm64 를 지원합니다.에뮬레이션이라 기동이 조금 느리고 CPU 를 더 씁니다. 다만 실패의 원인이 되지는 않으므로,
db프로파일에서 문제가 생기면 아키텍처보다 8-2 의 init 로그를 먼저 확인하십시오. 실제로 겪은 사고는 전부 아키텍처가 아니라 init 실패였습니다.
| 언제 | 무엇 |
|---|---|
| 도메인 서비스가 완성될 때마다 | compose app 프로파일에 추가 (지금 auth · user · pet · place) |
| EC2 PostgreSQL | 수집 데이터를 공유해야 할 때. ⛔아직 각자 로컬 |
| pet 이 생기면 | scripts/seed.sh — 테스트 데이터 시드 |
| nginx 를 붙일 때 | edge 프로파일 · nginx.conf |
| extract 를 만들 때 | pipeline 프로파일에 추가 |
아직 compose 에 없는 것입니다.
edge nginx
pipeline extract (ingest 는 들어감)
app 도메인 서비스 8개 (auth · user · pet · place 는 들어감)
해당 저장소가 완성되고 ghcr 에 이미지가 올라간 뒤에 추가합니다.
| 용어 | 뜻 |
|---|---|
| 이미지 | 설치가 끝난 상태를 통째로 찍어 둔 것. postgis/postgis:17-3.5 |
| 컨테이너 | 이미지를 띄운 것. 프로세스 하나 |
| 볼륨 | 컨테이너를 지워도 남기고 싶은 데이터를 두는 곳. postgres_data |
| 네트워크 | 컨테이너끼리 이름으로 서로를 찾는 범위. pawtrail |
| 프로파일 (Compose) | 어느 컨테이너를 띄울지 고르는 태그. db · infra … |
| 프로파일 (스프링) | 서비스가 어디서 도는지. local · dev · prod. 위와 다른 것 |
.env |
Compose 가 읽는 환경변수 파일. 커밋 안 됨 |
| 헬스체크 | 컨테이너가 "정말로 준비됐나" 를 주기적으로 묻는 것. (healthy) |
depends_on |
다른 컨테이너가 healthy 가 된 뒤에 뜨라는 순서 |
host.docker.internal |
컨테이너 안에서 호스트(내 컴퓨터) 를 가리키는 이름 |
| 토픽 | Kafka 의 우체통 하나. account.created |
| DLQ | 재시도가 끝난 메시지가 가는 토픽. {토픽}.dlq |
| 멱등 | 여러 번 실행해도 결과가 한 번과 같음. create-topics.sh 가 그러함 |
| init 스크립트 | postgres 가 처음 뜰 때 한 번만 도는 셸. DB·계정을 만듦 |
| PostGIS | PostgreSQL 에 좌표·거리 계산을 더하는 확장 |
| pg_trgm | 오타·부분 일치 검색을 위한 확장 |
| ghcr | GitHub 의 컨테이너 이미지 저장소. ghcr.io/paw-trail/... |
| 매니페스트 목록 | 아키텍처별 이미지를 묶어 둔 것. 받는 쪽이 자기 아키텍처를 골라 감 |
| 멀티아치 이미지 | 매니페스트 목록을 가진 이미지. buildx build --platform linux/amd64,linux/arm64 로 만듦 |
| 레이어 | 이미지를 이루는 층. 같은 층은 다시 올리지 않음 |