TechEarl

How to Run MySQL in Docker (With Persistent Storage)

Run a MySQL server in a container, give it a named volume so your data survives the next docker rm, set the root password the right way, and connect to it from the host or another container.

Ishan KarunaratneIshan Karunaratne⏱️ 9 min readUpdated
Share thisCopied

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?

bash
docker run -d --name mysql \
  -e MYSQL_ROOT_PASSWORD=change-me \
  -p 3306:3306 \
  -v mysql-data:/var/lib/mysql \
  mysql:8.4

That 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.

Try it with your own values

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 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

bash
docker run -d --name :container_name \
  -e MYSQL_ROOT_PASSWORD=:root_password \
  -p :host_port:3306 \
  -v :volume:/var/lib/mysql \
  mysql::mysql_version

The 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 also MYSQL_RANDOM_ROOT_PASSWORD=yes if you want one generated (read it with docker logs), and MYSQL_ALLOW_EMPTY_PASSWORD=yes for 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:

bash
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_version

MYSQL_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:

bash
# 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 prune

Full 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):

bash
mysql -h 127.0.0.1 -P :host_port -u root -p
# password prompt: enter :root_password

Use 127.0.0.1, not localhostlocalhost 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:

bash
docker exec -it :container_name mysql -u root -p

From another Docker container on the same network:

bash
# 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 -p

Inside 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:

code
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:

bash
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_version

Trade-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 mysql user 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 + tar rather than cp.

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:

bash
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):

bash
# 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_name

The 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:

yaml
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 set error. The init script refuses to start without it (or one of the alternatives). Set the env var, or use MYSQL_RANDOM_ROOT_PASSWORD=yes for a generated one.
  • Skipping -v for "just testing" and losing data on the next docker 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 :volume first.
  • caching_sha2_password auth-plugin errors from older clients. MySQL 8.0+ defaults to caching_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_password to 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

FAQ

Sources

Authoritative references this article was fact-checked against.

TagsDockerMySQLMySQL 8.4DatabasePersistenceDevOps

Found this useful? Pass it on.

Copied
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts