สร้าง VPS บน Hostinger (รับสิทธิพิเศษ)#
-
สมัครผ่านลิงก์พิเศษ: https://www.hostinger.com/melivecode ↗ เพื่อรับส่วนลดพิเศษสำหรับผู้ติดตามช่องหมีไลฟ์โค้ด

-
ใส่โค้ด
MELIVECODEเพื่อรับส่วนลดเพิ่มเติม อีก 20%

-
ในขั้นตอนการติดตั้งระบบปฏิบัติการ เลือก Ubuntu Linux

-
เมื่อสร้าง VPS เสร็จ จะได้ IP สาธารณะสำหรับผู้ใช้
root
-
เปิด Command Prompt/Terminal แล้วเชื่อมต่อด้วยคำสั่ง ssh ด้วย
root
ssh <User>@<VPS_IP>bashใส่รหัสผ่านเพื่อเข้าสู่เซิร์ฟเวอร์

1. วัตถุประสงค์#
บทความนี้:
- ติดตั้ง Docker บน Ubuntu อย่างถูกต้อง
- บูตสแตรปโปรเจกต์ที่มีทั้ง API (Express + MySQL) และ Frontend (Next.js)
- รันทดสอบแบบ Local (ยังไม่ใช้ Docker)
- Dockerize ทั้ง API และ Frontend แล้วรันด้วย Docker Compose
- ใช้ Jenkins ในการทำ CI/CD เพื่อ Automate Deploy Full-stack App ขึ้น Cloud VPS
2. ติดตั้ง Docker บน Ubuntu#
อ้างอิงจากคู่มือทางการของ Docker สำหรับ Ubuntu https://docs.docker.com/engine/install/ubuntu/ ↗
เตรียมระบบและติดตั้งแพ็กเกจพื้นฐาน
sudo apt update
sudo apt install ca-certificates curlbashติดตั้ง GPG key ของ Docker (สำหรับตรวจสอบความถูกต้องของแพ็กเกจ)
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.ascbashเพิ่ม Docker Repository ให้กับ APT
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/nullbashคำสั่งนี้ลงทะเบียน Repository อย่างเป็นทางการของ Docker เพื่อให้ติดตั้ง/อัปเดตแพ็กเกจจากแหล่งที่เชื่อถือได้
อัปเดตรายการแพ็กเกจและติดตั้ง Docker + เครื่องมือที่เกี่ยวข้อง
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginbashทดสอบการติดตั้งด้วยคอนเทนเนอร์ Hello World
sudo docker run hello-worldbash-
หากสำเร็จ จะแสดงข้อความยืนยัน:
plaintextHello from Docker! This message shows that your installation appears to be working correctly.
3. CI/CD ด้วย Jenkins#
3.1 CI/CD คืออะไร?#
CI/CD คือกระบวนการอัตโนมัติสำหรับ Build, Test, Deploy โค้ด
CI (Continuous Integration) = Build + Test อัตโนมัติทุกครั้งที่ Push โค้ด
CD (Continuous Delivery/Deployment) = ปล่อยแอปใหม่ทุกครั้งที่โค้ดผ่าน CI
3.2 Jenkins คืออะไร?#
Jenkins คือเซิร์ฟเวอร์ Automation (Open Source) ยอดนิยมสำหรับงาน CI/CD
- ตรวจจับการ Push โค้ด อัตโนมัติ
- รัน Pipeline (Build, Test, Deploy ฯลฯ)
- ใช้งานร่วมกับ Docker, Kubernetes, Cloud ฯลฯ ได้
3.3 ติดตั้ง Jenkins บน Ubuntu#
อ้างอิงการติดตั้ง Jenkins บน Ubuntu: https://www.jenkins.io/doc/book/installing/linux/ ↗
เตรียมระบบและติดตั้งแพ็กเกจพื้นฐาน
sudo apt update
sudo apt install git
sudo apt install fontconfig openjdk-21-jre
java -versionplaintextเพิ่ม Jenkins apt repo และติดตั้ง
sudo wget -O /etc/apt/keyrings/jenkins-keyring.asc https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
echo "deb [signed-by=/etc/apt/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/" | sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt update
sudo apt install jenkinsplaintextตรวจสอบว่า Jenkins ทำงาน
sudo service jenkins statusplaintextเพิ่มสิทธิ์ user Jenkins ใช้ docker
sudo groupadd -f docker
sudo usermod -aG docker jenkins
sudo service docker restart
sudo service jenkins restartplaintextเปิด Jenkins ที่ http://<VPS_IP>:8080/ ↗
ดึงรหัสผ่านเริ่มต้นของ Jenkins สำหรับผู้ดูแลระบบ (คัดลอกและวางลงใน Setup Wizard)
sudo nano /var/lib/jenkins/secrets/initialAdminPasswordplaintext

