Running MySQL in Docker is one of the most common reasons people pick up Docker in the first place: you need a database for the project you're building, you don't want to install MySQL on your machine, and you definitely don't want to manage two of them. The official mysql image makes this a one-liner once you know about two things — the root password env var and the volume that keeps your data alive.
How do I run MySQL in Docker?
docker run -d --name mysql \
-e MYSQL_ROOT_PASSWORD=change-me \
-p 3306:3306 \
-v mysql-data:/var/lib/mysql \
mysql:8.4That starts MySQL 8.4 in the background, sets the root password to change-me, publishes port 3306 on the host so you can connect from outside the container, and mounts a named volume called mysql-data at MySQL's data directory. The volume is what makes the database persist across container removal — without it, the next docker rm takes your data with it.
The rest of this article is what each of those flags does, the persistence section in detail, how to actually connect to the running instance, the version question (8.4 LTS vs 8.0), and the pitfalls.
Configure the version, ports, password, and volume. The commands below update as you type.
Jump to:
- Pick a MySQL version: 8.4 LTS, 8.0, or 5.7
- The basic run command
- Why you need a volume
- Practical usage: connecting to the running MySQL
- Bind mount alternative
- Backups
- Docker Compose version
- Common pitfalls
- FAQ
Pick a MySQL version: 8.4 LTS, 8.0, or 5.7
The official image tags worth knowing:
mysql:8.4— MySQL 8.4 LTS. Released in April 2024, the recommended choice for new projects in 2026 with long-term support through 2032.mysql:8.0— MySQL 8.0. Past Extended Support (April 2026), now Sustaining Support — no new fixes from Oracle. Use for compatibility with existing apps; plan a migration. See How to Upgrade MySQL 8.0 to 8.4 LTS.mysql:5.7— End of life October 2023. Don't start a new project here; use it only to run legacy code while you migrate.
For all the examples below I use mysql::mysql_version; swap to your version of choice.
The basic run command
docker run -d --name :container_name \
-e MYSQL_ROOT_PASSWORD=:root_password \
-p :host_port:3306 \
-v :volume:/var/lib/mysql \
mysql::mysql_versionThe flags:
-d— run in background. The container survives the shell you started it from.--name :container_name— stable handle for later commands (docker logs,docker stop).-e MYSQL_ROOT_PASSWORD=...— required on first run. The init script refuses to start without it. There's alsoMYSQL_RANDOM_ROOT_PASSWORD=yesif you want one generated (read it withdocker logs), andMYSQL_ALLOW_EMPTY_PASSWORD=yesfor testing only.-p :host_port:3306— publish container port 3306 on the host. If 3306 is taken on your host (you have another MySQL), use a different host port:-p 3307:3306.-v :volume:/var/lib/mysql— the persistent volume. Critical. See next section.
You can also pre-create the database, user, and password with extra env vars:
docker run -d --name :container_name \
-e MYSQL_ROOT_PASSWORD=:root_password \
-e MYSQL_DATABASE=myapp \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=apppass \
-p :host_port:3306 \
-v :volume:/var/lib/mysql \
mysql::mysql_versionMYSQL_DATABASE is created on first init. MYSQL_USER + MYSQL_PASSWORD creates a non-root user with full privileges on that database. This is the recipe for app-specific credentials.
Why you need a volume
The MySQL container stores its data in /var/lib/mysql inside the container. That path lives in the container's writable layer, which is part of the container — not the image. When you docker rm the container (or docker compose down removes it), the writable layer goes too. All your tables, every row of data, gone.
A named volume (-v mysql-data:/var/lib/mysql) is independent of the container. Docker manages where the bytes live on disk (under /var/lib/docker/volumes/mysql-data/ on Linux; inside the Docker Desktop VM on Mac/Windows), and the volume survives container removal. You can:
- Stop and remove the MySQL container.
- Start a new MySQL container with the same
-v mysql-data:/var/lib/mysql. - Your data is still there.
That's the whole point. Run the command without -v once, populate some data, docker rm -f the container, and observe that everything is gone. With the volume, the same sequence keeps the data.
Volume operations you'll use:
# List volumes
docker volume ls
# Inspect a volume (driver, mountpoint, labels)
docker volume inspect :volume
# Remove a volume (must not be in use)
docker volume rm :volume
# Remove all unused volumes — DATA LOSS, asks for confirmation
docker volume pruneFull breakdown: Docker Volumes vs Bind Mounts.
Practical usage: connecting to the running MySQL
From the host with the mysql CLI (if you have it installed):
mysql -h 127.0.0.1 -P :host_port -u root -p
# password prompt: enter :root_passwordUse 127.0.0.1, not localhost — localhost triggers MySQL's Unix-socket connection which doesn't exist on the host for a containerized server.
From the host without installing the mysql CLI — use docker exec to use the one inside the container:
docker exec -it :container_name mysql -u root -pFrom another Docker container on the same network:
# Create a user-defined network once
docker network create app-net
# Connect both containers
docker run -d --name :container_name --network app-net \
-e MYSQL_ROOT_PASSWORD=:root_password \
-v :volume:/var/lib/mysql \
mysql::mysql_version
docker run -it --rm --network app-net mysql::mysql_version \
mysql -h :container_name -u root -pInside the user-defined network, MySQL is reachable as :container_name on its standard port 3306 — no need to publish anything to the host. This is the right shape for an app talking to its DB.
From a host application (Node, Python, PHP), use a connection string like:
mysql://root:change-me@127.0.0.1:3306/myapp
Substitute your host port.
Bind mount alternative
If you'd rather store the data at a specific path on your host (e.g., to back it up with your normal disk backup tool), use a bind mount instead of a named volume:
mkdir -p ~/docker-data/mysql
docker run -d --name :container_name \
-e MYSQL_ROOT_PASSWORD=:root_password \
-p :host_port:3306 \
-v ~/docker-data/mysql:/var/lib/mysql \
mysql::mysql_versionTrade-offs vs named volume:
- Bind mount: you choose the host path, easy to back up via your normal file-level tools. UID/GID on the host can mismatch the
mysqluser inside the container, causing permission errors. - Named volume: Docker manages it, no permission headaches, but the on-disk path is internal. Backup needs
docker run+tarrather thancp.
For databases I prefer named volumes. The permission issues with bind mounts on Mac/Windows (Docker Desktop's virtualized filesystem) are real and annoying. Use bind mounts when you have a strong reason — usually integration with an existing host-side backup pipeline.
Backups
With mysqldump from the container:
docker exec :container_name \
mysqldump -u root -p:root_password --single-transaction --all-databases \
> backup-$(date +%Y%m%d).sql--single-transaction is the InnoDB-safe way to dump without locking tables — important on a live database.
Backing up the named volume directly (rarer, more invasive):
# Stop MySQL first to ensure a consistent on-disk state
docker stop :container_name
docker run --rm \
-v :volume:/source:ro \
-v "$(pwd):/backup" \
alpine tar czf /backup/mysql-data-$(date +%Y%m%d).tar.gz -C /source .
docker start :container_nameThe volume-level backup is faster for large databases but requires downtime. The mysqldump approach is portable across MySQL versions and works online.
Docker Compose version
For a real app stack you'll usually want Compose:
services:
db:
image: mysql::mysql_version
environment:
MYSQL_ROOT_PASSWORD: :root_password
MYSQL_DATABASE: myapp
MYSQL_USER: appuser
MYSQL_PASSWORD: apppass
ports:
- "::host_port:3306"
volumes:
- :volume:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p:root_password"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
volumes:
:volume:Then docker compose up -d. Other services in the same docker-compose.yml can connect to db:3306 — no port publishing needed for in-stack traffic.
Common pitfalls
MYSQL_ROOT_PASSWORD not seterror. The init script refuses to start without it (or one of the alternatives). Set the env var, or useMYSQL_RANDOM_ROOT_PASSWORD=yesfor a generated one.- Skipping
-vfor "just testing" and losing data on the nextdocker rm. Always use a volume for state you care about, even on dev. - Init env vars (
MYSQL_DATABASE,MYSQL_USER) silently ignored on second run. Those vars are only processed on first init of a clean data directory. If the volume already has data, the init script skips initialization. To re-init,docker volume rm :volumefirst. caching_sha2_passwordauth-plugin errors from older clients. MySQL 8.0+ defaults tocaching_sha2_password. Older drivers (PHP 7.2-, old Node mysql driver) can't speak it. Either upgrade the driver or pass--default-authentication-plugin=mysql_native_passwordto the server. See How to Migrate from MySQL 5.7 to 8.0 for the full breakdown.- Port 3306 already in use. You have another MySQL on the host. Pick a different host port:
-p 3307:3306. - Permission denied on Mac with bind mounts. Docker Desktop's virtual filesystem and MySQL's UID 999 don't always cooperate. Switch to a named volume; or for bind mounts use the more permissive Docker Desktop file-sharing settings.
What to do next
- Run MariaDB in Docker — the same shape with MariaDB instead.
- Run Adminer in Docker — a lightweight DB GUI to talk to this MySQL.
- How to Run WordPress in Docker (with MySQL via Compose) — pair this MySQL with WordPress.
- Docker Volumes vs Bind Mounts — full picture of persistence options.
- MySQL Cheat Sheet — once you're in, the SQL/CLI reference.
FAQ
Sources
Authoritative references this article was fact-checked against.
- Official MySQL image — Docker Hubhub.docker.com
- MySQL Reference Manual — Dockerdev.mysql.com

