가시다님의 CI/CD 스터디 내용을 정리한 포스트 입니다.
- 단계별 진화 컨테이너를 통한 애플리케이션 개발 환경 진화 과정을 체험하면서 CI/CD 솔루션들이 어떤 배경에서 만들어지고 기능하는지 알 수 있는 값진 시간이였습니다.
- Jenkins, Gogs Rep, Docker Hub등으로 CI/CD 환경을 구현해보고 샘플이지만 애플리케이션을 배포하는 실습을 통해서 개념들을 좀 더 잘 이해할 수 있었습니다.(강의를 해주신 가시다님께 다시 한 번 감사드립니다.)
저는 MacOS 기반에서 기존에 OrbStack가 설치되어 있어서 OrbStack기반(Docker Desktop도 가능)으로 실습 환경을 구성하였으며 추가로 vscod extension에서 docker를 설치하였습니다.
간단한 코드를 사용해서 컨테이너를 수동으로 개발하는 과정을 실행해봅니다.
# 코드 작성
mkdir 1.1 && cd 1.1
echo "print ('Hello Docker')" > hello.py
cat > Dockerfile <<EOF
FROM python:3
COPY . /app
WORKDIR /app
CMD python3 hello.py
EOF
# 컨테이너 이미지 빌드
docker pull python:3
docker build . -t hello
docker image ls -f reference=hello
# 컨테이너 실행
docker run --rm hello
# 코드 수정
echo "print ('Hello CloudNet@')" > hello.py
# 컨테이너 이미지 빌드 : latest 활용 해보자!
docker build . -t hello:1
docker image ls -f reference=hello
docker tag hello:1 hello:latest
docker image ls -f reference=hello
# 컨테이너 실행
docker run --rm hello:1
docker run --rm hello

실습에서는 간단하게 1개의 버젼을 추가했지만 실제 개별환경에서는 수많은 버전들이 생성될 것이다.
컨테이너 이미지에 태그(Tag)를 적절히 사용하면 CI/CD 프로세스에서 이미지의 버전을 관리하고 추적할 수 있습니다. 이를 통해 안정적인 빌드와 배포를 보장하며, 롤백과 추적성을 용이하게 만듭니다.
다음과 같이 태그를 사용할 때는 사용 방안을 마련하여 관리하도록 해야 할 것 같습니다.
• 태그는 중복 사용하지 않기: 동일한 태그가 다른 빌드를 가리키면 혼란 초래 • 자동화: CI/CD 시스템에서 빌드, 태그 지정, 푸시를 자동화하여 실수를 방지 • 안정성과 최신 상태 병행 • 안정 버전 태그(e.g., 1.0.0)를 유지 • latest 태그로 최신 상태 제공
컴파일을 추가(Java)하는 작업 환경을 만들어 봅니다.
# 코드 작성
mkdir 1.2 && cd 1.2
cat > Hello.java <<EOF
class Hello {
public static void main(String[] args) {
System.out.println("Hello Docker");
}
}
EOF
cat > Dockerfile <<EOF
FROM openjdk
COPY . /app
WORKDIR /app
RUN javac Hello.java # The complie command
CMD java Hello
EOF
# 컨테이너 이미지 빌드
docker pull openjdk
docker build . -t hello:2
docker tag hello:2 hello:latest
docker image ls -f reference=hello
# 컨테이너 실행
docker run --rm hello:2
docker run --rm hello

