Docker 실습 환경

이 코스의 모든 실습은 Docker 컨테이너 안의 MySQL 8.0에서 진행합니다. 여러분의 노트북에 MySQL을 직접 설치할 필요가 없고, 실습을 망쳐도 명령 한 줄이면 처음 상태로 되돌릴 수 있기 때문입니다.

이 페이지는 그 실습 환경이 어떻게 구성되어 있는지를 설명합니다. Step 01에서 컨테이너를 처음 띄울 때, Step 23에서 복제 클러스터를 만들 때, 그리고 "왜 내 환경에서는 이 값이 이렇게 나오지?" 싶을 때 다시 찾아오면 됩니다.

전체 구성

이 디렉터리는 두 개의 독립적인 스택으로 이루어져 있습니다.

스택파일컨테이너포트언제 씁니까
학습용 단일 노드docker-compose.ymllearn-mysql83307Step 01 ~ Step 25 (거의 전 과정)
복제 실습용 2노드replication/docker-compose.ymlrepl-source, repl-replica3308, 3309Step 23 (백업과 복제)

학습용 스택은 두 개의 보조 파일을 마운트해서 씁니다.

  • conf/my.cnf — 서버 설정. 슬로우 쿼리 로그, 타임존, sql_mode 등을 "관찰하기 좋게" 맞춰 둡니다.
  • initdb/00-grants.sql — 컨테이너가 처음 만들어질 때 딱 한 번 자동 실행되어 learner 계정에 권한을 부여합니다.

두 스택은 포트도, 컨테이너 이름도, 볼륨도 겹치지 않습니다. 복제 실습을 하는 동안에도 learn-mysql8은 그대로 살아 있습니다.

사용법

학습용 환경 (Step 01부터 계속 씁니다)

cd mysql8/docker
docker compose up -d

# 헬스체크가 healthy 가 될 때까지 기다립니다 (보통 20~40초)
docker inspect -f '{{.State.Health.Status}}' learn-mysql8

# 접속
mysql -h 127.0.0.1 -P 3307 -ulearner -plearn1234 shop

컨테이너 안에서 바로 접속해도 됩니다.

docker exec -it learn-mysql8 mysql -ulearner -plearn1234 shop

실습이 꼬였다면 아래 한 줄로 완전히 초기화합니다. 볼륨까지 삭제되므로 그동안 넣은 데이터는 전부 사라집니다.

docker compose down -v && docker compose up -d

복제 실습 환경 (Step 23에서만)

cd mysql8/docker/replication
docker compose up -d
../../step-23-backup-replication/setup-replication.sh

# 실습이 끝나면 볼륨까지 정리
docker compose down -v

실습 파일

아래 네 개 파일이 이 실습 환경의 전부입니다. 순서대로 보면, docker-compose.yml이 컨테이너를 정의하고, 그 안에서 conf/my.cnf가 서버 설정으로 마운트되며, 컨테이너가 처음 뜰 때 initdb/00-grants.sql이 자동 실행되어 계정 권한을 세팅합니다. replication/docker-compose.yml은 Step 23에서만 쓰는 별도 스택입니다.

docker-compose.yml

Step 01에서 docker compose up -d로 실행하는, 이 코스의 메인 컨테이너 정의입니다.

  • ports: "3307:3306" — 컨테이너의 3306을 호스트의 3307로 내보냅니다. 로컬에 이미 MySQL이 3306으로 떠 있어도 충돌하지 않습니다. 그래서 접속할 때 항상 -P 3307을 붙여야 합니다.
  • MYSQL_DATABASE: shop, MYSQL_USER: learner, MYSQL_PASSWORD: learn1234 — 최초 기동 시 shop 데이터베이스와 learner 계정이 자동으로 만들어집니다. root 비밀번호는 root1234입니다.
  • command--character-set-server=utf8mb4 / --collation-server=utf8mb4_0900_ai_ci — 한글과 이모지를 안전하게 저장하기 위한 기본값입니다. my.cnf에도 같은 값이 있지만, 커맨드라인 인자가 최종적으로 이깁니다.
  • volumes 세 줄이 핵심입니다. ./conf/my.cnf/etc/mysql/conf.d/learn.cnf읽기 전용(:ro) 마운트되고, ./initdb/docker-entrypoint-initdb.d로, ../sql/sql로 마운트됩니다. 마지막 덕분에 컨테이너 안에서 source /sql/01_schema.sql 같은 식으로 샘플 데이터베이스 스크립트를 바로 실행할 수 있습니다.
  • healthcheckmysqladmin pinginterval: 5s로 최대 retries: 20번 시도합니다. 즉 최대 100초까지 기동을 기다려 줍니다. MySQL은 첫 기동 시 데이터 디렉터리를 초기화하느라 시간이 꽤 걸리므로, healthy가 뜨기 전에 접속하면 "Can't connect" 에러가 나는 게 정상입니다.
  • volumes: mysql8-data — 이름 있는 볼륨이라 docker compose down만으로는 지워지지 않습니다. 데이터를 정말 지우려면 -v 옵션이 필요합니다.
