Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Deploy to Amazon Linux EC2 (compose-prod)

on:
push:
branches: [ main ] # main 브랜치 푸시 시 배포

jobs:
deploy:
runs-on: ubuntu-latest

steps:
# 1) 코드 체크아웃
- name: Checkout
uses: actions/checkout@v4

# 2) JDK 설치 (Spring 빌드)
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'

# 3) Gradle 빌드 (테스트 생략 예시)
- name: Build Spring Boot
run: ./gradlew clean build -x test

# 4) EC2로 파일 전송
# - compose 파일: docker-compose-prod.yml
# - backendProject 폴더 안의 Dockerfile
# - CI에서 생성한 JAR (backendProject/build/libs/*.jar)
# - (있다면) .env
- name: Upload artifacts to EC2
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
source: |
docker-compose-prod.yml
backendProject/Dockerfile
backendProject/build/libs/*.jar
.env
target: ${{ secrets.EC2_APP_DIR }}

Comment on lines +33 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

.env 파일을 레포에서 전달하는 방식은 위험/취약 — 서버 측 생성으로 전환 권장

.env를 레포에서 전송하면 비밀정보 노출 위험이 있고, 레포에 파일이 없을 경우 scp 단계가 실패할 수 있습니다. 업로드 대상에서 .env를 제거하고, 다음과 같이 SSH 단계에서 서버 내에 비밀을 작성하는 방식을 권장합니다.

업로드 목록에서 제거:

           source: |
             docker-compose-prod.yml
             backendProject/Dockerfile
             backendProject/build/libs/*.jar
-            .env

서버에서 .env 생성(예시 — 필요한 키만 포함):

      - name: Write .env on EC2
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            set -e
            APP_DIR="${{ secrets.EC2_APP_DIR }}"
            mkdir -p "$APP_DIR"
            cat > "$APP_DIR/.env" << 'EOF'
            SPRING_PROFILES_ACTIVE=prod
            # 예: DB_URL, DB_USER, DB_PASSWORD, JWT_SECRET, ...
            DB_URL=${{ secrets.DB_URL }}
            DB_USER=${{ secrets.DB_USER }}
            DB_PASSWORD=${{ secrets.DB_PASSWORD }}
            JWT_SECRET=${{ secrets.JWT_SECRET }}
            EOF
            chmod 600 "$APP_DIR/.env"

원하시면 위 내용을 현재 비밀 키 이름 체계에 맞춰 구체화해 드리겠습니다.

# 5) EC2에서 배포 (Amazon Linux)
- name: Deploy on EC2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
set -e
APP_DIR="${{ secrets.EC2_APP_DIR }}"
cd "$APP_DIR"

# Docker/compose 설치(없을 때)
if ! command -v docker >/dev/null 2>&1; then
if command -v dnf >/dev/null 2>&1; then
sudo dnf -y update
sudo dnf -y install docker
else
sudo yum -y update
sudo yum -y install docker
fi
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker $USER || true
fi
if ! docker compose version >/dev/null 2>&1; then
if command -v dnf >/dev/null 2>&1; then
sudo dnf -y install docker-compose-plugin
else
sudo yum -y install docker-compose-plugin || true
fi
fi

# 기존 컨테이너 중지 후 재빌드/재기동
sudo docker compose -f docker-compose-prod.yml down || true
sudo docker compose -f docker-compose-prod.yml build --no-cache
sudo docker compose -f docker-compose-prod.yml up -d

# (옵션) 안 쓰는 이미지 정리
sudo docker image prune -f

# 6) (선택) CloudFront 캐시 무효화
- name: Configure AWS credentials
if: ${{ secrets.CF_DISTRIBUTION_ID != '' }}
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}

- name: Invalidate CloudFront
if: ${{ secrets.CF_DISTRIBUTION_ID != '' }}
run: |
Comment on lines +88 to +97

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

actionlint 경고: if 조건식에서 secrets 컨텍스트 사용 불가 — env로 우회 필요

actionlint 힌트대로 현재 위치의 if에서 secrets 컨텍스트를 사용할 수 없습니다. job-level env에 값을 매핑한 뒤 env를 참조하도록 수정하세요.

해당 if 라인 수정:

-      - name: Configure AWS credentials
-        if: ${{ secrets.CF_DISTRIBUTION_ID != '' }}
+      - name: Configure AWS credentials
+        if: ${{ env.CF_DISTRIBUTION_ID != '' }}
@@
-      - name: Invalidate CloudFront
-        if: ${{ secrets.CF_DISTRIBUTION_ID != '' }}
+      - name: Invalidate CloudFront
+        if: ${{ env.CF_DISTRIBUTION_ID != '' }}

job-level env 추가(파일 상단 jobs.deploy 아래에 삽입):

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      CF_DISTRIBUTION_ID: ${{ secrets.CF_DISTRIBUTION_ID }}
🧰 Tools
🪛 actionlint (1.7.7)

88-88: context "secrets" is not allowed here. available contexts are "env", "github", "inputs", "job", "matrix", "needs", "runner", "steps", "strategy", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details

(expression)


96-96: context "secrets" is not allowed here. available contexts are "env", "github", "inputs", "job", "matrix", "needs", "runner", "steps", "strategy", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details

(expression)

🤖 Prompt for AI Agents
.github/workflows/deploy.yml around lines 88 to 97: actionlint flags using the
secrets context directly in an if expression here; move the secret into a
job-level env and update the if to reference env.CF_DISTRIBUTION_ID instead of
secrets.CF_DISTRIBUTION_ID. Add under jobs.deploy (near top of jobs block) an
env mapping CF_DISTRIBUTION_ID: ${{ secrets.CF_DISTRIBUTION_ID }}, then change
the two if lines in this block to: if: ${{ env.CF_DISTRIBUTION_ID != '' }} so
the workflow uses env for conditional checks.

aws cloudfront create-invalidation \
--distribution-id "${{ secrets.CF_DISTRIBUTION_ID }}" \
--paths "/*"
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package likelion.mlb.backendProject.global.health;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HealthController {
@GetMapping("/health")
public ResponseEntity<String> healthCheck() {
return ResponseEntity.ok("ok");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers("/login.html", "/login-success.html").permitAll()
.requestMatchers("/api/**", "/oauth2/**", "/login.html").permitAll()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.requestMatchers("/health").permitAll()
.requestMatchers("/ws/**").permitAll()
.requestMatchers(
"/swagger-ui/**",
Expand Down
139 changes: 139 additions & 0 deletions docker-compose-prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
version: '3.8'

services:

api1:
build:
context: ./backendProject
container_name: mlb-api-1
ports:
- "8080:8080"
env_file:
- .env
depends_on:
mlb-redis1:
condition: service_healthy
mlb-elasticsearch:
condition: service_healthy
mlb-kafka:
condition: service_healthy
networks:
- backend

Comment on lines +5 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

API 컨테이너에 restart 정책과 헬스체크 추가 권장

배포 안정성을 위해 재시작 정책을 추가하고, 로드밸런서/오케스트레이션과의 연계를 위해 컨테이너 헬스체크를 정의하는 것을 권장합니다. 현 이미지에 curl/wget이 없을 수 있어, 가능하면 Dockerfile에 HEALTHCHECK를 정의하는 방식을 추천합니다.

Compose 측 최소 변경(재시작 정책):

   api1:
     build:
       context: ./backendProject
     container_name: mlb-api-1
+    restart: unless-stopped
     ports:
       - "8080:8080"

Dockerfile(backendProject/Dockerfile) 예시(이미지에 curl 설치 가능 시):

# 이미지에 curl이 없다면 설치 (예: debian/ubuntu 기반)
# RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

HEALTHCHECK --interval=30s --timeout=3s --retries=10 CMD curl -fsS http://localhost:8080/health || exit 1

검증 요청:

  • 현재 애플리케이션 베이스 이미지에 curl/wget/bash 등 어떤 툴이 포함되어 있는지 확인 부탁드립니다. 포함 여부에 따라 HEALTHCHECK 구현 방식을 조정하겠습니다.

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== docker-compose-prod.yml (first 200 lines) =="
sed -n '1,200p' docker-compose-prod.yml || true
echo

echo "== Check for backendProject/Dockerfile =="
if [ -f backendProject/Dockerfile ]; then
  echo "Found backendProject/Dockerfile - showing up to 300 lines:"
  sed -n '1,300p' backendProject/Dockerfile || true
else
  echo "backendProject/Dockerfile not found. Searching for Dockerfiles and relevant keywords in repo..."
  rg -n --hidden --glob '!.git' '(^FROM)|HEALTHCHECK|curl|wget|bash' || true
fi

Length of output: 4134


API 컨테이너에 restart 정책 추가 및 HEALTHCHECK 적용 필요 — 수정 필요

검증 결과: docker-compose-prod.yml의 api1/api2에 restart 정책이 빠져 있고, backendProject/Dockerfile(runtime: openjdk:17-jre-slim)에는 HEALTHCHECK가 주석 처리되어 있어 현재 헬스체크가 동작하지 않습니다. 아래 변경을 권장합니다.

주의할 점:

  • Spring Actuator를 사용 중이면 헬스 엔드포인트(/actuator/health 등)를 확인해 HEALTHCHECK 경로를 맞춰주세요.
  • 이미지에 curl/wget이 없으면 A 옵션 대신 B(TCP) 옵션을 사용하거나 Dockerfile에 curl을 설치하세요.

수정 대상:

  • docker-compose-prod.yml: api1, api2에 restart 정책 추가
  • backendProject/Dockerfile: runtime stage에 HEALTHCHECK 추가(옵션 A: curl 사용 / 옵션 B: TCP 검사)

제안된 변경(diff 예시)

docker-compose-prod.yml (간단 변경)

   api1:
     build:
       context: ./backendProject
     container_name: mlb-api-1
+    restart: unless-stopped
     ports:
       - "8080:8080"
@@
   api2:
     build:
       context: ./backendProject
     container_name: mlb-api-2
+    restart: unless-stopped
     ports:
       - "8081:8080"

backendProject/Dockerfile — Option A (curl 설치 후 Actuator 엔드포인트 검사)

FROM openjdk:17-jre-slim
WORKDIR /app
ENV JAVA_TOOL_OPTIONS="-XX:+UseG1GC -Xms256m -Xmx512m"

# curl 설치
RUN apt-get update && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/build/libs/*.jar /app/app.jar
EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD curl -fsS http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["java","-jar","/app/app.jar"]

backendProject/Dockerfile — Option B (추가 패키지 없이 TCP 포트 확인)

FROM openjdk:17-jre-slim
WORKDIR /app
ENV JAVA_TOOL_OPTIONS="-XX:+UseG1GC -Xms256m -Xmx512m"

COPY --from=builder /app/build/libs/*.jar /app/app.jar
EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD-SHELL "echo > /dev/tcp/127.0.0.1/8080" || exit 1

ENTRYPOINT ["java","-jar","/app/app.jar"]

간단 요청: 어떤 HEALTHCHECK 방식을 선호하는지(Actuator 엔드포인트 사용 / TCP 검사 / curl 설치) 알려주시면 Dockerfile 예시를 그에 맞춰 조정해 드리겠습니다.

api2:
build:
context: ./backendProject
container_name: mlb-api-2
ports:
- "8081:8080"
env_file:
- .env
depends_on:
mlb-redis1:
condition: service_healthy
mlb-elasticsearch:
condition: service_healthy
mlb-kafka:
condition: service_healthy
networks:
- backend

Comment on lines +23 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

두 번째 API 컨테이너도 동일하게 재시작 정책과 헬스체크 정렬

api2에도 동일 정책을 적용해 이중화 시 한쪽 다운 시 자동 복구를 보장하세요.

  api2:
     build:
       context: ./backendProject
     container_name: mlb-api-2
+    restart: unless-stopped
     ports:
       - "8081:8080"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
api2:
build:
context: ./backendProject
container_name: mlb-api-2
ports:
- "8081:8080"
env_file:
- .env
depends_on:
mlb-redis1:
condition: service_healthy
mlb-elasticsearch:
condition: service_healthy
mlb-kafka:
condition: service_healthy
networks:
- backend
api2:
build:
context: ./backendProject
container_name: mlb-api-2
restart: unless-stopped
ports:
- "8081:8080"
env_file:
- .env
depends_on:
mlb-redis1:
condition: service_healthy
mlb-elasticsearch:
condition: service_healthy
mlb-kafka:
condition: service_healthy
networks:
- backend
🤖 Prompt for AI Agents
In docker-compose-prod.yml around lines 23 to 40, api2 is missing the same
restart policy and healthcheck configuration used for the other API container;
add a restart policy (e.g., restart: unless-stopped or restart: always) and a
healthcheck block mirroring the existing API service (providing test, interval,
timeout, retries and start_period) so the service can be auto-restarted and
marked healthy/unhealthy for failover; ensure the new healthcheck uses the same
command/port and timing values as the other API to keep behavior consistent.

# Redis - 3 인스턴스
mlb-redis1:
image: redis:7.2
container_name: mlb-redis1
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 10
networks:
- backend

mlb-redis2:
image: redis:7.2
container_name: mlb-redis2
ports:
- "6380:6379"
networks:
- backend

mlb-redis3:
image: redis:7.2
container_name: mlb-redis3
ports:
- "6381:6379"
networks:
- backend

# Elasticsearch
mlb-elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
container_name: mlb-elasticsearch
environment:
- node.name=es01
- discovery.type=single-node
- xpack.security.enabled=false
- xpack.security.transport.ssl.enabled=false
- ES_JAVA_OPTS=-Xms512m -Xmx512m
ports:
- "9200:9200"
- "9300:9300"
volumes:
- es_data:/usr/share/elasticsearch/data
healthcheck:
# TCP 열림 확인(의존 패키지 없이 동작)
test: ["CMD-SHELL", "bash -lc 'echo > /dev/tcp/127.0.0.1/9200'"]
interval: 10s
timeout: 3s
retries: 30
networks:
- backend

Comment on lines +71 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

Elasticsearch 보안/노출 설정 점검 필요: xpack 비활성화 + 포트 외부 공개는 프로덕션에서는 위험

  • xpack.security 비활성화 상태에서 9200/9300을 호스트로 노출하고 있어 외부 접근이 가능해집니다. 내부 네트워크에서만 접근하도록 포트 노출 제거 또는 보안 그룹/방화벽 제한을 권장합니다.
  • 헬스체크가 bash의 /dev/tcp 의존(“bash -lc”)에 묶여 있습니다. 이미지에 bash가 없으면 실패합니다. 가능하면 HTTP 핑(curl/wget)을 사용하거나 Dockerfile에 HEALTHCHECK를 정의하세요.

예시(포트 외부 미노출):

   mlb-elasticsearch:
     image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
     container_name: mlb-elasticsearch
@@
-    ports:
-      - "9200:9200"
-      - "9300:9300"
+    # 내부 통신만 필요하다면 포트 노출 제거 권장
+    # ports:
+    #  - "9200:9200"
+    #  - "9300:9300"

보안을 유지해야 하는 프로덕션이라면:

  • xpack.security.enabled=true로 전환하고 사용자/비밀번호 설정, TLS 구성 고려.
  • 외부 노출 시 보안 그룹/방화벽으로 접근 제한 필수.

긴급: docker-compose-prod.yml — Elasticsearch 보안과 헬스체크 수정 필요

검증 결과: 원 리뷰의 우려가 타당합니다. 현재 구성(xpack 비활성화 + 9200/9300 호스트 노출, bash 기반 헬스체크)은 프로덕션에서 위험합니다.

파일/위치

  • docker-compose-prod.yml — Lines 71-94 (서비스: mlb-elasticsearch)

문제 요약

  • environment에서 xpack.security.enabled=false 및 xpack.security.transport.ssl.enabled=false — 보안 비활성화 상태.
  • ports에 "9200:9200", "9300:9300" — 호스트에 포트가 노출되어 외부 접근 가능.
  • healthcheck가 "bash -lc 'echo > /dev/tcp/127.0.0.1/9200'"에 의존 — 이미지에 bash/해당 기능이 없으면 실패 가능.

권장 조치 (우선순위)

  • 프로덕션에서는 xpack.security.enabled=true 및 transport TLS 활성화(xpack.security.transport.ssl.enabled=true)로 전환하고 사용자/비밀번호와 TLS(인증서) 설정을 반드시 구성하세요.
  • 호스트 노출 제거: ports 매핑 제거 또는 내부 전용(expose 또는 네트워크 격리) 사용. 외부 노출이 필요하면 보안 그룹/방화벽으로 접근을 제한하세요.
  • 헬스체크 수정: bash-/dev/tcp 의존을 제거하고 HTTP 엔드포인트(예: /_cluster/health)를 이용한 체크로 교체하거나, 필요한 바이너리(curl/wget)를 포함해 Dockerfile에서 헬스체크를 정의하세요.

권장 변경 예시

   mlb-elasticsearch:
     image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
     container_name: mlb-elasticsearch
     environment:
       - node.name=es01
       - discovery.type=single-node
-      - xpack.security.enabled=false
-      - xpack.security.transport.ssl.enabled=false
+      # 프로덕션: 보안 활성화 권장 (추가 설정 필요: 계정/비밀번호, TLS 인증서)
+      - xpack.security.enabled=true
+      - xpack.security.transport.ssl.enabled=true
@@
-    ports:
-      - "9200:9200"
-      - "9300:9300"
+    # 내부 통신만 필요하면 호스트 포트 노출 제거 또는 expose 사용
+    # ports:
+    #  - "9200:9200"
+    #  - "9300:9300"
@@
-    healthcheck:
-      test: ["CMD-SHELL", "bash -lc 'echo > /dev/tcp/127.0.0.1/9200'"]
+    healthcheck:
+      # 이미지에 curl/wget이 없을 수 있으므로 Dockerfile에서 헬스체크 정의 고려
+      test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:9200/_cluster/health || exit 1"]

짧게 요약하면: 현재 설정은 프로덕션 환경에 부적합합니다. 보안 활성화, 포트 노출 제거/제한, 헬스체크 방법 개선을 적용해 주세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Elasticsearch
mlb-elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
container_name: mlb-elasticsearch
environment:
- node.name=es01
- discovery.type=single-node
- xpack.security.enabled=false
- xpack.security.transport.ssl.enabled=false
- ES_JAVA_OPTS=-Xms512m -Xmx512m
ports:
- "9200:9200"
- "9300:9300"
volumes:
- es_data:/usr/share/elasticsearch/data
healthcheck:
# TCP 열림 확인(의존 패키지 없이 동작)
test: ["CMD-SHELL", "bash -lc 'echo > /dev/tcp/127.0.0.1/9200'"]
interval: 10s
timeout: 3s
retries: 30
networks:
- backend
# Elasticsearch
mlb-elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
container_name: mlb-elasticsearch
environment:
- node.name=es01
- discovery.type=single-node
# 프로덕션: 보안 활성화 권장 (추가 설정 필요: 계정/비밀번호, TLS 인증서)
- xpack.security.enabled=true
- xpack.security.transport.ssl.enabled=true
- ES_JAVA_OPTS=-Xms512m -Xmx512m
# 내부 통신만 필요하면 호스트 포트 노출 제거 또는 expose 사용
# ports:
# - "9200:9200"
# - "9300:9300"
volumes:
- es_data:/usr/share/elasticsearch/data
healthcheck:
# 이미지에 curl/wget이 없을 수 있으므로 Dockerfile에서 헬스체크 정의 고려
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:9200/_cluster/health || exit 1"]
interval: 10s
timeout: 3s
retries: 30
networks:
- backend
🤖 Prompt for AI Agents
In docker-compose-prod.yml around lines 71-94 (service: mlb-elasticsearch), the
compose disables Elasticsearch security, exposes 9200/9300 to the host, and uses
a fragile bash /dev/tcp healthcheck; update this for production by enabling
xpack (set xpack.security.enabled=true and
xpack.security.transport.ssl.enabled=true) and ensure you provision TLS certs
and Elasticsearch users/passwords (or integrate with a secrets store), remove
the host port mappings (delete "9200:9200" and "9300:9300" or replace with
expose + internal network only / firewall rules) so ES is not directly reachable
from the host network, and replace the healthcheck with an HTTP-based check
(call /_cluster/health or /_node/health with curl/wget) or ensure the image
provides a supported healthcheck binary rather than relying on bash /dev/tcp.

# Zookeeper (for Kafka)
mlb-zookeeper:
image: confluentinc/cp-zookeeper:7.4.1
container_name: mlb-zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ports:
- "2181:2181"
healthcheck:
test: ["CMD-SHELL", "bash -lc 'echo > /dev/tcp/127.0.0.1/2181'"]
interval: 10s
timeout: 3s
retries: 30
networks:
- backend

# Kafka
mlb-kafka:
image: confluentinc/cp-kafka:7.4.1
container_name: mlb-kafka
depends_on:
- mlb-zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: mlb-zookeeper:2181
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://mlb-kafka:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
healthcheck:
test: ["CMD-SHELL", "bash -lc 'echo > /dev/tcp/127.0.0.1/9092'"]
interval: 10s
timeout: 3s
retries: 30
networks:
- backend

volumes:
es_data:

networks:
backend:
driver: bridge