๐Ÿ‹ Docker ๋„คํŠธ์›Œํ‚น & ๋ณผ๋ฅจ ์ดํ•ด (1-5)

hyun_wยท2025๋…„ 8์›” 20์ผ

Docker ๋„คํŠธ์›Œํ‚น๊ณผ ๋ณผ๋ฅจ ์ •์˜

Docker ๋„คํŠธ์›Œํ‚น ๐ŸŒ

Docker ๋„คํŠธ์›Œํ‚น์€ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์„œ๋กœ, ๊ทธ๋ฆฌ๊ณ  ํ˜ธ์ŠคํŠธ ๋จธ์‹ ์ด๋‚˜ ์™ธ๋ถ€ ๋„คํŠธ์›Œํฌ์™€ ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. Docker๋Š” ์—ฌ๋Ÿฌ ์ข…๋ฅ˜์˜ ๋‚ด์žฅ(built-in) ๋„คํŠธ์›Œํฌ ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์ œ๊ณตํ•˜๋ฉฐ, ๊ฐ€์žฅ ํ”ํ•˜๊ฒŒ ์‚ฌ์šฉ๋˜๋Š” ๋“œ๋ผ์ด๋ฒ„๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

Docker ๋ณผ๋ฅจ ๐Ÿ’พ

Docker ๋ณผ๋ฅจ์€ ์ปจํ…Œ์ด๋„ˆ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์˜๊ตฌ์ ์œผ๋กœ ์ €์žฅํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜์ž…๋‹ˆ๋‹ค. ์ปจํ…Œ์ด๋„ˆ๋Š” ํœ˜๋ฐœ์„ฑ(ephemeral)์ด๊ธฐ ๋•Œ๋ฌธ์— ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์‚ญ์ œ๋˜๋ฉด ๊ทธ ์•ˆ์— ์žˆ๋˜ ๋ฐ์ดํ„ฐ๋„ ํ•จ๊ป˜ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค. ๋ณผ๋ฅจ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ์ปจํ…Œ์ด๋„ˆ์˜ ์ˆ˜๋ช…์ฃผ๊ธฐ์™€ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ›  ์‹ค์Šต ์˜ˆ์ œ: MySQL + Node.js API ์—ฐ๋™

์˜ค๋ฅ˜ ๋ฐœ์ƒ

์˜ค๋ฅ˜ ํ•ด๊ฒฐ

๐Ÿ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

docker-basic-demo/
โ”œโ”€โ”€ api/
โ”‚   โ”œโ”€โ”€ package.json
โ”‚   โ”œโ”€โ”€ server.js
โ”‚   โ””โ”€โ”€ Dockerfile
โ””โ”€โ”€ database/
    โ””โ”€โ”€ init.sql

1๏ธโƒฃ MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •

# database/init.sql

SET NAMES utf8mb4;
SET CHARACTER SET utf8mb4;

-- Character set configuration
CREATE DATABASE IF NOT EXISTS simple_blog 
  DEFAULT CHARACTER SET utf8mb4 
  DEFAULT COLLATE utf8mb4_unicode_ci;
USE simple_blog;

-- Table creation with utf8mb4
CREATE TABLE IF NOT EXISTS posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Sample data with Korean content
INSERT INTO posts (title, content) VALUES
('Docker ๋„คํŠธ์›Œํ‚น ๊ธฐ์ดˆ', 'Docker ์ปจํ…Œ์ด๋„ˆ ๊ฐ„ ํ†ต์‹  ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›Œ๋ด…์‹œ๋‹ค.'),
('๋ณผ๋ฅจ์œผ๋กœ ๋ฐ์ดํ„ฐ ์œ ์ง€ํ•˜๊ธฐ', 'Docker ๋ณผ๋ฅจ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์˜๊ตฌ์ ์œผ๋กœ ๋ณด์กด๋ฉ๋‹ˆ๋‹ค.'),
('์ฒซ ๋ฒˆ์งธ ์‹ค์Šต', '๊ฐ„๋‹จํ•œ MySQL + Node.js ์—ฐ๋™ ์‹ค์Šต์ž…๋‹ˆ๋‹ค.');

SELECT 'Database initialization completed' AS status;

2๏ธโƒฃ Node.js API ์„œ๋ฒ„ ๊ตฌ์„ฑ

Node.js ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

# api

npm init -y

api/package.json

{
  "name": "simple-blog-api",
  "version": "1.0.0",
  "description": "๊ฐ„๋‹จํ•œ ๋ธ”๋กœ๊ทธ API",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "mysql2": "^3.6.0",
    "cors": "^2.8.5"
  }
}

api/server.js

const express = require('express');
const mysql = require('mysql2/promise');
const cors = require('cors');

const app = express();
const PORT = process.env.PORT || 3000;

// ๋ฏธ๋“ค์›จ์–ด ์„ค์ •
app.use(cors());
app.use(express.json());

// MySQL ์—ฐ๊ฒฐ ์„ค์ •
const dbConfig = {
  host: process.env.DB_HOST || 'localhost',
  port: process.env.DB_PORT || 3306,
  user: process.env.DB_USER || 'root',
  password: process.env.DB_PASSWORD || 'secret',
  database: process.env.DB_NAME || 'simple_blog',
  timezone: '+09:00',
  charset: 'utf8mb4',
  supportBigNumbers: true,
  bigNumberStrings: true
};

let connection;

// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํ•จ์ˆ˜
async function connectDatabase() {
  try {
    connection = await mysql.createConnection(dbConfig);
    console.log('โœ… MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์„ฑ๊ณต');
    console.log(`๐Ÿ“ ์—ฐ๊ฒฐ ์ •๋ณด: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`);
  } catch (error) {
    console.error('โŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์‹คํŒจ:', error.message);
    console.log('๐Ÿ”„ 5์ดˆ ํ›„ ์žฌ์‹œ๋„...');
    setTimeout(connectDatabase, 5000);
  }
}

// ๋ผ์šฐํŠธ ์ •์˜
// 1. API ์ƒํƒœ ํ™•์ธ
app.get('/health', async (req, res) => {
  try {
    const [rows] = await connection.execute('SELECT COUNT(*) as count FROM posts');
    res.json({
      status: 'OK',
      message: 'API ์„œ๋ฒ„๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค',
      timestamp: new Date().toISOString(),
      database: 'connected',
      posts_count: rows[0].count
    });
  } catch (error) {
    res.status(500).json({
      status: 'ERROR',
      message: 'Database connection failed',
      error: error.message
    });
  }
});

// 2. ๋ชจ๋“  ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ
app.get('/posts', async (req, res) => {
  try {
    const [rows] = await connection.execute(
      'SELECT id, title, content, created_at FROM posts ORDER BY created_at DESC'
    );
    res.json({
      success: true,
      data: rows,
      count: rows.length
    });
  } catch (error) {
    console.error('๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ ์˜ค๋ฅ˜:', error);
    res.status(500).json({
      success: false,
      message: '๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค',
      error: error.message
    });
  }
});

// 3. ์ƒˆ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ
app.post('/posts', async (req, res) => {
  const { title, content } = req.body;

  if (!title || !content) {
    return res.status(400).json({
      success: false,
      message: '์ œ๋ชฉ๊ณผ ๋‚ด์šฉ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค'
    });
  }

  try {
    const [result] = await connection.execute(
      'INSERT INTO posts (title, content) VALUES (?, ?)',
      [title, content]
    );

    const [newPost] = await connection.execute(
      'SELECT id, title, content, created_at FROM posts WHERE id = ?',
      [result.insertId]
    );

    res.status(201).json({
      success: true,
      message: '๊ฒŒ์‹œ๊ธ€์ด ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค',
      data: newPost[0]
    });
  } catch (error) {
    console.error('๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ์˜ค๋ฅ˜:', error);
    res.status(500).json({
      success: false,
      message: '๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค',
      error: error.message
    });
  }
});

// 4. ํŠน์ • ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ
app.get('/posts/:id', async (req, res) => {
  const postId = req.params.id;

  try {
    const [rows] = await connection.execute(
      'SELECT id, title, content, created_at FROM posts WHERE id = ?',
      [postId]
    );

    if (rows.length === 0) {
      return res.status(404).json({
        success: false,
        message: '๊ฒŒ์‹œ๊ธ€์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค'
      });
    }

    res.json({
      success: true,
      data: rows[0]
    });
  } catch (error) {
    console.error('๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ ์˜ค๋ฅ˜:', error);
    res.status(500).json({
      success: false,
      message: '๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค',
      error: error.message
    });
  }
});

// 5. ๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ
app.delete('/posts/:id', async (req, res) => {
  const postId = req.params.id;

  try {
    const [result] = await connection.execute(
      'DELETE FROM posts WHERE id = ?',
      [postId]
    );

    if (result.affectedRows === 0) {
      return res.status(404).json({
        success: false,
        message: '๊ฒŒ์‹œ๊ธ€์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค'
      });
    }

    res.json({
      success: true,
      message: '๊ฒŒ์‹œ๊ธ€์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค'
    });
  } catch (error) {
    console.error('๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ ์˜ค๋ฅ˜:', error);
    res.status(500).json({
      success: false,
      message: '๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค',
      error: error.message
    });
  }
});