name: learn-mysql8   # 폴더명(docker) 기반 기본 프로젝트명 충돌 방지

services:
  mysql:
    image: mysql:8.0
    container_name: learn-mysql8
    restart: unless-stopped
    ports:
      - "3307:3306"          # 로컬에 이미 MySQL이 있어도 충돌하지 않도록 3307 사용
    environment:
      MYSQL_ROOT_PASSWORD: root1234
      MYSQL_DATABASE: shop
      MYSQL_USER: learner
      MYSQL_PASSWORD: learn1234
      TZ: Asia/Seoul
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_0900_ai_ci
    volumes:
      - mysql8-data:/var/lib/mysql
      - ./conf/my.cnf:/etc/mysql/conf.d/learn.cnf:ro
      - ./initdb:/docker-entrypoint-initdb.d:ro   # 최초 기동 시 1회 자동 실행 (learner 권한 부여)
      - ../sql:/sql:ro                            # 컨테이너 안에서 source /sql/01_schema.sql 로 실행 가능
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot1234"]
      interval: 5s
      timeout: 3s
      retries: 20

volumes:
  mysql8-data:

conf/my.cnf

컨테이너의 /etc/mysql/conf.d/learn.cnf로 마운트되는 서버 설정 파일입니다. 운영용 튜닝이 아니라 "내부 동작을 관찰하기 좋은" 설정이라는 점이 중요합니다.

  • default_time_zone = '+09:00' — 컨테이너 기본 타임존은 UTC입니다. 이걸 KST로 고정해 두었기 때문에 Step 12에서 NOW()가 한국 시간으로 나옵니다.
  • innodb_buffer_pool_size = 256M — 학습용이라 일부러 작게 잡았습니다. 실제 운영에서는 물리 메모리의 50~70%를 씁니다.
  • slow_query_log = 1 + long_query_time = 0.5 + log_queries_not_using_indexes = 10.5초 이상 걸린 쿼리와 인덱스를 안 탄 쿼리가 모두 /var/lib/mysql/slow.log에 쌓입니다. Step 15(인덱스)와 Step 24(모니터링)에서 이 로그를 직접 열어 봅니다. 처음 실습할 때 로그가 금방 커지는 게 정상입니다.
  • general_log = 0 — 일반 쿼리 로그는 꺼 둔 채로 시작합니다. 모든 쿼리를 다 기록하기 때문에 켜 두면 로그가 순식간에 불어납니다. Step 24에서 필요할 때만 SET GLOBAL general_log = 1로 잠깐 켰다가 다시 끕니다.
  • event_scheduler = ON — Step 20의 CREATE EVENT 실습이 실제로 동작하려면 이 값이 켜져 있어야 합니다. MySQL 기본값은 OFF입니다.
  • local_infile = 1 — Step 03의 LOAD DATA LOCAL INFILE 대량 적재 실습을 위해 켭니다.
  • sql_modeONLY_FULL_GROUP_BYSTRICT_TRANS_TABLES일부러 남겨 두었습니다. Step 06에서 GROUP BY 에러를 직접 겪어 봐야 왜 이 모드가 있는지 이해할 수 있기 때문입니다. 에러가 난다고 이 값을 끄지 마세요.
[mysqld]
# ─────────────────────────────────────────────────────────────
# 학습용 MySQL 8 설정. 운영 설정이 아니라 "관찰하기 좋은" 설정입니다.
# ─────────────────────────────────────────────────────────────

# 문자셋 / 콜레이션
character_set_server = utf8mb4
collation_server     = utf8mb4_0900_ai_ci

# 타임존 (Step 12 날짜 함수 실습에서 확인)
default_time_zone = '+09:00'

# InnoDB 버퍼 풀: 학습용이라 작게. 운영에서는 물리 메모리의 50~70%.
innodb_buffer_pool_size = 256M

# 슬로우 쿼리 로그 (Step 24에서 사용)
slow_query_log       = 1
slow_query_log_file  = /var/lib/mysql/slow.log
long_query_time      = 0.5
log_queries_not_using_indexes = 1

# 일반 쿼리 로그는 기본 OFF (필요할 때 Step 24에서 동적으로 켭니다)
general_log = 0

# 이벤트 스케줄러 (Step 20 EVENT 실습)
event_scheduler = ON

# LOAD DATA LOCAL INFILE 허용 (Step 03 대량 적재 실습)
local_infile = 1

# sql_mode: 8.0 기본값을 그대로 사용 (STRICT_TRANS_TABLES, ONLY_FULL_GROUP_BY 포함)
# 일부러 끄지 않습니다. Step 06에서 ONLY_FULL_GROUP_BY 를 직접 겪어봐야 하기 때문입니다.
sql_mode = ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

[client]
default-character-set = utf8mb4

initdb/00-grants.sql

/docker-entrypoint-initdb.d에 마운트되어, 컨테이너의 데이터 볼륨이 비어 있을 때(= 최초 기동 시) 딱 한 번 자동 실행됩니다. 이미 볼륨이 있으면 실행되지 않으므로, 이 파일을 고쳐도 docker compose down -v로 볼륨을 지우고 다시 띄우지 않으면 반영되지 않습니다.

  • GRANT ALL PRIVILEGES ON *.* TO 'learner'@'%'learner가 DB 생성/삭제(Step 02), performance_schema·sys 조회(Step 24), 프로세스 목록 조회(Step 19)까지 다 할 수 있게 열어 줍니다.
  • GRANT SYSTEM_VARIABLES_ADMIN / SESSION_VARIABLES_ADMIN — MySQL 8.0의 동적 권한(Dynamic Privileges)ALL PRIVILEGES에 포함되지 않습니다. SET GLOBAL이나 SET PERSIST를 쓰려면 반드시 따로 부여해야 합니다. Step 24에서 일반 쿼리 로그를 동적으로 켤 때 이 권한이 필요합니다.
  • ⚠️ 이건 학습 환경이라서 이렇게 하는 것입니다. 운영 환경에서 애플리케이션 계정에 ALL PRIVILEGES ON *.*를 주는 것은 심각한 보안 사고입니다. 올바른 최소 권한 원칙은 Step 22에서 배웁니다.
-- =====================================================================
-- 00-grants.sql
--   컨테이너가 "처음 만들어질 때" 딱 한 번 자동 실행됩니다.
--   (docker-entrypoint-initdb.d 규칙. 볼륨이 이미 있으면 실행되지 않습니다)
--
-- learner 계정에 학습에 필요한 권한을 넉넉히 부여합니다.
--   - DB 생성/삭제 (Step 02)
--   - performance_schema / sys 조회 (Step 24)
--   - 세션·글로벌 변수 조회 및 변경 (Step 01, 24)
--   - 프로세스 목록 조회 (Step 19, 24)
--
-- ⚠️ 이것은 "학습 환경"이라서 이렇게 하는 것입니다.
--    운영에서는 절대 이러면 안 됩니다. 최소 권한 원칙은 Step 22에서 배웁니다.
-- =====================================================================

GRANT ALL PRIVILEGES ON *.* TO 'learner'@'%';

-- 8.0 의 동적 권한(Dynamic Privileges)은 ALL PRIVILEGES 에 포함되지 않으므로 따로 부여
GRANT SYSTEM_VARIABLES_ADMIN ON *.* TO 'learner'@'%';   -- SET GLOBAL / SET PERSIST
GRANT SESSION_VARIABLES_ADMIN ON *.* TO 'learner'@'%';

FLUSH PRIVILEGES;

replication/docker-compose.yml

