Contents

Run MySQL in Docker: Setup, Connections, Backup and Restore

Start it with one command

First create a folder mysql-demo. Its initdb/ folder holds two init SQL files (explained below):

mysql-demo/
└── initdb/
    ├── 01-schema.sql
    └── 02-seed.sql

initdb/01-schema.sql:

CREATE TABLE products (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  price INT NOT NULL
);

initdb/02-seed.sql:

INSERT INTO products (name, price) VALUES ('珍珠奶茶', 65), ('鳳梨酥', 45), ('牛肉麵', 180);

Start MySQL from the mysql-demo folder:

docker run -d --name mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  -e MYSQL_DATABASE=app \
  -e MYSQL_USER=app \
  -e MYSQL_PASSWORD=apppass \
  -p 127.0.0.1:3306:3306 \
  -v mysql_data:/var/lib/mysql \
  -v "$(pwd)/initdb:/docker-entrypoint-initdb.d:ro" \
  --restart unless-stopped \
  mysql:8.4
docker run -d --name mysql `
  -e MYSQL_ROOT_PASSWORD=secret `
  -e MYSQL_DATABASE=app `
  -e MYSQL_USER=app `
  -e MYSQL_PASSWORD=apppass `
  -p 127.0.0.1:3306:3306 `
  -v mysql_data:/var/lib/mysql `
  -v "${PWD}/initdb:/docker-entrypoint-initdb.d:ro" `
  --restart unless-stopped `
  mysql:8.4
docker run -d --name mysql ^
  -e MYSQL_ROOT_PASSWORD=secret ^
  -e MYSQL_DATABASE=app ^
  -e MYSQL_USER=app ^
  -e MYSQL_PASSWORD=apppass ^
  -p 127.0.0.1:3306:3306 ^
  -v mysql_data:/var/lib/mysql ^
  -v "%cd%/initdb:/docker-entrypoint-initdb.d:ro" ^
  --restart unless-stopped ^
  mysql:8.4
$ docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=app -e MYSQL_USER=app -e MYSQL_PASSWORD=apppass -p 127.0.0.1:3306:3306 -v mysql_data:/var/lib/mysql -v "$(pwd)/initdb:/docker-entrypoint-initdb.d:ro" --restart unless-stopped mysql:8.4
Unable to find image 'mysql:8.4' locally
8.4: Pulling from library/mysql
e896da32f635: Pull complete
3527249a8025: Pull complete
6b54d15c954d: Pull complete
15ba404f5528: Pull complete
ad73434ffdad: Pull complete
bf11ec3e7e56: Pull complete
81077ec0e5ac: Pull complete
f51a9a1f0d20: Pull complete
4ba189089539: Pull complete
d4cc7515d7b7: Pull complete
Digest: sha256:24989179f58856db763f418010c1169fecc944e3ec1ea7e921572d83eb070f70
Status: Downloaded newer image for mysql:8.4
d92a7561ad58f958488c606bb07753aeffcc06639878a4c87ed676f6e1f99389

Windows versions (click a tab to switch):

docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=app -e MYSQL_USER=app -e MYSQL_PASSWORD=apppass -p 127.0.0.1:3306:3306 -v mysql_data:/var/lib/mysql -v "$(pwd)/initdb:/docker-entrypoint-initdb.d:ro" --restart unless-stopped mysql:8.4
docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=app -e MYSQL_USER=app -e MYSQL_PASSWORD=apppass -p 127.0.0.1:3306:3306 -v mysql_data:/var/lib/mysql -v "${PWD}/initdb:/docker-entrypoint-initdb.d:ro" --restart unless-stopped mysql:8.4
docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=app -e MYSQL_USER=app -e MYSQL_PASSWORD=apppass -p 127.0.0.1:3306:3306 -v mysql_data:/var/lib/mysql -v "%cd%/initdb:/docker-entrypoint-initdb.d:ro" --restart unless-stopped mysql:8.4
OptionDescription
MYSQL_ROOT_PASSWORDroot password (required, or use MYSQL_RANDOM_ROOT_PASSWORD=yes to generate a random one)
MYSQL_DATABASEDatabase created on first start
MYSQL_USER / MYSQL_PASSWORDCreates a regular user with full privileges on MYSQL_DATABASE
-p 127.0.0.1:3306:3306Only reachable from this machine, not from outside
-v mysql_data:/var/lib/mysqlData is stored in a volume named mysql_data
-v .../initdb:/docker-entrypoint-initdb.d:roSQL to run during initialization

What happens on the first start

$ docker logs mysql 2>&1 | grep -E "Entrypoint|initdb|ready for connections"
2026-10-13 00:46:59+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.4.11-1.el9 started.
2026-10-13 00:46:59+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
2026-10-13 00:46:59+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.4.11-1.el9 started.
2026-10-13 00:46:59+00:00 [Note] [Entrypoint]: Initializing database files
2026-10-13 00:47:02+00:00 [Note] [Entrypoint]: Database files initialized
2026-10-13 00:47:02+00:00 [Note] [Entrypoint]: Starting temporary server
2026-10-13T00:47:04.184489Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: /var/run/mysqld/mysqlx.sock
2026-10-13T00:47:04.185012Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.4.11'  socket: '/var/run/mysqld/mysqld.sock'  port: 0  MySQL Community Server - GPL.
2026-10-13 00:47:04+00:00 [Note] [Entrypoint]: Temporary server started.
2026-10-13 00:47:06+00:00 [Note] [Entrypoint]: Creating database app
2026-10-13 00:47:06+00:00 [Note] [Entrypoint]: Creating user app
2026-10-13 00:47:06+00:00 [Note] [Entrypoint]: Giving user app access to schema app
2026-10-13 00:47:06+00:00 [Note] [Entrypoint]: /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/01-schema.sql
2026-10-13 00:47:06+00:00 [Note] [Entrypoint]: /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/02-seed.sql
2026-10-13 00:47:06+00:00 [Note] [Entrypoint]: Stopping temporary server
2026-10-13 00:47:07+00:00 [Note] [Entrypoint]: Temporary server stopped
2026-10-13 00:47:07+00:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up.
2026-10-13T00:47:08.596982Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock
2026-10-13T00:47:08.597330Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.4.11'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.

The log shows that on the first start (when the data folder is empty) the image's startup script:

  1. Creates the database files (Initializing database files).
  2. Starts a temporary server (Starting temporary server). Note its port: 0: the temporary server accepts no network connections, only socket connections.
  3. Creates the database app and the user app, and grants privileges.
  4. Runs 01-schema.sql and 02-seed.sql from /docker-entrypoint-initdb.d/ in file name order.
  5. Stops the temporary server and starts with the real configuration. Only the last line, with port: 3306, means you can actually connect.

The whole process takes about 10 seconds. /docker-entrypoint-initdb.d/ accepts .sql, .sql.gz and .sh files, and they run only on the first initialization; restarting the container later does not run them again.

Choosing a version

tagDescription
mysql:8.4Long-term support (LTS) release, recommended for production
mysql:9Innovation release: newer features, shorter support period
mariadb:11MariaDB, a highly compatible fork of MySQL; environment variables start with MARIADB_

Connecting

From inside the container

The container already includes the mysql client. For interactive use, run docker exec -it mysql mysql -uapp -p, enter the password and you get the mysql> prompt. You can also run SQL directly with -e:

$ docker exec mysql mysql -uapp -papppass app -t -e "SELECT * FROM products;"
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+--------------+-------+
| id | name         | price |
+----+--------------+-------+
|  1 | 珍珠奶茶 |    65 |
|  2 | 鳳梨酥    |    45 |
|  3 | 牛肉麵    |   180 |
+----+--------------+-------+

$ docker exec mysql mysql -uroot -psecret -t -e "SELECT VERSION(), @@character_set_server, @@collation_server;"
mysql: [Warning] Using a password on the command line interface can be insecure.
+-----------+------------------------+--------------------+
| VERSION() | @@character_set_server | @@collation_server |
+-----------+------------------------+--------------------+
| 8.4.11    | utf8mb4                | utf8mb4_0900_ai_ci |
+-----------+------------------------+--------------------+
  • -t prints a table (in non-interactive mode the default is tab-separated output).
  • The Chinese data is stored and read correctly: the server character set is utf8mb4 and the collation is utf8mb4_0900_ai_ci. These are MySQL 8 defaults, so no extra setup is needed. The table borders on the Chinese rows don't line up because of how the mysql client counts the width of CJK characters; the data itself is fine.
  • The [Warning] Using a password on the command line interface can be insecure. that appears every time warns that a password in a command can be seen by others in the process list. You can ignore it in demos.

From the host: localhost vs 127.0.0.1

On the host, connect any MySQL client (MySQL Workbench, DBeaver, the mysql command) to 127.0.0.1:3306. Write 127.0.0.1, not localhost. Here a container with --network host stands in for a mysql client on the host:

$ docker run --rm --network host mysql:8.4 mysql -h localhost -uapp -papppass -e "SELECT 1"
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)

$ docker run --rm --network host mysql:8.4 mysql -h 127.0.0.1 -uapp -papppass -e "SELECT USER(), CURRENT_USER()"
mysql: [Warning] Using a password on the command line interface can be insecure.
USER()	CURRENT_USER()
app@172.18.0.1	app@%
  • -h localhost: fails with Can't connect to local MySQL server through socket. When the MySQL client sees localhost, it connects through a Unix socket file instead of TCP, but there is no such socket file on the host (it is inside the container).
  • -h 127.0.0.1: works, using TCP through the port published with -p.
  • USER() shows app@172.18.0.1: from MySQL's point of view, the connection comes from the Docker network's gateway IP, not from 127.0.0.1. app@% in CURRENT_USER() means this account may connect from any host.

From other containers

Put MySQL and your application on the same user-defined network and they can connect by container name (the default bridge network has no name resolution; see Networking and container communication):

$ docker network create appnet
595dfaad48e7460c5996b90a62402e50e31fb790a1f04b3cdc67ac12be482db6

$ docker network connect appnet mysql

$ docker run --rm --network appnet mysql:8.4 mysql -h mysql -uapp -papppass app -e "SELECT COUNT(*) FROM products"
mysql: [Warning] Using a password on the command line interface can be insecure.
COUNT(*)
3

The application's connection string:

mysql://app:apppass@mysql:3306/app

The host name is the container name mysql (in Compose, the service name), and the port is the container's own 3306. Containers talking to each other don't need -p.

Managing it with Compose

For real projects, use Compose. Add compose.yaml, .env and a custom config file my.cnf to mysql-demo:

mysql-demo/
├── compose.yaml
├── .env
├── my.cnf
└── initdb/
    ├── 01-schema.sql
    └── 02-seed.sql

.env:

MYSQL_ROOT_PASSWORD=change-this-root-password
MYSQL_PASSWORD=change-this-app-password

my.cnf:

[mysqld]
max_connections = 300
innodb_buffer_pool_size = 256M

compose.yaml:

services:
  db:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: app
      MYSQL_USER: app
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      TZ: Asia/Taipei
    ports:
      - "127.0.0.1:3306:3306"
    volumes:
      - db_data:/var/lib/mysql
      - ./initdb:/docker-entrypoint-initdb.d:ro
      - ./my.cnf:/etc/mysql/conf.d/custom.cnf:ro
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" --silent"]
      interval: 10s
      retries: 10
      start_period: 30s
    restart: unless-stopped

volumes:
  db_data:
SettingDescription
TZ: Asia/TaipeiSets the container's time zone; MySQL's NOW() uses it
./my.cnf:/etc/mysql/conf.d/custom.cnfMySQL reads the .cnf files in /etc/mysql/conf.d/
$$MYSQL_ROOT_PASSWORD$$ stops Compose from substituting the variable itself, so the shell inside the container reads the container's environment variable
-h 127.0.0.1Important, see below
$ docker compose up -d --wait
[+] up 3/3
 ✔ Network mysql-demo_default Created                                                                                                             0.0s
 ✔ Volume mysql-demo_db_data  Created                                                                                                             0.0s
 ✔ Container mysql-demo-db-1  Healthy                                                                                                            10.9s

$ docker compose exec db sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SELECT 1"'
mysql: [Warning] Using a password on the command line interface can be insecure.
1
1

$ docker compose ps
NAME              IMAGE       COMMAND                  SERVICE   CREATED          STATUS                    PORTS
mysql-demo-db-1   mysql:8.4   "docker-entrypoint.s…"   db        12 seconds ago   Up 11 seconds (healthy)   127.0.0.1:3306->3306/tcp, 33060/tcp

$ docker compose exec db sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -t -e "SELECT @@max_connections, @@innodb_buffer_pool_size DIV 1048576 AS buffer_pool_mb, @@system_time_zone, NOW();"'
mysql: [Warning] Using a password on the command line interface can be insecure.
+-------------------+----------------+--------------------+---------------------+
| @@max_connections | buffer_pool_mb | @@system_time_zone | NOW()               |
+-------------------+----------------+--------------------+---------------------+
|               300 |            256 | CST                | 2026-10-13 08:49:41 |
+-------------------+----------------+--------------------+---------------------+

$ docker compose exec db sh -c 'mysql -uapp -p"$MYSQL_PASSWORD" app -t -e "SELECT * FROM products"'
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+--------------+-------+
| id | name         | price |
+----+--------------+-------+
|  1 | 珍珠奶茶 |    65 |
|  2 | 鳳梨酥    |    45 |
|  3 | 牛肉麵    |   180 |
+----+--------------+-------+
  • --wait returns once the service is healthy. Healthy took about 11 seconds, matching the initialization time we saw earlier.
  • max_connections is 300 and buffer_pool_mb is 256, so my.cnf took effect.
  • @@system_time_zone is CST (the Asia/Taipei abbreviation, UTC+8), and NOW() shows Taipei time.
  • The init SQL ran as well: products has three rows.

The health check trap: localhost reports healthy too early

A MySQL health check you often see online is mysqladmin ping -h localhost. Here is a test with the 127.0.0.1 above changed to localhost:

$ docker compose up -d --wait
[+] up 3/3
 ✔ Network mysql-demo_default Created                                                                                                             0.0s
 ✔ Volume mysql-demo_db_data  Created                                                                                                             0.0s
 ✔ Container mysql-demo-db-1  Healthy                                                                                                             5.8s

$ docker compose exec db sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SELECT 1"'
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)

$ docker compose logs db 2>&1 | grep -E "Temporary server|init process done|port: 3306"
db-1  | 2026-10-13 08:49:02+08:00 [Note] [Entrypoint]: Temporary server started.

$ docker compose exec db sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SELECT 1"'
mysql: [Warning] Using a password on the command line interface can be insecure.
1
1
  • It reports Healthy after only 5.8 seconds, but connecting as root right afterwards fails, and the log shows the server is still at Temporary server started.
  • Trying again 10 seconds later works.

The reason: the temporary server during initialization only accepts socket connections, and -h localhost happens to use the socket, so the ping gets an answer. And mysqladmin ping succeeds as soon as the server responds, even if the account can't log in yet. Compose then thinks the database is ready and starts the services that depend on it (depends_on with condition: service_healthy), so the application's first connection may fail.

With -h 127.0.0.1 the check uses TCP. The temporary server accepts no network connections (port: 0), so the check only reports healthy once the real server is running.

Backup and restore

Export SQL with mysqldump, deliberately drop the table, then restore from the backup:

$ docker exec mysql sh -c 'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction app' > app.sql
mysqldump: [Warning] Using a password on the command line interface can be insecure.

$ ls -lh app.sql && grep -c "INSERT INTO" app.sql
-rw-r--r--    1 root     root        2.0K Oct 13 00:47 app.sql
1

$ docker exec mysql mysql -uroot -psecret -e "DROP TABLE app.products"
mysql: [Warning] Using a password on the command line interface can be insecure.

$ docker exec mysql mysql -uapp -papppass app -e "SELECT COUNT(*) FROM products"
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 1146 (42S02) at line 1: Table 'app.products' doesn't exist

$ docker exec -i mysql sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" app' < app.sql
mysql: [Warning] Using a password on the command line interface can be insecure.

$ docker exec mysql mysql -uapp -papppass app -t -e "SELECT * FROM products"
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+--------------+-------+
| id | name         | price |
+----+--------------+-------+
|  1 | 珍珠奶茶 |    65 |
|  2 | 鳳梨酥    |    45 |
|  3 | 牛肉麵    |   180 |
+----+--------------+-------+
  • The backup command is wrapped in sh -c '...', so $MYSQL_ROOT_PASSWORD is expanded inside the container and the password never appears in the host's command history. The outer single quotes stop the host's shell from substituting the variable first.
  • --single-transaction exports InnoDB tables from a consistent snapshot without locking them during the backup. Always use it in production.
  • The backup file app.sql is 2.0K and contains 1 INSERT INTO statement (all three rows are in one INSERT).
  • After DROP TABLE, the query returns Table 'app.products' doesn't exist.
  • The restore uses docker exec -i to pass app.sql from the host to mysql in the container through standard input, and all the data comes back.

For restores use -i only, not -it. When standard input is a file instead of a terminal, -t fails right away:

$ docker exec -it mysql sh -c "echo hi" < /dev/null
cannot attach stdin to a TTY-enabled container because stdin is not a terminal

Other common backup commands:

# Back up all databases (including users and privileges)
docker exec mysql sh -c 'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases --single-transaction' > all.sql

# Back up and compress
docker exec mysql sh -c 'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction app' | gzip > app.sql.gz

# Restore a compressed backup
gunzip < app.sql.gz | docker exec -i mysql sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" app'

Backup and restore for Windows users

PowerShell doesn't support <, and > in Windows PowerShell 5.1 saves files as UTF-16, which breaks the import. Instead, create the file inside the container and copy it with docker cp. This works on every system:

# Backup: export to /tmp/app.sql inside the container, then copy it out
docker exec mysql sh -c 'mysqldump -uroot -p$MYSQL_ROOT_PASSWORD --single-transaction --result-file=/tmp/app.sql app'
docker cp mysql:/tmp/app.sql app.sql

# Restore: copy the file in, then import it inside the container
docker cp app.sql mysql:/tmp/restore.sql
docker exec mysql sh -c 'mysql -uroot -p$MYSQL_ROOT_PASSWORD app < /tmp/restore.sql'
# Backup: export to /tmp/app.sql inside the container, then copy it out
docker exec mysql sh -c 'mysqldump -uroot -p$MYSQL_ROOT_PASSWORD --single-transaction --result-file=/tmp/app.sql app'
docker cp mysql:/tmp/app.sql app.sql

# Restore: copy the file in, then import it inside the container
docker cp app.sql mysql:/tmp/restore.sql
docker exec mysql sh -c 'mysql -uroot -p$MYSQL_ROOT_PASSWORD app < /tmp/restore.sql'
REM Backup: export to /tmp/app.sql inside the container, then copy it out
docker exec mysql sh -c "mysqldump -uroot -p$MYSQL_ROOT_PASSWORD --single-transaction --result-file=/tmp/app.sql app"
docker cp mysql:/tmp/app.sql app.sql

REM Restore: copy the file in, then import it inside the container
docker cp app.sql mysql:/tmp/restore.sql
docker exec mysql sh -c "mysql -uroot -p$MYSQL_ROOT_PASSWORD app < /tmp/restore.sql"
  • --result-file makes mysqldump write the file directly, without terminal redirection.
  • The < is inside the sh -c string, so the shell inside the container handles it, and PowerShell can run the command too.
  • -p$MYSQL_ROOT_PASSWORD has no quotes, avoiding PowerShell 5.1's problems with embedded quotes (don't use spaces in the password).

These commands were tested on Docker 29, and all 3 rows were restored:

$ docker exec mysql sh -c 'mysql -uroot -p$MYSQL_ROOT_PASSWORD app -e "CREATE TABLE t (id INT); INSERT INTO t VALUES (1),(2),(3);"'
mysql: [Warning] Using a password on the command line interface can be insecure.

$ docker exec mysql sh -c 'mysqldump -uroot -p$MYSQL_ROOT_PASSWORD --single-transaction --result-file=/tmp/app.sql app'
mysqldump: [Warning] Using a password on the command line interface can be insecure.

$ docker cp mysql:/tmp/app.sql app.sql

$ ls -l app.sql
-rw-rw-rw-    1 root     root          1788 Oct 14 09:48 app.sql

$ docker exec mysql sh -c 'mysql -uroot -p$MYSQL_ROOT_PASSWORD app -e "DROP TABLE t"'
mysql: [Warning] Using a password on the command line interface can be insecure.

$ docker cp app.sql mysql:/tmp/restore.sql

$ docker exec mysql sh -c 'mysql -uroot -p$MYSQL_ROOT_PASSWORD app < /tmp/restore.sql'
mysql: [Warning] Using a password on the command line interface can be insecure.

$ docker exec mysql sh -c 'mysql -uroot -p$MYSQL_ROOT_PASSWORD app -e "SELECT COUNT(*) FROM t"'
mysql: [Warning] Using a password on the command line interface can be insecure.
COUNT(*)
3

Tested: changing the password variable has no effect

MYSQL_ROOT_PASSWORD and the other variables are only used on the first initialization (when the volume is empty). Remove the container, keep the volume, and create the container again with a different password:

$ docker rm -f mysql
mysql

$ docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=different -v mysql_data:/var/lib/mysql mysql:8.4
2a0f52d379b574033ae1571ab6873f37e8ab79ccd3830f6feac629696b74ae42

$ docker logs mysql 2>&1 | grep -c "Initializing database"
0

$ docker exec mysql mysql -uroot -pdifferent -e "SELECT 1"
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)

$ docker exec mysql mysql -uroot -psecret -e "SELECT 1"
mysql: [Warning] Using a password on the command line interface can be insecure.
1
1
  • Initializing database appears 0 times in the log: the volume already has data, so the startup script skipped initialization.
  • The new password different can't log in; the old password secret still works.

In other words, once data exists, changing MYSQL_ROOT_PASSWORD does nothing. To change the password, log in with the old one and run SQL:

ALTER USER 'root'@'%' IDENTIFIED BY 'new-password';
ALTER USER 'root'@'localhost' IDENTIFIED BY 'new-password';

Or, if you don't need the data, delete the volume and initialize again (docker compose down -v, all data is lost).

Troubleshooting

Access denied for user 'root'@'...'

$ docker exec mysql mysql -uroot -pwrong -e "SELECT 1"
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
  • Wrong password, or the volume was initialized with an old password (see the test above).
  • root has two accounts by default, root@% and root@localhost, so it can connect from any host. For users you create, make sure the host is %; otherwise they can only connect from specific hosts.

Can't connect to local MySQL server through socket

The client treats localhost as a socket connection; use 127.0.0.1 (see the test above).

Exits right after starting

Check the log: docker logs mysql. Common causes:

  • MYSQL_ROOT_PASSWORD is not set (and neither is MYSQL_ALLOW_EMPTY_PASSWORD or MYSQL_RANDOM_ROOT_PASSWORD).
  • The volume contains data initialized by another version (for example 5.7 or MariaDB) that is incompatible with the current one.
  • The host is out of memory.

Upgrading from 5.7 / 8.0

Don't mount the old volume directly into the new image. Export with mysqldump first, import into an empty database on the new version, and verify in a test environment first.

Recap

  • Put init SQL in /docker-entrypoint-initdb.d/; it only runs on the first initialization.
  • Connect from the host with 127.0.0.1; localhost switches to the socket.
  • Write the health check as mysqladmin ping -h 127.0.0.1.
  • Back up with mysqldump --single-transaction, and actually test the restore.

Exercises

Exercise 1: List all databases in the MySQL container with one command.

Answer
docker exec mysql sh -c 'mysql -uroot -p$MYSQL_ROOT_PASSWORD -e "SHOW DATABASES"'

Exercise 2: The Compose MySQL service sets ports: "127.0.0.1:3306:3306". When an application in the same Compose project connects, what host name and port does it use?

Answer

The host name is the service name db, and the port is the container's own 3306. ports only affects connections from the host; containers don't need it to reach each other.

FAQ

Non-ASCII text shows up garbled?

MySQL 8 already defaults to utf8mb4. Garbled text usually comes from the client connection's character set or the encoding of the imported file. Backups saved with > in Windows PowerShell 5.1 are UTF-16; use the docker cp approach from this chapter.

Should I use MySQL or MariaDB?

They are highly compatible, and common apps such as WordPress support both. MariaDB's environment variables start with MARIADB_ instead.

I changed MYSQL_ROOT_PASSWORD but it has no effect?

The password variable is only used while the volume is empty. After that, change the password with SQL. See the test in this chapter.

Updated October 14, 2026