1. Home
  2. Blog
  3. DevOps

DevOps

Docker Volumes vs Bind Mounts: Persist Container Data the Right Way

Remove a container and its data goes with it, unless you store it somewhere safer. Learn the difference between volumes, bind mounts and tmpfs, when to use each, and how to back up what matters.

9 min read
Docker volumes vs bind mounts: persist container data the right way

Key takeaways

  • A container’s writable layer is deleted along with the container, so important data needs a mount.
  • Named volumes are managed by Docker, and are the usual choice for databases and production data.
  • Bind mounts map a host folder, which makes them ideal for live code during development.
  • docker compose down -v deletes volumes, so use it with care, and keep tested backups.

You start a database in a container, add some data, remove the container and start a new one. The data is gone. Almost everyone learns this lesson the hard way. Docker volumes and bind mounts exist to keep data alive beyond the life of any one container.

This guide explains how each one works and how to choose. The commands and files here are standard examples, based on Docker’s documented behaviour, and we did not run them for this article, so try them first on throwaway data. Details can differ between Docker versions and on Docker Desktop. If you are new to containers, begin with our Docker tutorial for beginners.

Why container data disappears

A Docker image is made of read-only layers. When a container starts, Docker adds a thin writable layer on top, and any changes go there. That layer survives a stop and restart, but it is removed when the container is deleted.

docker run -d --name db -e POSTGRES_PASSWORD=devonly postgres:17
# ... create tables and add data ...
docker rm -f db
docker run -d --name db -e POSTGRES_PASSWORD=devonly postgres:17
# The data you added is not there

This is by design, since it lets you replace containers freely. It does mean that anything you need to keep has to live outside the container. There are three ways to do that:

Type Stored in Survives container removal?
Named volume An area managed by Docker Yes
Bind mount A host path that you choose Yes
tmpfs mount Host memory No

Docker volumes vs bind mounts side by side

Aspect Named volume Bind mount
Managed by Docker You
Location Docker’s storage area Any path on the host
Depends on host folders? No Yes
Editing from the host Awkward by design Easy, changes appear instantly
Empty target pre-filled from image? Yes No, the host path hides it
Docker CLI commands Yes, docker volume No
Drivers for remote storage Yes No
Best for Databases, production data Source code in development

Named volumes

A volume is storage created and managed by Docker. You refer to it by name, and you do not need to care where it physically lives.

docker volume create pgdata

docker run -d --name db \
  -e POSTGRES_PASSWORD=devonly \
  -v pgdata:/var/lib/postgresql/data \
  postgres:17

Now the database files live in pgdata. Delete the container and start a new one with the same volume, and your data will still be there. On Linux, volumes usually sit under /var/lib/docker/volumes/, but you should not edit those files directly. On Docker Desktop, they live inside a virtual machine, not in a normal folder.

The path inside the container depends on the image, so check the image’s documentation. Mounting to the wrong path is a classic mistake: everything appears to work, but the data is still being written to the container layer. Newer major versions of an image may also change this path, so read the release notes before upgrading.

Docker will create a named volume automatically if it does not exist. An anonymous volume, created with no name, also works, but it gets a long random ID and is easy to lose track of. Always give your volumes names.

Bind mounts

A bind mount maps a specific host path directly into the container. Both sides see the same files, immediately. This makes it perfect for development, as edits in your editor take effect in the running container without a rebuild:

docker run -d --name web \
  -p 8080:80 \
  -v "$(pwd)/site:/usr/share/nginx/html:ro" \
  nginx:1.27

Edit a file in site/, refresh the browser and you will see the change. The :ro makes the mount read-only inside the container, which is a good habit when the container has no need to write there.

Bind mounts come with some trade-offs:

  • They tie you to the host. The path has to exist on each machine, so setups are less portable.
  • They hide the image’s content. If you mount over /app, whatever the image had in there becomes invisible. A common example is node_modules going missing.
  • They expose the host. A container with a writable mount can change or delete real host files. Never mount sensitive paths like / or the Docker socket unless you fully understand the risk.
  • Performance can vary. On Docker Desktop for macOS and Windows, file sharing crosses a virtualization boundary, and can be slower than on Linux.

tmpfs mounts

A tmpfs mount lives in the host’s memory, and is never written to the container layer. It vanishes when the container stops, so it suits temporary files, caches and short-lived sensitive data:

docker run -d --tmpfs /app/cache:size=64m myapp:1.0

tmpfs is available on Linux hosts. It can also speed up databases in throwaway test environments, where you do not care about keeping the data.

-v or –mount?

Both flags work. --mount is longer, but it is more explicit, and many people prefer it in scripts:

# Short form
docker run -v pgdata:/var/lib/postgresql/data postgres:17

# Explicit form
docker run --mount type=volume,source=pgdata,target=/var/lib/postgresql/data postgres:17

# Bind mount, read-only
docker run --mount type=bind,source="$(pwd)"/site,target=/usr/share/nginx/html,readonly nginx:1.27

There is one meaningful difference. With -v, a missing host path gets created for you as an empty folder, which can hide a typo. With --mount and a bind type, Docker reports an error instead. Also, with -v, the left side decides the type: a name means a volume, and a path means a bind mount. So -v data:/x and -v ./data:/x do very different things.

Volumes in Docker Compose

Most real projects declare storage in a Compose file. This one uses both kinds, each for its proper job:

services:
  web:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src      # bind mount: live code
    depends_on:
      - db

  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: devonly
    volumes:
      - pgdata:/var/lib/postgresql/data   # named volume: data

volumes:
  pgdata:

Named volumes must be declared in the top-level volumes section. Compose prefixes the actual name with the project name. See our Docker Compose tutorial for a full walkthrough.

Tip: docker compose down keeps named volumes, but docker compose down -v deletes them, along with your data. Be especially careful about scripts and shell history that include -v.

Useful volume commands

Command What it does
docker volume ls Lists volumes
docker volume create name Creates a volume
docker volume inspect name Shows details, including its location
docker volume rm name Deletes a volume and its data
docker volume prune Removes unused volumes
docker system df -v Shows disk use, including volumes

Check what prune will remove before you confirm it, since this cannot be undone. Volumes that no container currently uses may still hold important data.

Back up and restore a volume

A volume can be backed up by running a short-lived container that mounts both the volume and a host folder:

# Back up
docker run --rm \
  -v pgdata:/data:ro \
  -v "$(pwd)":/backup \
  alpine tar czf /backup/pgdata.tar.gz -C /data .

# Restore into a new volume
docker volume create pgdata_restored
docker run --rm \
  -v pgdata_restored:/data \
  -v "$(pwd)":/backup \
  alpine tar xzf /backup/pgdata.tar.gz -C /data

For a live database, copying files like this can capture an inconsistent state. Stop the container first, or better, use the database’s own tool, such as pg_dump for PostgreSQL. And a backup you have never restored is really just a hope, so test the restore.

File permissions

“Permission denied” is the most common problem with mounts. Files keep their numeric user and group IDs on Linux, so if a container runs as user 1000 and the host folder belongs to another user, writes will fail. Some ways to fix this:

  • Run the container with a matching user: docker run --user "$(id -u):$(id -g)" ...
  • Set up correct ownership in your image, as described in our Dockerfile best practices.
  • Prefer named volumes for data, as a new volume takes on the ownership of the image’s folder when it is first used.
  • On systems using SELinux, bind mounts may need a :z or :Z label.

Avoid the temptation of chmod 777, as our guide to Linux commands explains.

Which should you use?

Situation Use
Database data Named volume
Live-editing source code Bind mount
A configuration file from your repository Read-only bind mount
Sharing data between containers Named volume
Temporary cache or scratch files tmpfs
Throwaway test database tmpfs, or no mount
Application code in production Build it into the image

That last row matters. In production, code belongs in the image, so that what you tested is what you run. In clusters, data storage works differently again, through persistent volumes, which are covered in the wider Kubernetes documentation. Our Docker vs Kubernetes guide explains how the two relate.

Common mistakes

  • Mounting to the wrong path. The data still lives in the container layer, and it disappears later on.
  • Running down -v casually. This deletes your named volumes.
  • Using relative paths with -v. Forms like -v data:/x create a volume rather than a folder mapping.
  • Putting secrets in writable bind mounts. Mount them read-only, or use a secrets feature.
  • Pointing two database containers at one volume. This can corrupt the data.
  • Having no backups. A volume is not a backup. It lives on the same disk.

Final thoughts

The rule of thumb is short: named volumes for data, bind mounts for code you are editing, and tmpfs for things that should not last. Try it on a throwaway database. Create a named volume, add a row, delete the container, start a new one, and confirm that your row survived. Then practise a backup and a restore before you need one.

Frequently asked questions

What is the difference between a Docker volume and a bind mount?

A volume is managed by Docker and stored in its own area. A bind mount maps a specific host path that you control, directly into the container.

Does stopping a container delete its data?

No. Data in the writable layer survives a stop and restart. It is lost when the container is removed, unless it is stored in a volume or bind mount.

Do Docker volumes persist after docker compose down?

Yes, named volumes remain after docker compose down. Adding the -v flag deletes them, along with any data they contain.

Where are Docker volumes stored?

On Linux, usually under /var/lib/docker/volumes. On Docker Desktop, they live inside a virtual machine. Use docker volume inspect to see the location.

Can multiple containers share a volume?

Yes, several containers can mount one volume. The application must handle concurrent access safely, and databases generally should not share data files this way.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Structured Output Explained: Getting Reliable JSON From AI Models

LLM structured output explained with validation we ran in Pydantic: JSON mode and schemas, constrained decoding, the ways model output…

Read article →
DevOps

SSH Keys Explained: How They Work, ssh-keygen, Ed25519 and Safe Habits

SSH keys explained with keys we generated: public vs private keys, ssh-keygen, Ed25519 vs RSA, fingerprints, passphrases, ssh-agent, authorized_keys, config…

Read article →
DevOps

HTTP Caching Explained: Cache-Control, ETag and 304 Responses, With a Live Test

HTTP caching explained with a small server we ran: Cache-Control directives, max-age, no-cache vs no-store, ETag revalidation and 304 responses,…

Read article →
Test Automation

API Contract Testing Explained: Catch Breaking Changes Before They Ship

API contract testing explained with a schema check we ran: what a contract is, consumer-driven contracts, Pact, OpenAPI, which changes…

Read article →
Software Testing

Risk-Based Testing Explained: Score Risks, Build a Matrix and Test What Matters

Risk-based testing explained step by step: identifying risks, scoring likelihood and impact, building a risk matrix, mapping risk to test…

Read article →
Databases & SQL

SQL Constraints Explained: NOT NULL, UNIQUE, CHECK and FOREIGN KEY, Tested

SQL constraints explained with tests we ran: NOT NULL, UNIQUE, CHECK, DEFAULT, PRIMARY and FOREIGN KEY, the NULL loopholes, SQLite's…

Read article →
Databases & SQL

SQL Date Functions Explained: Adding, Comparing and Grouping Dates Without Bugs

SQL date functions explained with queries we ran: adding intervals, month-end surprises, date differences, the BETWEEN trap, grouping by day,…

Read article →
Software Development

Python asyncio Explained: async, await, gather and TaskGroup With Real Timings

Python asyncio explained with code we timed: coroutines, await, gather, TaskGroup, error handling, timeouts, limiting concurrency and the blocking-call mistake…

Read article →
Software Development

Debounce vs Throttle in JavaScript: How They Work, When to Use Each, Tested

Debounce vs throttle in JavaScript explained with timings we measured in Node.js: how each works, simple implementations, leading and trailing…

Read article →
Software Development

Python Context Managers Explained: The with Statement, contextlib and Real Uses

Python context managers explained with code we ran: how with works, __enter__ and __exit__, contextlib.contextmanager, suppress, ExitStack, database transactions and…

Read article →
Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →