Back

Full-Stack Deploy ขึ้น Cloud VPS ด้วย DockerBlur image

สร้าง VPS บน Hostinger (รับสิทธิพิเศษ)#

  • สมัครผ่านลิงก์พิเศษ: https://www.hostinger.com/melivecode เพื่อรับส่วนลดพิเศษสำหรับผู้ติดตามช่องหมีไลฟ์โค้ด
    ตัวอย่างแพ็กเกจจาก Hostinger

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

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

  • เมื่อสร้าง VPS เสร็จ จะได้ IP สาธารณะสำหรับผู้ใช้ root ตัวอย่างหน้าจอข้อมูลเซิร์ฟเวอร์

  • เปิด Command Prompt/Terminal แล้วเชื่อมต่อด้วยคำสั่ง ssh ด้วย root

ssh <User>@<Public IP Address>
bash

ใส่รหัสผ่านเพื่อเข้าสู่เซิร์ฟเวอร์ ตัวอย่างการเชื่อมต่อผ่าน SSH


1. วัตถุประสงค์#

บทความนี้:

  • ติดตั้ง Docker บน Ubuntu อย่างถูกต้อง
  • บูตสแตรปโปรเจกต์ที่มีทั้ง API (Express + MySQL) และ Frontend (Next.js)
  • รันทดสอบแบบ Local (ยังไม่ใช้ Docker)
  • Dockerize ทั้ง API และ Frontend แล้วรันด้วย Docker Compose

2. ติดตั้ง Docker บน Ubuntu#

อ้างอิงจากคู่มือทางการของ Docker สำหรับ Ubuntu https://docs.docker.com/engine/install/ubuntu/

2.1 เตรียมระบบและติดตั้งแพ็กเกจพื้นฐาน#

sudo apt update
sudo apt install ca-certificates curl
bash

2.2 ติดตั้ง 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.asc
bash

2.3 เพิ่ม 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/null
bash

คำสั่งนี้ลงทะเบียน Repository อย่างเป็นทางการของ Docker เพื่อให้ติดตั้ง/อัปเดตแพ็กเกจจากแหล่งที่เชื่อถือได้

2.4 อัปเดตรายการแพ็กเกจและติดตั้ง Docker + เครื่องมือที่เกี่ยวข้อง#

sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
bash

2.5 ทดสอบการติดตั้งด้วยคอนเทนเนอร์ Hello World#

sudo docker run hello-world
bash

สิ่งที่เกิดขึ้นเบื้องหลัง:

  • Docker จะตรวจว่ามีอิมเมจ hello-world:latest ในเครื่องหรือไม่ หากไม่มี จะเห็นข้อความ Unable to find image 'hello-world:latest' locally

  • จากนั้น Docker จะดึงอิมเมจจาก Docker Hub (ขึ้น Pull complete)

  • เสร็จแล้วจึงสร้างคอนเทนเนอร์และรันโปรแกรมสั้นๆ ในอิมเมจน้ัน

  • หากสำเร็จ จะแสดงข้อความยืนยัน:

    Hello from Docker!
    This message shows that your installation appears to be working correctly.
    plaintext

3. Project Bootstrap (Local only — ยังไม่ใช้ Docker)#

เริ่มจากโครงสร้างของโปรเจคที่โฟลเดอร์หลักจะมีทั้ง 01_api และ 02_frontend

cd 01_api
npm init -y
npm i express mysql2 cors dotenv
bash

สร้างไฟล์ 01_api/.gitignore:

# Node
node_modules
.env
npm-debug.log*
yarn-error.log
plaintext

3.1 สร้าง 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

3.2 ตั้งค่า DB โลคัล (XAMPP)#

  1. เปิด XAMPP → Start Apache & MySQL
  2. เปิด phpMyAdmin: http://localhost/phpmyadmin/
  3. สร้างฐานข้อมูล: test
  4. รันสคริปต์ 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')
sql

3.3 ทดสอบ API แบบ Local#

cd 01_api
node index.js
bash

เปิด:


4. สร้าง Next.js Frontend#

สร้างโปรเจกต์ไว้ข้างๆโฟลเดอร์ API:

npx create-next-app@15.5.0 02_frontend
bash

แก้ 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: #f6fdf8;
  --text: #0a2e17;
  --muted: #5f7a68;
  --card: #ffffff;
  --ring: #81c784;
  --border: #cde8d3;
  --shadow: 0 8px 24px rgba(104, 187, 120, 0.15);
}

