MariaDB vs PostgreSQL Container Storage: Bind Mount vs Volume Performance Truth

Was this helpful?

Your database was fine on bare metal. Then you containerized it, pointed the data directory at “something persistent,” and suddenly latency graphs look like a seismograph.
Writes stall. Checkpoints spike. Someone says “just use volumes, bind mounts are slow,” and someone else says the opposite with the confidence of a late-night Twitter thread.

Here’s the truth from production trenches: “bind mount vs volume” is not a performance claim. It’s a path to a file system, plus a few layers of indirection that may—or may not—matter.
What matters is what’s behind that path: the kernel, the file system, the storage stack, the container runtime, and your database’s durability choices.

The real choices you’re making (not the ones people argue about)

When someone asks “bind mount or volume for MariaDB/Postgres?”, they’re really asking a bundle of questions they don’t realize they’re asking:

  • Am I writing to the container’s writable layer (OverlayFS) or not? That’s the biggest “don’t do it” for databases.
  • What’s the actual backing file system? ext4, XFS, ZFS, btrfs, and “mystery network block storage” behave differently under fsync pressure.
  • Is the storage local or networked? Local NVMe and a network CSI volume may both be “a volume,” and they’ll behave like different species.
  • What are my durability settings? PostgreSQL fsync, synchronous_commit, WAL placement; MariaDB InnoDB innodb_flush_log_at_trx_commit and doublewrite. These are not cosmetic knobs.
  • What’s my write amplification? Page sizes, WAL/binlog, doublewrite, checkpoints, autovacuum/purge. You’re paying for writes you didn’t realize you ordered.
  • Am I constrained by IOPS, latency, CPU, or dirty page writeback? If you don’t know, you will optimize the wrong thing with great enthusiasm.

Put bluntly: a bind mount to a fast local XFS on NVMe will wipe the floor with a “Docker volume” sitting on congested network storage.
Conversely, a Docker-managed volume backed by local block storage can be as fast as a bind mount. The label is not the bottleneck.

Joke #1: A database running on the container writable layer is like storing your passport in a sandwich bag—technically possible, emotionally reckless.

Interesting facts and historical context (why we’re here)

A few short, concrete facts explain why storage debates around containers get weird:

  1. PostgreSQL’s WAL has existed since the mid-1990s (as PostgreSQL matured out of POSTGRES), designed for crash safety on imperfect disks—long before “cloud volumes” were a thing.
  2. InnoDB became the default MySQL storage engine in 5.5 (2010 era), and MariaDB inherited that lineage; InnoDB’s durability story leans heavily on redo logs and doublewrite behavior.
  3. Linux’s page cache is the performance workhorse for both databases; “direct I/O everywhere” is not the default strategy for either, and fighting the cache often hurts.
  4. fsync semantics vary by file system and mount options; the same DB config can be safe on one setup and “mostly safe” on another, which is a polite way to say “surprise data loss.”
  5. OverlayFS became mainstream with Docker’s overlay2 driver because it’s efficient for image layering, not because it loves database workloads that do fsync and random writes.
  6. Historically, network filesystems were a common footgun for databases; modern systems are better, but the “latency + fsync + jitter” triangle still ruins weekends.
  7. Docker named volumes were popularized for portability (“move the app without caring where data lives”), but databases care very much where the bytes physically land.
  8. Kubernetes PV abstractions made storage easier to request and harder to understand; “I asked for 100Gi” does not imply “I got low-latency synchronous writes.”

MariaDB vs PostgreSQL: how their I/O patterns punish storage

PostgreSQL: WAL first, then data files, then checkpoint drama

PostgreSQL writes changes to WAL (Write-Ahead Log) and later flushes dirty buffers to data files, with checkpoints coordinating “how far WAL is safe to recycle.”
The WAL fsync path is your durability heartbeat. If it gets slow, your commits get slow. And it gets slow in a very honest way: storage latency shows up as transaction latency.

Typical pain points in containers:

  • WAL on slow storage (or worse: on the container writable layer): commit latency spikes.
  • Checkpoint spikes: you see periodic write storms and latency cliffs.
  • Autovacuum I/O: sustained random reads/writes, which surface IOPS limits quickly.
  • fsync and barriers: if your “volume” lies about durability, PostgreSQL won’t know until you do.

MariaDB (InnoDB): redo logs, doublewrite, and background flushing

