the Right Way
Containerized databases behave differently from classic database installations: init directories, volume persistence and health check synchronization are concepts that need to be understood before the first production container goes live. Anyone who skips these fundamentals ends up fighting data loss, race conditions and non-reproducible development environments later on.
Table of Contents
- 1. Containerized databases vs. classic installation
- 2. The init directory: automatic database initialization
- 3. Volume persistence: data that survives container restarts
- 4. Health check synchronization with dependent services
- 5. Configuring environment variables and credentials securely
- 6. Schema migrations in containerized environments
- 7. Backup and restore for containerized databases
- 8. Performance tuning for containerized databases
- 9. Initialization strategies compared
- 10. Summary
- 11. FAQ
1. Containerized databases vs. classic installation
The key difference between a containerized database and a classically installed database instance lies in the lifecycle. A traditional MySQL installation on a server runs for years, gets modified through package updates and carries a long historical state. A containerized database is ephemeral by design: the container image stays unchanged, all persistent data lives in a volume, and recreating the container from the same image should be possible at any time.
This model has significant advantages for development and deployment: every developer can spin up an identical database environment with a single docker compose up. Staging and production environments use the same image, which minimizes configuration drift. But it requires a shift in how initialization is approached: the schema, initial data, users and configuration must not be set up manually after the first start, they need to be defined declaratively and reproducibly so that containerized databases can be recreated at any point.
The most common mistakes when getting started with containerized databases are: writing data into the container instead of a volume, manually changing the schema after the first start, missing health checks, and race conditions caused by the application starting before the database is ready. These points are addressed systematically in the following sections.
2. The init directory: automatic database initialization
MySQL, PostgreSQL and MariaDB all provide an init directory that runs automatically the very first time the container starts. For MySQL it is /docker-entrypoint-initdb.d/. Files in this directory are executed in alphabetical order: SQL files as queries, shell scripts as bash scripts. This makes it possible to define the schema, initial data, users and permissions entirely declaratively, without any manual steps needed after the first container start.
Important: the init directory only runs if the data volume is still empty. On a container restart with an existing volume, the init directory is skipped. That is the desired behavior for containerized databases: initialization is idempotent for new environments, but it never overwrites existing production data. Schema changes after the initial setup are the job of migration tools, not the init directory.
# Directory structure for containerized database initialization
# Files are executed in alphabetical order on first start
docker-init/
├── 01-schema.sql # Create all tables and indexes
├── 02-users.sql # Create database users and grant permissions
├── 03-seed-data.sql # Insert required seed/reference data
└── 04-stored-procs.sh # Shell script to load stored procedures
# 01-schema.sql: Create tables on first init
CREATE DATABASE IF NOT EXISTS `shop` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `shop`;
CREATE TABLE IF NOT EXISTS `products` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`sku` VARCHAR(64) NOT NULL UNIQUE,
`name` VARCHAR(255) NOT NULL,
`price` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_sku` (`sku`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
# 02-users.sql: Least-privilege user setup
CREATE USER IF NOT EXISTS 'app'@'%' IDENTIFIED BY '${MYSQL_APP_PASSWORD}';
GRANT SELECT, INSERT, UPDATE, DELETE ON `shop`.* TO 'app'@'%';
FLUSH PRIVILEGES;
A practical pattern for containerized databases: keep the init scripts in the repository under docker/mysql/initdb.d/ and mount them into the container as a volume. This guarantees that every developer starting a new environment gets the same initial state. For larger projects such as Magento shops, a dump of the development database is a good starting point, complemented by anonymized test data for day-to-day development.
3. Volume persistence: data that survives container restarts
The volume is the most critical aspect of containerized databases. Without a volume, all data is lost when the container is removed. docker compose down without the -v flag removes the container but not the volume. With docker compose down -v the volume is deleted as well. Every operator working with containerized databases needs to know this distinction. In production, named volumes are recommended over bind mounts for database data, because named volumes are managed by the Docker daemon and do not depend on the host's directory structure.
For containerized databases in production: the volume directory on the host must be part of the backup strategy. Named volumes live under /var/lib/docker/volumes/. A consistent backup requires either locking the database or using tools such as mysqldump, pg_dump or Percona XtraBackup, which create a consistent snapshot without fully locking the database. The volume alone is not a backup.
4. Health check synchronization with dependent services
Race conditions between the database container and the application container are one of the most common problems with containerized databases. Docker Compose starts services according to the depends_on directive, but by default it only waits for the container to have started, not for the database to accept connections. A MySQL container can still be in initialization mode for several seconds after starting, during which no connections are possible.
The fix for containerized databases is depends_on combined with the condition: service_healthy option. Docker Compose then waits until the database container's health check reports a healthy status before starting the dependent service. This requires the database container to have a working health check configured. Alternatively, wait scripts such as wait-for-it.sh or dockerize can be run inside the application container to wait for the database connection to become available.
# docker-compose.yml: Health-synchronized startup for containerized databases
version: "3.9"
services:
mysql:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
MYSQL_DATABASE: shop
volumes:
# Named volume for data persistence across container recreations
- mysql_data:/var/lib/mysql
# Init scripts, executed only on empty volume (first start)
- ./docker/mysql/initdb.d:/docker-entrypoint-initdb.d:ro
healthcheck:
# mysqladmin ping confirms the server accepts connections
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--silent"]
interval: 10s
timeout: 5s
retries: 10
start_period: 60s
php:
image: mironsoft/magento-php:8.4-fpm
depends_on:
mysql:
# Wait until MySQL health check reports healthy
condition: service_healthy
redis:
condition: service_healthy
redis:
image: redis:7.4-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
retries: 5
volumes:
mysql_data:
# Named volume managed by Docker daemon
driver: local
5. Configuring environment variables and credentials securely
Passing database passwords in containerized databases through environment variables is the standard approach, but it is only secure if the values are not stored in plain text in docker-compose.yml. MySQL and PostgreSQL support the _FILE variant for sensitive values: MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password reads the password from a file instead of an environment variable. These files are provided as Docker secrets or through a read-only mount.
A commonly overlooked security gap in containerized databases: MySQL images create a root user without a host restriction by default when MYSQL_ROOT_PASSWORD is set. The application should always use a user with minimal privileges that only has access to the specific database. The root password is reserved exclusively for administrative tasks. This separation is part of the initialization scripts in /docker-entrypoint-initdb.d/.
6. Schema migrations in containerized environments
The init directory handles the initial state of containerized databases. Schema changes during ongoing operation require a dedicated migration tool. Flyway, Liquibase or an application's own migration system (Laravel migrations, Magento db_schema.xml, Django migrations) track the version state of the schema and apply only the changes that have not yet been applied. Ideally the migration tool runs as its own container that performs the migration and then exits.
The pattern for containerized databases: a migration container with depends_on: mysql: condition: service_healthy runs the migrations. The application container in turn depends on the migration container: depends_on: migrations: condition: service_completed_successfully. This guarantees that the schema is always up to date before the first application request is processed. This pattern works both for initial deployments and for updates that include schema changes.
7. Backup and restore for containerized databases
The backup pattern for containerized databases differs from classic database backups: instead of accessing the data directory directly, the backup command runs inside the running container. docker exec is the tool of choice here. A mysqldump can be written directly to a compressed file on the host without creating temporary files inside the container.
For production environments with containerized databases, a dedicated backup container as a cron job or Docker service is recommended, one that creates a daily dump, compresses it and transfers it to external storage. The volume directory itself can serve as a physical backup, but only if the database has either been stopped or the backup tool supports transactionally consistent snapshots.
#!/usr/bin/env bash
# backup-database.sh: Backup containerized MySQL database
set -euo pipefail
CONTAINER="project_mysql_1"
BACKUP_DIR="/var/backups/mysql"
DATE=$(date +%Y%m%d-%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/shop_${DATE}.sql.gz"
mkdir -p "$BACKUP_DIR"
# Run mysqldump inside the container, pipe output directly to gzip
docker exec "$CONTAINER" mysqldump \
--single-transaction \
--routines \
--triggers \
--databases shop \
| gzip -9 > "$BACKUP_FILE"
echo "[OK] Backup written to: $BACKUP_FILE"
echo "[OK] Size: $(du -sh "$BACKUP_FILE" | cut -f1)"
# Keep only last 7 daily backups
find "$BACKUP_DIR" -name "shop_*.sql.gz" -mtime +7 -delete
# Restore example:
# gunzip -c "$BACKUP_FILE" | docker exec -i "$CONTAINER" mysql -u root -p shop
8. Performance tuning for containerized databases
MySQL and PostgreSQL are optimized for classic server installations with dedicated RAM. In containerized databases, the database process runs in a container that shares RAM with other containers. The default MySQL configuration assumes a server with 128 MB of RAM, which is completely inadequate for a production shop with hundreds of connections. innodb_buffer_pool_size should be set to 60 to 70 percent of the memory reserved for the container, so for a container with a 4 GB limit that would be roughly 2.4 to 2.8 GB.
For containerized databases, a custom configuration file mounted into the container as a read-only volume is recommended. That keeps the configuration outside the image, version-controlled and adjustable per environment. Redis containers benefit from setting maxmemory together with an appropriate maxmemory-policy (for e-commerce usually allkeys-lru), which prevents Redis from consuming unlimited memory and losing data once it runs out.
9. Initialization strategies compared
There are several approaches to initializing containerized databases that differ in complexity, maintainability and suitability for different environments.
| Strategy | Use case | Advantage | Drawback |
|---|---|---|---|
| Init directory | Development, new deployments | Automatic, no intervention needed | Only on an empty volume |
| Migration container | Production, continuous updates | Versioned, can be rolled back | Additional infrastructure |
| Volume import | Data migration, staging refresh | Fast for large data volumes | Manual step |
| Custom image with pre-initialized volume | CI/CD, test environments | Ready to start immediately | Image size, data baked into image |
| Application-native migration (Magento, Django) | Application updates | Integrated into deploy process | Coupled to the application |
For most production projects with containerized databases, a combination is recommended: an init directory for the initial database state and a migration tool for all subsequent schema changes. The init directory ensures new environments can be created reproducibly. The migration tool manages the evolution of the schema during ongoing operation. Together, this combination covers the full lifecycle.
Mironsoft
Docker database setup, migration automation and backup strategies
Need your containerized databases set up for production?
We set up containerized databases with init scripts, health check synchronization, secure credentials and a complete backup strategy for your project, for Magento, Shopware and custom APIs alike.
Database setup
Init scripts, health checks and migration containers for reproducible environments
Backup automation
Daily mysqldump backups with retention policy and off-site transfer
Performance tuning
Optimizing the InnoDB buffer pool and Redis configuration for containerized workloads
10. Summary
Initializing containerized databases the right way means treating the database's entire lifecycle, from the first creation through schema updates to backups, as reproducible and automated. The init directory /docker-entrypoint-initdb.d/ sets the initial database state for new environments. Named volumes preserve data persistence across container restarts. Health checks with service_healthy conditions in depends_on prevent race conditions at startup.
Credentials belong in Docker secrets or _FILE variables, not in docker-compose.yml. Schema migrations are managed by dedicated migration containers within the deploy process. Backups are created via docker exec mysqldump and stored externally. Performance configuration comes from read-only mounted configuration files kept outside the image. Anyone who applies these points consistently ends up with a database infrastructure that is reliable and reproducible in both development and production.
Containerized Databases: The Essentials at a Glance
Init & persistence
/docker-entrypoint-initdb.d/ for the initial state. Named volumes for persistence. The init directory only runs on an empty volume.
Health check synchronization
depends_on: condition: service_healthy prevents race conditions. The database container needs a health check with a sufficient start_period.
Credentials & migrations
MYSQL_ROOT_PASSWORD_FILE instead of plain text. A migration container handles schema changes during ongoing operation.
Backup & tuning
docker exec mysqldump --single-transaction for consistent backups. Set the InnoDB buffer pool to 70% of the container's memory limit.