Step 23(백업과 복제)에서만 쓰는 2노드 복제 클러스터입니다. docker compose up -d로 띄운 뒤 step-23-backup-replication/setup-replication.sh를 실행하면 복제가 구성됩니다. 포트(3308/3309)와 컨테이너 이름(repl-source/repl-replica)이 학습용 스택과 완전히 분리되어 있으므로, learn-mysql8은 영향을 받지 않습니다.

  • source (3308, server_id=1)--log-bin=mysql-bin으로 바이너리 로그를 켭니다. 이것이 복제와 PITR(시점 복구)의 전제조건입니다. --binlog-format=ROW는 8.0 기본값이지만 안전한 복제를 위해 명시했습니다.
  • --gtid-mode=ON + --enforce-gtid-consistency=ON — GTID 기반 복제를 씁니다. 후자는 GTID와 호환되지 않는 SQL(예: 트랜잭션 안의 CREATE TABLE ... SELECT)을 아예 막아 줍니다. 실습 중 이런 문법이 거부되면 버그가 아니라 의도된 동작입니다.
  • --binlog-expire-logs-seconds=604800 — binlog를 7일간 보관합니다. 8.0에서 새로 생긴 옵션으로, 예전의 expire_logs_days를 대체합니다.
  • replica (3309, server_id=2)--relay-log=relay-bin으로 릴레이 로그를 지정하고, 레플리카에서도 --log-bin을 켭니다. 나중에 이 노드를 source로 승격하거나 체인 복제를 할 때를 대비한 것입니다.
  • read_only / super_read_onlycompose 파일에서 켜지 않은 이유가 주석에 적혀 있습니다. 부팅 시점에 켜면 컨테이너 초기화(root 계정 생성 등) 자체가 막혀 버리기 때문입니다. 그래서 setup-replication.sh가 복제를 구성한 직후에 SQL로 켭니다.
  • depends_on: source: condition: service_healthy — 레플리카는 source의 헬스체크가 통과한 뒤에야 시작합니다. 그래도 setup-replication.sh는 두 노드가 모두 준비된 다음에 실행해야 합니다.
  • 실습이 끝나면 docker compose down -v볼륨까지 정리하세요. 남겨 두면 다음에 다시 띄울 때 이전 GTID 상태가 남아 복제 구성이 꼬입니다.
# =====================================================================
# Step 23 — 복제(Replication) 실습용 2노드 클러스터
# ---------------------------------------------------------------------
#   source  : 3308 → 컨테이너 repl-source  (server_id=1)
#   replica : 3309 → 컨테이너 repl-replica (server_id=2)
#
# ※ 기존 학습용 컨테이너 learn-mysql8(3307)과 완전히 분리된 별도 스택입니다.
#    learn-mysql8 은 절대 건드리지 않습니다.
#
# 실행:
#   cd mysql8/docker/replication
#   docker compose up -d
#   ../../step-23-backup-replication/setup-replication.sh
#
# 정리(볼륨까지 삭제):
#   docker compose down -v
# =====================================================================
services:
  source:
    image: mysql:8.0
    container_name: repl-source
    ports:
      - "3308:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root1234
      MYSQL_DATABASE: shop
      TZ: Asia/Seoul
    command:
      - --server-id=1
      - --log-bin=mysql-bin              # 바이너리 로그 ON (복제/PITR 의 전제조건)
      - --binlog-format=ROW              # 8.0 기본값. 안전한 복제를 위해 명시
      - --gtid-mode=ON                   # GTID 복제
      - --enforce-gtid-consistency=ON    # GTID 와 호환되지 않는 SQL 금지
      - --binlog-expire-logs-seconds=604800   # binlog 7일 보관 (8.0 신규. expire_logs_days 대체)
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_0900_ai_ci
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot1234"]
      interval: 5s
      timeout: 3s
      retries: 30

  replica:
    image: mysql:8.0
    container_name: repl-replica
    ports:
      - "3309:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root1234
      TZ: Asia/Seoul
    command:
      - --server-id=2
      - --log-bin=mysql-bin              # 레플리카도 binlog 를 켠다(체인 복제/승격 대비)
      - --binlog-format=ROW
      - --gtid-mode=ON
      - --enforce-gtid-consistency=ON
      - --relay-log=relay-bin
      # read_only / super_read_only 는 부트 시 켜면 컨테이너 초기화(root 생성 등)를
      # 막으므로, setup-replication.sh 가 복제 구성 직후 SQL 로 켭니다.
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_0900_ai_ci
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot1234"]
      interval: 5s
      timeout: 3s
      retries: 30
    depends_on:
      source:
        condition: service_healthy