컨테이너 이미지 내부에 파일 목록 중 불필요한 파일은 어떻게 해야할까?
불필요한 파일이란 빌드 시 생성되었지만 실행에 필요하지 않은 파일들이고 다음과 것들이 있을 것 같습니다.
• 캐시 파일: /tmp, /var/cache 등
• 빌드 도구: 컴파일 후 필요 없는 컴파일러나 디버깅 도구
• 개발 관련 파일: 소스 코드, 문서 등
• 비밀 정보: .env 파일, 설정 파일, 인증서 등컨테이너 이미지 내부에 포함된 파일 목록을 확인하고 불필요한 파일을 제거하는 것은 보안과 성능 측면에서 매우 중요하다고 생각합니다. 이를 통해 이미지를 경량화하고 잠재적인 보안 취약점을 줄이고 디버깅 시간 단축등 관리 간소화를 기대 할 수 있을 것 같습니다.
그리고 멀티 스테이지 빌드를 활용해 빌드 환경과 실행 환경을 분리하는 방법도 하나의 대안이 될 것 같습니다.
멀티스테이지 빌드란? 컨테이너 이미지를 만들면서 빌드 등에는 필요하지만, 최종 컨테이너 이미지에는 필요 없는 환경을 제거할 수 있도록 단계를 나누어 기반 이미지를 만드는 방법입니다.
출처: https://kimjingo.tistory.com/63 [김징어의 Devlog:티스토리]
멀티스테이지 빌드를 통해 경량화되고 좀더 보안이 강화된 컨테이너를 배포 할 수 있게 됩니다.

# 코드 작성
mkdir 1.3 && cd 1.3
cat > Hello.java <<EOF
class Hello {
public static void main(String[] args) {
System.out.println("Hello Multistage container build");
}
}
EOF
cat > Dockerfile <<EOF
FROM openjdk:11 AS buildstage #JDK toolchain
COPY . /app
WORKDIR /app
RUN javac Hello.java
FROM openjdk:11-jre-slim # JRE Only
COPY --from=buildstage /app/Hello.class /app/
WORKDIR /app
CMD java Hello
EOF
# 컨테이너 이미지 빌드 : 용량 비교 해보자!
docker build . -t hello:3
docker tag hello:3 hello:latest
docker image ls -f reference=hello
# 컨테이너 실행
docker run --rm hello:3
docker run --rm hello
# 컨테이너 이미지 내부에 파일 목록을 보면 어떤가요?
docker run --rm hello ls -l
docker run --rm hello javac --help
내부 파일 목록을 보면 불필요한 파일도 없으며 컨테이너의 사이즈도 경량화 된 것을 확인 할 수 있다.

JIB(Java Image Builder)은 Google이 개발한 Java 애플리케이션 컨테이너 이미지 빌더입니다. JIB은 Dockerfile이나 Docker 데몬 없이 Java 애플리케이션의 Docker 및 OCI 컨테이너 이미지를 생성할 수 있는 도구로, Maven과 Gradle 플러그인 형태로 제공됩니다.
Docker 빌드 흐름 : 도커 파일 작성 → 이미지 빌드 → 저장소에 이미지 푸시
jib 빌드 흐름 : 프로젝트에서 빌드와 동시에 이미지 만들어지고 저장소에 푸시까지 처리. 개발자가 편하다!
간단하게 web server 컨테이너를 만들어 봅니다.
# 코드 작성
mkdir 1.5 && cd 1.5
cat > server.py <<EOF
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from datetime import datetime
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
now = datetime.now()
response_string = now.strftime("The time is %-I:%M %p, UTC.\n")
self.wfile.write(bytes(response_string, "utf-8"))
def startServer():
try:
server = ThreadingHTTPServer(('', 80), RequestHandler)
print("Listening on " + ":".join(map(str, server.server_address)))
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()
if __name__== "__main__":
startServer()
EOF
cat > Dockerfile <<EOF
FROM python:3.12
ENV PYTHONUNBUFFERED 1
COPY . /app
WORKDIR /app
CMD python3 server.py
EOF
# 컨테이너 이미지 빌드 : 용량 비교 해보자!
docker pull python:3.12
docker build . -t timeserver:1 && docker tag timeserver:1 timeserver:latest
docker image ls -f reference=timeserver

# 컨테이너 실행
docker run -d -p 8080:80 --name=timeserver timeserver
# 컨테이너 접속 및 로그 확인
curl http://localhost:8080
docker logs timeserver

# 컨테이너 이미지 내부에 파일 확인
docker exec -it timeserver ls -l
# 컨테이너 이미지 내부에 server.py 파일 수정 후 반영 확인 : VSCODE 경우 docker 확장프로그램 활용
docker exec -it timeserver cat server.py

