mysql에서 time-zone (타임존)을 Asia/Seoul 및 KST로 정의하는 방법
- Docker / Percona Server 8.0 / Ubuntu 24.04 환경에서 다음 출력을 만들기 위한 핵심 정리.
SELECT @@global.time_zone, @@session.time_zone, @@system_time_zone;
-- Asia/Seoul Asia/Seoul KST
1. 설정 방법 (Setup Procedure)
1.1. 전체 그림
- 3개 파일로 끝난다. 각 파일이 책임지는 변수가 다르다.
| 파일 | 라인 | 책임지는 변수 |
| docker-compose.yml | environment.TZ: Asia/Seoul | @@system_time_zone (= KST) |
| Dockerfile | ARG MYSQL_TZ=Asia/Seoul + 빌드 시 init.sql 굽기 | @@global.time_zone (= Asia/Seoul) |
| config/my.cnf | timezone 라인 없음 | — |
1.2. 디렉터리 구조
mysql-80/
├── Dockerfile
├── docker-compose.yml
├── config/
│ └── my.cnf
└── data/ # bind mount, 영속화
1.3. Step 1 — `Dockerfile` 작성
- 빌드 시점에 init SQL을 이미지 내부로 굽는다.
ARG MYSQL_TZ=Asia/Seoul
RUN printf "SET PERSIST time_zone = '%s';\n" "${MYSQL_TZ}" \
> /docker-entrypoint-initdb.d/01-set-timezone.sql; \
chmod a+r /docker-entrypoint-initdb.d/01-set-timezone.sql
1.4. Step 2 — `docker-compose.yml` 작성
services:
mysql:
build:
context: .
dockerfile: Dockerfile
image: percona-mysql-8.0:ubuntu24.04
container_name: percona-mysql-8.0
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: test1324
TZ: Asia/Seoul # ← OS 레이어 (system_time_zone 결정)
ports:
- "3306:3306"
volumes:
- ./config/my.cnf:/etc/mysql/conf.d/my.cnf:ro
- ./data:/var/lib/mysql
1.5. Step 3 — `config/my.cnf` 작성
- timezone 관련 어떤 라인도 두지 않는다.
- `bind-address`, `port`, `innodb_buffer_pool_size`, `binlog_format`, `server_id` 등 부류 A 옵션만 둔다.
1.6. Step 4 — 첫 부팅
mkdir -p data
sudo chown 1001:1001 data # 컨테이너 내부 mysql 사용자 uid/gid
docker compose up -d --build
1.7. Step 5 — 검증
docker compose exec mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -B -e \
"SELECT @@global.time_zone, @@session.time_zone, @@system_time_zone;"
- 기대 출력:
@@global.time_zone @@session.time_zone @@system_time_zone
Asia/Seoul Asia/Seoul KST
- PERSIST 영속화 확인:
docker compose exec mysql cat /var/lib/mysql/mysqld-auto.cnf
- JSON 안에 `"time_zone": {"Value": "Asia/Seoul", ...}` 가 박혀 있어야 한다.
1.8. 동작 흐름 요약
| 시점 | 일어나는 일 |
| 빌드 | Dockerfile의 RUN printf가 이미지 내부에 /docker-entrypoint-initdb.d/01-set-timezone.sql 생성 |
| 첫 부팅 Phase A | mysqld --initialize-insecure — datadir 생성. my.cnf에 timezone 라인 없으므로 통과 |
| 첫 부팅 Phase B | 임시 mysqld → mysql_tzinfo_to_sql가 1700+개 zone 적재 → init.sql 실행 → SET PERSIST → mysqld-auto.cnf 작성 |
| 첫 부팅 Phase C | 본 mysqld → my.cnf + mysqld-auto.cnf 모두 읽음 → Asia/Seoul 적용. TZ env가 glibc를 거쳐 system_time_zone=KST 결정 |
| 이후 부팅 | datadir이 채워져 있으므로 Phase A·B 스킵. Phase C만 실행되며 mysqld-auto.cnf로부터 자동 적용 |
2. 세 변수가 결정되는 곳
| 변수 | 결정 주체 | 본 프로젝트에서 |
| @@system_time_zone | OS의 TZ 환경변수 / /etc/localtime (부팅 시 1회 추출, 읽기 전용) | docker-compose.yml의 TZ: Asia/Seoul → glibc가 KST로 라벨링 |
| @@global.time_zone | my.cnf, mysqld-auto.cnf, 또는 런타임 SET (뒤로 갈수록 우선순위 높음) | SET PERSIST 결과로 mysqld-auto.cnf에 영속화 |
| @@session.time_zone | 연결 시 global을 상속, 클라이언트가 SET time_zone으로 덮어쓸 수 있음 | global 상속 |
- 표시 가능한 형식
| 형식 | 예 | 검증 자원 | 비고 |
| 특수 키워드 | SYSTEM | 없음 | "OS TZ 따라간다" |
| 오프셋 | +09:00 | 단순 파싱 | 어디서든 안전, DST 추종 불가 |
| 약식 | KST | OS tzdata | OS 의존 |
| Named (IANA) | Asia/Seoul | mysql.time_zone_name 테이블 | DST·역사 변경 정확 추종 — 본 요구 |
3. my.cnf에 `Asia/Seoul`을 못 넣는 이유 (핵심 제약)
- 컨테이너 첫 부팅의 3단계 라이프사이클 때문이다.
- Phase A (`mysqld --initialize-insecure`): datadir 생성. 이 시점 `mysql.time_zone_name` 테이블은 비어 있음. my.cnf에 named zone이 있으면 → `Illegal or unknown default time zone 'Asia/Seoul'` → abort.
- Phase B (임시 부트스트랩 mysqld): `mysql_tzinfo_to_sql`이 1700+개 IANA zone을 적재. 이후 `/docker-entrypoint-initdb.d/*.sql` 실행.
- Phase C (본 mysqld): my.cnf + `mysqld-auto.cnf` 모두 읽음. tz 테이블이 채워진 상태라 검증 통과.
- 실측 결과
| 위치/형식 | named Asia/Seoul | 오프셋 +09:00 |
| my.cnf (default-time-zone) | ❌ Phase A abort | ✅ 부팅 OK, literal +09:00 |
| /docker-entrypoint-initdb.d/*.sql (SET PERSIST) | ✅ 부팅 OK, literal Asia/Seoul | ✅ (불필요) |
- 전통 패키지 설치(apt)에서는 왜 문제없나
- `mysql_tzinfo_to_sql` 적재(t1) → 운영자가 my.cnf에 named zone 추가(t2) 사이에 시간 간격이 있다.
- Docker는 이 과정이 entrypoint 안에서 한 번에 압축되므로, named zone을 끼워넣을 슬롯이 `/docker-entrypoint-initdb.d/`뿐이다.
4. SET PERSIST와 mysqld-auto.cnf
4.1. SET 종류
| 구문 | 런타임 변경 | mysqld-auto.cnf 기록 | 다음 부팅 적용 |
| SET GLOBAL var = ... | ✅ | ❌ | ❌ |
| SET PERSIST var = ... | ✅ | ✅ | ✅ |
| SET PERSIST_ONLY var = ... | ❌ | ✅ | ✅ |
| SET SESSION var = ... | ✅ (해당 연결만) | ❌ | ❌ |
4.2. mysqld-auto.cnf 형식
- 위치: `/var/lib/mysql/mysqld-auto.cnf` (datadir 내부, JSON)
{
"Version": 2,
"mysql_dynamic_variables": {
"time_zone": {
"Value": "Asia/Seoul",
"Metadata": { "Host": "localhost", "User": "root", "Timestamp": 1778041898752849 }
}
}
}
- mysqld가 Phase C에서 my.cnf 다음에 이 파일을 읽고 적용. 동시 정의 시 이 파일이 우선.
5. 대안 비교 (왜 다른 방식은 기각됐나)
| 대안 | literal 결과 | 프로젝트 파일 수 | 문제 |
| my.cnf에 default-time-zone='+09:00' | +09:00 | 3 | literal 불일치, DST 추종 불가 |
| my.cnf에 default-time-zone='Asia/Seoul' | — | 3 | Phase A abort |
| config/init/*.sql 별도 마운트 | Asia/Seoul | 4 | "별도 스크립트 파일 없이" 요구 위반 |
| Dockerfile에서 굽기 (채택) | Asia/Seoul | 3 | 이미지 내부 한 줄 박힘 (단점 거의 없음) |
6. 운영 시나리오
6.1. 새 환경 구축
mkdir -p data && sudo chown 1001:1001 data
docker compose up -d --build
6.2. 데이터 유지하면서 zone 변경 (가장 흔한 케이스)
docker compose exec mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" \
-e "SET PERSIST time_zone = 'Asia/Tokyo';"
docker compose restart mysql
- 응용 풀(connection pool)도 함께 재기동해 드라이버 캐시를 무효화할 것.
6.3. 빌드 인자로 다른 zone 지정 + datadir 새로
docker compose down
sudo rm -rf data && mkdir data && sudo chown 1001:1001 data
docker compose build --build-arg MYSQL_TZ=Asia/Tokyo mysql
docker compose up -d
6.4. TIMESTAMP vs DATETIME
- `TIMESTAMP` — InnoDB에 항상 UTC로 저장. 읽기/쓰기 시 `@@session.time_zone`으로 자동 변환. → zone 바뀌면 같은 row가 다른 wall-clock으로 보임.
- `DATETIME` — 변환 없이 그대로 저장. → zone 바뀌어도 같은 wall-clock.
7. 트러블슈팅
7.1. 자주 만나는 에러
| 증상 / 로그 | 원인 | 조치 |
| Illegal or unknown default time zone 'Asia/Seoul' | my.cnf에 named zone | my.cnf에서 timezone 라인 제거 |
| unknown variable 'time_zone=...' | time_zone을 my.cnf에 직접 둠 | default-time-zone 옵션명을 쓰거나 라인 제거 |
| Unable to lock ./ibdata1 error: 11 | 다른 컨테이너가 datadir 점유 | docker ps -a로 잔존 컨테이너 제거 |
| Permission denied on /var/lib/mysql/ | 호스트 ./data 권한 | sudo chown -R 1001:1001 ./data |
| @@global.time_zone이 SYSTEM으로 보임 | mysqld-auto.cnf 미생성 (datadir이 채워진 채 컨테이너만 새로 띄움 → init 단계 스킵) | SET PERSIST time_zone='Asia/Seoul'; + restart |
| @@session.time_zone만 다른 값 | 응용 드라이버가 연결 시 SET time_zone 발행 | 드라이버의 serverTimezone / init_command 점검 |
| @@system_time_zone이 KST가 아님 | compose TZ: 미설정/오설정 | docker compose exec mysql sh -c 'readlink /etc/localtime' |
7.2. 첫 부팅 실패 후 복구
- Phase A가 abort해도 일부 파일(ibdata1, mysql/ 등)이 이미 만들어져 있을 수 있음.
- 이 상태에서 다시 띄우면 entrypoint가 `if [ ! -d "$DATADIR/mysql" ]`을 false로 보고 init 단계를 통째로 건너뛴다. → datadir 완전 삭제 후 재시작:
docker compose down
sudo rm -rf data/* && sudo chown 1001:1001 data
docker compose up -d
7.3. 처음 보는 zone으로 바꾸려는데 실패
-- 1. tz 테이블이 비어있는지 확인
SELECT COUNT(*) FROM mysql.time_zone_name;
SELECT * FROM mysql.time_zone_name WHERE Name='Asia/Tokyo';
- 비어있다면 수동 적재:
docker compose exec mysql sh -c 'mysql_tzinfo_to_sql /usr/share/zoneinfo \
| mysql -uroot -p"$MYSQL_ROOT_PASSWORD" mysql'
8. 응용 계층 고려사항
8.1. JDBC
- URL 옵션:
- `serverTimezone=Asia/Seoul` — 드라이버가 사용하는 zone (서버와 일치시킬 것)
- `forceConnectionTimeZoneToSession=true` — 드라이버가 `SET time_zone` 강제
8.2. Python (FastAPI / asyncmy / aiomysql)
create_async_engine(
"mysql+asyncmy://...",
connect_args={"init_command": "SET time_zone='Asia/Seoul'"},
)
8.3. ORM 캐싱
- ORM이 첫 연결 시 가져온 zone을 캐시하고 변경을 인지 못 할 수 있음.
- 운영 중 `SET PERSIST`로 바꾸려면 응용 풀을 함께 리프레시.
9. 한 줄 핵심
- mysqld가 부팅 절차에서 my.cnf 옵션을 검증할 때, 그 검증이 `mysql.*` 시스템 데이터베이스의 데이터를 필요로 하면, 그 옵션은 데이터가 채워진 후의 시점이 보장되는 곳(=`/docker-entrypoint-initdb.d/`의 SQL)에 두어야 한다. 그 결과는 `mysqld-auto.cnf`에 영속화된다.
- 부록 — 명령 빠른 참조
# 빌드
docker compose build mysql
# 빌드 인자로 zone 바꾸기
docker compose build --build-arg MYSQL_TZ=Asia/Tokyo mysql
# 첫 부팅 (빈 datadir 가정)
sudo chown 1001:1001 data
docker compose up -d
# 검증
docker compose exec mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -B -e \
"SELECT @@global.time_zone, @@session.time_zone, @@system_time_zone;"
# PERSIST 영속화 확인
docker compose exec mysql cat /var/lib/mysql/mysqld-auto.cnf
# tzinfo 적재 확인
docker compose exec mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -B -e \
"SELECT COUNT(*) FROM mysql.time_zone_name;"
# 운영 중 zone 변경
docker compose exec mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" \
-e "SET PERSIST time_zone = 'Asia/Seoul';"
docker compose restart mysql
# datadir 초기화 후 처음부터
docker compose down
sudo rm -rf data && mkdir data && sudo chown 1001:1001 data
docker compose up -d --build
'Database > Maria,Mysql' 카테고리의 다른 글
| percona mysql을 docker로 설치하는 방법 (0) | 2023.06.18 |
|---|---|
| WITH RECURSIVE (재귀 쿼리)의 정리 (0) | 2022.09.04 |
| join에서 where과 on의 차이, outer join에 대한 정리 (0) | 2022.08.30 |
| WITH와 CTE를 이용한 재귀적 사용법 (재귀 쿼리) (0) | 2022.08.29 |
| with와 CTE (common table expression) (0) | 2022.08.29 |
댓글