간이 3 Tier Application 배포 실습 (2+1) - Client+Server / Database
1. 로컬 개발 환경 구성
1-1. 프로젝트 구조 생성
2tier-app/
├── app/
│ ├── Dockerfile
│ ├── package.json
│ ├── app.js
│ └── config.js
├── db/
│ ├── Dockerfile
│ ├── init.sql
│ └── my.cnf
└── docker-compose.yml
1-2. App 서비스 코드 작성
- app/package.json
{
"name": "2tier-app",
"version": "1.0.0",
"description": "2-Tier Architecture Demo",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "^4.17.1",
"mysql2": "^2.3.3",
"dotenv": "^10.0.0"
}
}
- app/config.js
// 환경에 따른 설정 관리
const env = process.env.NODE_ENV || 'development';
const config = {
development: {
db: {
host: process.env.DB_HOST || 'db', // docker-compose 서비스 이름
user: process.env.DB_USER || 'nodeuser',
password: process.env.DB_PASSWORD || 'password123',
database: process.env.DB_NAME || 'nodeapp'
},
port: process.env.PORT || 3000
},
production: {
db: {
host: process.env.DB_HOST, // VM의 IP 주소
user: process.env.DB_USER || 'nodeuser',
password: process.env.DB_PASSWORD || 'password123',
database: process.env.DB_NAME || 'nodeapp'
},
port: process.env.PORT || 3000
}
};
module.exports = config[env];
- app/app.js
const express = require('express');
const mysql = require('mysql2/promise');
const app = express();
const config = require('./config');
console.log(`환경: ${process.env.NODE_ENV || 'development'}`);
console.log(`DB 연결 설정: ${JSON.stringify(config.db)}`);
// JSON 파싱 미들웨어
app.use(express.json());
// MySQL 연결 설정
const pool = mysql.createPool({
host: config.db.host,
user: config.db.user,
password: config.db.password,
database: config.db.database,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// 홈페이지
app.get('/', (req, res) => {
res.send(`
<html>
<head>
<title>2-Tier Architecture Demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
color: #2c3e50;
}
.container {
background-color: #f9f9f9;
border-radius: 5px;
padding: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 10px;
border: 1px solid #ddd;
text-align: left;
}
th {
background-color: #f2f2f2;
}
.server-info {
margin-top: 20px;
padding: 10px;
background-color: #e8f4f8;
border-radius: 5px;
}
form {
margin-top: 20px;
padding: 15px;
background-color: #f0f0f0;
border-radius: 5px;
}
input[type="text"], input[type="email"] {
width: 100%;
padding: 8px;
margin: 5px 0 15px;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
.tab {
overflow: hidden;
border: 1px solid #ccc;
background-color: #f1f1f1;
margin-top: 20px;
}
.tab button {
background-color: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
color: black;
}
.tab button:hover {
background-color: #ddd;
}
.tab button.active {
background-color: #ccc;
}
.tabcontent {
display: none;
padding: 6px 12px;
border: 1px solid #ccc;
border-top: none;
}
#userList {
display: block;
}
</style>
</head>
<body>
<div class="container">
<h1>2-Tier Architecture Demo</h1>
<p>이 페이지는 Docker로 구성된 2-Tier 아키텍처 애플리케이션입니다.</p>
<div class="server-info">
<h3>서버 정보:</h3>
<ul>
<li>환경: ${process.env.NODE_ENV || 'development'}</li>
<li>App Server: ${require('os').hostname()} (컨테이너)</li>
<li>DB Server: ${config.db.host}</li>
<li>Node.js 버전: ${process.version}</li>
</ul>
</div>
<div class="tab">
<button class="tablinks active" onclick="openTab(event, 'userList')">사용자 목록</button>
<button class="tablinks" onclick="openTab(event, 'addUser')">사용자 추가</button>
<button class="tablinks" onclick="openTab(event, 'deleteUser')">사용자 삭제</button>
</div>
<div id="userList" class="tabcontent">
<h2>사용자 목록</h2>
<div id="users-table">데이터베이스에서 사용자 정보를 불러오는 중...</div>
</div>
<div id="addUser" class="tabcontent">
<h2>새 사용자 추가</h2>
<form id="addUserForm">
<label for="username">사용자명:</label>
<input type="text" id="username" name="username" required>
<label for="email">이메일:</label>
<input type="email" id="email" name="email" required>
<button type="submit">사용자 추가</button>
</form>
<div id="addUserResult"></div>
</div>
<div id="deleteUser" class="tabcontent">
<h2>사용자 삭제</h2>
<p>삭제할 사용자의 ID를 입력하세요:</p>
<form id="deleteUserForm">
<label for="userId">사용자 ID:</label>
<input type="text" id="userId" name="userId" required>
<button type="submit">사용자 삭제</button>
</form>
<div id="deleteUserResult"></div>
</div>
<script>
// 탭 기능
function openTab(evt, tabName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tabName).style.display = "block";
evt.currentTarget.className += " active";
}
// 사용자 목록 로딩
function loadUsers() {
fetch('/api/users')
.then(response => response.json())
.then(data => {
const tableEl = document.getElementById('users-table');
if (data.error) {
tableEl.innerHTML = '<p style="color: red;">에러: ' + data.error + '</p>';
} else if (data.length === 0) {
tableEl.innerHTML = '<p>사용자 데이터가 없습니다.</p>';
} else {
let tableHtml = '<table><tr><th>ID</th><th>사용자명</th><th>이메일</th><th>생성일</th></tr>';
data.forEach(user => {
tableHtml += '<tr><td>' + user.id + '</td><td>' + user.username + '</td><td>' + user.email + '</td><td>' + new Date(user.created_at).toLocaleString() + '</td></tr>';
});
tableHtml += '</table>';
tableEl.innerHTML = tableHtml;
}
})
.catch(err => {
document.getElementById('users-table').innerHTML = '<p style="color: red;">데이터 로딩 실패: ' + err.message + '</p>';
});
}
// 초기 사용자 목록 로딩
loadUsers();
// 사용자 추가 폼 제출 처리
document.getElementById('addUserForm').addEventListener('submit', function(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const email = document.getElementById('email').value;
const resultEl = document.getElementById('addUserResult');
resultEl.innerHTML = '처리 중...';
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, email }),
})
.then(response => response.json())
.then(data => {
if (data.error) {
resultEl.innerHTML = '<p style="color: red;">에러: ' + data.error + '</p>';
} else {
resultEl.innerHTML = '<p style="color: green;">사용자가 성공적으로 추가되었습니다. ID: ' + data.id + '</p>';
document.getElementById('username').value = '';
document.getElementById('email').value = '';
loadUsers(); // 사용자 목록 새로고침
}
})
.catch(err => {
resultEl.innerHTML = '<p style="color: red;">요청 실패: ' + err.message + '</p>';
});
});
// 사용자 삭제 폼 제출 처리
document.getElementById('deleteUserForm').addEventListener('submit', function(e) {
e.preventDefault();
const userId = document.getElementById('userId').value;
const resultEl = document.getElementById('deleteUserResult');
resultEl.innerHTML = '처리 중...';
fetch('/api/users/' + userId, {
method: 'DELETE'
})
.then(response => {
if (!response.ok) {
return response.json().then(err => { throw new Error(err.error || '삭제 실패'); });
}
return response.json();
})
.then(data => {
resultEl.innerHTML = '<p style="color: green;">사용자가 성공적으로 삭제되었습니다.</p>';
document.getElementById('userId').value = '';
loadUsers(); // 사용자 목록 새로고침
})
.catch(err => {
resultEl.innerHTML = '<p style="color: red;">에러: ' + err.message + '</p>';
});
});
</script>
</div>
</body>
</html>
`);
});
// API 엔드포인트 - 사용자 목록 조회
app.get('/api/users', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM users');
res.json(rows);
} catch (error) {
console.error('Database error:', error);
res.status(500).json({ error: '데이터베이스 조회 중 오류가 발생했습니다.' });
}
});
// API 엔드포인트 - 사용자 추가
app.post('/api/users', async (req, res) => {
try {
const { username, email } = req.body;
if (!username || !email) {
return res.status(400).json({ error: '사용자명과 이메일은 필수 입력 항목입니다.' });
}
const [result] = await pool.query(
'INSERT INTO users (username, email) VALUES (?, ?)',
[username, email]
);
res.status(201).json({
id: result.insertId,
username,
email,
message: '사용자가 성공적으로 추가되었습니다.'
});
} catch (error) {
console.error('Database error:', error);
res.status(500).json({ error: '사용자 추가 중 오류가 발생했습니다.' });
}
});
// TODO: API 엔드포인트 - 사용자 삭제 (구현 필요)
// app.delete('/api/users/:id', async (req, res) => {
// // 이 부분을 구현해 주세요!
// });
// 서버 상태 확인 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', time: new Date().toISOString() });
});
// 서버 시작
app.listen(config.port, () => {
console.log(`App server is running on port ${config.port}`);
console.log(`Database connection configured to: ${config.db.host}`);
});
- app/Dockerfile
FROM node:16-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
1-3. DB 서비스 코드 작성
- db/init.sql
CREATE DATABASE IF NOT EXISTS nodeapp;
USE nodeapp;
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 기본 사용자 데이터 추가
INSERT INTO users (username, email) VALUES
('user1', 'user1@example.com'),
('user2', 'user2@example.com'),
('user3', 'user3@example.com');
-- Grafana 모니터링을 위한 권한 설정
CREATE USER IF NOT EXISTS 'grafana'@'%' IDENTIFIED BY 'grafana';
GRANT SELECT ON nodeapp.* TO 'grafana'@'%';
FLUSH PRIVILEGES;
- db/my.cnf
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
default-authentication-plugin = mysql_native_password
[client]
default-character-set = utf8mb4
- db/Dockerfile
FROM mysql:8.0
COPY my.cnf /etc/mysql/conf.d/
COPY init.sql /docker-entrypoint-initdb.d/
EXPOSE 3306
1-4. Docker Compose 설정 (로컬 개발용)
- docker-compose.yml
version: '3'
services:
app:
build: ./app
container_name: 2tier-app
ports:
- "80:3000"
depends_on:
- db
environment:
- NODE_ENV=development
restart: always
db:
build: ./db
container_name: 2tier-db
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=rootpassword
- MYSQL_DATABASE=nodeapp
- MYSQL_USER=nodeuser
- MYSQL_PASSWORD=password123
volumes:
- mysql-data:/var/lib/mysql
restart: always
grafana:
image: grafana/grafana:latest
container_name: 2tier-grafana
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- grafana-data:/var/lib/grafana
restart: always
volumes:
mysql-data:
grafana-data:
2. 로컬 환경에서 애플리케이션 실행 및 테스트
docker-compose 실행을 한 뒤에 http://localhost로 접속해서 정상 작동하는지 확인
# 프로젝트 루트 디렉토리에서
docker-compose up -d
# 서비스 상태 확인
docker-compose ps


3. Docker Hub에 이미지 업로드
# Docker Hub에 로그인
docker login
# 이미지에 태그 지정 (본인의 Docker Hub 계정명으로 변경)
docker tag 2tier-app_app:latest {계정명}/2tier-app:latest
docker tag 2tier-app_db:latest {계정명}/2tier-db:latest
# 이미지 푸시
docker push {계정명}/2tier-app:latest
docker push {계정명}/2tier-db:latest



4. 클라우드 환경 VM 설정
VM 인스턴스 생성 (DB서버, App서버) 후 퍼블릭 IP 할당

5. 각 VM에 Docker 및 Docker Compose 설치
# 패키지 업데이트
sudo apt update
sudo apt upgrade -y
# Docker 설치
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io
# 사용자를 docker 그룹에 추가
sudo usermod -aG docker ubuntu
# Docker Compose 설치
sudo apt install -y docker-compose
# 변경사항 적용을 위해 로그아웃 후 다시 로그인
exit
6. DB 서버 설정
- SSH로 DB서버에 접속
# Mac 접속 방법
ssh -i {키페어파일} ubuntu@{DB_SERVER_PUBLIC_IP}
# Window는 PuTTY or WSL
- ~/db-server/docker-compose.yml 파일 생성하기
mkdir -p ~/db-server
cd ~/db-server
cat > docker-compose.yml << EOF
version: '3'
services:
db:
image: ${docker-username}/2tier-db:latest
container_name: 2tier-db
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=rootpassword
- MYSQL_DATABASE=nodeapp
- MYSQL_USER=nodeuser
- MYSQL_PASSWORD=password123
volumes:
- mysql-data:/var/lib/mysql
restart: always
grafana:
image: grafana/grafana:latest
container_name: 2tier-grafana
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- grafana-data:/var/lib/grafana
restart: always
volumes:
mysql-data:
grafana-data:
EOF
# Docker Compose 실행
docker-compose up -d
+) 참고 : ${docker-username}에 내 username을 입력한 docker-compose.yml 코드
version: '3'
services:
db:
image: tierr/2tier-db:latest
container_name: 2tier-db
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=rootpassword
- MYSQL_DATABASE=nodeapp
- MYSQL_USER=nodeuser
- MYSQL_PASSWORD=password123
volumes:
- mysql-data:/var/lib/mysql
restart: always
grafana:
image: grafana/grafana:latest
container_name: 2tier-grafana
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- grafana-data:/var/lib/grafana
restart: always
volumes:
mysql-data:
grafana-data:

