현대적인 시스템 엔지니어에게 Docker는 이제 선택이 아니라 매우 중요한 기술 중 하나입니다. 과거에는 새로운 서비스를 구축할 때 서버에 직접 웹 서버를 설치하고, 프로그래밍 언어와 라이브러리를 설치하고, 데이터베이스를 구성하는 방식이 일반적이었습니다. 하지만 이 방식은 서버마다 환경이 달라지고, 프로그램의 버전과 설정이 충돌하며, 새로운 서버로 서비스를 이전할 때 많은 작업이 필요하다는 문제가 있습니다.
이러한 문제를 해결하기 위해 등장한 기술이 컨테이너(Container)입니다. Docker는 애플리케이션과 실행에 필요한 라이브러리, 설정 등을 하나의 독립된 실행 환경으로 구성할 수 있도록 도와주는 대표적인 컨테이너 플랫폼입니다.
이번 글에서는 Docker의 기본 개념부터 Image와 Container의 차이, Dockerfile 작성 방법, Docker Compose를 이용한 여러 서비스의 통합 운영, 그리고 Nginx, FastAPI, PostgreSQL, Redis를 활용한 실제 서버 구성까지 알아보겠습니다.
먼저 기존 서버 운영 방식과 컨테이너 방식을 비교해 보겠습니다.
전통적인 서버 구성은 다음과 같습니다.
Physical Server
│
▼
Operating System
│
├── Nginx 설치
│
├── Python 설치
│
├── FastAPI 설치
│
├── PostgreSQL 설치
│
└── Redis 설치
이 방식에서는 모든 프로그램이 하나의 운영체제에 직접 설치됩니다.
문제는 서비스가 많아질수록 발생합니다.
Python 3.10 필요
Python 3.12 필요
PostgreSQL 15 필요
PostgreSQL 17 필요
라이브러리 A 버전 1.0 필요
라이브러리 A 버전 2.0 필요
서로 다른 서비스가 서로 다른 환경을 요구하면 충돌이 발생할 수 있습니다.
Docker를 사용하면 다음과 같이 구성할 수 있습니다.
Physical Server
│
▼
Linux Operating System
│
▼
Docker Engine
│
├── Nginx Container
│
├── FastAPI Container
│
├── PostgreSQL Container
│
└── Redis Container
각 서비스는 독립적인 컨테이너에서 실행됩니다.
즉, Nginx는 Nginx 컨테이너에서, FastAPI는 Python 환경을 포함한 컨테이너에서, PostgreSQL은 별도의 데이터베이스 컨테이너에서 실행됩니다.
Docker는 애플리케이션을 컨테이너 형태로 실행하고 관리할 수 있도록 도와주는 플랫폼입니다.
Docker의 핵심 개념은 다음과 같습니다.
Source Code
│
▼
Dockerfile
│
▼
Docker Image
│
▼
Docker Container
각 단계를 이해하는 것이 중요합니다.
Dockerfile은 컨테이너 환경을 만드는 방법을 정의한 파일입니다.
예를 들어 Python 기반 FastAPI 애플리케이션이라면 다음과 같은 내용을 작성할 수 있습니다.
FROM python:3.12
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
이 파일은 다음 작업을 수행합니다.
Python 3.12 이미지 사용
↓
/app 디렉터리 생성
↓
requirements.txt 복사
↓
Python 라이브러리 설치
↓
애플리케이션 코드 복사
↓
Uvicorn 실행
Docker Image는 컨테이너를 실행하기 위한 실행 환경입니다.
쉽게 말하면 컨테이너의 설계도라고 생각할 수 있습니다.
Dockerfile
│
│ docker build
▼
Docker Image
│
│ docker run
▼
Container
예를 들어 FastAPI 이미지를 생성할 수 있습니다.
docker build -t my-fastapi .
이미지를 확인하는 명령어입니다.
docker images
출력 예시는 다음과 같습니다.
REPOSITORY TAG IMAGE ID
my-fastapi latest a1b2c3d4e5
python 3.12 f6g7h8i9j0
Docker Image는 변경되지 않는 실행 환경으로 관리하는 것이 일반적입니다. 새로운 코드가 적용되면 새로운 이미지를 만들고 새로운 컨테이너를 실행하는 방식으로 운영할 수 있습니다.
Container는 Docker Image를 실제로 실행한 상태입니다.
예를 들어 다음과 같이 실행할 수 있습니다.
docker run -d \
--name fastapi-app \
-p 8000:8000 \
my-fastapi
명령어의 의미는 다음과 같습니다.
docker run
-d
백그라운드 실행
--name fastapi-app
컨테이너 이름 지정
-p 8000:8000
포트 연결
my-fastapi
실행할 Docker Image
실행 중인 컨테이너는 다음 명령어로 확인할 수 있습니다.
docker ps
모든 컨테이너를 확인하려면 다음과 같이 실행합니다.
docker ps -a
컨테이너 내부에 접속할 수도 있습니다.
docker exec -it fastapi-app /bin/bash
컨테이너 로그는 다음과 같이 확인합니다.
docker logs fastapi-app
실무에서는 서비스 장애가 발생했을 때 컨테이너의 상태와 로그를 먼저 확인하는 경우가 많습니다.
Docker 컨테이너의 가장 큰 장점은 서비스 간의 격리입니다.
예를 들어 하나의 서버에서 여러 애플리케이션을 운영한다고 가정해 보겠습니다.
Server
│
├── Application A
│ └── Python 3.10
│
├── Application B
│ └── Python 3.12
│
└── Application C
└── Node.js 22
일반적인 환경에서는 여러 버전을 직접 관리해야 합니다.
Docker에서는 다음과 같이 분리할 수 있습니다.
Docker Host
│
├── Container A
│ └── Python 3.10
│
├── Container B
│ └── Python 3.12
│
└── Container C
└── Node.js 22
각 컨테이너는 서로 독립적인 실행 환경을 가집니다.
따라서 하나의 서비스에서 라이브러리를 변경해도 다른 서비스에 미치는 영향을 줄일 수 있습니다.
여러 컨테이너가 함께 동작하려면 서로 통신해야 합니다.
예를 들어 다음과 같은 서비스가 있습니다.
Internet
│
▼
Nginx
│
▼
FastAPI
│
├── PostgreSQL
│
└── Redis
Docker에서는 컨테이너 네트워크를 이용하여 서비스를 연결할 수 있습니다.
예를 들어 다음과 같이 구성합니다.
Nginx Container
│
▼
FastAPI Container
│
├──────── PostgreSQL Container
│
└──────── Redis Container
같은 Docker 네트워크에 있는 컨테이너는 일반적으로 서비스 이름을 이용해 통신할 수 있습니다.
예를 들어 PostgreSQL 서비스 이름이 postgres라면 애플리케이션 설정은 다음과 같은 형태가 됩니다.
postgresql://user:password@postgres:5432/appdb
여기서 중요한 점은 localhost가 아닙니다.
Docker 환경에서 FastAPI 컨테이너의 localhost는 FastAPI 컨테이너 자신을 의미합니다.
따라서 다음 설정은 문제가 될 수 있습니다.
postgresql://user:password@localhost:5432/appdb
같은 Docker 네트워크의 PostgreSQL 컨테이너에 연결하려면 서비스 이름을 사용해야 합니다.
postgresql://user:password@postgres:5432/appdb
이 부분은 Docker 환경에서 매우 자주 발생하는 설정 오류입니다.
컨테이너는 삭제 후 다시 생성될 수 있습니다.
따라서 중요한 데이터를 컨테이너 내부에만 저장하면 문제가 발생할 수 있습니다.
예를 들어 PostgreSQL 컨테이너를 생각해 보겠습니다.
PostgreSQL Container
│
└── Database Data
컨테이너를 삭제하면 데이터도 함께 사라질 수 있습니다.
이를 방지하기 위해 Docker Volume을 사용합니다.
PostgreSQL Container
│
▼
Docker Volume
│
▼
Persistent Storage
예를 들어 다음과 같이 실행할 수 있습니다.
docker volume create postgres_data
컨테이너 실행 시 Volume을 연결합니다.
docker run -d \
--name postgres \
-v postgres_data:/var/lib/postgresql/data \
postgres:17
이제 컨테이너를 다시 생성해도 Volume을 유지하면 데이터베이스 데이터를 보존할 수 있습니다.
실무에서는 특히 다음 데이터에 대한 영속성 관리가 중요합니다.
Database Data
Application Upload Files
Log Files
Configuration Files
Backup Files
Docker Compose는 여러 개의 컨테이너를 하나의 설정 파일로 관리하는 도구입니다.
예를 들어 다음과 같은 서비스를 각각 실행한다고 가정해 보겠습니다.
docker run nginx
docker run fastapi
docker run postgres
docker run redis
서비스가 많아지면 네트워크, 포트, Volume, 환경 변수 등을 각각 관리해야 합니다.
Docker Compose를 사용하면 하나의 YAML 파일로 구성할 수 있습니다.
docker-compose.yml
예를 들어 다음과 같은 구조입니다.
services:
nginx:
image: nginx
backend:
build: ./backend
postgres:
image: postgres:17
redis:
image: redis:7
전체 서비스를 한 번에 실행할 수 있습니다.
docker compose up -d
서비스를 중지할 수도 있습니다.
docker compose down
실행 상태를 확인합니다.
docker compose ps
로그를 확인합니다.
docker compose logs
특정 서비스의 로그만 확인할 수도 있습니다.
docker compose logs backend
실제 서비스에서는 다음과 같은 구조를 많이 사용할 수 있습니다.
Internet
│
▼
Nginx
│
┌──────────┴──────────┐
│ │
▼ ▼
Frontend FastAPI
│
┌──────────┴──────────┐
│ │
▼ ▼
PostgreSQL Redis
각 서비스의 역할은 다음과 같습니다.
외부 요청을 받는 웹 서버입니다.
Internet
│
▼
Nginx
HTTPS 인증서를 적용하고 FastAPI로 요청을 전달하는 Reverse Proxy 역할을 수행할 수 있습니다.
애플리케이션의 실제 비즈니스 로직을 처리합니다.
/api/users
/api/login
/api/expenses
사용자 정보와 서비스 데이터를 저장합니다.
Users
Expenses
Categories
Transactions
캐시와 세션, 임시 데이터 등을 빠르게 처리할 수 있습니다.
Cache
Session
Queue
Temporary Data
프로젝트 구조를 다음과 같이 구성합니다.
my-service/
│
├── backend/
│ ├── main.py
│ ├── requirements.txt
│ └── Dockerfile
│
├── nginx/
│ └── nginx.conf
│
└── docker-compose.yml
docker-compose.yml 파일 예제입니다.
services:
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- backend
backend:
build:
context: ./backend
environment:
DATABASE_URL: postgresql://appuser:password@postgres:5432/appdb
REDIS_HOST: redis
depends_on:
- postgres
- redis
postgres:
image: postgres:17
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7
volumes:
postgres_data:
서비스를 실행합니다.
docker compose up -d
그러면 Docker Compose가 다음 작업을 수행합니다.
Docker Network 생성
↓
PostgreSQL 시작
↓
Redis 시작
↓
FastAPI 시작
↓
Nginx 시작
Nginx는 외부 요청을 FastAPI로 전달하는 역할을 할 수 있습니다.
예를 들어 다음과 같이 구성합니다.
events {
}
http {
upstream backend {
server backend:8000;
}
server {
listen 80;
location /api/ {
proxy_pass http://backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For
$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto
$scheme;
}
}
}
요청 흐름은 다음과 같습니다.
https://example.com/api/users
│
▼
Nginx
│
▼
FastAPI Container
│
▼
PostgreSQL
외부에서는 FastAPI의 8000번 포트가 직접 노출되지 않도록 구성하는 것이 일반적입니다.
Internet
│
▼
Port 80 / 443
│
▼
Nginx
│
▼
Internal Docker Network
│
▼
FastAPI :8000
이러한 구조는 보안과 운영 측면에서 관리하기 좋습니다.
Docker Compose 환경에서는 중요한 설정값을 환경 변수로 관리할 수 있습니다.
예를 들어 .env 파일을 만듭니다.
POSTGRES_DB=appdb
POSTGRES_USER=appuser
POSTGRES_PASSWORD=StrongPassword
Compose 파일에서는 다음과 같이 사용할 수 있습니다.
postgres:
image: postgres:17
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
하지만 .env 파일에는 중요한 정보가 포함될 수 있으므로 Git에 등록하지 않는 것이 중요합니다.
.gitignore에 추가합니다.
.env
실무에서는 다음 정보를 코드에 직접 작성하지 않아야 합니다.
Database Password
API Key
JWT Secret
SSH Private Key
Cloud Access Key
SSL Private Key
서비스 운영 중에는 다음 명령어를 자주 사용합니다.
전체 서비스 시작입니다.
docker compose up -d
전체 서비스 상태 확인입니다.
docker compose ps
로그를 실시간으로 확인합니다.
docker compose logs -f
특정 서비스의 로그를 확인합니다.
docker compose logs -f backend
서비스를 재시작합니다.
docker compose restart backend
새로운 이미지로 서비스를 다시 생성합니다.
docker compose up -d --build
서비스를 중지하고 컨테이너와 네트워크를 제거합니다.
docker compose down
데이터베이스 Volume까지 제거하려면 다음 명령어를 사용할 수 있습니다.
docker compose down -v
다만 운영 서버에서 -v 옵션은 매우 주의해야 합니다. PostgreSQL과 같은 데이터베이스의 Volume까지 삭제될 수 있기 때문입니다.
실무에서는 다음과 같은 문제가 자주 발생합니다.
먼저 상태를 확인합니다.
docker ps -a
로그를 확인합니다.
docker logs 컨테이너이름
Docker Compose를 사용한다면 다음과 같이 확인합니다.
docker compose logs backend
Docker 네트워크를 확인합니다.
docker network ls
컨테이너 정보를 확인합니다.
docker inspect 컨테이너이름
애플리케이션 설정에서 localhost를 사용하고 있지 않은지도 확인해야 합니다.
잘못된 설정
localhost:5432
권장 설정
postgres:5432
PostgreSQL이나 MariaDB 컨테이너에 Volume이 연결되어 있는지 확인해야 합니다.
docker volume ls
Volume을 사용하지 않았다면 컨테이너를 삭제하고 재생성하는 과정에서 데이터가 손실될 수 있습니다.
다음과 같은 오류가 발생할 수 있습니다.
Bind for 0.0.0.0:80 failed
Port is already allocated
이미 다른 프로그램이 해당 포트를 사용하고 있는 경우입니다.
Linux에서는 다음 명령어로 확인할 수 있습니다.
ss -tulnp
또는 다음 명령어를 사용할 수 있습니다.
lsof -i :80
개발 환경과 운영 환경을 분리하는 것이 좋습니다.
Development
│
▼
Docker Compose
│
▼
GitHub
│
▼
CI/CD
│
▼
Staging
│
▼
Production
운영 환경에서는 다음 사항을 고려해야 합니다.
환경 변수 분리
개발/운영 Compose 파일 분리
이미지 버전 관리
데이터베이스 Volume 관리
정기적인 백업
로그 관리
Health Check
자동 재시작 정책
모니터링
보안 업데이트
특히 latest 태그만 사용하는 것보다 명확한 버전 태그를 관리하는 것이 좋습니다.
myapp:1.0.0
myapp:1.0.1
myapp:1.1.0
문제가 발생했을 때 이전 버전으로 되돌리는 것도 쉬워집니다.
1.1.0 배포
↓
장애 발생
↓
1.0.1 이미지로 변경
↓
컨테이너 재생성
Docker는 앞에서 학습한 GitHub Actions 기반 CI/CD와 함께 사용할 때 더욱 강력합니다.
전체 구조는 다음과 같습니다.
Developer
│
│ git push
▼
GitHub Repository
│
▼
GitHub Actions
│
├── Test
│
├── Build
│
├── Docker Image 생성
│
└── Image Registry Push
│
▼
Production Server
│
├── Docker Pull
│
└── Docker Compose Up
이 구조를 사용하면 서버에 직접 소스 코드를 복사하지 않고 검증된 Docker Image를 배포할 수 있습니다.
코드 변경
↓
테스트
↓
Docker Image 생성
↓
Image Registry 저장
↓
운영 서버 다운로드
↓
Container 교체
이것이 현대적인 DevOps 환경의 기본적인 흐름입니다.
지금까지 학습한 내용을 실제 프로젝트로 구성한다면 다음과 같은 구조를 추천할 수 있습니다.
system-service/
│
├── frontend/
│ ├── Vue.js
│ └── Dockerfile
│
├── backend/
│ ├── FastAPI
│ ├── requirements.txt
│ └── Dockerfile
│
├── nginx/
│ └── default.conf
│
├── postgres/
│
├── docker-compose.yml
│
├── .env
│
└── .github/
└── workflows/
└── ci-cd.yml
서비스 구조는 다음과 같습니다.
Internet
│
▼
Nginx
│
├───────────────┐
│ │
▼ ▼
Vue.js FastAPI
│
┌──────┴──────┐
│ │
▼ ▼
PostgreSQL Redis
이 프로젝트를 완성하면 다음 기술을 한 번에 학습할 수 있습니다.
Rocky Linux
Linux Network
Nginx
Docker
Docker Compose
FastAPI
PostgreSQL
Redis
Git
GitHub
GitHub Actions
CI/CD
Docker와 Docker Compose는 현대적인 서버 운영 환경에서 매우 중요한 기술입니다. 특히 하나의 서버에서 여러 서비스를 운영하거나, 개발 환경과 운영 환경을 동일하게 유지하고 싶을 때 큰 장점을 제공합니다.
이번 글에서 반드시 기억해야 할 핵심 개념은 다음과 같습니다.
Dockerfile
↓
Image
↓
Container
Docker Compose
↓
여러 Container 통합 관리
Docker Network
↓
Container 간 통신
Docker Volume
↓
데이터 영속성
GitHub Actions
↓
자동 빌드와 자동 배포
시스템 엔지니어의 관점에서 Docker를 학습할 때는 단순히 컨테이너를 실행하는 수준에 머물러서는 안 됩니다. 실제 서버 환경에서 Nginx Reverse Proxy, FastAPI 애플리케이션, PostgreSQL 데이터베이스, Redis 캐시를 Docker Compose로 구성하고, GitHub Actions와 연결하여 자동 배포까지 구현하는 것이 가장 효과적인 학습 방법입니다.