MariaDB’s InnoDB engine leans on redo logs to make commits durable (depending on innodb_flush_log_at_trx_commit) and uses a doublewrite buffer mechanism to reduce partial page write corruption.
In practice, this means more write amplification in exchange for safety under crash conditions.

Typical pain points:

  • Redo log fsync cadence: with strict durability, the log device latency controls throughput.
  • Doublewrite + data pages: you may pay for writing the same page twice (or more), which punishes slow media.
  • Background flushing: if your storage can’t keep up, InnoDB can stall foreground work.
  • Binary logs (if enabled): extra sequential writes that look “cheap” until the fsync policy bites.

What this means for “bind mount vs volume”

PostgreSQL is usually more latency-sensitive per commit (WAL fsync path is immediately visible), while InnoDB can buffer and smear pain until it can’t, then stalls more dramatically.
Both hate unpredictable latency. Both hate bursty throttling. Both hate “your fsync is actually a suggestion.”

Bind mounts vs Docker volumes: what actually differs

Let’s stop treating these as mystical objects.

  • Bind mount: container path maps to an existing host path. You control the host directory, file system, mount options, SELinux labels, and lifecycle.
  • Docker named volume: Docker manages a directory (usually under /var/lib/docker/volumes) and mounts it into the container. The backing storage is still the host file system unless a volume driver changes it.

Performance differences are usually indirect:

  • Operational defaults: volumes tend to land on the same disk as Docker’s root, which is often not the disk you intended for databases.
  • Mount options and file system choice: bind mounts make it more natural to choose “db-friendly” file systems and mount options.
  • Security labeling overhead: on SELinux systems, relabeling and context can add overhead or cause denial; usually correctness issues first, perf second.
  • Backup/restore workflows: bind mounts integrate with host tooling; volumes integrate with Docker tooling. Either can be fine; both can be abused.

If you’re using a plain local host file system, both bind mounts and volumes bypass OverlayFS. That’s the key. Your database files should not live on the overlay writable layer.

OverlayFS/overlay2: the villain with a reasonable alibi

OverlayFS is designed for layering images. It’s great at what it was built for: lots of mostly-read-only files with occasional copy-on-write changes.
Databases are the opposite: rewrite the same files, sync them, do it forever, and complain loudly when you add latency.

The failure mode looks like:

  • Small random writes turn into more complex CoW operations.
  • Metadata churn increases: file attributes, directory entries, copy-ups.
  • fsync behavior becomes costlier, especially under pressure.

If your DB is on overlay2, you’re benchmarking Docker’s storage driver, not MariaDB or PostgreSQL.

Kubernetes angle: HostPath, local PVs, network PVs, and why your DB hates them differently

Kubernetes takes your simple question and turns it into a buffet of abstractions:

  • HostPath: basically a bind mount to a node path. Fast and simple. Also ties your pod to a node and can be an availability trap.
  • Local Persistent Volumes: like HostPath, but with scheduling and lifecycle semantics. Often the best “local disk” option if you accept node affinity.
  • Network block volumes: iSCSI, NVMe-oF, cloud block devices—often good latency, still has jitter and multi-tenant contention possibilities.
  • Network filesystems: NFS-like semantics or distributed FS. Can work, but the “fsync wall” is real and shows up as commit latency.

For stateful databases, don’t treat “PVC bound” as “problem solved.” You need to know what you got: latency profile, fsync behavior, throughput under concurrent writers, and how it fails.

How to benchmark without lying to yourself

If you want the performance truth, benchmark the whole path: DB → libc → kernel → file system → block layer → device.
Don’t benchmark in a way that avoids fsync if your production durability requires fsync.

Also, don’t just run one tool once and declare victory. Containerized systems have noisy neighbors, CPU throttling, writeback storms, and background compaction.
You want to capture variance, not just averages.

One paraphrased idea from Werner Vogels: “Everything fails, all the time—design so you can operate through it.” (paraphrased idea)

Fast diagnosis playbook

When someone says “Postgres in Docker is slow” or “MariaDB in Kubernetes is jittery,” do this in order. Don’t freestyle.

  1. First: confirm where the data actually lives.
    If it’s on overlay2, stop and fix that before benchmarking anything else.
  2. Second: measure fsync/commit latency path.
    PostgreSQL: check WAL/commit latency metrics; MariaDB: redo log flush behavior and stalls.
    If commits are slow, storage latency is guilty until proven innocent.
  3. Third: identify the constraint type.
    IOPS-limited random writes vs throughput-limited sequential writes vs CPU throttling vs memory pressure causing writeback storms.
  4. Fourth: check file system and mount options.
    ext4 vs XFS vs ZFS, barriers, discard, atime, journaling mode, and whether you’re on a loopback file.
  5. Fifth: look for background I/O killers.
    Checkpoints, autovacuum, purge, compaction, backup jobs, node-level log shipping, antivirus/scan agents.
  6. Sixth: only then compare bind mount vs volume.
    If both point to the same underlying file system, the perf difference is usually a rounding error—unless you’ve introduced different options or different devices.

Practical tasks: commands, outputs, and what decision to make

These are real ops tasks. Each includes commands, what the output means, and the decision you make next.
Run them on the host and inside containers as appropriate.

Task 1: Prove whether the DB is on overlay2 or a real mount

cr0x@server:~$ docker inspect -f '{{.GraphDriver.Name}} {{json .Mounts}}' pg01
overlay2 [{"Type":"volume","Name":"pgdata","Source":"/var/lib/docker/volumes/pgdata/_data","Destination":"/var/lib/postgresql/data","Driver":"local","Mode":"z","RW":true,"Propagation":""}]

What it means: GraphDriver is overlay2 for the container layer, but the data directory is a volume mount to a host path under Docker’s volume directory. That’s good: DB data bypasses overlay2.

Decision: If you see the DB datadir not listed under .Mounts (or it points to container filesystem), you’re on overlay2. Move the datadir to a bind mount or a volume immediately.

Task 2: Find the actual filesystem type backing your bind mount/volume

cr0x@server:~$ df -T /var/lib/docker/volumes/pgdata/_data
Filesystem     Type  1K-blocks       Used  Available Use% Mounted on
/dev/nvme0n1p2  xfs   468587520  122884164  345703356  27% /

What it means: Your Docker volumes live on XFS on /. That might be NVMe (good) or a shared root disk (often bad).

Decision: If this is not the disk you intended, move DB storage to a dedicated mount (bind mount or a volume driver). Root disks get noisy fast.

Task 3: Check whether your “volume” is actually a loopback file (quiet disaster)

cr0x@server:~$ mount | grep -E '/var/lib/docker|/var/lib/kubelet|loop'
/dev/loop0 on /var/lib/docker type ext4 (rw,relatime)
/dev/nvme0n1p2 on / type xfs (rw,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota)

What it means: Docker is on a loop device. That often means “a file-backed block device” (like devicemapper in a file, or some nested storage).
Loopback adds overhead and can turn fsync into a performance prank.

Decision: Don’t run serious databases on loopback-backed Docker storage. Fix the host storage layout first.

Task 4: Confirm mount options that affect durability and latency

cr0x@server:~$ findmnt -no TARGET,FSTYPE,OPTIONS /var/lib/docker/volumes/pgdata/_data
/ xfs rw,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota

What it means: You can see if you have suspicious options like nobarrier (danger), weird journaling tweaks, or heavy metadata options.

Decision: If you find mount options that reduce safety to chase performance, stop. Fix performance by improving the device and layout, not by removing seatbelts.

Task 5: Check PostgreSQL settings that directly translate to storage behavior

cr0x@server:~$ docker exec -it pg01 psql -U postgres -c "SHOW data_directory; SHOW wal_level; SHOW synchronous_commit; SHOW full_page_writes; SHOW checkpoint_timeout;"
         data_directory         
-------------------------------
 /var/lib/postgresql/data
(1 row)

 wal_level 
-----------
 replica
(1 row)

 synchronous_commit 
-------------------
 on
(1 row)

 full_page_writes 
------------------
 on
(1 row)

 checkpoint_timeout 
--------------------
 5min
(1 row)

What it means: This setup is durability-forward. WAL is required for replicas, commits are synchronous, and full-page writes are on (safety vs torn pages).

Decision: If performance is bad, don’t start by flipping durability off. Start by moving WAL to faster storage and fixing latency.

Task 6: Check MariaDB/InnoDB durability knobs that hit fsync

cr0x@server:~$ docker exec -it mariadb01 mariadb -uroot -psecret -e "SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; SHOW VARIABLES LIKE 'sync_binlog'; SHOW VARIABLES LIKE 'innodb_doublewrite';"
+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1     |
+--------------------------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| sync_binlog   | 1     |
+---------------+-------+
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| innodb_doublewrite| ON    |
+-------------------+-------+

What it means: You’re paying the “real durability” bill. Every commit flushes redo log; binlog is synced; doublewrite is on.

Decision: If you need this durability, you must buy storage that can handle it. If you don’t need it (rare in production), be explicit about risk and document it.

Task 7: Observe per-device latency and queue depth (host view)

cr0x@server:~$ iostat -x 1 3
Linux 6.5.0 (server)  12/31/2025  _x86_64_ (16 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           6.12    0.00    2.31    8.44    0.00   83.13

Device            r/s     w/s   rkB/s   wkB/s  avgrq-sz avgqu-sz   await  r_await  w_await  svctm  %util
nvme0n1         221.0   842.0  7824.0 29812.0     74.1     9.82   11.6     4.2     13.5   0.6   64.3

What it means: await ~11.6ms and avgqu-sz ~9.8 suggests queued writes. For a busy DB, 10–15ms write latency can absolutely show up as slow commits.

Decision: If await is high and %util is high, you’re storage-bound. Fix storage layout or migrate to faster media before touching DB query tuning.

Task 8: Catch filesystem writeback storms (memory pressure pretending to be storage)

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0  81232  65488 9231440   0    0   124   980 2840 6120  7  2 86  5  0
 3  1      0  42112  65284 9150020   0    0   118  8450 3120 7450  6  3 63 28  0
 4  2      0  23840  65110 9121000   0    0   102 16820 3401 8012  6  4 52 38  0

What it means: Increasing b (blocked), rising bo (blocks out), and rising wa suggest dirty pages being flushed hard. This can look like “storage is slow” but the trigger is memory pressure.

Decision: If memory is tight, give the node more RAM, reduce competing workloads, or tune DB memory so the kernel isn’t forced into panic flushing.

Task 9: Verify cgroup CPU throttling (the fake storage bottleneck)

cr0x@server:~$ docker inspect -f '{{.HostConfig.NanoCpus}} {{.HostConfig.CpuQuota}} {{.HostConfig.CpuPeriod}}' pg01
0 200000 100000

What it means: CPU quota is 2 cores (200000/100000). If you hit that ceiling, background workers (checkpoints, flushing, vacuum) slow down and storage looks “weird.”

Decision: If DB is CPU throttled, raise limits before migrating storage. A CPU-starved DB can’t drive I/O efficiently.

Task 10: Measure PostgreSQL commit and checkpoint behavior from stats

cr0x@server:~$ docker exec -it pg01 psql -U postgres -c "SELECT checkpoints_timed, checkpoints_req, buffers_checkpoint, buffers_backend, maxwritten_clean FROM pg_stat_bgwriter;"
 checkpoints_timed | checkpoints_req | buffers_checkpoint | buffers_backend | maxwritten_clean 
------------------+-----------------+-------------------+----------------+------------------
              118 |              43 |           9823412 |         221034 |            1279
(1 row)

What it means: High buffers_checkpoint indicates checkpoint-driven writes. If latency spikes line up with checkpoints, storage throughput and writeback behavior are implicated.

Decision: Consider spreading checkpoint I/O (tune checkpoint settings), but only after confirming storage isn’t fundamentally underpowered.

Task 11: Check PostgreSQL WAL location and consider isolating it

cr0x@server:~$ docker exec -it pg01 bash -lc "ls -ld /var/lib/postgresql/data/pg_wal && df -T /var/lib/postgresql/data/pg_wal"
drwx------ 1 postgres postgres 4096 Dec 31 09:41 /var/lib/postgresql/data/pg_wal
Filesystem     Type  1K-blocks       Used  Available Use% Mounted on
/dev/nvme0n1p2  xfs   468587520  122884164  345703356  27% /

What it means: WAL shares the same file system as data. That’s common, but not always ideal.

Decision: If commit latency is your pain, mount WAL to a faster or less contended device (separate volume/bind mount). WAL is small but demanding.

Task 12: Confirm MariaDB datadir placement and filesystem

cr0x@server:~$ docker exec -it mariadb01 bash -lc "mariadb -uroot -psecret -e \"SHOW VARIABLES LIKE 'datadir';\" && df -T /var/lib/mysql"
+---------------+----------------+
| Variable_name | Value          |
+---------------+----------------+
| datadir       | /var/lib/mysql/|
+---------------+----------------+
Filesystem     Type  1K-blocks       Used  Available Use% Mounted on
/dev/nvme0n1p2  xfs   468587520  122884164  345703356  27% /

What it means: MariaDB data is on XFS at root. Good if root is fast and dedicated; bad if root is shared with everything else.

Decision: If you see this landing on a generic root disk, relocate to a dedicated mount with known performance characteristics.

Task 13: Quick and dirty fsync reality check with fio (host path used by bind/volume)

cr0x@server:~$ fio --name=fsync-test --directory=/var/lib/docker/volumes/pgdata/_data --rw=write --bs=4k --size=256m --ioengine=sync --fsync=1 --numjobs=1 --runtime=20 --time_based --group_reporting
fsync-test: (g=0): rw=write, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=sync, iodepth=1
...
  write: IOPS=820, BW=3280KiB/s (3360kB/s)(64.0MiB/20003msec)
    clat (usec): min=220, max=42000, avg=1216.55, stdev=3100.12

What it means: This approximates “small writes with fsync.” Average latency ~1.2ms but max 42ms: that tail latency is exactly what becomes “random slow commits.”

Decision: If max latencies are ugly, fix storage contention or move to better media. DB tuning can’t negotiate with physics.

Task 14: Check whether your database is silently hitting filesystem limits (inodes, space, reserved blocks)

cr0x@server:~$ df -h /var/lib/docker/volumes/pgdata/_data && df -i /var/lib/docker/volumes/pgdata/_data
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p2  447G  118G  330G  27% /
Filesystem     Inodes IUsed   IFree IUse% Mounted on
/dev/nvme0n1p2   224M  3.2M   221M    2% /

What it means: Space and inodes are fine. When these get tight, DBs behave oddly: autovacuum fails, temp files fail, crashes on extension install, you name it.

Decision: If free space is low or inode usage is high, fix that before performance work. Full disks make performance “interesting” in the worst way.

Task 15: Confirm no one is “helpfully” backing up by rsyncing the live datadir

cr0x@server:~$ ps aux | grep -E 'rsync|tar|pg_basebackup|mariabackup' | grep -v grep
root     19344  8.2  0.1  54360  9820 ?        Rs   09:44   0:21 rsync -aH --delete /var/lib/docker/volumes/pgdata/_data/ /mnt/backup/pgdata/

What it means: Someone is rsyncing the live data directory. That is both a performance tax and (for PostgreSQL) not a consistent backup method unless done correctly.

Decision: Stop doing filesystem-level copies of a live DB unless you’re using proper snapshot semantics and DB-coordinated backup methods.

Common mistakes: symptom → root cause → fix

1) Symptom: “Container DB is slow, but host is fast”

