All projects
DevOps case study · KOLVerse

KOLVerse CI/CD Pipeline

How I took three apps from manual builds to Jenkins pipelines with immutable Docker images, a production approval gate, and health-checked releases on AWS.

TL;DRPipeline owner2025 – 2026

From manual builds to gated, health-checked releases for 3 apps.

Role
Designed and ran delivery for 3 apps
Ownership
I owned delivery; the team shipped through it
Core stack
Jenkins · Docker · GitLab Registry · AWS EC2 · Terraform
The problem

Three apps were built and deployed by hand, with no traceable link between a commit and the image running in production.

KOLVerse · DevOps & CI/CD

From repository
to production.

How three application repositories became repeatable Docker artifacts, branch-aware Jenkins pipelines, credential-bound EC2 deployments, Prisma-ready releases, and health-checked production services.

GitLabJenkinsBunDockerGitLab RegistryAWS EC2TerraformALBRoute 53
00
Starting point

The delivery problem before automation

KOLVerse consists of a public Next.js Website, a NestJS/Prisma API, and an internal Next.js Admin. GitLab held source, but production still needed deterministic dependencies, repeatable artifacts, safe credentials, controlled server updates, database migration, and end-to-end verification.

Before
DeveloperGitLabManual buildServer
Target
DeveloperGitLabJenkinsDockerRegistryEC2Production
What I built

Dockerfiles, Jenkinsfiles, build scripts, Compose deploys, health checks, and the Terraform behind them.

What stays private

Registry URLs, credentials, hostnames and AWS account details are replaced with examples throughout.

01
Containerization

Prepare a reproducible API image

The first task was making the API build independently of a developer machine. Application source, Prisma migrations, Bun’s lockfile, Docker configuration, build scripts, and Jenkinsfile remain versioned together.

Deployment-facing repository structureapi/
plaintext
api/
├── src/
├── prisma/
├── scripts/
│   ├── build.staging.sh
│   └── build.prod.sh
├── jenkins/
│   └── notifyTelegram.groovy
├── package.json
├── bun.lock
├── Dockerfile
├── .dockerignore
└── Jenkinsfile
Actual API runtime stageapi/Dockerfile
dockerfile
FROM oven/bun:1.3.6-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV TZ=Asia/Bangkok
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prisma ./prisma
USER bun
EXPOSE 8080
CMD ["sh", "-c", "bunx prisma migrate deploy && bun dist/src/main.js"]
Builder

Installs the locked dependency graph and compiles NestJS.

Runner

Copies only runtime dependencies, dist, package metadata, and Prisma.

Non-root

USER bun reduces process privilege.

Startup

Migration must succeed before NestJS starts, making schema readiness part of deployment readiness.

View complete API Dockerfileapi.Dockerfile
View full source

Loading source…

View Website production Dockerfilewebsite.Dockerfile.production
View full source

Loading source…

02
Docker boundary

Control the context, then prove the container locally

Actual API .dockerignoreapi/.dockerignore
plaintext
node_modules
dist
.git
.gitignore
.idea
.vscode
*.swp
.env
.env.*
!.env.example
*.log
coverage
test
Dockerfile*
docker-compose*
.dockerignore
node_modules / dist

Rebuild inside the image instead of accepting host output.

.git / IDE

Exclude history and editor state.

.env and .env.*

Prevent secrets entering the Docker context.

!.env.example

Keep variable documentation without values.

View complete .dockerignoreapi.dockerignore
View full source

Loading source…

Pre-CI verification pathdeveloper terminal
bash
docker build --platform linux/amd64 -t kolverse-api:local .
docker run --rm --env-file .env.local -p 8080:8080 kolverse-api:local
curl -fsS http://127.0.0.1:8080/api/v1/health-check

This build → run → health sequence validates the production architecture, runtime configuration, Prisma startup, port 8080, and health route before Jenkins automates it.

03
Branch model

Give each GitLab branch a delivery meaning

developmentInstall · lint · format · test · build
stagingValidation + staging image and deployment
main / productionValidation + approval + production release

Validation runs on all discovered branches. Release preparation only runs for staging, main, and production.

04
Jenkins setup

Create one Multibranch Pipeline per application

Multibranch jobs discover branches and load the root Jenkinsfile. Branch conditions then select validation, staging, or production behavior. Screenshots show the real interface with job names and build history replaced.

Sanitized Jenkins dashboard with fictional jobs
Jenkins dashboard: one Multibranch job per application. Job names replaced.
Sanitized API Multibranch job
API Multibranch job with branch-aware builds. Repository name and build metadata replaced.
Job typeMultibranch Pipeline
Script pathJenkinsfile
ConnectiongitLabConnection('example-gitlab')
Retention20 builds
Timeout45 minutes
ConcurrencyDisabled
Validation agentBun container
SCMGitLab
05
Pipeline as code

Execute dependency, quality, test, and build gates

CIArtifactRuntimeEdgeSuccess

Interactive stage components follow the real pipeline structure; public identifiers are examples.

Validation stagesapi/Jenkinsfile · sanitized
groovy
stage('Install') { steps { sh 'bun install --frozen-lockfile' } }
stage('Validate') {
  steps {
    sh 'bun run lint:check'
    sh 'bun run format:check'
  }
}
stage('Test') { steps { sh 'bun run test' } }
stage('Build App') { steps { sh 'bun run build' } }
Frozen install

CI must use the dependency graph committed in bun.lock.

Lint / format

Either failure stops the release path.

Tests

API runs bun run test; Admin runs lint, format and build.

Build

NestJS must compile before an artifact can be released.

View complete sanitized API Jenkinsfileapi.Jenkinsfile
View full source

Loading source…

View complete sanitized Website Jenkinsfilewebsite.Jenkinsfile
View full source

Loading source…

06
Release control

Create a traceable tag, then require approval

Immutable release identityapi/Jenkinsfile · Prepare Release
groovy
def shortSha = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
def imageDate = sh(script: "date -u +'%Y%m%d'", returnStdout: true).trim()
env.RELEASE_ENV = env.BRANCH_NAME == 'staging' ? 'staging' : 'production'
env.TAG = "${imageDate}-${shortSha}"
Example20260815-a1b2c3d

Fictional date and SHA demonstrate traceability without publishing repository history.

Production gateapi/Jenkinsfile · sanitized
groovy
stage('Build & Push Production') {
  when { anyOf { branch 'main'; branch 'production' } }
  steps {
    input message: 'Deploy API to PRODUCTION?', ok: 'Deploy'
    // authenticated build and push follows
  }
}

Website and API require a human decision before production. Adding the same gate to Admin is next on the list.

Sanitized successful API production pipeline
A successful API production release with the manual approval step. Repository, revision and approver replaced.
07
Artifact

Build once and publish to GitLab Registry

Actual production image scriptapi/scripts/build.prod.sh
bash
# Sanitized example — not a production registry
ENV=production
TAG=${1:-v1.0.0}
REGISTRY="registry.example.com/sample/api"
IMAGE="$REGISTRY/$ENV"

docker build \
  --platform linux/amd64 \
  -f "$ROOT_DIR/Dockerfile" \
  -t "$IMAGE:$TAG" \
  "$ROOT_DIR"

docker push "$IMAGE:$TAG"
docker rmi "$IMAGE:$TAG" || true
Git commitYYYYMMDD-SHAlinux/amd64GitLab Registryimmutable image

The script builds the repository root with the actual Dockerfile, pushes the tag, and removes the local copy. EC2 pulls an existing artifact instead of compiling production source.

View complete production build scriptapi-build.prod.sh
View full source

Loading source…

08
Runtime deployment

Prepare EC2, then update only the API

Example API EC2
├── Docker Engine
├── Docker Compose
├── /srv/example-app/api
│   ├── docker-compose.yaml
│   └── .env.production
└── app + infrastructure containers

