Running Databases in Containers: PostgreSQL, MySQL and MongoDB

Intermediate
13 min

Running Databases in Containers: PostgreSQL, MySQL and MongoDB

Databases are the most common thing developers run in Docker, and the easiest to lose data with. This lesson covers the three most popular official images, the variables that configure them, where each stores its data, how to seed a fresh database with initialisation scripts, and how to back up and restore. After it you will run any of them safely for development and know what changes for production.

The Three Images at a Glance

| | PostgreSQL | MySQL | MongoDB | |---|---|---|---| | Image | postgres:17 | mysql:8.4 | mongo:8 | | Required variable | POSTGRES_PASSWORD | MYSQL_ROOT_PASSWORD | MONGO_INITDB_ROOT_USERNAME + _PASSWORD | | Create user and db | POSTGRES_USER, POSTGRES_DB | MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE | MONGO_INITDB_DATABASE | | Data directory | /var/lib/postgresql/data | /var/lib/mysql | /data/db | | Init scripts | /docker-entrypoint-initdb.d/ (.sql, .sql.gz, .sh) | same path (.sql, .sh) | same path (.js, .sh) | | Port | 5432 | 3306 | 27017 | | Readiness check | pg_isready -U app -d shop | mysqladmin ping -h localhost | mongosh --eval "db.adminCommand('ping')" | | CLI | psql | mysql | mongosh |

All three accept the _FILE variant of their password variables, so Compose secrets work without change. Pin a major version tag: postgres:17 receives patch updates while postgres:latest may jump a major version and refuse to open the old data directory.

Persisting Data

Without a volume, the database writes into the container's writable layer and everything disappears with docker rm. Always mount a named volume on the data directory:

bash
docker run -d --name pg \ -e POSTGRES_PASSWORD=devpass -e POSTGRES_USER=app -e POSTGRES_DB=shop \ -v pg-data:/var/lib/postgresql/data \ -p 127.0.0.1:5432:5432 \ postgres:17

Prefer named volumes over bind mounts for database files: they avoid permission problems and are much faster on Docker Desktop. docker compose down keeps the volume; docker compose down -v deletes it.

Initialisation Scripts

Each image runs the files in /docker-entrypoint-initdb.d/ in alphabetical order, only when the data directory is empty. This is the place for schema creation and seed data in development:

sql
-- db/init/01-schema.sql CREATE TABLE products ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, price_cents INTEGER NOT NULL ); INSERT INTO products (name, price_cents) VALUES ('Notebook', 499), ('Pen', 149);

Mount the directory read-only, as in the sample code. If the scripts do not seem to run, the volume already holds an initialised database; remove it with docker compose down -v and start again. For MongoDB, .js files run through mongosh against MONGO_INITDB_DATABASE.

Connecting from Applications and Tools

Inside the Compose network, applications use the service name as the host: postgres://app:app@postgres:5432/shop, mysql://app:app@mysql:3306/shop, mongodb://root:pass@mongo:27017/shop?authSource=admin. From the host, use the published port on 127.0.0.1. The quickest client is the one already in the image:

bash
docker compose exec postgres psql -U app -d shop -c '\dt' docker compose exec mysql mysql -u root -p"$MYSQL_ROOT_PASSWORD" shop docker compose exec mongo mongosh -u root -p pass --authenticationDatabase admin

Backup and Restore

Dump through docker exec and redirect on the host; restore by piping into the client with -i (and no -t, which would corrupt the stream):

bash
# PostgreSQL docker compose exec -T postgres pg_dump -U app -Fc shop > shop.dump docker compose exec -T postgres pg_restore -U app -d shop --clean < shop.dump # MySQL docker compose exec -T mysql mysqldump -u root -p"$PW" shop > shop.sql docker compose exec -T mysql mysql -u root -p"$PW" shop < shop.sql # MongoDB (archive to stdout) docker compose exec -T mongo mongodump --archive -u root -p pass > shop.archive docker compose exec -T mongo mongorestore --archive -u root -p pass < shop.archive

Logical dumps are the portable option; copying the volume directory with tar is only safe while the database is stopped.

Production Considerations

  • Do not publish the database port; the application reaches it over the internal network.
  • Use POSTGRES_PASSWORD_FILE (or the equivalent) with a secret instead of inline passwords, and add restart: unless-stopped.
  • Upgrading a major version requires a dump and restore (or pg_upgrade); the new image will not start on the old data directory.
  • Beyond a single host, a managed database service is usually simpler than orchestrating stateful containers yourself.
Quick Quiz
Question 1 of 3

When do the scripts in `/docker-entrypoint-initdb.d/` run?

Key Takeaways

  • Each official image is configured by environment variables, supports _FILE variants, and stores data at a documented path that must be a named volume.
  • Pin major versions; a latest jump can make the data directory unreadable.
  • Initialisation scripts run once on an empty data directory; reset with down -v to re-run them.
  • Applications connect by service name; host tools use 127.0.0.1 on a published port only in development.
  • Back up with pg_dump, mysqldump or mongodump through exec -T, and never publish the database port in production.

Next lesson: Dockerizing a Node.js Application — build a production-ready image for an Express or Fastify service.

Running Databases in Containers: PostgreSQL, MySQL and MongoDB - Docker | CodeYourCraft | CodeYourCraft