ติดตั้ง suggested plugins, สร้าง Admin User, ยืนยัน Jenkins URL

สร้าง Admin User

ยืนยัน Jenkins URL

4. Full-Stack Project Bootstrap#
เริ่มจากโครงสร้างของโปรเจคที่โฟลเดอร์หลักจะมีทั้ง 01_api และ 02_frontend
mkdir 01_api
cd 01_api
npm init -y
npm i express mysql2 cors dotenvbashสร้างไฟล์ 01_api/.gitignore:
# Node
node_modules
.env
npm-debug.log*
yarn-error.logplaintextสร้าง API ด้วย Express app
สร้างไฟล์ 01_api/index.js:
const express = require('express');
const mysql = require('mysql2/promise');
const cors = require('cors');
require('dotenv').config({ path: '.env.local' });
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors());
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'test',
port: Number(process.env.DB_PORT || 3306),
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
});
app.get('/health', async (req, res) => {
try {
const [rows] = await pool.query('SELECT 1 AS ok');
res.json({ status: 'ok', db: rows[0].ok === 1 });
} catch (e) {
console.error(e);
res.status(500).json({ status: 'error', message: e.message });
}
});
app.get('/attractions', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM attraction');
res.json(rows);
} catch (e) {
console.error(e);
res.status(500).json({ error: 'Internal Server Error' });
}
});
const port = Number(process.env.PORT || 3001);
app.listen(port, () => console.log(`API listening on http://localhost:${port}`));jsตั้งค่า Local Database (XAMPP)
- เปิด XAMPP → Start Apache & MySQL
- เปิด phpMyAdmin:
http://localhost/phpmyadmin/ - สร้างฐานข้อมูล:
test - รันสคริปต์ SQL ด้านล่างเพื่อสร้าง/ใส่ข้อมูลตัวอย่าง:
CREATE TABLE `attraction` (
`id` int(11) PRIMARY KEY AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`detail` varchar(500) NOT NULL,
`coverimage` varchar(100) NOT NULL,
`latitude` decimal(11,7) NOT NULL,
`longitude` decimal(11,7) NOT NULL
);
INSERT INTO `attraction` (`id`, `name`, `detail`, `coverimage`, `latitude`, `longitude`) VALUES
(1, 'Phi Phi Islands', 'Phi Phi Islands are a group of islands in Thailand between the large island of Phuket and the Malacca Coastal Strait of Thailand.', 'https://www.melivecode.com/attractions/1.jpg', '7.7376190', '98.7068755'),
(2, 'Eiffel Tower', 'Eiffel Tower is one of the most famous structures in the world. Eiffel Tower is named after a leading French architect and engineer. It was built as a symbol of the World Fair in 1889.', 'https://www.melivecode.com/attractions/2.jpg', '48.8583736', '2.2922926'),
(3, 'Times Square', 'Times Square has become a global landmark and has become a symbol of New York City. This is a result of Times Square being a modern, futuristic venue, with huge advertising screens dotting its surroundings.', 'https://www.melivecode.com/attractions/3.jpg', '40.7589652', '-73.9893574'),
(4, 'Mount Fuji', 'Mount Fuji is the highest mountain in Japan, about 3,776 meters (12,388 feet) situated to the west of Tokyo. Mount Fuji can be seen from Tokyo on clear days.', 'https://www.melivecode.com/attractions/4.jpg', '35.3606422', '138.7186086'),
(5, 'Big Ben', 'Westminster Palace Clock Tower which is most often referred to as Big Ben. This is actually the nickname for the largest bell that hangs in the vent above the clock face.', 'https://www.melivecode.com/attractions/5.jpg', '51.5007325', '-0.1268141'),
(6, 'Taj Mahal', 'The Taj Mahal or Tachomhal is a burial building made of ivory white marble. The Taj Mahal began to be built in 1632 and was completed in 1643.', 'https://www.melivecode.com/attractions/6.jpg', '27.1751496', '78.0399535'),
(7, 'Stonehenge', 'Stonehenge is a monument prehistoric In the middle of a vast plain in the southern part of the British. The monument itself consists of 112 gigantic stone blocks arranged in 3 overlapping circles.', 'https://www.melivecode.com/attractions/7.jpg', '51.1788853', '-1.8284037'),
(8, 'Statue of Liberty', 'The Statue of Liberty is a colossal neoclassical sculpture on Liberty Island in New York Harbor in New York City, in the United States. The copper statue, a gift from the people of France to the people of the United States.', 'https://www.melivecode.com/attractions/8.jpg', '40.6891670', '-74.0444440'),
(9, 'Sydney Opera House', 'The Sydney Opera House is a multi-venue performing arts centre in Sydney. Located on the banks of the Sydney Harbour, it is often regarded as one of the most famous and distinctive buildings and a masterpiece of 20th century architecture.', 'https://www.melivecode.com/attractions/9.jpg', '-33.8586110', '151.2141670'),
(10, 'Great Pyramid of Giza', 'The Great Pyramid of Giza is the oldest and largest of the pyramids in the Giza pyramid complex bordering present-day Giza in Greater Cairo, Egypt. It is the oldest of the Seven Wonders of the Ancient World, and the only one to remain largely intact.', 'https://www.melivecode.com/attractions/10.jpg', '29.9791670', '31.1341670'),
(11, 'Hollywood Sign', 'The Hollywood Sign is an American landmark and cultural icon overlooking Hollywood, Los Angeles, California. It is situated on Mount Lee, in the Beachwood Canyon area of the Santa Monica Mountains. Spelling out the word Hollywood in 45 ft (13.7 m)-tall white capital letters and 350 feet (106.7 m) long.', 'https://www.melivecode.com/attractions/11.jpg', '34.1340610', '-118.3215920'),
(12, 'Wat Phra Kaew', 'Wat Phra Kaew, commonly known in English as the Temple of the Emerald Buddha and officially as Wat Phra Si Rattana Satsadaram, is regarded as the most sacred Buddhist temple in Thailand. The complex consists of a number of buildings within the precincts of the Grand Palace in the historical centre of Bangkok.', 'https://www.melivecode.com/attractions/12.jpg', '13.7513890', '100.4925000')sql4.3 ทดสอบ API แบบ Local
cd 01_api
node index.jsbashเปิด:
4.4 สร้าง Next.js Frontend
สร้างโปรเจกต์ไว้ข้างๆโฟลเดอร์ API:
npx create-next-app@15.5.0 02_frontendbashแก้ 02_frontend/package.json:
{
"scripts": {
"dev": "next dev --turbopack -p 3000",
"build": "next build --turbopack",
"start": "next start -p 3000",
"lint": "eslint"
}
}jsonแก้ 02_frontend/next.config.mjs:
/** @type {import('next').NextConfig} */
const API_HOST = process.env.API_HOST || 'http://localhost:3001';
const nextConfig = {
output: 'standalone',
env: {
NEXT_PUBLIC_API_HOST: API_HOST,
},
};
export default nextConfig;jsสร้าง 02_frontend/app/page.js:
"use client";
import { useState, useEffect } from "react";
export default function Page() {
const [rows, setRows] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function getAttractions() {
try {
const apiHost = process.env.NEXT_PUBLIC_API_HOST;
const res = await fetch(`${apiHost}/attractions`, { cache: "no-store" });
if (!res.ok) throw new Error("Failed to fetch");
const data = await res.json();
setRows(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
getAttractions();
}, []);
if (loading) {
return (
<main className="container">
<div className="empty">Loading...</div>
</main>
);
}
if (error) {
return (
<main className="container">
<div className="empty">Error: {error}</div>
</main>
);
}
return (
<main className="container">
<header className="header">
<h1 className="title">Attractions</h1>
<p className="subtitle">Discover points of interest nearby</p>
</header>
{!rows || rows.length === 0 ? (
<div className="empty">No attractions found.</div>
) : (
<section className="grid" aria-live="polite">
{rows.map((x) => (
<article key={x.id} className="card" tabIndex={0}>
{x.coverimage && (
<div className="media">
<img
src={x.coverimage}
alt={x.name}
className="img"
loading="lazy"
decoding="async"
/>
</div>
)}
<div className="body">
<h3 className="card-title">{x.name}</h3>
{x.detail && <p className="detail">{x.detail}</p>}
<div className="meta">
<small>
Lat: <span className="code">{x.latitude}</span> · Lng:{" "}
<span className="code">{x.longitude}</span>
</small>
</div>
</div>
</article>
))}
</section>
)}
</main>
);
}jsสร้าง 02_frontend/app/globals.css:
:root {
--bg: #0b0f0d; /* very dark background */
--text: #d9fadd; /* soft greenish-white text */
--muted: #88b39a; /* muted green-gray */
--card: #111613; /* dark card */
--ring: #4ade80; /* green accent */
--border: #1f2b25; /* dark border */
--shadow: 0 8px 24px rgba(74, 222, 128, 0.15); /* green shadow */
}
/* Reset + layout */
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
min-height: 100%;
scroll-behavior: smooth;
}
/* Container */
.container { margin: 0 auto; padding: 2rem 1rem 3rem; max-width: 1040px; }
/* Header */
.header { display: grid; gap: .25rem; margin-bottom: 1.25rem; }
.title { font-size: clamp(1.5rem, 2.4vw, 2.25rem); line-height: 1.2; font-weight: 800; letter-spacing: -0.01em; color: var(--text); }
.subtitle { color: var(--muted); font-size: .95rem; }
/* Empty state */
.empty {
margin-top: 2rem; padding: 2rem; border: 1px dashed var(--border);
border-radius: 12px; text-align: center; color: var(--muted);
background: color-mix(in srgb, var(--card) 85%, #0a1f14);
}
/* Grid */
.grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
margin-top: .5rem;
}
/* Card */
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
overflow: clip;
box-shadow: var(--shadow);
display: grid;
grid-template-rows: auto 1fr;
transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
outline: none;
}
.card:focus-visible {
transform: translateY(-2px);
border-color: var(--ring);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 40%, transparent);
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 12px 28px rgba(74, 222, 128, 0.25);
}
/* Media */
.media {
aspect-ratio: 16 / 9;
background: linear-gradient(180deg, rgba(74, 222, 128, 0.12), rgba(22, 163, 74, 0.05));
}
.img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Body */
.body { padding: 0.9rem 1rem 1rem; display: grid; gap: 0.55rem; }
.card-title { font-size: 1.05rem; font-weight: 700; letter-spacing: -0.01em; line-height: 1.35; color: var(--text); }
.detail {
color: var(--muted); font-size: .95rem; line-height: 1.55;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
}
.meta { margin-top: .15rem; color: var(--muted); font-size: .85rem; }
.code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: .85em;
background: color-mix(in srgb, var(--border) 50%, transparent);
padding: .15rem .35rem;
border-radius: 6px;
color: #a7f3d0;
}
/* Responsive */
@media (min-width: 768px) {
.grid { gap: 1.25rem; }
.body { padding: 1rem 1.05rem 1.1rem; }
.card-title { font-size: 1.12rem; }
.detail { -webkit-line-clamp: 3; }
}
@media (max-width: 360px) {
.container { padding: 1.5rem .75rem 2.25rem; }
.grid { gap: .75rem; }
}
@media (prefers-reduced-motion: reduce) {
.card, .card:hover, .card:focus-visible {
transition: none;
transform: none;
}
}cssตัวอย่างไฟล์แวดล้อม:
02_frontend/.env.example
02_frontend/.envplaintextสร้างไฟล์ .env.example และ .env:
# API configuration for local development
API_HOST=http://localhost:3001
# Next.js configuration
NODE_ENV=development
NEXT_TELEMETRY_DISABLED=1plaintextแก้ 02_frontend/.gitignore ให้ เพิ่ม บรรทัดต่อไปนี้เพื่อให้ตัวอย่าง env ถูกติดตามใน Git:
!.env.exampleplaintextรันทั้งสองโปรเจกต์แบบ Local
# Terminal A (API)
cd 01_api
node index.js
# Terminal B (Next.js)
cd 02_frontend
npm run devbash- Frontend → http://localhost:3000/ ↗
- API ที่ถูกเรียก → http://localhost:3001/attractions ↗