/* 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; }
.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 oklab, var(--card) 85%, #d8f3dc);
}

/* 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 oklab, var(--ring) 40%, transparent); }
.card:hover { transform: translateY(-2px); box-shadow: 0 12px 28px rgba(128, 203, 161, 0.25); }

/* Media */
.media { aspect-ratio: 16 / 9; background: linear-gradient(180deg, rgba(129, 199, 132, 0.1), rgba(46, 125, 50, 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; }
.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 oklab, var(--border) 50%, transparent); padding: .15rem .35rem; border-radius: 6px; }

/* 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/.env
plaintext

สร้างไฟล์ .env.example และ .env:

# API configuration for local development
API_HOST=http://localhost:3001

# Next.js configuration
NODE_ENV=development
NEXT_TELEMETRY_DISABLED=1
plaintext

แก้ 02_frontend/.gitignore ให้ เพิ่ม บรรทัดต่อไปนี้เพื่อให้ตัวอย่าง env ถูกติดตามใน Git:

!.env.example
plaintext

4.1 รันทั้งสองโปรเจกต์แบบ Local#

# Terminal A (API)
cd 01_api
node index.js

# Terminal B (Next.js)
cd 02_frontend
npm run dev
bash

ตัวอย่างหน้าจอการทดสอบ Frontend และ API


5. Dockerize ทั้งสแต็ก#

หลังทดสอบ Local ผ่านแล้ว สร้างไฟล์ Docker

5.1 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"]
dockerfile

5.2 Frontend 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"]
dockerfile

5.3 Docker Compose — docker-compose.yml#

สำหรับ Mac ชิป M2: เปลี่ยน phpmyadmin/phpmyadmin:latestarm64v8/phpmyadmin

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: bridge
yaml

ไฟล์ตัวอย่างตัวแปรแวดล้อม — .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:3001
plaintext

สคริปต์ 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')
sql

5) Publish to GitHub (Public)#

  1. ลบ .git ซ้อน ในโฟลเดอร์ 02_frontend

    • บน Windows ต้องเปิด View → Show → Hidden items เพื่อมองเห็นโฟลเดอร์ซ่อน แล้วลบ .git
  2. ตรวจสอบ ว่าคุณ Sign in GitHub บน VS Code แล้ว

    • VS Code → Accounts → Sign in with GitHub
  3. จาก VS Code เลือก Publish to GitHub (Public) ที่ Root ของโปรเจกต์ (โฟลเดอร์รวม 01_api, 02_frontend, docker-compose.yml)

ได้ URL ของ GitHub แล้ว เช่น https://github.com/<you>/<repository>.git


6) Deploy ขึ้น Cloud (VPS ที่ Hostinger)#

6.1 รีโมตไปที่ Server#

  • เปิด Command Prompt/Terminal แล้วเชื่อมต่อด้วยคำสั่ง ssh ด้วย root
ssh <User>@<Public IP Address>
bash

6.2 โคลนโปรเจกต์จาก GitHub#

cd ~
git clone https://github.com/<you>/<repository>.git
cd <repository>
cp .env.example .env   # แก้ค่าให้เหมาะกับ Production
bash

ค่าแนะนำใน .env (ตัวอย่าง):

MYSQL_ROOT_PASSWORD=yourStrongRootPass
MYSQL_DATABASE=attractions_db
MYSQL_USER=attractions_user
MYSQL_PASSWORD=yourStrongDbPass
MYSQL_PORT=3306

PHPMYADMIN_PORT=8080

API_PORT=3001
DB_PORT=3306

FRONTEND_PORT=3000

API_HOST=http://<VPS Public IP Address>:3001
plaintext

6.3 รันด้วย Docker Compose#

docker compose up -d --build
docker compose ps
bash
  • API: http://<SERVER_IP>:3001/attractions
  • Frontend: http://<SERVER_IP>:3000/
Full-Stack Deploy ขึ้น Cloud VPS ด้วย Docker
ผู้เขียน กานต์ ยงศิริวิทย์ / Karn Yongsiriwit
เผยแพร่เมื่อ November 9, 2025
ลิขสิทธิ์ CC BY-NC-SA 4.0

กำลังโหลดความคิดเห็น...

ความคิดเห็น 0