# 컨테이너 접속 후 확인
curl http://localhost:8080
The time is 1:32 PM, UTC. #수정 후 Build하지 않으므로 그대로임
# 컨테이너 삭제
docker rm -f timeserver
코드를 변경 후에 매번 이미지 빌드를 수행한다.
#
cat > server.py <<EOF
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from datetime import datetime
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
now = datetime.now()
response_string = now.strftime("The time is %-I:%M:%S %p, CloudNeta Study.\n")
self.wfile.write(bytes(response_string, "utf-8"))
def startServer():
try:
server = ThreadingHTTPServer(('', 80), RequestHandler)
print("Listening on " + ":".join(map(str, server.server_address)))
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()
if __name__== "__main__":
startServer()
EOF
#
# 컨테이너 이미지 빌드 : 용량 비교 해보자!
docker build . -t timeserver:2 && docker tag timeserver:2 timeserver:latest
docker image ls -f reference=timeserver
# 컨테이너 실행
docker run -d -p 8080:80 --name=timeserver timeserver
# 컨테이너 접속 및 로그 확인
curl http://localhost:8080

# 컨테이너 삭제
docker rm -f timeserver
이러한 과정을 코드의 수정이 있을때마다 반복해서 해야하는데 불편하다!
조금더 편한 개발을 위해서 도커의 볼륨 바인딩을 통해 호스트의 디렉토리를 폴더 맵핑을 시키고, 코드 내용을 동적 반영한다.
#
# 코드 작성
mkdir 1.6 && cd 1.6
cat > server.py <<EOF
from reloading import reloading
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from datetime import datetime
class RequestHandler(BaseHTTPRequestHandler):
@reloading # By adding the @reloading tag to our method, it will be reloaded from disk every time it runs so we can change our do_GET function while it’s running.
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
now = datetime.now()
response_string = now.strftime("The time is %H:%M:%S, Docker End.")
self.wfile.write(bytes(response_string,"utf-8"))
def startServer():
try:
server = ThreadingHTTPServer(('',80), RequestHandler)
print("Listening on " + ":".join(map(str, server.server_address)))
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()
if __name__== "__main__":
startServer()
EOF
# reloading 라이브러리 설치 필요
cat > Dockerfile <<EOF
FROM python:3
RUN pip install reloading
ENV PYTHONUNBUFFERED 1
COPY . /app
WORKDIR /app
CMD python3 server.py
EOF
cat > docker-compose.yaml <<EOF
services:
frontend:
build: .
command: python3 server.py
volumes:
- type: bind
source: .
target: /app
environment:
PYTHONDONTWRITEBYTECODE: 1 # Sets a new environment variable so that Python can be made to reload our source
ports:
- "8080:80"
EOF
#
docker compose build; docker compose up -d
# 컴포즈로 실행 시 이미지와 컨테이너 네이밍 규칙을 알아보자!
docker compose ps
docker compose images
#
curl http://localhost:8080
docker compose logs

# VSCODE 에서 호스트에서 server.py 코드 수정(볼륨 공유 상태)
cat server.py
...
response_string = now.strftime("The time is %H:%M:%S, Docker EndEndEnd!")
self.wfile.write(bytes(response_string,"utf-8"))
...
#
curl http://localhost:8080

#
docker compose down
# 작업 디렉토리 생성 후 이동
mkdir cicd-labs
cd cicd-labs
#
cat <<EOT > docker-compose.yaml
services:
jenkins:
container_name: jenkins
image: jenkins/jenkins
restart: unless-stopped
networks:
- cicd-network
ports:
- "8080:8080"
- "50000:50000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- jenkins_home:/var/jenkins_home
gogs:
container_name: gogs
image: gogs/gogs
restart: unless-stopped
networks:
- cicd-network
ports:
- "10022:22"
- "3000:3000"
volumes:
- gogs-data:/data
volumes:
jenkins_home:
gogs-data:
networks:
cicd-network:
driver: bridge
EOT
# 배포
docker compose up -d
docker compose ps
# 기본 정보 확인
for i in gogs jenkins ; do echo ">> container : $i <<"; docker compose exec $i sh -c "whoami && pwd"; echo; done
# 도커를 이용하여 각 컨테이너로 접속
docker compose exec jenkins bash
exit
docker compose exec gogs bash
exit

