Skip to main content

How to Run MySQL in Docker with Docker Compose

· 15 min read
Jagdish Kumawat
Founder @ Dewiride

You don't need to install MySQL to use it. With Docker Desktop, one small compose.yaml file runs the official MySQL 9.7 image with its data stored safely in a volume, and one command starts it. This guide builds that file line by line, starts MySQL for the first time, and shows exactly what to look for in the output.

Why Run MySQL in Docker​

  • No installer. MySQL runs inside a container, separate from the rest of your computer.
  • The same version for everyone. The file names the exact MySQL line, so every developer and every machine runs the same database.
  • Easy to stop and remove. One command stops it; your data waits in a volume until you start it again.

This is the development database for the series How to Connect a .NET C# Web API to a MySQL Database, but nothing here is specific to .NET.

Which MySQL version​

Oracle publishes two kinds of MySQL release. When I checked Docker Hub on 2 November 2026, the official mysql image tags pointed here:

TagVersionWhat it is
9.7, lts9.7.4Long-term support (LTS): years of bug and security fixes, no surprise changes
latest, innovation26.10.0Innovation: the newest features, replaced every quarter

This guide uses mysql:9.7. It follows the 9.7 LTS line and picks up its patch releases, and it never jumps to a new major version on its own.

What it costs​

MySQL Community Server and the official Docker image are free. Docker Desktop is free for personal use, education, non-commercial open source and small businesses ("fewer than 250 employees AND less than $10 million in annual revenue"); larger organizations need a paid Docker subscription, according to the Docker Desktop license agreement. On Linux you can use the free Docker Engine instead (How to Install Docker on Ubuntu Server).

Prerequisites​

  • Docker Desktop, installed and running. Check both tools in a terminal:
Terminal
docker --version
docker compose version
Output
Docker version 29.9.2, build a82e528
Docker Compose version v5.6.1
  • About 1 GB of free disk space. The MySQL 9.7 image takes 954 MB once downloaded.
  • A text editor such as VS Code.

Every docker command in this guide works unchanged in bash, zsh and PowerShell.

Step 1: Create a Project Folder​

Terminal
mkdir dewiride-inventory-api
cd dewiride-inventory-api

Compose treats this folder as the project: it looks for compose.yaml and .env here.

Step 2: Write compose.yaml​

Create a file named compose.yaml in the folder. Build it in small pieces.

Name the project. This name prefixes everything Compose creates (the container, its network and its volume), so you can recognize them later:

compose.yaml
name: dewiride-inventory

Add the MySQL service and its image. A service is one container that Compose manages:

compose.yaml (add below name)
services:
mysql:
# The official MySQL image, long-term support (LTS) line 9.7.
image: mysql:9.7

Create a database and an app account on the first start. The official image reads these environment variables when its data folder is empty:

compose.yaml (add inside mysql, below image)
environment:
# Created the first time MySQL starts with an empty data volume.
MYSQL_DATABASE: inventory
MYSQL_USER: inventory_app
# The passwords come from the .env file next to this one.
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?Set MYSQL_PASSWORD in the .env file}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD in the .env file}
  • MYSQL_DATABASE creates an empty database named inventory.
  • MYSQL_USER and MYSQL_PASSWORD create an account that gets full rights on that one database, and nothing else. Your app signs in with this account.
  • MYSQL_ROOT_PASSWORD sets the password of root, MySQL's administrator.
  • ${…:?…} takes each password from .env and refuses to start if it's missing. How to Keep Passwords Out of compose.yaml with an .env File explains this syntax.

Publish MySQL's port to your own computer only:

compose.yaml (add inside mysql, below environment)
ports:
# Reachable from this computer only, not from your network.
- "127.0.0.1:3306:3306"

3306 is MySQL's standard port. The part before it, 127.0.0.1, means "this computer only". Without it, Docker listens on every network interface, and anyone on your network could try to sign in. On Linux servers, Docker's published ports even bypass the firewall, as Self-Host Dewiride Analytics — Part 2 shows.

Keep the data in a named volume:

compose.yaml (add inside mysql, below ports)
volumes:
# The data lives in a named volume, so it survives restarts and re-creation.
- mysql-data:/var/lib/mysql

/var/lib/mysql is where MySQL stores its files inside the container. Mapping it to a named volume, mysql-data, keeps the data outside the container, so stopping, removing or upgrading the container doesn't lose it.

Add a health check:

compose.yaml (add inside mysql, below volumes)
healthcheck:
# Healthy as soon as MySQL answers over TCP. "mysqladmin ping" needs no
# password: an "Access denied" answer still proves the server is up.
test: ["CMD", "mysqladmin", "ping", "--host=127.0.0.1", "--silent"]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
start_interval: 2s

