Seeding Test Data for Databases in Docker Dev Containers
AI generated
FROM
RUN
Docker · MySQL · Seed Data · Test Data
Seeding Test Data
for Databases in Docker Dev Containers

Seed data is fundamentally different from plain init scripts: init scripts create the schema, seed data fills it with realistic, repeatable test data. Anyone mixing up both concepts ends up either with empty development databases or with inconsistent test states between team members.

16 min read Anonymized Dumps · Faker Seeders · Volume Snapshots MySQL 8.4 · Docker Compose · Magento

1. Why seed data is different from init scripts

Init scripts in official MySQL or PostgreSQL images create the database schema on the container's first start, generating tables and indexes, and usually run exactly once. Seed data is conceptually separate from that: it fills an already existing schema with realistic data sets useful for development and manual testing, and unlike init scripts is meant to be repeatable as often as needed, for example when a developer accidentally broke test data and needs a clean starting state.

The difference becomes especially visible in Magento projects: the schema with all its EAV tables is created via setup:install or init scripts, but realistic seed data with hundreds of products, categories and test customers has to be loaded separately in order to meaningfully test checkout flows, search functionality or discount rules manually. Without well thought out seed data, every developer works with an almost empty database, hiding bugs that only surface at realistic data volumes.

This article shows different strategies for seed data in Docker dev containers, from anonymized production dumps through programmatic seeders to fast volume snapshots that enable a complete reset in seconds instead of minutes.

2. Seed strategy: anonymized production dump

The most realistic source for seed data is an export of the actual production database, though never unmodified, always with anonymized personal data. Names, email addresses, phone numbers and payment information must be replaced with placeholders or generated fake values before being used as local seed data, both for privacy reasons and to keep developers from accidentally handling real customer data.


#!/usr/bin/env bash
# anonymize-and-export.sh — create anonymized seed data from production
set -euo pipefail

PROD_DB="shop_production"
SEED_FILE="./seed/anonymized-dump.sql"

mysqldump --single-transaction --no-tablespaces "$PROD_DB" > /tmp/raw-dump.sql

# Anonymize personal data before it ever leaves the secure environment
mysql "$PROD_DB" <<'SQL'
UPDATE customer_entity
SET email = CONCAT('customer', entity_id, '@example.test'),
    firstname = 'Test',
    lastname = CONCAT('User', entity_id);

UPDATE sales_order_address
SET telephone = '000000000', street = 'Test Street 1';
SQL

mysqldump --single-transaction --no-tablespaces "$PROD_DB" > "$SEED_FILE"
echo "[OK] Anonymized seed data written to $SEED_FILE"

Important for this strategy: anonymization must happen before the export, never after, because a dump already exported with real data has already left the protected production environment. An anonymized dump as a source of seed data provides realistic data distributions and edge cases that synthetic generators often miss, for example unusually long product names or unusual price combinations.

3. Seed strategy: programmatic seeders with Faker

Where no production dump is available, for example on new projects without live data, programmatic seeders using libraries like Faker take over generating seed data. The advantage over a static SQL dump: the amount and structure of test data can be flexibly parameterized, for example "generate 500 products in 10 categories with 50 test customers", without maintaining a fixed file.


<?php
declare(strict_types=1);

namespace Mironsoft\DevTools\Console\Command;

use Faker\Factory as FakerFactory;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Seeds the development database with realistic, reproducible test data.
 * Intended to run inside the app container against the local dev database.
 */
final class SeedTestDataCommand extends Command
{
    protected static $defaultName = 'dev:seed';

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // Fixed seed value keeps generated data identical across every run
        $faker = FakerFactory::create('de_DE');
        $faker->seed(42);

        $pdo = new \PDO('mysql:host=db;dbname=shop_dev', 'root', 'secret');

        for ($i = 0; $i < 500; $i++) {
            $stmt = $pdo->prepare(
                'INSERT INTO catalog_product_seed (sku, name, price) VALUES (?, ?, ?)'
            );
            $stmt->execute([
                sprintf('SEED-%04d', $i),
                $faker->words(3, true),
                $faker->randomFloat(2, 5, 500),
            ]);
        }

        $output->writeln('[OK] Seeded 500 products with deterministic Faker data');
        return Command::SUCCESS;
    }
}

The decisive trick in this approach is $faker->seed(42): the fixed seed value makes Faker produce exactly the same seed data on every run, instead of delivering random new values each time. This makes bug reports reproducible, because a colleague running the same seed script gets exactly the same test data, instead of having to guess which randomly generated product triggered the observed error.

4. Writing idempotent seed scripts correctly

A common mistake in seed data scripts: they only work the first time they run and fail with duplicate key errors on every subsequent call, because they bluntly execute INSERT statements without regard for data already present. An idempotent seed script, in contrast, must be executable as often as needed and always end up in the same, defined final state, regardless of how many times it has run before.


-- WRONG: fails with duplicate key error on every run after the first
INSERT INTO catalog_product_seed (sku, name, price)
VALUES ('SEED-0001', 'Test Product', 19.99);

-- RIGHT: idempotent via TRUNCATE before re-seeding
TRUNCATE TABLE catalog_product_seed;

INSERT INTO catalog_product_seed (sku, name, price)
VALUES ('SEED-0001', 'Test Product', 19.99);

-- ALTERNATIVE: idempotent via upsert when truncation is not desired
INSERT INTO catalog_product_seed (sku, name, price)
VALUES ('SEED-0001', 'Test Product', 19.99)
ON DUPLICATE KEY UPDATE
  name = VALUES(name),
  price = VALUES(price);

For seed data that should be fully controlled and reproducible, TRUNCATE before re-inserting is usually the more robust choice, since it guarantees an exactly defined final state. The upsert approach with ON DUPLICATE KEY UPDATE is better suited when additional, manually created test data should survive alongside the generated seed data.

5. Seed data as a dedicated service with docker compose run

Instead of manually exec-ing seed logic into a running container, it is worth having a dedicated Compose service solely responsible for loading seed data and stopping afterward. This service shares the network and database access with the main stack, but does not run permanently, instead being started specifically via docker compose run.


# docker-compose.yml — dedicated seeding service, not started with "up"
services:
  app:
    build: .
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: shop_dev
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      retries: 10

  seed:
    build: .
    depends_on:
      db:
        condition: service_healthy
    command: ["bin/console", "dev:seed"]
    profiles: ["seed"]

# Start the stack normally without seeding
docker compose up -d

# Run seed data explicitly, only when actually needed
docker compose run --rm seed

The seed profile key ensures this service is never accidentally started along with a normal docker compose up. Instead, it only runs explicitly via docker compose run --rm seed, turning seed data runs into a deliberate, clearly visible action instead of an invisible side effect of a normal stack startup.

6. Fast resets via volume snapshots instead of reseeding

With larger data volumes, a complete reseed can take several minutes, which is disruptive in day to day development when a developer just wants to quickly get back to a clean starting state. It is much faster to back up already seeded seed data as a volume snapshot and simply restore that volume when needed, instead of running the entire seed script again.


#!/usr/bin/env bash
# snapshot-seed-data.sh — back up an already-seeded volume for instant restore
set -euo pipefail

VOLUME_NAME="shop-dev_db_data"
SNAPSHOT_DIR="./seed/snapshots"
mkdir -p "$SNAPSHOT_DIR"

echo "[INFO] Creating snapshot of seeded volume..."
docker run --rm \
  -v "${VOLUME_NAME}:/source:ro" \
  -v "$(pwd)/${SNAPSHOT_DIR}:/backup" \
  alpine tar czf /backup/seeded-baseline.tar.gz -C /source .

echo "[OK] Snapshot saved to ${SNAPSHOT_DIR}/seeded-baseline.tar.gz"

#!/usr/bin/env bash
# restore-seed-data.sh — instant reset to the seeded baseline, seconds not minutes
set -euo pipefail

docker compose down
docker volume rm shop-dev_db_data || true
docker volume create shop-dev_db_data

docker run --rm \
  -v "shop-dev_db_data:/target" \
  -v "$(pwd)/seed/snapshots:/backup:ro" \
  alpine tar xzf /backup/seeded-baseline.tar.gz -C /target

docker compose up -d
echo "[OK] Restored to seeded baseline in seconds"

This approach deliberately separates two different operations: the one time, potentially slow generation of seed data, and the frequent, always fast reset back to exactly that state. For developers needing a clean database state several times a day, for example during intensive manual testing, this pattern saves considerable waiting time compared to a complete reseed.

7. Seed data for Magento specific test scenarios

Magento already ships a baseline of seed data with its sample data packages, sufficient for a first impression but rarely covering the specific test scenarios of a real project. For realistic testing of discount rules, multi level category trees or multi website setups, project specific seed data is needed, loaded via custom data patches or CLI commands instead of relying on the generic sample data.