- Admin 초기 Password를 확인해 둔다.
docker compose exec jenkins cat >/var/jenkins_home/secrets/initialAdminPassword
- 로그인 후에는 Jenkins URL 설정을 PC의 IP를 입력합니다.
- Plug-in들을 설치해 줍니다.

# Jenkins 컨테이너 내부에 도커 실행 파일 설치
docker compose exec --privileged -u root jenkins bash
-----------------------------------------------------
id
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update && apt install docker-ce-cli curl tree jq -y
docker info
docker ps
which docker
# Jenkins 컨테이너 내부에서 root가 아닌 jenkins 유저도 docker를 실행할 수 있도록 권한을 부여
groupadd -g 2000 -f docker
chgrp docker /var/run/docker.sock
ls -l /var/run/docker.sock
usermod -aG docker jenkins
cat /etc/group | grep docker
exit
--------------------------------------------
# jenkins item 실행 시 docker 명령 실행 권한 에러 발생 : Jenkins 컨테이너 재기동으로 위 설정 내용을 Jenkins app 에도 적용 필요
docker compose restart jenkins
sudo docker compose restart jenkins # Windows 경우 이후부터 sudo 붙여서 실행하자
# jenkins user로 docker 명령 실행 확인
docker compose exec jenkins id
docker compose exec jenkins docker info
docker compose exec jenkins docker ps

터미널에서 아래 명령어를 먼저 수행합니다.
open "http://127.0.0.1:3000/install"
웹관리페이지가 열리면 아래 정보를 입력하고 "install gogs"를 선택해줍니다.
http://<각자 자신의 PC IP>:3000/

Gogs 컨테이너에 접속해서 아래 명령어를 수행하여 확인해 봅니다.
docker compose exec gogs ls -l /data
docker compose exec gogs ls -l /data/gogs
docker compose exec gogs ls -l /data/gogs/conf
docker compose exec gogs cat /data/gogs/conf/app.ini



#
docker compose exec jenkins bash
-----------------------------------
whoami
pwd
cd /var/jenkins_home/
tree
#
git config --global user.name "<Gogs 계정명>"
git config --global user.name "devops"
git config --global user.email "a@a.com"
git config --global init.defaultBranch main
#
git clone <각자 Gogs dev-app repo 주소>
git clone http://192.168.254.124:3000/devops/dev-app.git
Cloning into 'dev-app'...
Username for 'http://192.168.254.124:3000': devops # Gogs 계정명
Password for 'http://devops@192.168.254.124:3000': <토큰> # 혹은 계정암호
...

#
tree dev-app
cd dev-app
git branch
git remote -v
origin http://172.30.1.26:3000/devops/dev-app.git (fetch)
origin http://172.30.1.26:3000/devops/dev-app.git (push)
# server.py 파일 작성
cat > server.py <<EOF
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from datetime import datetime
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
now = datetime.now()
response_string = now.strftime("The time is %-I:%M:%S %p, CloudNeta Study.\n")
self.wfile.write(bytes(response_string, "utf-8"))
def startServer():
try:
server = ThreadingHTTPServer(('', 80), RequestHandler)
print("Listening on " + ":".join(map(str, server.server_address)))
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()
if __name__== "__main__":
startServer()
EOF
# Dockerfile 생성
cat > Dockerfile <<EOF
FROM python:3.12
ENV PYTHONUNBUFFERED 1
COPY . /app
WORKDIR /app
CMD python3 server.py
EOF
# VERSION 파일 생성
echo "0.0.1" > VERSION
#
git add .
git commit -m "Add dev-app"
git push -u origin main
...


dockerhub에서 private repository를 만듭니다.

