Dockerize MySQL cho môi trường dev: Dùng Init Scripts tự động khởi tạo Schema và dữ liệu mẫu

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

Setup xong trong 5 phút — làm ngay đi

Mỗi lần onboard người mới vào team, mình lại thấy cảnh quen thuộc: dev mới ngồi mày mò cài MySQL, tạo database, import schema, chạy seed data — mất cả buổi sáng mà chưa viết được dòng code nào. Hoặc tệ hơn, môi trường dev của mỗi người một kiểu, bug chỉ reproduce được trên máy này mà không được trên máy kia.

Docker + init scripts xử lý gọn vấn đề đó. Container MySQL tự động tạo schema và seed data ngay lần chạy đầu tiên. docker compose up là xong, không cần làm gì thêm. Ở team mình, từ khi áp dụng cách này, thời gian setup môi trường của dev mới giảm từ 2-3 tiếng xuống còn khoảng 10 phút chờ pull image.

Tạo cấu trúc thư mục này:

project/
├── docker-compose.yml
└── mysql/
    ├── init/
    │   ├── 01_schema.sql
    │   └── 02_seed_data.sql
    └── conf/
        └── my.cnf

File docker-compose.yml:

services:
  db:
    image: mysql:8.0
    container_name: myapp_db
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: myapp
      MYSQL_USER: devuser
      MYSQL_PASSWORD: devpass
    ports:
      - "3306:3306"
    volumes:
      - ./mysql/init:/docker-entrypoint-initdb.d
      - ./mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

File mysql/init/01_schema.sql:

CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    title VARCHAR(255) NOT NULL,
    body TEXT,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

File mysql/init/02_seed_data.sql:

INSERT INTO users (username, email) VALUES
    ('alice', '[email protected]'),
    ('bob', '[email protected]');

INSERT INTO posts (user_id, title, body) VALUES
    (1, 'Hello World', 'Post đầu tiên của Alice'),
    (2, 'Docker is great', 'Bob chia sẻ về Docker');

Chạy thôi:

docker compose up -d
# Đợi ~10 giây rồi kiểm tra
docker exec -it myapp_db mysql -u devuser -pdevpass myapp -e "SELECT * FROM users;"

Schema và data đã có sẵn. Clone repo về, chạy một lệnh là đủ — bất kể máy Windows, Mac hay Linux.

Cơ chế hoạt động — hiểu để không bị bất ngờ