A proven pattern is a custom bin/magento command that specifically creates test customers with different customer groups, products with special prices, and orders in various statuses. This project specific seed data mirrors real business logic, such as tiered B2B discounts or seasonal offers, and thereby surfaces bugs that would never show up with Magento's generic sample data.

8. Keeping seeds consistent in CI pipelines

The same seed data used locally for manual testing should ideally also be used in the CI pipeline for automated end to end tests. A central seed script that runs identically both locally and on the CI runner prevents the situation where a test passes locally but fails in CI because different test data is present there than on the developer's machine.

Important here: CI environments should never use the full anonymized production dump, since that is too large and too slow for fast CI runs. A more compact subset of the seed data is better suited for CI, generated via the same Faker seeder with the same fixed seed values, but with significantly reduced data volume, for example 50 instead of 500 products, to keep pipeline runtimes short.

9. Seed strategies compared

Choosing the right seed strategy depends heavily on the project stage and the available data sources.

Strategy Realism Setup effort Speed
Anonymized production dump Very high High (anonymization required) Slow with large dumps
Faker seeder Medium Medium Fast and parameterizable
Volume snapshot Same as snapshot source Low after initial setup Very fast (seconds)

For maximum realism on complex bug reports, the anonymized production dump is unbeatable, but costs the most setup effort due to the mandatory anonymization. Faker seeders quickly deliver parameterizable seed data for new projects without production history. Volume snapshots are not a standalone data generator, but a speed optimization for frequent resets of already existing seed data.

Mironsoft

Seed scripts, test data strategies and Docker development environments for Magento

Realistic test data for your whole team?

We build anonymized dump pipelines, idempotent Faker seeders and volume snapshot workflows for your Magento and PHP projects, so every developer works with the same, reproducible seed data.

Anonymization pipeline

Secure, GDPR compliant creation of seed data from production data

Faker seeder development

Idempotent, parameterizable seed scripts for new projects without production history

Volume snapshot workflow

Second fast resets to a clean, seeded starting state

10. Summary

Seed data solves a different problem than init scripts: instead of creating the schema, it fills a database with realistic, repeatable test data for development and manual review. Anonymized production dumps deliver maximum realism, programmatic Faker seeders with a fixed seed value deliver reproducible, parameterizable data volumes without production history, and idempotent scripts with TRUNCATE or upsert ensure repeated seeding never fails with duplicate key errors.

Volume snapshots complement these strategies with fast resets in seconds instead of minutes, while dedicated Compose services with a profile flag turn seed data runs into a deliberate, visible action instead of an invisible side effect. Anyone using the same seed mechanisms consistently between local development and the CI pipeline avoids the frustrating situation where a test passes locally but fails in CI, simply because different seed data was underlying it.

Seed Data for Docker Dev Containers — The Essentials at a Glance

Init vs. seed

Init scripts create the schema, seed data fills it with realistic, repeatably loadable test data.

Anonymization

Always replace personal data before export, never distribute an unprotected dump.

Idempotence

TRUNCATE before reseeding or ON DUPLICATE KEY UPDATE prevents errors on repeated runs.

Volume snapshots

Back up an already seeded volume and restore it in seconds instead of reseeding every time.

11. FAQ: Test Data and Seed Scripts for Docker Dev Containers

1Difference between init scripts and seed data?
Init scripts create the schema once, seed data fills it repeatably with realistic test data.
2Can I use a prod dump unfiltered?
No, personal data must be anonymized before export, mandatory for privacy reasons.
3Why Faker with a fixed seed value?
Ensures identical test data on every run, makes bug reports reproducible across team members.
4What does idempotence mean here?
Runnable as often as needed with the same final state, instead of failing with duplicate key errors.
5How do I prevent unwanted seeding on up?
Via a dedicated service with a profile flag, only started explicitly through docker compose run.
6How does a fast snapshot reset work?
Back up the seeded volume as an archive, then remove, recreate and restore from it, seconds not minutes.
7Aren't Magento's sample data packages enough?
For a first impression yes, for project specific scenarios like B2B discounts usually not, custom seed data needed.
8Should CI and local use the same seeds?
Ideally yes, with reduced data volume in CI for short runtimes and consistent results.
9How much seed data for realistic tests?
Usually a few hundred products and several dozen test customers, to surface pagination and filter behavior.
10TRUNCATE or ON DUPLICATE KEY UPDATE?
TRUNCATE guarantees an exact final state, ON DUPLICATE KEY UPDATE preserves additional manual test data.