local pc에서 컨테이너 이미지를 만든 후 도커 허브에 만든 private repository에 이미지를 push해 봅니다.
# Dockerfile 파일 작성
cat > Dockerfile <<EOF
FROM busybox
CMD echo "Hello world! This is my first Docker image."
EOF
cat Dockerfile
# 빌드
DOCKERID=<자신의 도커 계정명>
DOCKERREPO=<자신의 도커 저장소>
DOCKERID=attop
DOCKERREPO=dev-test-app
docker build -t $DOCKERID/$DOCKERREPO .
docker images | grep $DOCKERID/$DOCKERREPO
# 실행 확인
docker run $DOCKERID/$DOCKERREPO
# 로그인 확인
docker login
# 푸시
docker push $DOCKERID/$DOCKERREPO

도커 허브 private repository에 이미지가 업로드 되었습니다.

Jenkins에서 새로운 Item을 생성해서 간단한 동작 방식을 알아봅니다.
Jenkins Dashboard에서 New Itme을 선택한 후 "first"라고 item name을 지정하고 type은 "freestyle project"를 선택합니다.

Build Steps 메뉴에서 Execute shell에 아래 커맨드를 작성하고 저장해 줍니다.
echo "docker check" | tee test.txt
docker ps

빌드를 수행한 후 결과값을 보면 정상적으로 수행되었음을 확인 할 수 있습니다.

작업을 한 후의 결과물들이 워크스페이스에 저장되게 됩니다.

vscode를 통해서 jenkins 컨테이너에 접속해서 "/var/jenkins_home/workspace/first/test.txt"를 확인해 봅니다.

간단하게 jenkins의 동작 방식을 알아보았습니다.
앞서 Gogs private repository를 만들었고 최신 소스를 jenkins에서 가져오기 위해서 jenkins에 계정 정보를 등록해 줍니다.
Jenkins관리 항목에서 "Security/Credentials" 선택합니다.

"(global)" 선택합니다.

아래 정보를 입력한 후 생성해 줍니다.


Jenkins Dashboard에서 New Itme을 선택한 후 "second"라고 item name을 지정하고 type은 "freestyle project"를 선택 후 생성합니다.
생성된 second item을 선택 후 configure 메뉴에서 tring Parameter 클릭한 후 아래 값을 입력해 줍니다.
매개변수 명(FirstPara) , Default Value(CICD)

소스코드관리에서 Git 선택하고 아래 값을 입력 해 줍니다.
- Repository URL : http://***<mac IP>***:3000/***<Gogs 계정명>***/dev-app ← .git 은 제거
- Credentials +Add 클릭(Jenkins :
- Branches to build (Branch Specifier) : */main

Build Steps에서 Execute shell을 선택 후 아래 명령어를 입력해 줍니다.
echo "{$FirstPara}"
cat VERSION

Save 후 ‘파라미터와 함께 빌드’를 선택하고 수행해 줍니다. (값은 기본값으로)

Console Output 확인을 확인해 봅니다.
gogs git과 관련된 작업들이 수행되고 지정된 파라미터 값과 cat 명령어를 통한 version 파일도 확인됩니다.

작업 공간 workspace 확인 할 수 있습니다.

도커허브의 계정을 jenkins에 등록해 줍니다.

gogs와 도커허브 2개의 계정이 등록되어있습니다.



Jenkins Dashboard에서 New Itme을 선택한 후 "pipeline-ci"라고 item name을 지정하고 type은 "Pipeline"를 선택 후 생성합니다.
생성된 item을 선택 후 configure 메뉴에서 Build Triggers 클릭한 후 Pipeline 항목에서 "pipeline script"를 선택하고 아래 script을 입력 한 후 저장을 합니다.
pipeline {
agent any
environment {
DOCKER_IMAGE = 'attop/dev-app'
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'http://172.30.1.26:3000/devops/dev-app.git', // Git에서 코드 체크아웃
credentialsId: 'gogs-dev-app' // Credentials ID
}
}
stage('Read VERSION') {
steps {
script {
// VERSION 파일 읽기
def version = readFile('VERSION').trim()
echo "Version found: ${version}"
// 환경 변수 설정
env.DOCKER_TAG = version
}
}
}
stage('Docker Build and Push') {
steps {
script {
docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-credentials') {
// DOCKER_TAG 사용
def appImage = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
appImage.push()
}
}
}
}
}
post {
success {
echo "Docker image ${DOCKER_IMAGE}:${DOCKER_TAG} has been built and pushed successfully!"
}
failure {
echo "Pipeline failed. Please check the logs."
}
}
}