Root cause: DB data is on overlay2/container writable layer, not on a mount.

Fix: Mount the datadir as a bind mount or named volume to a real host filesystem. Confirm with docker inspect mounts.

2) Symptom: Random 200–2000ms commit spikes

Root cause: Storage tail latency from noisy neighbors, network PV jitter, or device cache flush behavior. Often shows up as fsync stalls.

Fix: Move WAL/redo logs to lower-latency storage, isolate DB disks, and measure latency distributions (not just average throughput).

3) Symptom: PostgreSQL periodic latency cliffs every few minutes

Root cause: Checkpoints causing write storms; dirty buffers accumulate then flush in bursts.

Fix: Tune checkpoint pacing and ensure storage can absorb sustained writes. If storage is marginal, tuning is lipstick on a pig with a pager.

4) Symptom: MariaDB stalls under load, then recovers

Root cause: InnoDB background flushing can’t keep up; redo log or doublewrite amplifies writes; eventual stalls.

Fix: Improve storage latency/IOPS, validate durability settings, and avoid sharing the disk with unrelated write-heavy services.

5) Symptom: “Switching to Docker volumes made it slower”

Root cause: Volume lives on root filesystem with different mount options, possibly slower media, or contended by Docker image pulls and logs.

Fix: Put volumes on dedicated storage (bind mount to dedicated mount point, or configure Docker data-root to faster disk).

6) Symptom: “Bind mounts are slow on my Mac/Windows laptop”

