Skip to content
Open
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
15 changes: 15 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
node_modules/
dist/
build/
coverage/
.idea/
.vscode/
.git/
.gitignore
.dockerignore
Dockerfile
docker-compose.yml
k8s/
README*.md
*.log
.env*
86 changes: 86 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# syntax=docker/dockerfile:1.7
# Multi-stage build for egovframe-template-simple-react.
# Stage 1 builds the static Vite bundle with Node.
# Stage 2 serves it from nginx as a non-root user.

# ---------- Build ----------
FROM node:22-alpine AS build
WORKDIR /workspace

# Reproducible install: use lock file
COPY package.json package-lock.json ./
RUN npm ci

COPY index.html vite.config.js ./
COPY public ./public
COPY src ./src

# SERVER_URL 은 기본값(/api) 상대경로를 사용하므로 빌드 시 백엔드 호스트를 주입하지 않는다.
# 백엔드 호스트는 런타임에 nginx /api/ 프록시(BACKEND_URL)로 결정된다.
RUN npm run build

# ---------- Runtime ----------
FROM nginxinc/nginx-unprivileged:1.27-alpine

# 백엔드 주소. nginx 공식 이미지의 envsubst 단계(/docker-entrypoint.d/20-envsubst-on-templates.sh)가
# 컨테이너 기동 시 default.conf.template 의 ${BACKEND_URL} 를 이 값으로 치환한다.
ENV BACKEND_URL=http://egov-simple-backend:8080

# Static bundle
COPY --from=build /workspace/dist /usr/share/nginx/html

# nginx 설정 템플릿. /etc/nginx/templates/*.template -> envsubst -> /etc/nginx/conf.d/ 로 전개된다.
# ${BACKEND_URL} 만 치환하고 nginx 변수($uri 등)는 그대로 두기 위해 치환 대상을 한정한다.
ENV NGINX_ENVSUBST_TEMPLATE_SUFFIX=.template \
NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx/conf.d \
NGINX_ENVSUBST_FILTER=BACKEND_URL

COPY <<'NGINX' /etc/nginx/templates/default.conf.template
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;

# 백엔드 API 리버스 프록시.
# 프론트엔드는 SERVER_URL=/api 기준으로 호출하므로(/api/board, /api/auth/login-jwt 등)
# 여기서 /api prefix 를 제거하고 백엔드(context-path=/)로 전달한다.
# 쿠키 기반 JWT(httpOnly) 전달을 위해 헤더를 보존한다.
location /api/ {
proxy_pass ${BACKEND_URL}/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "";
proxy_cookie_path / /;
# 첨부파일 업로드 대비 (게시판/갤러리)
client_max_body_size 20m;
proxy_read_timeout 120s;
}

# SPA fallback for client-side routing
location / {
try_files $uri $uri/ /index.html;
}

# Cache static assets aggressively (Vite emits hashed filenames)
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}