Console output에서 수행 결과를 확인해 봅니다.

도커허브 확인
파이프라인에서 지정한 도커허브에 잘 push가 되었습니다.

호스트에서 컨테이너 이미지 생성 확인
컨테이너 이미지가 생성 되었습니다.

앞서 구성했던 내용을 종합해서 아래와 같은 일련의 과정으로 CI/CD 환경을 구성해 봅니다.
Gogs → Jenkins → DockerHub / DockerEngine(Run)
- 개발자가 gogs에 소스코드를 업데이트하면
- gogs가 jenkins에 트리거를 걸고
- jenkins에서는 구성되어 있는 Item(프로젝트나 Pipeline 등등)을 빌드를하고
- 컨테이너 이미지는 도커허브 저장소에 push를 한다.
- 그리고 도커엔진에서 컨테이너를 기동을 하고 이미지가 없으면 도커허브에서 이미지를 다운로드 받는다.

gogs 에 app.ini 파일 수정해 줍니다.
LOCAL_NETWORK_ALLOWLIST = 자신의 PC IP

그리고 Gogs 설정에서 아래의 값들을 webhook에 셋팅해 줍니다.
- Payload URL : http://(localIP):8080/gogs-webhook/?job=**SCM-Pipeline**/
- Content Type : application/json
- Secret : qwe123
- When should this webhook be triggered? : Just the push event
- Active : Check

웹훅이 생성되었습니다.

Jenkins Dashboard에서 New Itme을 선택한 후 "SCM-Pipeline"라고 item name을 지정하고 type은 "Pipeline"를 선택 후 생성합니다.
생성된 파이프라인의 설정값에서 아래에 값들을 입력해 줍니다.
- GitHub project : `http://172.30.1.26:3000/devops/dev-app` - Use Gogs secret : **qwe123** - Build Triggers : **Build when a change is pushed to Gogs 체크** - Pipeline script from SCM - SCM : Git - Repo URL(`http://***<mac IP>***:3000/***<Gogs 계정명>***/dev-app`) - Credentials(**devops/*****) - Branch(***/main**) - Script Path : **Jenkinsfile**
# Jenkinsfile 빈 파일 작성
docker compose exec jenkins touch /var/jenkins_home/dev-app/Jenkinsfile
# 버전 0.0.2 수정 : 아래 입력 잘 안될 경우 VSCODE(Docker플러그인)에서 직접 수정
docker compose exec jenkins sh -c 'echo "0.0.2" > /var/jenkins_home/dev-app/VERSION'
pipeline {
agent any
environment {
DOCKER_IMAGE = 'attop/dev-app' // Docker 이미지 이름
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'http://172.30.1.26:3000/devops/dev-app.git', // Git에서 코드 체크아웃
credentialsId: 'gogs-dev-app' // Credentials ID
}
}
stage('Read VERSION') {
steps {
script {
// VERSION 파일 읽기
def version = readFile('VERSION').trim()
echo "Version found: ${version}"
// 환경 변수 설정
env.DOCKER_TAG = version
}
}
}
stage('Docker Build and Push') {
steps {
script {
docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-credentials') {
// DOCKER_TAG 사용
def appImage = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
appImage.push()
}
}
}
}
}
post {
success {
echo "Docker image ${DOCKER_IMAGE}:${DOCKER_TAG} has been built and pushed successfully!"
}
failure {
echo "Pipeline failed. Please check the logs."
}
}
}
#
docker compose exec jenkins bash
---------------------------------
cd /var/jenkins_home/dev-app/
git add . && git commit -m "Jenkinsfile add & VERSION 0.0.2 Changed" && git push -u origin main