Root cause: Docker Desktop file sharing crosses a virtualization boundary; host filesystem semantics are emulated and slower.

Fix: For local dev, prefer Docker volumes inside the Linux VM. For production Linux, this specific issue disappears.

7) Symptom: Database corruption after host crash, despite “fsync on”

Root cause: Storage stack lied about durable writes (write cache without power loss protection, unsafe mount options, misconfigured RAID controller).

Fix: Use enterprise-grade storage with power-loss protection; verify cache policies; do not disable barriers; test crash recovery behavior.

8) Symptom: High iowait but low disk %util

Root cause: Often network storage latency, cgroup throttling, or filesystem-level locks. The disk isn’t “busy,” it’s “far away.”

Fix: Measure end-to-end latency (block device stats, network PV metrics), and check CPU throttling and memory pressure.

Three corporate-world mini-stories

Mini-story 1: The incident caused by a wrong assumption

A mid-sized company migrated a payments service from VMs to Kubernetes. The team did the sensible thing on paper: stateful workloads got PVCs, stateless workloads got emptyDir.
They assumed the PVC meant “real disk.” They also assumed the storage class named “fast” meant “low latency.”

The service used PostgreSQL with synchronous commits. During the migration window, everything passed functional tests. Then Monday morning traffic hit, and p99 transaction latency went sideways.
The DB pods were not CPU-bound. They weren’t memory-bound. The nodes looked healthy. The app team blamed Postgres “in containers.”

The actual story: the “fast” storage class was a network filesystem-backed solution that was fine for web uploads and logs, but had nasty tail latency under fsync-heavy workloads.
The PVC did exactly what it promised: persistent storage. It did not promise predictable commit latency.

The fix was boring and structural: move WAL to a lower-latency block-backed class, keep data files on the existing class (or also move, depending on cost), and pin DB pods to nodes with better network paths.
The biggest improvement came from acknowledging that “persistent” is not the same as “database-grade.”

Mini-story 2: The optimization that backfired

Another org ran MariaDB in Docker on a fleet of Linux hosts. Performance was okay, but there were occasional write stalls.
Someone noticed the disk subsystem had a write cache and thought: “We can make fsync cheaper.”

They changed mount options and controller settings to reduce barriers and flush behavior. Benchmarks improved. Everyone high-fived.
Two weeks later, a host rebooted unexpectedly. MariaDB recovered, but a handful of tables had subtle corruption: not immediately catastrophic, but enough to create incorrect results.

The postmortem was painful because nothing was “obviously wrong” in metrics. The failure lived in the gap between what the database asked for (fsync means durable) and what the storage delivered (fast acknowledgment, later persistence).
The incident didn’t show up in a happy-path benchmark. It showed up in physics plus an unplanned power event.

They rolled back the unsafe settings, implemented proper backups with recovery drills, and upgraded storage controllers with power-loss protection.
Performance dipped slightly. The business accepted it. Incorrect data costs more than slower commits, and the accountants understand that language.

Mini-story 3: The boring but correct practice that saved the day

A third team had a habit that looked almost quaint: before any storage change, they ran the same small suite of tests and recorded results.
Not fancy. Just consistent. A few fio profiles, a database-specific write workload, and a snapshot/restore rehearsal.

They were rolling out a new container runtime configuration and moving Docker’s data-root to a different disk.
Someone suggested “it’s just Docker metadata, volumes won’t care.” The team tested anyway.

The tests showed a regression in fsync tail latency after the move. Not huge averages—just worse outliers.
The culprit turned out to be disk contention: the new disk also hosted system logs and a monitoring agent that occasionally spiked writes.

Because they tested before rollout, they caught it early, moved DB volumes to dedicated mounts, and avoided a slow-burn incident.
It wasn’t heroic. It was adult supervision.

Joke #2: The only thing more persistent than a database volume is an engineer insisting their last benchmark “was definitely representative.”

Checklists / step-by-step plan

