Skip to content

Commit a92fa07

Browse files
committed
Harden deployment: health checks, non-root user, compose-based CD, image cleanup
- Add HEALTHCHECK + /health endpoint, run as non-root USER node - Replace bare docker run with docker compose on VPS (up -d --wait) - Add tag push trigger (v*) alongside manual workflow_dispatch - Clean up old images: docker image prune on VPS, GHCR keep last 10 - Document node_modules volume override for dependency-heavy apps - Update README (en/ko) and VPS_DEPLOY.md to reflect all changes
1 parent 125ec24 commit a92fa07

7 files changed

Lines changed: 113 additions & 36 deletions

File tree

.github/workflows/cd.yml

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ name: Deploy
22

33
on:
44
workflow_dispatch:
5+
push:
6+
tags:
7+
- 'v*'
58

69
permissions:
710
contents: write
@@ -17,9 +20,15 @@ jobs:
1720

1821
- name: Extract version
1922
id: version
20-
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
23+
run: |
24+
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
25+
echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
26+
else
27+
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
28+
fi
2129
2230
- name: Check tag does not exist
31+
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
2332
run: |
2433
if git ls-remote --tags origin | grep -q "refs/tags/v${{ steps.version.outputs.version }}$"; then
2534
echo "::error::Tag v${{ steps.version.outputs.version }} already exists. Run './scripts/bump-version.sh' first."
@@ -49,15 +58,35 @@ jobs:
4958
username: ${{ secrets.VPS_USER }}
5059
key: ${{ secrets.VPS_SSH_KEY }}
5160
script: |
52-
docker pull ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}
53-
docker stop app || true
54-
docker rm app || true
55-
docker run -d \
56-
--name app \
57-
--restart unless-stopped \
58-
--env-file ~/.env.app \
59-
-p 3000:3000 \
60-
ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}
61+
IMAGE="ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}"
62+
mkdir -p ~/app
63+
cat > ~/app/docker-compose.yml << EOF
64+
services:
65+
app:
66+
image: $IMAGE
67+
env_file: $HOME/.env.app
68+
ports:
69+
- "3000:3000"
70+
restart: unless-stopped
71+
healthcheck:
72+
test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:3000/health"]
73+
interval: 10s
74+
timeout: 5s
75+
retries: 3
76+
start_period: 10s
77+
EOF
78+
cd ~/app
79+
docker compose pull
80+
docker compose up -d --wait
81+
docker image prune -f
82+
83+
- name: Clean up old GHCR images
84+
uses: actions/delete-package-versions@v5
85+
continue-on-error: true
86+
with:
87+
package-name: ${{ github.event.repository.name }}
88+
package-type: container
89+
min-versions-to-keep: 10
6190

6291
- name: Create GitHub Release
6392
uses: softprops/action-gh-release@v2

Dockerfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
FROM node:20-alpine
66

77
WORKDIR /app
8-
COPY app/ .
8+
COPY --chown=node:node app/ .
9+
10+
USER node
911

1012
EXPOSE 3000
13+
HEALTHCHECK --interval=10s --timeout=5s --retries=3 --start-period=10s \
14+
CMD wget -qO /dev/null http://localhost:3000/health || exit 1
1115
CMD ["node", "server.js"]

README.ko.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ docker compose up
6363

6464
- **언어 무관** — Dockerfile만 바꾸면 Node, Python, Go, Rust, Java, 정적 사이트 모두 가능
6565
- **CI 파이프라인** — Dockerfile 린트 (hadolint), docker-compose 검증, 빌드 테스트
66-
- **CD 파이프라인**원클릭으로 빌드 → GHCR 푸시 → VPS SSH 배포 + GitHub Release 자동 생성
66+
- **CD 파이프라인** — 빌드 → GHCR 푸시 → docker compose 헬스체크 기반 VPS 배포 + GitHub Release 자동 생성
6767
- **Dockerfile 예시** — Node, Python, Go, Rust, Java용 멀티스테이지 빌드 docs 제공
6868
- **버전 관리**`./scripts/bump-version.sh patch/minor/major`
6969
- **로컬 개발**`docker compose up`으로 볼륨 마운트 + 라이브 리로드
@@ -80,20 +80,22 @@ docker compose up
8080
| Compose 검증 | `docker-compose.yml` 문법 확인 |
8181
| 빌드 테스트 | Docker 이미지 빌드로 오류 사전 감지 |
8282