7. App 서버 설정
- SSH로 App 서버에 접속
# Mac 접속 방법
ssh -i {키페어파일} ubuntu@{APP_SERVER_PUBLIC_IP}
# Window는 PuTTY or WSL
- ~/app-server/docker-compose.yml 파일 생성하기
mkdir -p ~/app-server
cd ~/app-server
cat > docker-compose.yml << EOF
version: '3'
services:
app:
image: ${docker-username}/2tier-app:latest
container_name: 2tier-app
ports:
- "80:3000"
environment:
- NODE_ENV=production
- DB_HOST=${DB_SERVER_PUBLIC_IP}
- DB_USER=nodeuser
- DB_PASSWORD=password123
- DB_NAME=nodeapp
restart: always
volumes: {}
EOF
# Docker Compose 실행
docker-compose up -d
+) 참고 : ${docker-username}, ${DB_SERVER_PUBLIC_IP} 입력한 docker-compose.yml 코드
mkdir -p ~/app-server
cd ~/app-server
cat > docker-compose.yml << EOF
version: '3'
services:
app:
image: tierr/2tier-app:latest
container_name: 2tier-app
ports:
- "80:3000"
environment:
- NODE_ENV=production
- DB_HOST=210.109.82.32
- DB_USER=nodeuser
- DB_PASSWORD=password123
- DB_NAME=nodeapp
restart: always
volumes: {}
EOF
# Docker Compose 실행
docker-compose up -d