5. Dockerize ทั้งสแต็ก#
หลังทดสอบ Local ผ่านแล้ว สร้างไฟล์ Docker
Backend Dockerfile — 01_api/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3001
CMD ["node", "index.js"]dockerfileFrontend Dockerfile — 02_frontend/Dockerfile
# ---------- Stage 1: Build the Next.js application ----------
FROM node:20-alpine AS builder
ARG API_HOST=http://localhost:3001
WORKDIR /app
COPY package*.json ./
RUN npm ci && npm cache clean --force
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1 \
API_HOST=${API_HOST}
RUN npm run build
# ---------- Stage 2: Production runtime ----------
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]dockerfileDocker Compose — docker-compose.yml
services:
mysql:
image: mysql:8.0
container_name: attractions_mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: Asia/Bangkok
ports:
- "127.0.0.1:${MYSQL_PORT:-3306}:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks: [stack]
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
restart: unless-stopped
environment:
PMA_HOST: mysql
PMA_PORT: 3306
UPLOAD_LIMIT: 256M
ports:
- "${PHPMYADMIN_PORT:-8888}:80"
depends_on: [mysql]
networks: [stack]
api:
build: ./01_api
restart: unless-stopped
environment:
NODE_ENV: production
PORT: ${API_PORT}
DB_HOST: mysql
DB_PORT: ${DB_PORT}
DB_NAME: ${MYSQL_DATABASE}
DB_USER: ${MYSQL_USER}
DB_PASSWORD: ${MYSQL_PASSWORD}
TZ: Asia/Bangkok
ports:
- "${API_PORT:-3001}:3001"
depends_on: [mysql]
networks: [stack]
frontend:
build:
context: ./02_frontend
args:
- API_HOST=${API_HOST:-http://localhost:3001}
restart: unless-stopped
environment:
- NODE_ENV=production
ports:
- "${FRONTEND_PORT:-3000}:3000"
depends_on: [api]
networks: [stack]
volumes:
mysql_data:
networks:
stack:
driver: bridgeyamlไฟล์ตัวอย่างตัวแปรแวดล้อม — .env.example:
# MySQL Database Configuration
MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=attractions_db
MYSQL_USER=attractions_user
MYSQL_PASSWORD=attractions_pass
MYSQL_PORT=3306
# phpMyAdmin Configuration
PHPMYADMIN_PORT=8080
# API Configuration
API_PORT=3001
DB_PORT=3306
# Frontend Configuration
FRONTEND_PORT=3000
API_HOST=http://localhost:3001plaintextสคริปต์ seed DB อัตโนมัติ — init.sql:
CREATE TABLE `attraction` (
`id` int(11) PRIMARY KEY AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`detail` varchar(500) NOT NULL,
`coverimage` varchar(100) NOT NULL,
`latitude` decimal(11,7) NOT NULL,
`longitude` decimal(11,7) NOT NULL
);
INSERT INTO `attraction` (`id`, `name`, `detail`, `coverimage`, `latitude`, `longitude`) VALUES
(1, 'Phi Phi Islands', 'Phi Phi Islands are a group of islands in Thailand between the large island of Phuket and the Malacca Coastal Strait of Thailand.', 'https://www.melivecode.com/attractions/1.jpg', '7.7376190', '98.7068755'),
(2, 'Eiffel Tower', 'Eiffel Tower is one of the most famous structures in the world. Eiffel Tower is named after a leading French architect and engineer. It was built as a symbol of the World Fair in 1889.', 'https://www.melivecode.com/attractions/2.jpg', '48.8583736', '2.2922926'),
(3, 'Times Square', 'Times Square has become a global landmark and has become a symbol of New York City. This is a result of Times Square being a modern, futuristic venue, with huge advertising screens dotting its surroundings.', 'https://www.melivecode.com/attractions/3.jpg', '40.7589652', '-73.9893574'),
(4, 'Mount Fuji', 'Mount Fuji is the highest mountain in Japan, about 3,776 meters (12,388 feet) situated to the west of Tokyo. Mount Fuji can be seen from Tokyo on clear days.', 'https://www.melivecode.com/attractions/4.jpg', '35.3606422', '138.7186086'),
(5, 'Big Ben', 'Westminster Palace Clock Tower which is most often referred to as Big Ben. This is actually the nickname for the largest bell that hangs in the vent above the clock face.', 'https://www.melivecode.com/attractions/5.jpg', '51.5007325', '-0.1268141'),
(6, 'Taj Mahal', 'The Taj Mahal or Tachomhal is a burial building made of ivory white marble. The Taj Mahal began to be built in 1632 and was completed in 1643.', 'https://www.melivecode.com/attractions/6.jpg', '27.1751496', '78.0399535'),
(7, 'Stonehenge', 'Stonehenge is a monument prehistoric In the middle of a vast plain in the southern part of the British. The monument itself consists of 112 gigantic stone blocks arranged in 3 overlapping circles.', 'https://www.melivecode.com/attractions/7.jpg', '51.1788853', '-1.8284037'),
(8, 'Statue of Liberty', 'The Statue of Liberty is a colossal neoclassical sculpture on Liberty Island in New York Harbor in New York City, in the United States. The copper statue, a gift from the people of France to the people of the United States.', 'https://www.melivecode.com/attractions/8.jpg', '40.6891670', '-74.0444440'),
(9, 'Sydney Opera House', 'The Sydney Opera House is a multi-venue performing arts centre in Sydney. Located on the banks of the Sydney Harbour, it is often regarded as one of the most famous and distinctive buildings and a masterpiece of 20th century architecture.', 'https://www.melivecode.com/attractions/9.jpg', '-33.8586110', '151.2141670'),
(10, 'Great Pyramid of Giza', 'The Great Pyramid of Giza is the oldest and largest of the pyramids in the Giza pyramid complex bordering present-day Giza in Greater Cairo, Egypt. It is the oldest of the Seven Wonders of the Ancient World, and the only one to remain largely intact.', 'https://www.melivecode.com/attractions/10.jpg', '29.9791670', '31.1341670'),
(11, 'Hollywood Sign', 'The Hollywood Sign is an American landmark and cultural icon overlooking Hollywood, Los Angeles, California. It is situated on Mount Lee, in the Beachwood Canyon area of the Santa Monica Mountains. Spelling out the word Hollywood in 45 ft (13.7 m)-tall white capital letters and 350 feet (106.7 m) long.', 'https://www.melivecode.com/attractions/11.jpg', '34.1340610', '-118.3215920'),
(12, 'Wat Phra Kaew', 'Wat Phra Kaew, commonly known in English as the Temple of the Emerald Buddha and officially as Wat Phra Si Rattana Satsadaram, is regarded as the most sacred Buddhist temple in Thailand. The complex consists of a number of buildings within the precincts of the Grand Palace in the historical centre of Bangkok.', 'https://www.melivecode.com/attractions/12.jpg', '13.7513890', '100.4925000')sql6. Jenkinsfile (Pipeline)#
Jenkinsfile คือไฟล์สคริปต์ที่ใช้เขียนขั้นตอนอัตโนมัติในการ Build, Test และ Deploy โค้ดด้วย Jenkins โดยไฟล์นี้จะอยู่ที่ root ของโปรเจกต์
การมี Jenkinsfile ทำให้ทุกครั้งที่มีการเปลี่ยนแปลงโค้ด (push) ระบบจะสามารถรันขั้นตอนอัตโนมัติ
pipeline {
agent any
triggers {
// Poll SCM as fallback if webhook fails
pollSCM('H/2 * * * *')
}
environment {
// Build Information
BUILD_TAG = "${env.BUILD_NUMBER}"
// Compute short commit inside a step (not inlined in env with sh)
}
parameters {
booleanParam(
name: 'CLEAN_VOLUMES',
defaultValue: true,
description: 'Remove volumes (clears database)'
)
string(
name: 'API_HOST',
defaultValue: 'http://<<VPS_IP>>:3001',
description: 'API host URL for frontend to connect to.'
)
}
stages {
stage('Checkout') {
steps {
script {
echo "Checking out code..."
checkout scm
// safe: compute commit here
env.GIT_COMMIT_SHORT = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
echo "Deploying to production environment"
echo "Build: ${BUILD_TAG}, Commit: ${env.GIT_COMMIT_SHORT}"
}
}
}
stage('Validate') {
steps {
script {
echo "Validating Docker Compose configuration..."
sh 'docker compose config'
}
}
}
stage('Prepare Environment') {
steps {
script {
echo "Preparing environment configuration..."
// Load credentials from Jenkins (masked)
withCredentials([
string(credentialsId: 'MYSQL_ROOT_PASSWORD', variable: 'MYSQL_ROOT_PASS'),
string(credentialsId: 'MYSQL_PASSWORD', variable: 'MYSQL_PASS')
]) {
// Safely write .env without using `sh` interpolation
writeFile file: '.env', text: """\
MYSQL_ROOT_PASSWORD=${env.MYSQL_ROOT_PASS}
MYSQL_DATABASE=attractions_db
MYSQL_USER=attractions_user
MYSQL_PASSWORD=${env.MYSQL_PASS}
MYSQL_PORT=3306
PHPMYADMIN_PORT=8888
API_PORT=3001
DB_PORT=3306
FRONTEND_PORT=3000
NODE_ENV=production
API_HOST=${params.API_HOST}
""".stripIndent()
// Avoid printing secrets
echo ".env file created successfully"
}
}
}
}
stage('Deploy') {
steps {
script {
echo "Deploying to production using Docker Compose..."
// Stop existing containers
def downCommand = 'docker compose down'
if (params.CLEAN_VOLUMES) {
echo "WARNING: Removing volumes (database will be cleared)"
downCommand = 'docker compose down -v'
}
sh downCommand
// Build and start services
sh """
docker compose build --no-cache
docker compose up -d
"""
echo "Deployment completed"
}
}
}
stage('Health Check') {
steps {
script {
echo "Waiting for services to start..."
sh 'sleep 15'
echo "Performing health check..."
sh """
# Check if containers are running
docker compose ps
# Wait for API to be ready (max 60 seconds)
timeout 60 bash -c 'until curl -f http://localhost:3001/health; do sleep 2; done' || exit 1
# Check attractions endpoint
curl -f http://localhost:3001/attractions || exit 1
echo "Health check passed!"
"""
}
}
}
stage('Verify Deployment') {
steps {
script {
echo "Verifying all services..."
sh """
echo "=== Container Status ==="
docker compose ps
echo ""
echo "=== Service Logs (last 20 lines) ==="
docker compose logs --tail=20
echo ""
echo "=== Deployed Services ==="
echo "Frontend: http://localhost:3000"
echo "API: http://localhost:3001"
echo "phpMyAdmin: http://localhost:8888"
"""
}
}
}
}
post {
success {
echo "✅ Deployment completed successfully!"
echo "Build: ${BUILD_TAG}"
echo "Commit: ${env.GIT_COMMIT_SHORT}"
echo ""
echo "Access your application:"
echo " - Frontend: http://localhost:3000"
echo " - API: http://localhost:3001"
echo " - phpMyAdmin: http://localhost:8888"
}
failure {
echo "❌ Deployment failed!"
script {
echo "Printing container logs for debugging..."
sh 'docker compose logs --tail=50 || true'
}
}
always {
echo "Cleaning up old Docker resources..."
sh """
docker image prune -f
docker container prune -f
"""
}
}
}plaintext- Jenkinsfile จะเป็นตัวควบคุม CI/CD Pipeline แบบอัตโนมัติ ตั้งแต่รับโค้ด, ตรวจ config, เตรียม environment, build/deploy, health check และยืนยันการขึ้นระบบ
- ในโค้ดจะมี stage แบ่งเป็นขั้น (เช่น Checkout สำหรับอ่านโค้ด, Prepare Environment สร้างไฟล์ env, Deploy สำหรับสั่ง Docker, Health Check ตรวจสอบ API, Verify สรุปสถานะทุก container)
- ส่วน parameters สามารถปรับ toggle หรือตั้งค่าเช่น API_HOST ได้เอง ทำให้ reuse workflow เดิมในหลาย environment
- ใช้ withCredentials ดึงค่า password จาก Jenkins Credentials แบบปลอดภัย ไม่ต้องเขียน secret ในโค้ด
- ทุกขั้น echo/log ชัดเจน และเมื่อสำเร็จหรือ fail จะสรุป endpoint/แสดง logs ล่าสุด พร้อม cleanup resource เพื่อลด disk usage
7. Publish to GitHub (Public)#
ลบ .git ซ้อน ในโฟลเดอร์ 02_frontend
- บน Windows ต้องเปิด View → Show → Hidden items เพื่อมองเห็นโฟลเดอร์ซ่อน แล้วลบ
.git
ตรวจสอบ ว่าคุณ Sign in GitHub บน VS Code แล้ว
- VS Code → Accounts → Sign in with GitHub
จาก VS Code เลือก Publish to GitHub (Public) ที่ Root ของโปรเจกต์ (โฟลเดอร์รวม 01_api, 02_frontend, docker-compose.yml, .env.example, Jenkinsfile)
จะได้ URL ของ GitHub เช่น
https://github.com/<you>/<repository>.git
8. Automate Deploy ด้วย Jenkins ขึ้น VPS (Hostinger)#
เปิด Jenkins ที่ http://<VPS_IP>:8080/ ↗
8.1 สร้าง Jenkins Credentials (สำหรับ MySQL Password)#
ไปที่ Manage Jenkins > Credentials > (global) > Add Credentials

สร้าง Credentials โดยมีรายละเอียดตามด้านล่าง:
-
Kind: Secret text,
- Secret: rootpassword
- ID: MYSQL_ROOT_PASSWORD
-
Kind: Secret text,
- Secret: attractions_pass
- ID: MYSQL_PASSWORD

8.2 สร้าง Pipeline Job ใหม่ใน Jenkins#
- New Item → ตั้งชื่อ → เลือก Pipeline
- Enable:
- ✅ GitHub hook trigger for GITScm polling
- ✅ Poll SCM (Schedule:
H/2 * * * *)
- Pipeline Definition: Pipeline script from SCM
- SCM: Git
- Repository URL: (ใส่ GitHub ของคุณ)
- Branch Specifier:
*/main
8.3 ทดสอบ Build/Deploy บน Jenkins + Docker#
- เมื่อมีการ Push โค้ดขึ้น GitHub → Jenkins จะ auto build & deploy
- ดู log ที่ Jenkins Console Output
- ตรวจสอบการทำงานโดยเข้าไปที่
- phpMyAdmin →
http://<VPS_IP>:8888/
Username: attractions_user
Password: attractions_pass - API →
http://<VPS_IP>:3001/attractions - Frontend →
http://<VPS_IP>:3000/
- phpMyAdmin →