// ์„œ๋ฒ„ ์‹œ์ž‘
app.listen(PORT, async () => {
  console.log(`๐Ÿš€ API ์„œ๋ฒ„๊ฐ€ ํฌํŠธ ${PORT}์—์„œ ์‹คํ–‰์ค‘์ž…๋‹ˆ๋‹ค`);
  console.log(`๐Ÿ“‹ API ๋ฌธ์„œ: http://localhost:${PORT}/health`);

  // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์‹œ์ž‘
  await connectDatabase();
});

// ์ •์ƒ์ ์ธ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ
process.on('SIGINT', async () => {
  console.log('\n๐Ÿ›‘ ์„œ๋ฒ„๋ฅผ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค...');
  if (connection) {
    await connection.end();
    console.log('โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ข…๋ฃŒ');
  }
  process.exit(0);
});

api/Dockerfile

FROM node:18-alpine

# ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
WORKDIR /app

# package.json ๋ณต์‚ฌ ๋ฐ ์˜์กด์„ฑ ์„ค์น˜
COPY package*.json ./
RUN npm install

# ์†Œ์Šค ์ฝ”๋“œ ๋ณต์‚ฌ
COPY . .

# ํฌํŠธ ๋…ธ์ถœ
EXPOSE 3000

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰
CMD ["npm", "start"]

3๏ธโƒฃ ์‹ค์Šต ์‹คํ–‰

๋‹จ๊ณ„ 1: MySQL ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰

# 1. MySQL ๋ณผ๋ฅจ ์ƒ์„ฑ
docker volume create mysql-data

# 2. MySQL ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ (๋ณผ๋ฅจ ์—ฐ๊ฒฐ)
docker run -d \
  --name blog-mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  -e MYSQL_DATABASE=simple_blog \
  -p 3306:3306 \
  -v mysql-data:/var/lib/mysql \
  -v $(PWD)/database/init.sql:/docker-entrypoint-initdb.d/init.sql:ro \
  mysql:8.0 \
  --character-set-server=utf8mb4 \
  --collation-server=utf8mb4_unicode_ci \
  --default-time-zone='+09:00'

# 3. MySQL ์ปจํ…Œ์ด๋„ˆ ๋กœ๊ทธ ํ™•์ธ
docker logs blog-mysql
# 2. MySQL ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ (๋ณผ๋ฅจ ์—ฐ๊ฒฐ) code
docker run -d --name blog-mysql -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=simple_blog -p 3306:3306 -v mysql-data:/var/lib/mysql -v ${PWD}/database/init.sql:/docker-entrypoint-initdb.d/init.sql:ro mysql:8.0 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --default-time-zone='+09:00'

๋‹จ๊ณ„ 2: API ์„œ๋ฒ„ ๋นŒ๋“œ ๋ฐ ์‹คํ–‰

# 1. API ์„œ๋ฒ„ ์ด๋ฏธ์ง€ ๋นŒ๋“œ
cd api
docker build -t simple-blog-api .

# 2. API ์„œ๋ฒ„ ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ (MySQL๊ณผ ์—ฐ๊ฒฐ)
docker run -d \
  --name blog-api \
  -e DB_HOST=blog-mysql \
  -e DB_PASSWORD=secret \
  -e DB_NAME=simple_blog \
  -p 3000:3000 \
  --link blog-mysql \
  simple-blog-api

cd ..

๋‹จ๊ณ„ 3: ๋™์ž‘ ํ™•์ธ

# 1. ์ปจํ…Œ์ด๋„ˆ ์ƒํƒœ ํ™•์ธ
docker ps

# 2. API ์ƒํƒœ ํ™•์ธ
curl http://localhost:3000/health

# 3. ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ์กฐํšŒ
curl http://localhost:3000/posts

# 4. ์ƒˆ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ
curl -X POST http://localhost:3000/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"Docker ์‹ค์Šต", "content":"ํฌํŠธ ๋ฐ”์ธ๋”ฉ๊ณผ ๋ณผ๋ฅจ์„ ๋ฐฐ์› ์Šต๋‹ˆ๋‹ค!"}'

# 5. ๋‹ค์‹œ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ํ™•์ธ
curl http://localhost:3000/posts

0๊ฐœ์˜ ๋Œ“๊ธ€