8. 애플리케이션 테스트
웹 브라우저에서 http://{APP_SERVER_PUBLIC_IP}로 접속하여 애플리케이션이 정상 작동하는지 확인

9. Grafana 대시보드 설정
웹 브라우저에서 `http://{DB_SERVER_PUBLIC_IP}:3000`로 Grafana에 접속후 MySQL 을 Data source로 추가
- Host: ${public-ip-address}:3306
- Database: nodeapp
Save & Test 버튼을 눌러서 연결이 잘 되었는지도 확인

대시보드를 생성하고 아래 2개 패널을 구성
- 패널 1 : 전체 유저수와 최근 가입일
-- users 테이블에서 전체 유저 수와 최근 가입일을 조회
SELECT
COUNT(*) AS 'Total Users', -- 총 유저 수를 계산 (모든 행 수)
MAX(created_at) AS 'Last Registration' -- 가장 최근에 가입한 일시 (created_at 컬럼 중 최대값)
FROM users; -- 데이터 소스 테이블: users
- 패널 2 : 유저 목록
-- users 테이블에서 id, 사용자명, 이메일, 가입일을 조회
SELECT
id,
username,
email,
created_at
FROM users -- 조회 대상 테이블: users
ORDER BY created_at DESC; -- 가입일 기준으로 내림차순 정렬 (최신 가입자가 가장 위에 옴)

