1. Restart vs database reset
In the Docker world, it is very easy to fall into the trap of intuition from a “regular” application: we see a PostgreSQL container and think, “container = database.” But Docker says, “ha-ha, no.” In reality, a database as a service has two layers: the Postgres process (running in a container) and its data (usually living in a volume). And for our current story, “data” is not only rows in tables, but also the schema itself, flyway_schema_history, and the seed state from which the application later starts. So the command that removes the container may not touch the state that Flyway will look at on the next launch at all.
Let us pin down the core idea in one sentence: the container is a wrapper for the process, while the data is usually placed in a separate storage that survives the container. In practice, for PostgreSQL in Compose, this is almost always a named volume.
To make it easier to picture, keep this analogy in mind (from the “everything in the world is a kitchen” series). A PostgreSQL container is like a pot on a stove: you can remove it from the stove (stop the container), or you can throw away the pot and buy a new one (remove the container and create a new one). But you usually pour the soup (data) into a container-container (volume) that sits in the refrigerator. The soup lives longer than any specific pot.
Here is a mini-diagram of what we want to understand:
flowchart TD
%% The container is the process, the volume is the data (and they live separately)
A[compose up] --> B[Postgres container created]
B --> C[Container mounts named volume]
C --> D[Data is written to volume]
E[docker compose down] --> F[Container removed]
D -->|usually remains| G[Volume remains]
H[docker compose down -v] --> I[Container removed]
I --> J[Volume removed]
If you keep this picture in your head, half of Compose’s “magic” disappears.
2. Postgres data in compose.yaml
Now let us carefully look at how exactly our training project Container-Ready Catalog Service stores PostgreSQL data. It is important to see this with your own eyes: not “in Docker theory,” but in our repository. Because the command docker compose down does not work with your idea of a database, but with a specific configuration — and it is described in YAML.
A typical compose.yaml fragment for PostgreSQL (minimal and to the point) looks roughly like this:
services:
postgres:
image: postgres:18
environment:
# Name of the database that will be created on the container's first start
POSTGRES_DB: catalog
# User that will be created on the container's first start
POSTGRES_USER: catalog
# Password for this user (fine for local dev, not how we do it in production)
POSTGRES_PASSWORD: catalog
volumes:
# Key point: Postgres data is written to /var/lib/postgresql/data,
# but physically stored in a named volume that survives the container.
- postgres-data:/var/lib/postgresql/data
volumes:
# Declare a named volume (Docker will create it automatically if needed)
postgres-data:
Note: /var/lib/postgresql/data is the “home” of Postgres inside the container, where it writes its data files. But in Compose we do not leave these files inside the container’s writable layer; instead, we mount the named volume postgres-data there. That means the physical storage moves “outside” the container.
A question often comes up here: “How do I know that this volume really exists and has a life of its own?” The most direct way is to look at the list of volumes in Docker:
# Show all Docker volumes that currently exist on the machine
docker volume ls
# DRIVER VOLUME NAME
# local docker-java-catalog-service_postgres-data
An important nuance: the volume name in Docker is often not exactly postgres-data, but with a Compose project prefix. By default, Compose takes the project name from the directory name (so you may have docker-java-catalog-service_postgres-data, catalog_postgres-data, and so on). This is normal: this is how Docker protects you from accidental volume conflicts between different projects.
Another sober fact: if you removed the Postgres container, the volume may still continue to exist. It is not permanently “glued” to the container; it is a separate object in Docker.
3. What docker compose down does
The command docker compose down is usually perceived as “well, that’s it, shut down the environment.” And that is almost true — but with one big exception, which creates most of the pain. down removes the containers in your Compose project, removes the network that Compose created for them, and generally “disassembles the environment.” But it does not remove named volumes by default, because volumes are treated as data that you may still need.
Imagine you do this:
# Bring up the stack (containers + network + volume attachment)
docker compose up -d
# Disassemble the stack (containers + network), but keep the volume
docker compose down
# Bring the stack up again — the containers will be new, but the volume will be old
docker compose up -d
From the container perspective, this is “recreate.” From the named volume perspective, this is “use the old data storage.” In other words, you got a new PostgreSQL container (a new process), but you attached an old data directory to it.
And this is very visible in Flyway behavior. When the database is “clean,” Flyway applies migrations when the application starts. When the database is “not clean” (meaning the schema history already exists), Flyway usually quickly reports that everything is already done. A sample log after a repeat start following an ordinary down may look like this:
# Example: the database is NOT clean because flyway_schema_history already exists in the volume
app-1 | Flyway Community Edition ...
app-1 | Schema history table "public"."flyway_schema_history" already exists
app-1 | No migrations necessary
app-1 | Started CatalogApplication
This is a useful signal: you thought you “restarted everything from scratch,” but in reality you started on top of an already existing schema and data.
Another common confusion: beginners expect down to remove absolutely everything, including images. It does not. down does not remove the image you built for app, and it does not remove postgres:18, which you downloaded. It does not do “spring cleaning”; it simply disassembles the specific container stack.
Sometimes it is worth comparing down with stop for a second. docker compose stop usually stops the containers, but leaves them as objects (you can start them again later). docker compose down goes further: it removes the containers so that the next up creates new ones. But again, the volume remains, and that is exactly why a “new container” does not mean a “new empty database.”
4. What docker compose down -v does
Now to the command that differs outwardly by just one letter, but in meaning is like “close a tab” versus “nuke Windows.” The -v flag on docker compose down tells Compose: “please delete the volumes declared in the file together with the containers.”
That is, like this:
# Disassemble the stack and delete named volumes from compose.yaml at the same time
docker compose down -v
# Bring it up again — Postgres gets a new empty volume
docker compose up -d
This scenario means: the containers will be recreated, the network will be recreated, and PostgreSQL will get a completely new, empty volume, because you deleted the old one. For the database, this is “I was just born.”
And here Flyway becomes obvious again. After down -v and a new up, the application will once again apply migrations from scratch at startup, because the schema history table does not exist yet in the clean database:
# Example: the database is clean, Flyway creates the history table and reapplies migrations
app-1 | Flyway Community Edition ...
app-1 | Creating schema history table "public"."flyway_schema_history"
app-1 | Successfully applied 2 migrations
app-1 | Started CatalogApplication
An important thing in this lecture is not to go to extremes: down -v is not “more correct” than down. It is simply a different tool.
down -v is a reset of the local database to a clean state. It is useful when you need to reproducibly return to a baseline, rather than simply restart processes. It is like “start the level over” rather than “pause the game and continue.”
And yes, it really does delete data. At this point in the course, there is usually one student who happily runs down -v in a project where “I was carefully editing the database by hand for a week” — and suddenly realises that “manual editing” was not the best knowledge storage strategy. Docker is not evil — it is just honest.
5. How to choose down or down -v
Now we will turn two commands into a clear engineering decision: which one to use and why. It is important not to memorise the rule “always use -v,” but to understand the context. Otherwise, you will either keep losing data, or keep dragging “dirty” database state along, which hurts reproducibility.
The most common scenario for docker compose down is when you just need to “rebuild the environment” after changing the configuration, the application image, or simply to get rid of stuck containers. For example, you changed SPRING_PROFILES_ACTIVE, adjusted a port, tweaked the healthcheck, updated the app image, and want Compose to recreate the containers. In this case, the DB data does not interfere with you, and may even be useful: you do not want to lose the schema, Flyway history table, and working/seed records every time.
But docker compose down -v is needed as a deliberate “reset to a clean state.” Here it is important not to confuse the goal: -v does not fix the SQL error itself and is not a mandatory step after every Flyway failure. For our PostgreSQL baseline, the normal path after fixing a migration file is to bring the stack up again on the same volume: the schema, history table, and data remain, and the unapplied migration is simply tried again. -v is for a different task — when you want to deliberately erase local state, replay the full migration history from scratch, verify a seed baseline, or remove accumulated drift.
To make it easier for the brain, you can keep this table in mind:
| Command | Containers | Compose network | PostgreSQL named volume | Expected effect |
|---|---|---|---|---|
| docker compose down | removed | removed | stays | “restart the environment without losing the DB” |
| docker compose down -v | removed | removed | removed | “full DB reset to a clean state” |
Another practical point: down -v is not a “fix” for the problem as such. If your migration is broken (the SQL is invalid), deleting the volume does not make the SQL correct. It only returns you to a known point so that after fixing the migration you can make sure the project really starts “from scratch” on a clean database. The source of truth still lives in the repository.
Checking that the database is old
When a person sees that the application starts strangely, the first reaction is usually “well, I’ll just restart it a couple of times.” That is a normal human reaction, but it does not work well with containers. Here it is very important to replace guessing with observation: check whether the volume is still there, whether the containers are still there, how Compose named the project, and what state Postgres is in.
You can start with the simplest thing: see what is currently running and what has been created:
# -a also shows stopped containers (useful when analysing failures)
docker compose ps -a
# NAME STATUS
# catalog-postgres-1 Exited (0)
# catalog-app-1 Exited (1)
Next, inspect volumes. If you ran down, the containers will no longer appear in ps, but the volume will most likely remain:
# The volume remains even if the containers have already been removed
docker volume ls
# local docker-java-catalog-service_postgres-data
If you ran down -v, and after that docker volume ls no longer shows your volume, then the database really has been reset.
Sometimes it is useful to clarify “which volume corresponds to my project.” To do that, you can look at which volumes are declared for the project directly in compose.yaml, and remember that Docker stores them with the Compose project prefix. If you have several training repositories on your computer, then “postgres-data” may turn into several different Docker volumes with different prefixes — and that is normal.
And finally, the most practical signal is the logs. In this course, you already know how to read the logs of app and postgres together. The idea is the same here: if Flyway does not apply migrations after a restart, then the database is not clean; if it does, then the database is new. You do not even need to be a DBA — just pay attention to a few log lines.
6. Typical mistakes when resetting a local DB in Compose
Mistake #1: thinking that docker compose down “clears the database.”
Usually this shows up like this: you “shut down the environment,” bring it back up, and the migrations are not applied and the data “somehow” remains. In fact, nothing mystical happened: you simply left the named volume alone, and Postgres came up on the old state. That is expected and correct.
Mistake #2: using docker compose down -v as the first reaction to any problem.
Sometimes a student hits the first startup error and immediately deletes the volume. Yes, that “helps,” because you return to a clean state. But it becomes a habit that masks real configuration problems and makes life harder: you lose data every time and wait for migrations again, even when you do not need to.
Mistake #3: trying to “heal” a broken migration only with a reset.
If a migration fails because of an SQL error, deleting the volume gives you a clean database, but on the next launch you will fail at the same place again. Reset is part of the recovery workflow, but not a substitute for fixing the migration. The source of truth is Vx__*.sql in the repository, not your local database.
Mistake #4: confusing the “PostgreSQL container” and “PostgreSQL data.”
It looks like this: a person removed the container and is sure they “deleted the database.” But the data moved into the volume, and on the next up it will come back. To avoid being surprised by this, keep the combination in mind: container = process, volume = data. You removed only the process — the data remained.
Mistake #5: not checking the result of the reset by observable signs.
Sometimes people run down, then up, then only look at the fact that “the containers are running” and calm down. But now that is no longer enough. It is better to check two things: what Flyway says (whether it applied migrations) and what the API/health endpoint shows. If you expect a clean database and Flyway says “No migrations necessary,” then “clean” was just a dream, not reality.
GO TO FULL VERSION