Step-by-step: picking storage for Postgres or MariaDB in containers

  1. Decide what you need: strict durability vs “we can lose last second.” Write it down. Make the business sign it with their soul (or at least a ticket).
  2. Place DB data off overlay2: use bind mount or named volume, but ensure the backing filesystem is known and intended.
  3. Pick the backing filesystem intentionally: ext4 or XFS are common defaults. ZFS can be great with care, but don’t treat it as magic.
  4. Use dedicated storage for DB: separate mount point. Avoid the shared root disk. Avoid loopback devices.
  5. Isolate WAL/redo logs if commit latency matters: separate device/volume if needed. It’s often the cheapest meaningful win.
  6. Benchmark the path you will run: include fsync. Capture tail latency. Run under realistic concurrency.
  7. Confirm operational workflows: backups, restores, crash recovery drills, and how you migrate/resize volumes.
  8. Implement monitoring tied to failure modes: commit latency, checkpoint duration, fsync times, disk await, and node pressure.
  9. Document mount points and ownership: permissions, SELinux labels, and exactly which host path maps to what container path.
  10. Re-test after every platform change: kernel update, storage class change, runtime change, or node image change.

Checklist: “bind mount vs volume” decision

  • Use a bind mount when you want explicit control over disk choice, mount options, and operational tooling on the host.
  • Use a named volume when you want Docker-managed lifecycle and you trust where Docker’s data root lives (or you use a proper volume driver).
  • Avoid both if they land on network storage with unpredictable latency and you need strict commit performance; instead pick a better storage class/device.

FAQ

1) Are Docker volumes faster than bind mounts on Linux?

Usually they’re the same if they’re on the same underlying filesystem and device. Performance differences come from where they land and what’s backing them, not the mount type.

2) Is it ever okay to run Postgres/MariaDB on overlay2?

For throwaway dev and tests, sure. For anything you care about, no. OverlayFS is optimized for image layering, not database durability and write patterns.

3) Why does PostgreSQL feel more sensitive to storage latency than MariaDB?

PostgreSQL’s commit path often hits WAL fsync immediately (with synchronous commits). If storage latency spikes, your commits spike. InnoDB can buffer differently, then stall later.

4) If I disable fsync or set async commit, will performance be “fixed”?

It will be faster and less correct. You might be buying speed with future corruption or lost transactions after a crash. If you choose that trade, document it like it’s a controlled demolition.

5) Should I separate WAL and data onto different volumes?

Often yes when commit latency matters and you have mixed I/O. WAL is sequential-ish and latency-sensitive; data writes can be bursty at checkpoints. Separation reduces contention.

6) In Kubernetes, is HostPath a good idea for databases?

It can be fast and predictable because it’s local. But it ties you to a node and complicates failover. Local PVs are the more structured version of this idea.

7) Why did bind mounts perform terribly on my laptop?

Docker Desktop uses a VM. Bind mounts cross the host/VM boundary and can be slow. Volumes inside the VM are typically faster for dev.

8) What’s the single most common “volume performance” trap in production?

Assuming a persistent volume implies low latency and good fsync behavior. Persistence is about survival, not speed. Measure latency and tail behavior.

9) ext4 vs XFS for containerized databases?

Both can be excellent. What matters more is device quality, mount options, kernel version, and avoiding pathological layers (loopback, overlay for data, contended root disk).

10) What’s the fastest way to settle an argument between “bind mount is faster” and “volume is faster”?

Put both on the same underlying filesystem/device and run an fsync-including workload. If they differ meaningfully, you’ve discovered an environmental difference, not a philosophical truth.

Next steps you can do this week

  • Audit placement: list all DB containers/pods and prove their datadirs are on mounts, not overlay2.
  • Map storage reality: for each DB, record filesystem type, device, mount options, and whether it’s local or networked.
  • Measure fsync tail latency: run a small fio fsync test on the exact path used by the datadir/WAL/redo.
  • Split logs if needed: isolate PostgreSQL WAL or MariaDB logs to faster storage when commit latency is the problem.
  • Stabilize the node: remove competing write-heavy services from the DB disk, and verify CPU/memory limits aren’t throttling the DB into weird I/O patterns.
  • Write down your durability posture: Postgres and MariaDB both let you trade safety for speed. Make that decision explicit and review it quarterly.

The performance truth isn’t “bind mount vs volume.” It’s “what storage path did you really build, and does it respect fsync under pressure?”
Get that right, and MariaDB and PostgreSQL will both behave like the grown-up systems they are—predictable, fast enough, and unapologetically honest about physics.

← Previous
Docker Service Discovery Fails: DNS and Network Aliases Done Right
Next →
MariaDB vs SQLite for Tiny VPS Projects: When MariaDB Is Overkill

Leave a comment