Initializing Containerized Databases the Right Way
AI generated
Docker · Databases · MySQL · PostgreSQL · DevOps
Initializing Containerized Databases
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.

15 min read Init scripts · Volumes · Health checks · Migrations MySQL 8.4 · PostgreSQL 17 · Redis 7.4

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.

11. FAQ: Initializing Containerized Databases the Right Way

1When does the init directory run?
Only on the first start with an empty volume. It is skipped on container restarts, preventing accidental overwrites of production data.
2Bind mount vs. named volume?
Named volumes are managed by the Docker daemon under /var/lib/docker/volumes/, more portable and preferred for database data over bind mounts.
3Preventing race conditions between app and DB?
depends_on with condition: service_healthy. The database container needs a health check with mysqladmin ping or pg_isready.
4Is data lost with docker compose down?
No, named volumes are preserved. Only docker compose down -v also deletes the volumes, never run it unintentionally.
5Passing passwords securely?
Use MYSQL_ROOT_PASSWORD_FILE instead of MYSQL_ROOT_PASSWORD. Provide the file as a Docker secret or read-only volume.
6Creating a consistent backup?
docker exec container mysqldump --single-transaction creates a consistent InnoDB snapshot without table locking, pipe the output directly into gzip.
7Tuning the InnoDB buffer pool in a container?
Mount my.cnf as a read-only volume in /etc/mysql/conf.d/. Set innodb_buffer_pool_size to 60 to 70 percent of the container's mem_limit.
8Initializing PostgreSQL the same way as MySQL?
Yes, the same /docker-entrypoint-initdb.d/ method applies. The health check uses pg_isready instead of mysqladmin ping. The underlying principle is identical.
9What is a migration container?
A short-lived container for Flyway/Liquibase that runs migrations and then exits. The app waits for it using condition: service_completed_successfully.
10How long should start_period be for MySQL?
At least 60s for simple databases. With large init dumps or long crash recovery, 120s or more, size it generously.