심화 실습: 사용자 삭제 API 구현하기
현재 웹 애플리케이션에는 사용자 삭제 기능의 프론트엔드는 구현되어 있지만, 백엔드 API는 미구현 상태입니다. 이 심화 실습에서는 사용자 삭제 API를 구현하여 애플리케이션의 기능을 완성합니다.
우선 로컬 환경에서 삭제 API를 구현 및 테스트해보자.
- app.js 에 삭제 로직을 추가
// API 엔드포인트 - 사용자 삭제
app.delete('/api/users/:id', async (req, res) => {
try {
const id = Number(req.params.id);
const [result] = await pool.query('DELETE FROM users WHERE id = ?', [id]);
if (result.affectedRows === 0) {
return res.status(404).json({ message: '사용자를 찾을 수 없습니다.' });
}
res.status(200).json({ message: '사용자가 성공적으로 삭제되었습니다.' });
} catch (error) {
console.error('Database error:', error);
res.status(500).json({ error: '사용자 삭제 중 오류가 발생했습니다.' });
}
});
- 테스트를 위해 ID 가 12인 user_delete 유저를 삭제



Repository Image 최신화
코드의 변경점이 생겼으니 2tier-app을 저장소에 다시 push 해줘야 한다.
# 1. 이미지 다시 빌드 (최신 코드 반영)
docker build -t tierr/2tier-app:latest .
# 2. Docker Hub에 다시 push
docker push tierr/2tier-app:latest

Docker Hub 저장소에 tierr/2tier-app:latest 이미지가 최신화되었을 것이다.
카카오클라우드 VM환경에서 최신 이미지를 다운로드받아 컨테이너를 재실행해야 수정내역이 적용된다.
- VM 에서 실행중인 서버를 종료하고, pull로 최신 이미지를 받은 후, 컨테이너 재실행
docker-compose down # 컨테이너 down
docker-compose pull # 최신 이미지 강제 다운로드
docker-compose up -d # 최신 이미지로 실행