# Disable cache for index.html
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
NGINX

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://127.0.0.1:8080/ >/dev/null || exit 1
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,77 @@ npm run test
npm run test:run
```

## 컨테이너 배포 (Docker / Kubernetes)

정적 번들을 nginx 로 호스팅하는 컨테이너 이미지와 Kubernetes 매니페스트를 제공한다.
nginx 가 `/api/*` 요청을 백엔드로 리버스 프록시하므로, 프론트엔드는 백엔드 주소를 빌드 시점에 고정하지 않고
**동일 출처 상대경로(`/api`)** 로 호출한다. 백엔드 주소는 런타임에 `BACKEND_URL` 환경변수로 주입한다.

> API base 는 `src/config.js` 의 `SERVER_URL` 로 결정되며 기본값은 `/api` 이다.
> 별도 호스트로 직접 호출해야 하는 경우에만 빌드 시 `VITE_APP_API_BASE_URL` 로 절대 URL 을 지정한다.

### 1. 이미지 빌드

Dockerfile 은 heredoc(`COPY <<'NGINX' ...`) 구문을 사용하므로 **BuildKit 이 필요**하다.
Docker 20.10+ 에서는 기본 활성화되어 있으며, 비활성 환경이라면 `DOCKER_BUILDKIT=1` 을 지정한다.

```bash
DOCKER_BUILDKIT=1 docker build -t egovframe-template-simple-react:5.0.0 .
```

### 2. 로컬 실행 (docker compose)

#### 2-1. 프론트엔드만 실행 (백엔드는 외부에서 구동 중인 경우)

```bash
# 외부 백엔드 주소를 BACKEND_URL 로 지정 (기본값: http://egov-simple-backend:8080)
BACKEND_URL=http://host.docker.internal:8080 docker compose up -d web

# http://localhost:3000/ 접속 (WEB_PORT 로 변경 가능)
```

#### 2-2. 백엔드까지 함께 실행 — 로그인/CRUD 통합 (fullstack 프로파일)

백엔드 이미지와 DB 초기화 스크립트가 필요하다. 다음을 전제로 한다.

- [심플 홈페이지 Backend](https://github.com/eGovFramework/egovframe-template-simple-backend.git) 를 본 저장소와 **같은 상위 디렉토리**에 clone (DB 초기화 SQL 마운트 경로 기준, 다르면 `BACKEND_DB_DIR` 로 조정)
- 백엔드 이미지 사전 빌드: `egovframe-template-simple-backend:5.0.0`

```bash
# 백엔드 이미지 빌드 (백엔드 저장소에서)
git clone https://github.com/eGovFramework/egovframe-template-simple-backend.git
(cd egovframe-template-simple-backend && DOCKER_BUILDKIT=1 docker build -t egovframe-template-simple-backend:5.0.0 .)

# DB 비밀번호 등 .env 작성 (프론트엔드 저장소 루트)
cat > .env <<'ENV'
MYSQL_ROOT_PASSWORD=changeit_root
MYSQL_PASSWORD=changeit_app
ENV

# 프론트(nginx) + 백엔드 + MySQL 동시 기동
docker compose --profile fullstack up -d

# http://localhost:3000/ 접속 후 admin / Admin@1234 로 로그인
```

이 구성에서 브라우저 → nginx(`/api/*`) → 백엔드(`:8080`) → MySQL 경로로 로그인과 게시판 CRUD 가 한 번에 동작한다.

### 3. Kubernetes 배포

```bash
kubectl apply -f k8s/

# Service 는 ClusterIP 이므로 port-forward 로 접속한다.
kubectl port-forward svc/egov-simple-react 3000:8080
# http://localhost:3000/ 접속
```

백엔드 연동: `k8s/deployment.yaml` 의 `BACKEND_URL` 환경변수가 같은 네임스페이스의 백엔드 Service
(`http://egov-simple-backend:8080`)를 가리키도록 기본 설정되어 있다. 백엔드를 함께 배포한 뒤
클러스터 구성에 맞게 값을 조정한다(예: `http://egov-simple-backend.<namespace>.svc.cluster.local:8080`).
nginx 컨테이너는 `readOnlyRootFilesystem` 이므로 기동 시 `BACKEND_URL` 치환 결과를
`emptyDir`(`/etc/nginx/conf.d`)에 기록한다.

---

### 참조
Expand Down
62 changes: 62 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
services:
# 프론트엔드(nginx 정적 호스팅 + /api 리버스 프록시)
web:
build: .
image: egovframe-template-simple-react:${APP_VERSION:-5.0.0}
container_name: egov-simple-react
ports:
- "${WEB_PORT:-3000}:8080"
environment:
# nginx 가 /api/* 요청을 전달할 백엔드 주소.
# 기본값은 아래 fullstack 프로파일의 backend 서비스를 가리킨다.
- BACKEND_URL=${BACKEND_URL:-http://egov-simple-backend:8080}
restart: unless-stopped

# 백엔드(심플 홈페이지 Backend) — fullstack 프로파일에서만 기동되어 로그인/CRUD 까지 한 번에 동작한다.
# 전제: egovframe-template-simple-backend:${BACKEND_VERSION} 이미지가 미리 빌드되어 있어야 한다.
# git clone https://github.com/eGovFramework/egovframe-template-simple-backend.git
# cd egovframe-template-simple-backend && docker build -t egovframe-template-simple-backend:5.0.0 .
backend:
image: egovframe-template-simple-backend:${BACKEND_VERSION:-5.0.0}
container_name: egov-simple-backend
profiles: ["fullstack"]
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_TYPE=mysql
- DB_URL=jdbc:mysql://mysql-db:3306/${MYSQL_DATABASE:-egovdb}?useSSL=false&allowPublicKeyRetrieval=true
- DB_USERNAME=${MYSQL_USER:-egov}
- DB_PASSWORD=${MYSQL_PASSWORD:?MYSQL_PASSWORD must be set in .env}
depends_on:
mysql-db:
condition: service_healthy
restart: unless-stopped

mysql-db:
image: mysql:${MYSQL_VERSION:-8.0.39}
container_name: mysql-db
profiles: ["fullstack"]
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD must be set in .env}
MYSQL_DATABASE: ${MYSQL_DATABASE:-egovdb}
MYSQL_USER: ${MYSQL_USER:-egov}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?MYSQL_PASSWORD must be set in .env}
command: >
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
volumes:
- mysql-data:/var/lib/mysql
# 최초 기동 시 스키마(DDL)/데이터(DML)를 적재한다.
# 경로는 백엔드 저장소를 이 저장소와 같은 상위 디렉토리에 clone 했다고 가정한다.
# 위치가 다르면 BACKEND_DB_DIR 환경변수로 조정한다.
- ${BACKEND_DB_DIR:-../egovframe-template-simple-backend/DATABASE}/all_sht_ddl_mysql.sql:/docker-entrypoint-initdb.d/01-ddl.sql:ro
- ${BACKEND_DB_DIR:-../egovframe-template-simple-backend/DATABASE}/all_sht_data_mysql.sql:/docker-entrypoint-initdb.d/02-data.sql:ro
healthcheck:
test: ["CMD-SHELL", "mysql --protocol=TCP -h 127.0.0.1 -u root --password=$$MYSQL_ROOT_PASSWORD -e 'SELECT 1' || exit 1"]
interval: 5s
timeout: 5s
retries: 30
start_period: 10s
restart: unless-stopped

volumes:
mysql-data: {}
89 changes: 89 additions & 0 deletions k8s/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: egov-simple-react
labels:
app.kubernetes.io/name: egov-simple-react
app.kubernetes.io/part-of: egovframe-template
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: egov-simple-react
template:
metadata:
labels:
app.kubernetes.io/name: egov-simple-react
app.kubernetes.io/part-of: egovframe-template
spec:
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
containers:
- name: web
# Replace with your registry/tag, e.g. ghcr.io/<org>/egov-simple-react:5.0.0
image: egovframe-template-simple-react:5.0.0
imagePullPolicy: IfNotPresent
env:
# nginx 가 /api/* 요청을 전달할 백엔드 주소.
# 같은 네임스페이스에 백엔드 Service(egov-simple-backend:8080)가 있다고 가정한다.
# 클러스터 구성에 맞게 수정한다(예: http://egov-simple-backend.<ns>.svc.cluster.local:8080).
- name: BACKEND_URL
value: "http://egov-simple-backend:8080"
ports:
- name: http
containerPort: 8080
resources:
requests:
cpu: "50m"
memory: "32Mi"
limits:
cpu: "300m"
memory: "128Mi"
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 15
periodSeconds: 20
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: nginx-cache
mountPath: /var/cache/nginx
- name: nginx-run
mountPath: /var/run
- name: nginx-tmp
mountPath: /tmp
# readOnlyRootFilesystem 환경에서 envsubst 가 BACKEND_URL 을 치환한
# 설정을 기동 시 기록할 수 있도록 conf.d 를 쓰기 가능 볼륨으로 제공한다.
- name: nginx-conf
mountPath: /etc/nginx/conf.d
volumes:
- name: nginx-cache
emptyDir: {}
- name: nginx-run
emptyDir: {}
- name: nginx-tmp
emptyDir: {}
- name: nginx-conf
emptyDir: {}
15 changes: 15 additions & 0 deletions k8s/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: egov-simple-react
labels:
app.kubernetes.io/name: egov-simple-react
app.kubernetes.io/part-of: egovframe-template
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: egov-simple-react
ports:
- name: http
port: 8080
targetPort: http
11 changes: 9 additions & 2 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
// Configuration for different environments
export const SERVER_URL = process.env.NODE_ENV === 'test' ? 'http://localhost:8080' : 'http://localhost:8080';
// API 호출의 base URL.
// 빈 문자열(기본값)이면 동일 출처(same-origin) 상대경로로 호출되어
// 배포 환경에서는 앞단 nginx 가 /api 경로를 백엔드로 리버스 프록시한다.
// 별도 호스트로 직접 호출해야 할 때만 VITE_APP_API_BASE_URL 로 절대 URL 을 지정한다.
// 예) VITE_APP_API_BASE_URL=http://localhost:8080
// 미지정 시 "/api" prefix 를 사용해 nginx location /api/ 프록시 규칙과 맞춘다.
const API_BASE_URL = import.meta.env.VITE_APP_API_BASE_URL ?? "/api";

export const SERVER_URL = API_BASE_URL;

export const DEFAULT_BBS_ID = "BBSMSTR_AAAAAAAAAAAA"; // default = 공지사항 게시판 아이디
export const NOTICE_BBS_ID = "BBSMSTR_AAAAAAAAAAAA"; // 공지사항 게시판 아이디
Expand Down
10 changes: 10 additions & 0 deletions vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ export default defineConfig(({ mode }) => ({
base: "/",
server: {
port: 3000,
// 개발 서버에서도 배포(nginx)와 동일하게 /api prefix 를 백엔드로 프록시한다.
// 대상 호스트는 VITE_APP_API_PROXY_TARGET 로 바꿀 수 있고, 미지정 시 로컬 백엔드를 사용한다.
// /api/board -> {target}/board 형태로 prefix 를 제거해 전달한다.
proxy: {
"/api": {
target: process.env.VITE_APP_API_PROXY_TARGET || "http://localhost:8080",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
resolve: {
alias: [
Expand Down