Jenkins 트리거 빌드 확인

도커 저장소 확인

Gogs WebHook 기록 확인

server.py 와 VERSION 변경 후 Jenkins 트리거 작업 한번 더 확인하기
# server.py
# 버전 0.0.3 수정 : 아래 입력 잘 안될 경우 VSCODE(Docker플러그인)에서 직접 수정
docker compose exec jenkins sh -c 'echo "0.0.3" > /var/jenkins_home/dev-app/VERSION'
#
docker compose exec jenkins bash
---------------------------------
cd /var/jenkins_home/dev-app/
git add . && git commit -m "VERSION $(cat VERSION) Changed" && git push -u origin main
jenkins에서 자동으로 빌드 실행되었고 정상적으로 완료되었다.


도커허브에도 이미지가 push되어 있다.

pipeline {
agent any
environment {
DOCKER_IMAGE = 'attop/dev-app' // Docker 이미지 이름
CONTAINER_NAME = 'dev-app' // 컨테이너 이름
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'http://172.30.1.26:3000/devops/dev-app.git', // Git에서 코드 체크아웃
credentialsId: 'gogs-dev-app' // Credentials ID
}
}
stage('Read VERSION') {
steps {
script {
// VERSION 파일 읽기
def version = readFile('VERSION').trim()
echo "Version found: ${version}"
// 환경 변수 설정
env.DOCKER_TAG = version
}
}
}
stage('Docker Build and Push') {
steps {
script {
docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-credentials') {
// DOCKER_TAG 사용
def appImage = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
appImage.push()
appImage.push("latest") // 빌드 이미지 push 할 때, 2개의 버전(현재 버전, latest 버전)을 업로드
}
}
}
}
stage('Check, Stop and Run Docker Container') {
steps {
script {
// 실행 중인 컨테이너 확인
def isRunning = sh(
script: "docker ps -q -f name=${CONTAINER_NAME}",
returnStdout: true
).trim()
if (isRunning) {
echo "Container '${CONTAINER_NAME}' is already running. Stopping it..."
// 실행 중인 컨테이너 중지
sh "docker stop ${CONTAINER_NAME}"
// 컨테이너 제거
sh "docker rm ${CONTAINER_NAME}"
echo "Container '${CONTAINER_NAME}' stopped and removed."
} else {
echo "Container '${CONTAINER_NAME}' is not running."
}
// 5초 대기
echo "Waiting for 5 seconds before starting the new container..."
sleep(5)
// 신규 컨테이너 실행
echo "Starting a new container '${CONTAINER_NAME}'..."
sh """
docker run -d --name ${CONTAINER_NAME} -p 4000:80 ${DOCKER_IMAGE}:${DOCKER_TAG}
"""
}
}
}
}
post {
success {
echo "Docker image ${DOCKER_IMAGE}:${DOCKER_TAG} has been built and pushed successfully!"
}
failure {
echo "Pipeline failed. Please check the logs."
}
}
}
생성된 컨테이너 접속 확인





server.py 수정 후 VERSION 수정 후 push 후 생성된 컨테이너 접속 후 반영 확인
# server.py 수정
response_string = now.strftime("The time is %-I:%M:%S %p, CICD Study 1week!!!!!!!!!!!!!!!!!!!.\n")
# VERSION 수정
0.0.7 #내용 수정
# Jenkins 컨테이너 내부에서 git push
docker compose exec jenkins bash
---------------------------------
cd /var/jenkins_home/dev-app/
git add . && git commit -m "VERSION $(cat VERSION) Changed" && git push -u origin main
# 호스트 PC에서 반복 접속 실행 : 서비스 중단 시간 체크!
while true; do curl -s --connect-timeout 1 http://127.0.0.1:4000 ; date; sleep 1 ; done
version을 수정 및 push 후 업데이트된 컨테이너로 바뀌는 과정에서 9초의 단절이 발생되었습니다.