트러블 슈팅
상황)
App 서버의 docker-compose.yml에 DB_HOST를 잘못 적어서 파일을 수정했다.
docker-compose up -d --build 로 다시 빌드하려고 했는데 다음과 같은 에러가 발생했다.
- 에러 내용
ERROR: for c347c300bd9f_2tier-app 'ContainerConfig'
ERROR: for app 'ContainerConfig'
Traceback (most recent call last):
File "/usr/bin/docker-compose", line 33, in <module>
sys.exit(load_entry_point('docker-compose==1.29.2', 'console_scripts', 'docker-compose')())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/cli/main.py", line 81, in main
command_func()
File "/usr/lib/python3/dist-packages/compose/cli/main.py", line 203, in perform_command
handler(command, command_options)
File "/usr/lib/python3/dist-packages/compose/metrics/decorator.py", line 18, in wrapper
result = fn(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/cli/main.py", line 1186, in up
to_attach = up(False)
^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/cli/main.py", line 1166, in up
return self.project.up(
^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/project.py", line 697, in up
results, errors = parallel.parallel_execute(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/parallel.py", line 108, in parallel_execute
raise error_to_reraise
File "/usr/lib/python3/dist-packages/compose/parallel.py", line 206, in producer
result = func(obj)
^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/project.py", line 679, in do
return service.execute_convergence_plan(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/service.py", line 579, in execute_convergence_plan
return self._execute_convergence_recreate(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/service.py", line 499, in _execute_convergence_recreate
containers, errors = parallel_execute(
^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/parallel.py", line 108, in parallel_execute
raise error_to_reraise
File "/usr/lib/python3/dist-packages/compose/parallel.py", line 206, in producer
result = func(obj)
^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/service.py", line 494, in recreate
return self.recreate_container(
^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/service.py", line 612, in recreate_container
new_container = self.create_container(
^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/service.py", line 330, in create_container
container_options = self._get_container_create_options(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/service.py", line 921, in _get_container_create_options
container_options, override_options = self._build_container_volume_options(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/service.py", line 960, in _build_container_volume_options
binds, affinity = merge_volume_bindings(
^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/service.py", line 1548, in merge_volume_bindings
old_volumes, old_mounts = get_container_data_volumes(
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/compose/service.py", line 1579, in get_container_data_volumes
container.image_config['ContainerConfig'].get('Volumes') or {}
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
KeyError: 'ContainerConfig'
원인)
이런 에러는 Docker Compose가 컨테이너를 재생성하려고 할 때, 기존에 존재하던 컨테이너 또는 이미지가 손상되었거나 비정상 상태일 때 발생한다고 한다. 특히 image:로 지정한 이미지가 정상적인 구조를 가지고 있지 않거나, 중간에 수동 삭제/중단된 상태에서 Compose가 기존 정보를 재활용하려고 할 때 이런 오류가 터진다고 한다.
해결)
실행중인 컨테이너 목록을 확인해보니, 기존 컨테이너의 이름이 이상하게 변형되어있었다.
- 2tier-app -> c347c300bd9f_2tier-app
단순히 기존 컨테이너를 삭제하고 새로 만들어서 실행했더니 해결되었다.
# ----------------------------Code History----------------------------
# 빌드 -> 에러
docker-compose up -d --build
# compose 파일 유효성 검사 및 컨테이너 목록 확인
docker-compose config
docker ps -a
# 기존 컨테이너 삭제
docker rm c347c300bd9f_2tier-app
# 컨테이너 재구동
docker-compose up -d
docker ps
이 문제를 해결하는 중에 좋은 명령어를 하나 알게되었으니 기록
docker-compose config
docker-compose.yml 파일이 잘 작성됐는지 확인하고,
실제로 도커가 이해할 수 있는 형식으로 변환된 결과를 보여주는 유용한 명령어
# docker-compose.yml 파일 유효성 검사
docker-compose config
본 후기는 [카카오엔터프라이즈x스나이퍼팩토리] 카카오클라우드로 배우는 AIaaS 마스터 클래스 (B-log) 리뷰로 작성 되었습니다.
'학습일지 > K-Digital Traing' 카테고리의 다른 글
| [KDT] AIaaS 마스터클래스 3주차 - 클라우드 어플리케이션 배포 실습 (0) | 2025.04.10 |
|---|---|
| [KDT] AIaaS 마스터클래스 3주차 - 쿠버네티스 실습 (0) | 2025.04.09 |
| [KDT] AIaaS 마스터클래스 2주차 - VM 배포 실습 (0) | 2025.04.03 |
| [KDT] AIaaS 마스터클래스 2주차 - Prometheus, Grafana 실습 (0) | 2025.04.02 |
| [KDT] AIaaS 마스터클래스 1주차 - 네트워크 프로토콜 (0) | 2025.03.28 |