Docker runs test regularly and marks the container healthy once it succeeds. Two details make this check reliable:

  • No password is needed. MySQL's manual says mysqladmin ping returns success "even in case of an error such as Access denied, because this means that the server is running but refused the connection".
  • --host=127.0.0.1 forces a TCP connection. On its first start, the image runs a temporary MySQL with networking switched off while it creates your database. A TCP ping fails during that phase, so the container only turns healthy once the real server is listening on port 3306.

start_period gives the first start up to 60 seconds before failures count, and start_interval checks every 2 seconds during that time.

Declare the volume at the end of the file, with no indentation:

compose.yaml (add at the end)
volumes:
mysql-data:

The complete file:

compose.yaml
# The local development database for the Dewiride Inventory API.
# Start it with "docker compose up -d --wait" and stop it with "docker compose stop".
name: dewiride-inventory

services:
mysql:
# The official MySQL image, long-term support (LTS) line 9.7.
image: mysql:9.7
environment:
# Created the first time MySQL starts with an empty data volume.
MYSQL_DATABASE: inventory
MYSQL_USER: inventory_app
# The passwords come from the .env file next to this one.
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?Set MYSQL_PASSWORD in the .env file}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD in the .env file}
ports:
# Reachable from this computer only, not from your network.
- "127.0.0.1:3306:3306"
volumes:
# The data lives in a named volume, so it survives restarts and re-creation.
- mysql-data:/var/lib/mysql
healthcheck:
# Healthy as soon as MySQL answers over TCP. "mysqladmin ping" needs no
# password: an "Access denied" answer still proves the server is up.
test: ["CMD", "mysqladmin", "ping", "--host=127.0.0.1", "--silent"]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
start_interval: 2s

volumes:
mysql-data:

The complete compose.yaml in a macOS-style code window: a mysql service using the mysql:9.7 image, inventory database and inventory_app user, passwords from .env, port 3306 bound to 127.0.0.1, a mysql-data volume and a mysqladmin ping health check.

Step 3: Create the .env File​

Create .env in the same folder, with two strong random passwords. This command writes them without showing them on screen (How to Generate a Strong Random Password in the Terminal):

Terminal
printf "MYSQL_ROOT_PASSWORD=%s\nMYSQL_PASSWORD=%s\n" "$(openssl rand -hex 24)" "$(openssl rand -hex 24)" > .env
Decide the passwords before the first start

MySQL stores the accounts in its volume on the very first start. Changing .env later doesn't change them, and the new password fails with ERROR 1045. Fixed: ERROR 1045 (28000): Access denied for user shows how to change it properly.

Step 4: Check the File​

Terminal
docker compose config --quiet

No output means compose.yaml and .env are valid. (Leave out --quiet and the command prints the file with your real passwords filled in, so keep it.)

Step 5: Start MySQL​

Terminal
docker compose up -d --wait
  • up creates and starts everything in compose.yaml.
  • -d (detached) runs MySQL in the background and gives your terminal back.
  • --wait waits until the health check says healthy before returning.

The first time, Docker downloads the image, so it takes a minute or two (50 seconds for me). The output, with the download lines shortened:

Output
Image mysql:9.7 Pulling
…
Image mysql:9.7 Pulled
Network dewiride-inventory_default Creating
Volume dewiride-inventory_mysql-data Creating
Volume dewiride-inventory_mysql-data Creating
Network dewiride-inventory_default Creating
Volume dewiride-inventory_mysql-data Created
Volume dewiride-inventory_mysql-data Created
Network dewiride-inventory_default Created
Network dewiride-inventory_default Created
Container dewiride-inventory-mysql-1 Creating
Container dewiride-inventory-mysql-1 Created
Container dewiride-inventory-mysql-1 Starting
Container dewiride-inventory-mysql-1 Started
Container dewiride-inventory-mysql-1 Waiting
Container dewiride-inventory-mysql-1 Healthy

Compose created a network, the dewiride-inventory_mysql-data volume and the container, then waited until it was Healthy. (In a normal terminal you see a live progress view instead of these lines. It finishes with the same result.)

Step 6: Check That It's Healthy​

Terminal
docker compose ps
Output
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
dewiride-inventory-mysql-1 mysql:9.7 "docker-entrypoint.s…" mysql 26 seconds ago Up 25 seconds (healthy) 127.0.0.1:3306->3306/tcp

A terminal where docker compose up -d --wait pulls mysql:9.7, creates the network, volume and container and reports Healthy, followed by docker compose ps showing dewiride-inventory-mysql-1 Up (healthy) on 127.0.0.1:3306.

The two things to check are (healthy) and 127.0.0.1:3306->3306/tcp. MySQL is running, and only your own computer can reach it on port 3306.

Step 7: Read the Start-up Log​

Terminal
docker compose logs mysql

The first start prints about 50 lines. These are the ones that tell the story (the rest are shortened to …):

Output (first start, shortened)
mysql-1 | 2026-11-02 14:48:46+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 9.7.4-1.el9 started.
…
mysql-1 | 2026-11-02 14:48:46+00:00 [Note] [Entrypoint]: Initializing database files
…
mysql-1 | 2026-11-02T14:48:47.663552Z 6 [Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.
…
mysql-1 | 2026-11-02T14:48:50.317659Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
…
mysql-1 | 2026-11-02T14:48:50.330314Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '9.7.4' socket: '/var/run/mysqld/mysqld.sock' port: 0 MySQL Community Server - GPL.
mysql-1 | 2026-11-02 14:48:50+00:00 [Note] [Entrypoint]: Temporary server started.
…
mysql-1 | 2026-11-02 14:48:51+00:00 [Note] [Entrypoint]: Creating database inventory
mysql-1 | 2026-11-02 14:48:51+00:00 [Note] [Entrypoint]: Creating user inventory_app
mysql-1 | 2026-11-02 14:48:51+00:00 [Note] [Entrypoint]: Giving user inventory_app access to schema inventory
…
mysql-1 | 2026-11-02 14:48:53+00:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up.
…
mysql-1 | 2026-11-02T14:48:54.140715Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '9.7.4' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL.

Read from top to bottom:

  1. The image initializes an empty data folder, then starts a temporary server with port: 0, which means no network at all. That's why the health check pings over TCP.
  2. It creates the inventory database and the inventory_app account, then stops the temporary server.
  3. The real server starts, and ready for connections. Version: '9.7.4' … port: 3306 means MySQL is ready.

The two warnings look alarming but are expected:

  • root@localhost is created with an empty password: the image's start-up script initializes MySQL with --initialize-insecure and sets your MYSQL_ROOT_PASSWORD immediately afterwards, before the real server starts.
  • CA certificate ca.pem is self signed: MySQL generated its own TLS certificates, so connections can be encrypted from the start. How to Check if a MySQL Connection Uses TLS shows the result.

On every later start, the log skips straight to ready for connections. The data is already there.

Step 8: Connect to It​

MySQL is running. To run SQL against it, use the mysql client that's already inside the container: How to Connect to MySQL in a Docker Container with the mysql Client.

When you're done for the day, stop it with docker compose stop. How to Stop, Start and Remove Docker Compose Containers Without Losing Data explains which commands keep your data and which one deletes it.

Common Mistakes​

MistakeWhat happensDo this instead
ports: - "3306:3306" without 127.0.0.1MySQL accepts connections from your whole networkUse "127.0.0.1:3306:3306"
No volume for /var/lib/mysqlRemoving the container deletes the dataMap a named volume, as in Step 2
image: mysql or mysql:latestYou get whatever is newest, including a new Innovation release every quarterPin the line you want, such as mysql:9.7
A health check that pings over the socketThe container can turn healthy while the temporary set-up server is still runningPing with --host=127.0.0.1
Running docker compose up before creating .envCompose stops: required variable MYSQL_PASSWORD is missing a valueCreate .env first

Troubleshooting​

SymptomLikely causeFix
Cannot connect to the Docker daemonDocker Desktop isn't runningStart Docker Desktop and wait until it says it's running
docker compose up reports that port 3306 is already in useAnother MySQL, or another container, is using port 3306Stop the other one, or publish a different port, such as "127.0.0.1:3307:3306"
Stuck at Waiting, then unhealthyMySQL failed to start; often a broken data volume from an earlier experimentRead docker compose logs mysql for the [ERROR] line
Signing in with a new password gives ERROR 1045The password changed in .env after the first startSee the ERROR 1045 fix

FAQ​

Does this work on Windows and on Apple Silicon Macs?​

Yes. Docker Desktop runs the same commands on Windows, macOS and Linux, and the official mysql:9.7 image is published for both amd64 (Intel and AMD) and arm64 (Apple Silicon) processors.

Where exactly is my data stored?​

In a Docker volume named dewiride-inventory_mysql-data, which Docker manages for you. docker volume ls lists it. It stays until you remove it on purpose.

Can other computers connect to this MySQL?​

No. The port is published on 127.0.0.1, which only your own computer can reach. That's what you want for a development database.

Should I use the latest Innovation release instead?​

For learning and development, either works. For anything you'll keep running, the LTS line gets bug and security fixes for years without feature changes. That's why this guide pins mysql:9.7.

Conclusion​

One compose.yaml runs the official MySQL 9.7 LTS image with:

  • an inventory database and an app account;
  • passwords kept in .env;
  • a port only your computer can reach;
  • data that survives in a named volume;
  • a health check that only passes once the real server is ready.

docker compose up -d --wait starts it, and docker compose ps confirms it's healthy.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.