How to Use MySQL Database with Docker

How to Use MySQL Database with Docker

I resisted Docker for database work longer than I should have. My reasoning was that databases are stateful, containers are supposed to be disposable, and mixing the two felt risky. What changed my mind was spinning up a throwaway MySQL instance for a client demo in under thirty seconds, tearing it down, and doing it again with a different dataset five minutes later — no local install, no leftover config files, no version conflicts with other projects on my machine. That convenience is real, and once you understand how to handle persistence properly, it’s safe for a lot more than demos.

This guide covers running MySQL in Docker the right way, from a single throwaway container to a properly configured, production-lean setup.

Why Containerize MySQL

Architecture: Container vs. Persistent Data

graph TD
    A[Docker Host] --> B[MySQL Container - mysqld process]
    B --> C[Named Volume - /var/lib/mysql data]
    A --> D[Docker Network - app_network]
    E[Application Container] --> D
    B --> D
    F[Docker Host Port 3306] -.exposed to.-> B

The key concept: the MySQL process lives inside the container and can be destroyed and recreated freely, but the data should live in a Docker volume, which persists independently of the container’s lifecycle.

Running a Basic MySQL Container

docker run --name mysql-dev \
  -e MYSQL_ROOT_PASSWORD=StrongRootPass123! \
  -e MYSQL_DATABASE=myapp \
  -e MYSQL_USER=appuser \
  -e MYSQL_PASSWORD=AppUserPass123! \
  -p 3306:3306 \
  -d mysql:8.0

Check it’s running:

docker ps
docker logs mysql-dev

You should see something like:

[Entrypoint] MySQL init process done. Ready for start up.
[Server] /usr/sbin/mysqld: ready for connections.

Connect to it:

docker exec -it mysql-dev mysql -u appuser -p myapp

Persisting Data with Volumes

Without a volume, all your data disappears the moment you docker rm the container. Fix that with a named volume:

docker volume create mysql_data

docker run --name mysql-dev \
  -e MYSQL_ROOT_PASSWORD=StrongRootPass123! \
  -e MYSQL_DATABASE=myapp \
  -v mysql_data:/var/lib/mysql \
  -p 3306:3306 \
  -d mysql:8.0

Now you can safely docker stop mysql-dev && docker rm mysql-dev and recreate the container pointing at the same volume — your tables and rows are exactly as you left them.

Using Docker Compose for a Full Stack

For anything beyond a quick test, I use Compose so the database and application are defined together.

# docker-compose.yml
version: "3.9"

services:
  db:
    image: mysql:8.0
    container_name: mysql-app
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: StrongRootPass123!
      MYSQL_DATABASE: myapp
      MYSQL_USER: appuser
      MYSQL_PASSWORD: AppUserPass123!
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./init:/docker-entrypoint-initdb.d
    networks:
      - app_network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    build: .
    depends_on:
      db:
        condition: service_healthy
    environment:
      DB_HOST: db
      DB_USER: appuser
      DB_PASSWORD: AppUserPass123!
    networks:
      - app_network

volumes:
  mysql_data:

networks:
  app_network:

Bring it up:

docker compose up -d

The ./init folder mounted to /docker-entrypoint-initdb.d is a genuinely useful feature — any .sql or .sh file in there runs automatically the first time the container initializes an empty data directory, which is perfect for seeding schema and test data.

-- init/01-schema.sql
CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Custom Configuration

To tune MySQL inside the container (InnoDB buffer pool, character set, etc.), mount a config file instead of trying to edit the running container:

# my-custom.cnf
[mysqld]
innodb_buffer_pool_size = 512M
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
max_connections = 200
services:
  db:
    image: mysql:8.0
    volumes:
      - ./my-custom.cnf:/etc/mysql/conf.d/custom.cnf
      - mysql_data:/var/lib/mysql

Backing Up and Restoring a Containerized Database

# Backup
docker exec mysql-app mysqldump -u root -p"StrongRootPass123!" myapp > backup.sql

# Restore into a fresh container
docker exec -i mysql-app mysql -u root -p"StrongRootPass123!" myapp < backup.sql

For a full volume-level backup instead of a logical dump:

docker run --rm -v mysql_data:/volume -v $(pwd):/backup alpine \
  tar czf /backup/mysql_data_backup.tar.gz -C /volume .

Connecting an Application from Another Container

Inside a Compose network, containers reach each other by service name, not localhost:

# Python example
import mysql.connector

conn = mysql.connector.connect(
    host="db",          # service name from docker-compose.yml
    user="appuser",
    password="AppUserPass123!",
    database="myapp"
)

This is one of the most common beginner mistakes — trying to connect to localhost or 127.0.0.1 from inside another container, which refers to that container itself, not the database container.

Real-World Scenario: Disposable Test Databases in CI

In CI pipelines, I spin up a fresh MySQL container per test run, seed it, run the test suite, then discard it entirely:

# GitHub Actions example
services:
  mysql:
    image: mysql:8.0
    env:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: test_db
    ports:
      - 3306:3306
    options: >-
      --health-cmd="mysqladmin ping"
      --health-interval=10s
      --health-timeout=5s
      --health-retries=5

Every test run starts from a known-clean state — no test pollution from a previous run’s leftover data.

Performance Considerations for Containerized MySQL

Security Considerations

Troubleshooting Common Issues

Container exits immediately after starting. Check docker logs <container> — usually a misconfigured environment variable or a corrupted volume from a previous failed initialization.

“Access denied for user” after changing MYSQL_ROOT_PASSWORD. The environment variables only take effect on first initialization of an empty data directory. If the volume already has data, changing the env var does nothing — you need to change the password inside MySQL directly, or wipe the volume for a fresh start.

Data disappears after docker-compose down. By default, docker-compose down doesn’t remove volumes, but docker-compose down -v does — be careful with that flag in scripts.

Slow performance on Windows/macOS. This is usually related to the Docker Desktop virtualization layer for bind-mounted volumes; switching to named volumes typically resolves it.

Frequently Asked Questions

Is Docker suitable for production MySQL? Yes, many teams run production MySQL in containers, but it requires proper persistent volume management, resource limits, backups, and typically orchestration (Kubernetes, ECS, etc.) rather than a single ad-hoc docker run.

Which MySQL image should I use? The official mysql image on Docker Hub is actively maintained and the standard choice; specify a version tag like mysql:8.0 rather than latest.

How do I upgrade MySQL versions in a container? Stop the container, back up the volume, start a new container with the new image version pointed at the same volume, and let MySQL’s upgrade process run — always test this in a non-production environment first.

Can multiple containers share one MySQL data volume? No — only one mysqld process should write to a given data directory at a time. Use one MySQL container per data volume, and connect multiple application containers to that one database container over the network instead.

Interview Questions

  1. Why should MySQL data be stored in a Docker volume rather than the container’s writable layer?
  2. What’s the difference between a bind mount and a named volume, and when would you choose each for a database?
  3. How does docker-entrypoint-initdb.d work, and when does it actually execute?
  4. Why can’t a container reach a MySQL container using localhost?
  5. What are the risks of running production databases in containers, and how do orchestrators like Kubernetes address persistence?
  6. How would you safely upgrade the MySQL version of a running containerized database?

Summary and Key Takeaways

Docker didn’t replace my understanding of MySQL internals — if anything, it forced me to be more explicit about configuration, memory limits, and persistence than I ever was running MySQL directly on a host. That explicitness is a good habit either way.

References

Exit mobile version