83-
### CD (Actions 탭에서 수동 실행)
83+
### CD (수동 실행 또는 태그 푸시)
8484

8585
| 단계 | 설명 |
8686
|------|------|
8787
| 버전 확인 | 이미 존재하는 태그면 실패 |
8888
| 빌드 & 푸시 | 이미지 빌드 후 GitHub Container Registry에 푸시 |
89-
| 배포 | VPS에 SSH 접속, 새 이미지 풀, 컨테이너 재시작 |
89+
| 배포 | VPS에 SSH 접속, 새 이미지 풀, docker compose로 헬스체크 기반 재시작 |
90+
| 이미지 정리 | VPS 오래된 이미지 정리 + GHCR 최근 10개 버전 유지 |
9091
| GitHub Release | 자동 릴리스 노트와 함께 태그 생성 |
9192

9293
**배포 방법:**
9394

9495
1. GitHub Secrets 설정 (아래 참고)
9596
2. 버전 범프: `./scripts/bump-version.sh patch`
96-
3. **Actions** 탭 → **Deploy****Run workflow**
97+
3. **수동:** **Actions** 탭 → **Deploy****Run workflow**
98+
4. **자동:** 버전 태그 푸시 — `git tag v$(cat VERSION) && git push --tags`
9799

98100
### GitHub Secrets
99101

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ docker compose up
6363

6464
- **Language agnostic** — Swap the Dockerfile for any language (Node, Python, Go, Rust, Java, static)
6565
- **CI Pipeline** — Dockerfile lint (hadolint), docker-compose validation, build verification on every push
66-
- **CD Pipeline**One-click build → push to GHCR → deploy to VPS via SSH + auto GitHub Release
66+
- **CD Pipeline**Build → push to GHCR → health-checked deploy to VPS via docker compose + auto GitHub Release
6767
- **Dockerfile examples** — Multi-stage builds for Node, Python, Go, Rust, Java in docs
6868
- **Version management**`./scripts/bump-version.sh patch/minor/major`
6969
- **Local dev**`docker compose up` with volume mounts for live reload
@@ -80,20 +80,22 @@ docker compose up
8080
| Validate compose | Verifies `docker-compose.yml` syntax |
8181
| Build test | Builds the Docker image to catch build errors |
8282

83-
### CD (manual trigger via Actions tab)
83+
### CD (manual trigger or tag push)
8484

8585
| Step | What it does |
8686
|------|-------------|
8787
| Version guard | Fails if git tag already exists for this version |
8888
| Build & push | Builds image and pushes to GitHub Container Registry |
89-
| Deploy | SSHs into your VPS, pulls new image, restarts container |
89+
| Deploy | SSHs into your VPS, pulls new image, health-checked restart via docker compose |
90+
| Image cleanup | Prunes old images on VPS + keeps last 10 versions on GHCR |
9091
| GitHub Release | Creates a tagged release with auto-generated notes |
9192

9293
**How to deploy:**
9394

9495
1. Set up GitHub Secrets (see below)
9596
2. Bump version: `./scripts/bump-version.sh patch`
96-
3. Go to **Actions** tab → **Deploy****Run workflow**
97+
3. **Manual:** Go to **Actions** tab → **Deploy****Run workflow**
98+
4. **Auto:** Push a version tag — `git tag v$(cat VERSION) && git push --tags`
9799

98100
### GitHub Secrets
99101

app/server.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ const http = require('http');
33
const port = process.env.PORT || 3000;
44

55
const server = http.createServer((req, res) => {
6+
if (req.url === '/health') {
7+
res.writeHead(200, { 'Content-Type': 'application/json' });
8+
res.end(JSON.stringify({ status: 'ok' }));
9+
return;
10+
}
611
res.writeHead(200, { 'Content-Type': 'application/json' });
712
res.end(JSON.stringify({ status: 'ok', message: 'Hello from Docker!' }));
813
});

docker-compose.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,13 @@ services:
66
- "${PORT:-3000}:3000"
77
volumes:
88
- ./app:/app
9+
# Preserve container's installed dependencies from host mount override.
10+
# Uncomment for apps with dependencies (node_modules, vendor, venv, etc.):
11+
# - /app/node_modules
912
restart: unless-stopped
13+
healthcheck:
14+
test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:3000/health"]
15+
interval: 10s
16+
timeout: 5s
17+
retries: 3
18+
start_period: 10s

docs/VPS_DEPLOY.md

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,23 +55,37 @@ EOF
5555

5656
## 6. Deploy
5757

58+
**Option A: Manual trigger**
5859
1. Bump version: `./scripts/bump-version.sh patch`
5960
2. Commit and push
6061
3. Go to **Actions** tab → **Deploy****Run workflow**
6162

63+
**Option B: Tag push (automatic)**
64+
1. Bump version: `./scripts/bump-version.sh patch`
65+
2. Commit, tag, and push:
66+
```bash
67+
git add VERSION && git commit -m "Bump version"
68+
git tag v$(cat VERSION)
69+
git push && git push --tags
70+
```
71+
6272
The workflow will:
6373
1. Build your Docker image
6474
2. Push to GitHub Container Registry
65-
3. SSH into your VPS
66-
4. Pull the new image
67-
5. Stop the old container and start the new one
75+
3. SSH into your VPS and create a `docker-compose.yml` at `~/app/`
76+
4. Pull the new image and restart with health check verification (`docker compose up -d --wait`)
77+
5. Clean up old images on VPS (`docker image prune`) and GHCR (keep last 10 versions)
6878

6979
## Troubleshooting
7080

7181
### Container won't start
7282
```bash
7383
# Check logs
74-
docker logs app
84+
cd ~/app
85+
docker compose logs
86+
87+
# Check health status
88+
docker compose ps
7589

7690
# Check if port is in use
7791
sudo lsof -i :3000
@@ -92,21 +106,33 @@ chmod 600 ~/.ssh/deploy_key
92106
echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdin
93107
```
94108

95-
## Advanced: Docker Compose on VPS
109+
## Advanced: Multi-Container Setup
96110

97-
For multi-container setups, replace the SSH deploy step in `cd.yml`:
111+
The default deployment creates a single-service `docker-compose.yml` on VPS at `~/app/`.
112+
To add services (database, Redis, etc.), manually edit `~/app/docker-compose.yml` on your VPS:
98113

99114
```yaml
100-
- name: Deploy to VPS via SSH
101-
uses: appleboy/ssh-action@v1
102-
with:
103-
host: ${{ secrets.VPS_HOST }}
104-
username: ${{ secrets.VPS_USER }}
105-
key: ${{ secrets.VPS_SSH_KEY }}
106-
script: |
107-
cd ~/app
108-
docker compose pull
109-
docker compose up -d
115+
services:
116+
app:
117+
image: ghcr.io/your-user/your-repo:latest
118+
env_file: ~/.env.app
119+
ports:
120+
- "3000:3000"
121+
restart: unless-stopped
122+
depends_on:
123+
db:
124+
condition: service_healthy
125+
126+
db:
127+
image: postgres:16-alpine
128+
environment:
129+
POSTGRES_PASSWORD: ${DB_PASSWORD}
130+
volumes:
131+
- pgdata:/var/lib/postgresql/data
132+
restart: unless-stopped
133+
134+
volumes:
135+
pgdata:
110136
```
111137
112-
And place a `docker-compose.prod.yml` on your VPS at `~/app/docker-compose.yml`.
138+
> **Note:** The CD workflow overwrites `~/app/docker-compose.yml` on each deploy. For multi-container setups, update the SSH deploy script in `cd.yml` to preserve your additional services, or manage the compose file separately on the VPS.

0 commit comments

Comments
 (0)