Replace Gitea Actions with Jenkins CI/CD.
office.sejong.dev/pipeline/head There was a failure building this commit

Build and push images from a Jenkinsfile, then deploy over SSH with compose on main.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ysj7705andCursor committed 2026-09-11 16:17:41 +09:00
1 parent b0a6fc326c
commit f4f98704be
3 files changed
+145 -95

No files matched your search

-84
View File
@@ -1,84 +0,0 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
name: Type-check & Build
runs-on: ubuntu-latest
env:
# CAD/OCR/3D viewer assets make Vite exceed the default ~2GB heap
NODE_OPTIONS: --max-old-space-size=8192
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: npm ci
- name: Type-check and build
run: npm run build
image:
name: Build & Push image
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# Job image often has no docker binary; host sock may already be mounted by act_runner.
- name: Install Docker CLI
run: |
mkdir -p "$HOME/.local/bin"
curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-27.5.1.tgz \
| tar xz --strip-components=1 -C "$HOME/.local/bin" docker/docker
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
export PATH="$HOME/.local/bin:$PATH"
if [ -S /var/run/docker.sock ]; then
docker info
docker version
exit 0
fi
echo "Host /var/run/docker.sock is not available in this job."
echo "On act_runner, set container.docker_host: \"\" (use host Docker)"
echo "and/or container.valid_volumes: [/var/run/docker.sock]"
ls -la /var/run/docker.sock || true
docker info || true
exit 1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: docker.sejong.dev
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
docker.sejong.dev/sejong-office:latest
docker.sejong.dev/sejong-office:sha-${{ github.sha }}
- name: Log out of registry
if: always()
run: docker logout docker.sejong.dev
Vendored
+97
View File
@@ -0,0 +1,97 @@
pipeline {
agent any
environment {
NODE_OPTIONS = '--max-old-space-size=8192'
NODE_IMAGE = 'node:22-bookworm'
REGISTRY = 'docker.sejong.dev'
IMAGE = 'docker.sejong.dev/sejong-office'
SSH_CONFIG_NAME = 'sejong-office'
DEPLOY_DIR = '/volume1/docker/sejong-office'
}
options {
timestamps()
buildDiscarder(logRotator(numToKeepStr: '20'))
}
stages {
stage('Type-check & Build') {
steps {
sh '''
set -eu
docker run --rm \
-u "$(id -u):$(id -g)" \
-e HOME=/tmp \
-e NODE_OPTIONS \
-v "$PWD":/app \
-w /app \
"$NODE_IMAGE" \
bash -lc 'npm ci && npm run build'
'''
}
}
stage('Release') {
when {
allOf {
not { changeRequest() }
anyOf {
branch 'main'
expression {
def branch = env.GIT_BRANCH ?: env.BRANCH_NAME ?: ''
branch == 'main' || branch == 'origin/main' || branch == 'refs/heads/main'
}
}
}
}
stages {
stage('Build & Push image') {
steps {
withCredentials([usernamePassword(
credentialsId: 'docker-sejong-dev',
usernameVariable: 'REGISTRY_USERNAME',
passwordVariable: 'REGISTRY_PASSWORD'
)]) {
sh '''
set -eu
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USERNAME" --password-stdin
docker build -t "$IMAGE:latest" -t "$IMAGE:sha-${GIT_COMMIT}" .
docker push "$IMAGE:latest"
docker push "$IMAGE:sha-${GIT_COMMIT}"
'''
}
}
post {
always {
sh 'docker logout "$REGISTRY" || true'
}
}
}
stage('Deploy') {
steps {
sshPublisher(
failOnError: true,
publishers: [
sshPublisherDesc(
configName: env.SSH_CONFIG_NAME,
verbose: true,
transfers: [
sshTransfer(
execTimeout: 1800000,
usePty: true,
execCommand: """
sudo -n su -c 'set -eu; cd "${env.DEPLOY_DIR}"; docker compose pull; docker compose up -d'
""".stripIndent().trim()
)
]
)
]
)
}
}
}
}
}
}
+48 -11
View File
@@ -101,27 +101,64 @@ volumes:
documentserver_data: documentserver_data:
``` ```
## CI (Gitea Actions) ## CI/CD (Jenkins)
[`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml) 이 `main` PR/push에서 `npm run build`를 돌리고, **`main` push**에서만 Docker 이미지를 레지스트리에 push합니다. [`Jenkinsfile`](Jenkinsfile) 이 `main` PR/브랜치에서 `npm ci` + `npm run build`를 돌리고, **`main` 브랜치 빌드**(PR 제외)에서만 이미지를 push한 뒤 서버에 SSH로 배포합니다.
에이전트에 Docker가 있어야 합니다. Node 22는 `node:22-bookworm` 컨테이너로 돌리며, Vite 힙 한도는 `NODE_OPTIONS=--max-old-space-size=8192`입니다.
푸시 태그: `docker.sejong.dev/sejong-office:latest`, `docker.sejong.dev/sejong-office:sha-<commit>` 푸시 태그: `docker.sejong.dev/sejong-office:latest`, `docker.sejong.dev/sejong-office:sha-<commit>`
### 레포 Actions Secrets 배포는 [Publish Over SSH](https://plugins.jenkins.io/publish-over-ssh/)로 파일을 올리지 않고, 서버에 이미 있는 `docker-compose.yml` 디렉터리에서 `docker compose pull && docker compose up -d`만 실행합니다.
| Secret | 설명 | ### Jenkins Credentials
|--------|------|
| `REGISTRY_USERNAME` | push 전용 계정 |
| `REGISTRY_PASSWORD` | push 전용 토큰 (write) |
레지스트리 호스트는 `docker.sejong.dev`로 고정입니다. PR job은 이 시크릿을 쓰지 않습니다. 러너에 상시 `docker login` 하지 마세요. Username/password 자격 증명 ID: `docker-sejong-dev`
| 필드 | 설명 |
|------|------|
| Username | push 전용 계정 |
| Password | push 전용 토큰 (write) |
레지스트리 호스트는 `docker.sejong.dev`로 고정입니다. PR 빌드는 이 자격 증명을 쓰지 않습니다. 에이전트에 상시 `docker login` 하지 마세요.
Multibranch Pipeline(또는 Gitea/Git 브랜치 소스)로 이 레포를 연결하면 PR과 `main`이 각각 잡으로 뜹니다.
### Publish Over SSH
1. Jenkins에 **Publish Over SSH** 플러그인 설치
2. **Manage Jenkins → System → Publish over SSH** 에 배포 서버 등록 (호스트, 사용자, 키 또는 비밀번호)
3. 등록한 **Name**이 Jenkinsfile의 `SSH_CONFIG_NAME`과 같아야 함 (`sejong-office`)
4. 배포 명령은 SSH 접속 후 `sudo -n su -c '...'` 로 root에서 실행함. 대화형 `sudo su`와 비밀번호 입력은 CI에서 불가
5. Publish Over SSH에 넣은 **그 계정**으로 서버에 로그인한 뒤, 한 번만 NOPASSWD를 설정:
```bash
sudo visudo -f /etc/sudoers.d/sejong-office-deploy
```
파일 내용 (SSH 사용자 이름으로 바꿀 것):
```
youruser ALL=(ALL) NOPASSWD: /bin/su, /usr/bin/su
```
저장 후 비밀번호 없이 root가 되는지 확인:
```bash
sudo -n su -c 'whoami'
# root
```
`sudo -n`은 비밀번호를 묻지 않고, 설정이 안 되어 있으면 바로 실패합니다. Jenkinsfile에 비밀번호를 넣지 마세요.
서버 compose 경로는 Jenkinsfile `DEPLOY_DIR` (`/volume1/docker/sejong-office`)입니다. 이름이나 경로가 다르면 Jenkinsfile을 수정하면 됩니다.
### 서버 배포 체크리스트 ### 서버 배포 체크리스트
1. **pull 전용** 토큰으로 서버에서 `docker login docker.sejong.dev` (CI push 토큰과 분리) 1. 배포 디렉터리에 `docker-compose.yml`과 `.env`를 두고, **pull 전용** 토큰으로 `docker login docker.sejong.dev` (CI push 토큰과 분리)
2. `.env`에 `JWT_SECRET`, URL 설정 2. `.env`에 `JWT_SECRET`, URL 설정
3. 이미지 갱신 후: `docker compose pull && docker compose up -d` 3. `main` 배포 잡이 이미지 push 후 그 디렉터리에서 compose pull/up 실행
4. `JWT_SECRET`은 Git·이미지·Actions Secret에 넣지 않음 (서버 `.env`만) 4. `JWT_SECRET`은 Git·이미지·Jenkins Credential에 넣지 않음 (서버 `.env`만)
## 로컬 개발 ## 로컬 개발