Terraform supports separate Website, API, and Admin EC2 workloads. The API runtime also contains PostgreSQL, Redis, MinIO, Neo4j, OpenSearch, and observability services.

Targeted Compose deploymentapi/Jenkinsfile · sanitized
bash
cp docker-compose.yaml docker-compose.yaml.bak-$BUILD_NUMBER
perl -0pi -e 's#production-image:[^\n\r ]+#production-image:$TAG#' docker-compose.yaml
docker compose --profile production -f docker-compose.yaml config --quiet
docker compose --profile production -f docker-compose.yaml pull backend
docker compose --profile production -f docker-compose.yaml up -d --no-deps backend
Backup

Preserve the previous Compose definition.

Retag

Point backend at the new immutable image.

Validate

Fail before service changes when Compose config is invalid.

Pull

Download the built artifact.

Replace

--no-deps prevents infrastructure services restarting with the API.

Registry login crossed two shell contexts

Jenkins and remote EC2 are different shells. The fix pipes the bound password into docker login --password-stdin on EC2 before running Compose.

09
Migration and verification

Make schema and public traffic part of readiness

New imageContainer startsprisma migrate deployNestJS startsHost healthPublic health
Actual two-stage health verificationapi/Jenkinsfile
bash
# Example endpoints and ports
for i in $(seq 1 72); do
  curl -fsS http://127.0.0.1:8080/api/v1/health-check \
    >/dev/null 2>&1 && exit 0
  sleep 5
done
exit 1

curl -fsS --retry 24 --retry-delay 10 \
  https://api.example.com/api/v1/health-check >/dev/null
Host check

Pull → replacement → Prisma migration → NestJS on 8080.

Public check

Target health → ALB → TLS → Route 53 → public endpoint.

Migration risk

A backward-incompatible migration means reverting only the image may not restore the previous database/application contract.

Running did not mean publicly ready

ALB needs consecutive target checks while migration and startup take time. The pipeline polls localhost first, then retries the public URL.

10
Real production release

Follow one API deployment end to end

  1. 01

    Merge reaches main or production.

  2. 02

    GitLab webhook fires and Jenkins discovers the branch.

  3. 03

    Frozen install, lint, format, tests, and NestJS build pass.

  4. 04

    YYYYMMDD-shortSHA is generated.

  5. 05

    An operator approves production.

  6. 06

    The linux/amd64 image builds and pushes.

  7. 07

    Jenkins binds Registry, host, and SSH credentials.

  8. 08

    EC2 logs into Registry.

  9. 09

    Compose is backed up, retagged, and validated.

  10. 10

    Backend is pulled and replaced with --no-deps.

  11. 11

    Prisma migration runs before NestJS.

  12. 12

    Local then public health checks pass.

  13. 13

    Telegram reports the result.

KOLVerse production website
The production site after a release passes both health checks.
11
AWS traffic path

Follow traffic from DNS to the container

UserRoute 53ALB + ACMHost ruleTarget groupEC2Docker ComposeApplication

Terraform defines HTTPS, hostname routing, target groups, security boundaries, and Route 53 aliases. The API target group checks /api/v1/health-check.

12
Final architecture

The whole pipeline in one view

SourceCIArtifactRuntimeEdgeSuccess

Every box, credential boundary, artifact hand-off, server update and health gate on this diagram is covered in the steps above.

13
My contribution

What I implemented and maintained

Containerized Website, API, and Admin delivery paths.

Implemented three Jenkins Multibranch pipelines.

Connected GitLab events to Jenkins.

Configured credential-bound Registry and EC2 deployment.

Implemented branch-aware staging and production paths.

Added immutable release tagging.

Integrated Prisma migration into API readiness.

Added host and public-edge health verification.

Debugged webhooks, remote Registry login, Compose profiles, and ALB readiness.

Worked with supporting AWS/Terraform infrastructure and runbooks.

Next steps
  • Add the production approval gate to the Admin pipeline
  • Run Terraform plan/apply from CI with remote state