Set Up in 5 Minutes — Let’s Get to It
Every time a new developer joins the team, I see the same scene play out: they spend the entire morning wrestling with MySQL installation, creating databases, importing schemas, running seed data — and haven’t written a single line of code yet. Even worse, every dev’s local environment is slightly different, so bugs only reproduce on one machine and not another.
Docker + init scripts solves all of that cleanly. The MySQL container automatically creates the schema and seeds data on its very first run. docker compose up and you’re done — nothing else needed. On my team, since adopting this approach, new developer onboarding time dropped from 2–3 hours down to about 10 minutes waiting for the image to pull.
Create this directory structure:
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', 'First post by Alice'),
(2, 'Docker is great', 'Bob shares his thoughts on Docker');
Now run it:
docker compose up -d
# Wait ~10 seconds then check
docker exec -it myapp_db mysql -u devuser -pdevpass myapp -e "SELECT * FROM users;"
Schema and data are ready. Clone the repo, run one command, and you’re set — regardless of whether you’re on Windows, Mac, or Linux.
How It Works — Understanding It So There Are No Surprises
The /docker-entrypoint-initdb.d directory is where the official MySQL image “welcomes” your initialization scripts. When the container starts for the first time and the volume has no data, the entrypoint script automatically executes all files in that directory in alphabetical order. That’s all there is to it — no magic, just a for f in /docker-entrypoint-initdb.d/* loop in bash.
Four things to understand before using this:
- Execution order: Alphabetical — that’s why files are named
01_schema.sql,02_seed_data.sql. The schema must run before seed data, otherwise foreign keys will fail. - Runs only once: If the volume already has data from a previous run, init scripts are skipped entirely. This behavior is intentional — it prevents overwriting real data when restarting the container.
- Supports .sql and .sh: You can mix SQL files and shell scripts in the same directory, and they’ll run interleaved in filename order.
- Default database is pre-selected: The
MYSQL_DATABASEvariable sets the database context when scripts run — no need to addUSE myapp;at the top of each SQL file.
Want to reset and run init scripts from scratch? Delete the volume:
docker compose down -v # Remove container AND volume
docker compose up -d # Recreate from scratch, init scripts run again
Advanced — More Real-World Scenarios
Using Shell Scripts Instead of Plain SQL
Plain SQL isn’t enough when you need to create multiple databases, import large dump files (hundreds of MB), or run conditional logic. Shell scripts can handle all of that:
#!/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 complete"
The .sh file must have execute permission — this step is easy to forget:
chmod +x mysql/init/03_extra_setup.sh
Customizing MySQL Config for Dev
The mysql/conf/my.cnf file lets you relax MySQL’s strict constraints. Production needs them; dev doesn’t necessarily:
[mysqld]
# Reduce strict mode for a more relaxed dev experience
sql_mode = ONLY_FULL_GROUP_BY,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO
# Disable binary log — dev doesn't need replication, saves disk space
skip-log-bin
# Increase max_connections for multiple service connections
max_connections = 200
# Timezone
default-time-zone = '+07:00'
Checking Startup Health
The backend starting before MySQL is a classic mistake — the app crashes immediately because it can’t connect to the database. Healthchecks solve this cleanly:
services:
db:
image: mysql:8.0
# ... other config ...
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 ensures the backend only starts after MySQL is actually accepting connections. No more sleep 30 hacks in the entrypoint.
Practical Tips from Daily Experience
Separate Schema from Seed Data
Schema (DDL) and seed data (DML) should live in separate files — this principle sounds simple but matters more than you’d think. When the schema changes, only edit 01_schema.sql without touching the data. When you need to add test cases, only edit 02_seed_data.sql. Cleaner git diffs, easier code reviews, and easier bug isolation when something goes wrong.
Don’t Hardcode Passwords in docker-compose.yml
Use a .env file:
# .env (add to .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}
Simple rule: commit .env.example with placeholder values to git. The real .env goes in .gitignore. No exceptions, not even for private repos.
Make Seed Data Idempotent
Use INSERT IGNORE or INSERT ... ON DUPLICATE KEY UPDATE instead of plain INSERT. Run it as many times as you want without errors:
INSERT IGNORE INTO users (username, email) VALUES
('alice', '[email protected]'),
('bob', '[email protected]');
This is especially important when restoring from a partial backup — instead of re-running all init scripts, you can just re-run the seed data without worrying about duplicate key errors.
A Hard-Learned Lesson About Backups
3 AM. Disk suddenly full. MySQL mid-write, database corrupted. Sitting there restoring from backup with shaking hands — that was the moment I truly understood why backups matter. Since then, I’ve added a dedicated service to docker-compose that runs mysqldump on a cron schedule every day:
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 minutes to set up, keeps the last 3 days of backups, automatically cleans up old ones. Small, but it’s saved me more than once.
Viewing Init Script Logs When Debugging
Init script failing with no obvious cause? Check the container logs:
# View all logs when the container starts
docker compose logs db
# Follow in realtime and filter important lines
docker compose logs -f db 2>&1 | grep -E "(ERROR|init|schema)"
MySQL logs each file as it runs and pinpoints the exact error line — debugging usually wraps up in a few minutes.
I use this setup for every MySQL project, from personal side projects to teams of 10. Creating a new environment now takes exactly as long as pulling the image — no more manually creating tables or asking each other “did you import the schema yet?”