Thư mục /docker-entrypoint-initdb.d là nơi image MySQL chính thức “chào đón” các script khởi tạo của bạn. Khi container khởi động lần đầu và volume chưa có data, entrypoint script tự động thực thi tất cả file trong đó theo thứ tự alphabet. Chỉ vậy thôi — không có magic gì cả, chỉ là một vòng lặp for f in /docker-entrypoint-initdb.d/* trong bash.

Bốn điều cần nắm trước khi dùng:

  • Thứ tự thực thi: Theo alphabet — đó là lý do đặt tên 01_schema.sql, 02_seed_data.sql. Schema phải chạy trước seed data, nếu không foreign key sẽ fail.
  • Chỉ chạy một lần: Volume đã có data từ lần trước? Init scripts bị bỏ qua hoàn toàn. Behavior này là đúng — tránh overwrite data thật khi restart container.
  • Hỗ trợ .sql và .sh: Mix file SQL và shell script trong cùng thư mục được, chạy xen kẽ theo thứ tự tên file.
  • Database mặc định đã được chọn: Biến MYSQL_DATABASE xác định database context khi script chạy — không cần thêm USE myapp; ở đầu mỗi file SQL.

Muốn reset và chạy lại init scripts từ đầu? Xóa cả volume:

docker compose down -v   # Xóa container VÀ volume
docker compose up -d     # Tạo lại từ đầu, init scripts chạy lại

Nâng cao — các tình huống thực tế hơn

Dùng shell script thay SQL thuần

SQL thuần không đủ khi bạn cần tạo nhiều database, import file dump lớn (vài trăm MB), hoặc chạy logic có điều kiện. Shell script xử lý được hết:

#!/bin/bash
# mysql/init/03_extra_setup.sh

set -e

mysql -u root -p"$MYSQL_ROOT_PASSWORD" <<-EOSQL
    CREATE DATABASE IF NOT EXISTS myapp_test;
    GRANT ALL PRIVILEGES ON myapp_test.* TO 'devuser'@'%';
EOSQL

echo "Extra setup hoàn tất"

File .sh phải có quyền execute — bước này hay bị quên:

chmod +x mysql/init/03_extra_setup.sh

Tùy chỉnh MySQL config cho dev

File mysql/conf/my.cnf giúp tắt bớt các ràng buộc nghiêm ngặt của MySQL. Production cần strict, dev thì không nhất thiết:

[mysqld]
# Bỏ bớt strict mode để dev dễ thở hơn
sql_mode = ONLY_FULL_GROUP_BY,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO

# Tắt binary log — dev không cần replication, tiết kiệm disk
skip-log-bin

# Tăng max_connections cho trường hợp nhiều service kết nối
max_connections = 200

# Timezone
default-time-zone = '+07:00'

Kiểm tra trạng thái khởi động

Backend khởi động trước MySQL là lỗi kinh điển — app crash ngay lập tức vì không connect được DB. Healthcheck giải quyết gọn:

services:
  db:
    image: mysql:8.0
    # ... các config khác ...
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s

  backend:
    image: myapp_backend
    depends_on:
      db:
        condition: service_healthy

condition: service_healthy đảm bảo backend chỉ khởi động sau khi MySQL thực sự accept connection. Không cần sleep 30 hack trong entrypoint nữa.

Tips thực tế từ kinh nghiệm dùng hàng ngày

Tách biệt schema và seed data

Schema (DDL) và seed data (DML) nên nằm trong file riêng biệt — nguyên tắc này nghe đơn giản nhưng quan trọng hơn bạn nghĩ. Khi schema thay đổi, chỉ sửa 01_schema.sql, không đụng đến data. Khi cần thêm test case, chỉ sửa 02_seed_data.sql. Git diff gọn hơn, code review dễ hơn, và khi có bug cũng dễ khoanh vùng hơn.

Đừng hardcode password trong docker-compose.yml

Dùng file .env:

# .env (add vào .gitignore)
MYSQL_ROOT_PASSWORD=your_root_password
MYSQL_PASSWORD=your_dev_password
# docker-compose.yml
environment:
  MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
  MYSQL_PASSWORD: ${MYSQL_PASSWORD}

Quy tắc đơn giản: .env.example với giá trị placeholder thì commit lên git. .env thật thì gitignore. Không bao giờ ngoại lệ, kể cả repo private.

Seed data phải idempotent

Dùng INSERT IGNORE hoặc INSERT ... ON DUPLICATE KEY UPDATE thay vì INSERT thuần. Chạy bao nhiêu lần cũng không lỗi:

INSERT IGNORE INTO users (username, email) VALUES
    ('alice', '[email protected]'),
    ('bob', '[email protected]');

Cái này đặc biệt quan trọng khi bạn restore từ backup một phần — thay vì chạy lại toàn bộ init scripts, chỉ cần chạy lại seed data mà không lo duplicate key error.

Bài học xương máu về backup

3 giờ sáng. Disk full đột ngột. MySQL ghi dở file, database corruption. Ngồi restore từ backup mà tay run cầm — đó là lần đầu mình thực sự hiểu tại sao backup quan trọng đến vậy. Từ đó mình thêm hẳn một service vào docker-compose để mysqldump chạy theo cron mỗi ngày:

  db_backup:
    image: mysql:8.0
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./backups:/backups
    entrypoint: |
      sh -c 'while true; do
        mysqldump -h db -u root -p$$MYSQL_ROOT_PASSWORD myapp > /backups/myapp_$$(date +%Y%m%d_%H%M).sql;
        find /backups -name "*.sql" -mtime +3 -delete;
        sleep 86400;
      done'

5 phút setup, giữ backup 3 ngày gần nhất, tự động xóa cũ. Nhỏ nhưng đã cứu mình nhiều lần.

Xem log init scripts khi debug

Init script chạy lỗi mà không rõ nguyên nhân? Xem log container:

# Xem toàn bộ log khi container khởi động
docker compose logs db

# Follow realtime và lọc dòng quan trọng
docker compose logs -f db 2>&1 | grep -E "(ERROR|init|schema)"

MySQL log rõ từng file đang chạy và lỗi cụ thể ở dòng nào — thường debug xong trong vài phút.

Setup này mình áp dụng cho mọi project có MySQL, từ side project cá nhân đến team 10 người. Mỗi lần tạo môi trường mới chỉ mất đúng thời gian pull image về — không còn cảnh ngồi tạo tay từng bảng hay hỏi nhau “bạn đã import schema chưa” nữa.

Share: