skip to content

A `postgres` container ignores an updated `/docker-entrypoint-initdb.d` script on later starts. Why, and what do you do?

level: seniorimportance: should knowfreq 46%

answer

  1. Something about the first start only
  2. The entrypoint checks before it initialises
  3. An initialised data directory skips the hook
  4. Look for the skipping-initialization log line
  5. Bootstrap hook, not a migration tool

basics

~20 s

The image's entrypoint runs /docker-entrypoint-initdb.d only when it initializes an empty data directory. With a persistent data volume already initialized, it skips straight to starting the server, so edited scripts never run. Schema changes belong in a migration tool.

solid answer

~50 s

The initialization hook is a **bootstrap**, not a migration mechanism. On start, the Official Postgres image's entrypoint checks whether its data directory already contains a database; if it does, it logs a line to the effect that the directory appears to contain a database and initialization is being skipped, and executes the server directly. Everything under `/docker-entrypoint-initdb.d` — the `.sql`, `.sql.gz` and `.sh` files it runs in filename order — is executed only on the run that creates the cluster. So once a data volume exists, editing those files changes nothing. Confirm it by reading the container's logs for that skip line. The fix depends on the environment: in development or CI, remove the container and its data volume so the next start initializes fresh; anywhere with real data, apply the change as a schema migration owned by the application, never by deleting the volume.

code

bash · 9 lines
bash
docker logs billing-db 2>&1 | head -20

docker rm -f billing-db
docker volume rm billing-pgdata
docker run -d --name billing-db \
  -e POSTGRES_PASSWORD=devpass \
  -v billing-pgdata:/var/lib/postgresql/data \
  -v "$PWD/initdb":/docker-entrypoint-initdb.d:ro \
  postgres:16

go deeper

for a junior

Remember that a service image's initialization scripts run when its database is first created, not every time the container starts. If your SQL seems to be ignored, the data directory probably already exists.

for a middle

Explain the two branches of the image's entrypoint — initialize an empty data directory and run the hook files in filename order, or detect an existing cluster and skip straight to the server — and how a persistent volume decides which one you get.

for a senior

Show the diagnosis and the boundary: prove which branch ran from the container's logs, reset volumes only in development or CI, and move schema changes into versioned migrations owned by the application.

for a principal

Own the convention across teams: where seed data ends and schema migration begins, how environments are reset safely, and how you stop a bootstrap hook quietly becoming the only record of a production schema.

### The scenario A subscription-billing cron — a .NET service on a runtime image — talks to a Postgres container that was bootstrapped months ago by three files in `/docker-entrypoint-initdb.d`. Somebody adds a 47-line `03-invoice-run.sql` to that directory, recreates the container, and the 02:14 cron run fails with a relation not existing. The container is healthy, the logs contain no error, and the file is demonstrably present inside the container. This is one of the most reliably confusing Docker experiences there is, and the explanation is worth knowing precisely. ### What the entrypoint actually does The Official Postgres image's entrypoint script runs before the server. Its first job is to decide whether this is a *first* start. It looks at the data directory (`/var/lib/postgresql/data` by default, or wherever `PGDATA` points) and checks whether an initialized cluster is already there — a version marker file is the tell. Two branches follow: * **Empty directory.** The script initializes a fresh cluster, applies the initialization environment variables (`POSTGRES_USER`, `POSTGRES_DB`, the password), starts a temporary local-only server, and then runs everything in `/docker-entrypoint-initdb.d` in filename sort order: `.sql` files piped into the client, `.sql.gz` decompressed first, `.sh` files executed. Only then does it shut the temporary server down and start the real one. * **Already initialized.** It logs a message saying the directory appears to contain a database and that initialization is being skipped, and executes the server immediately. The hook directory is not even looked at. That second branch is the whole answer. The hook is tied to cluster creation, not to container creation, and the thing that persists across container recreation is the data volume. Mount a volume that was initialized in March and every container you create against it in September takes the skip branch. ### Diagnosing it in under a minute Read the container's logs from the start of the run and look for the skip message; its presence proves the branch that was taken. If you see the initialization sequence instead — cluster creation followed by the names of the files being run — then the hook did fire and your problem is inside the SQL, which will have logged an error. Two further checks separate the remaining cases: whether the data directory is non-empty, and whether the files are actually where the image expects them (mounted at the exact hook path, readable by the user the server runs as, and with an extension the script recognises — a file named `03-invoice-run.txt` is silently ignored). ### The opposite failure The mirror-image confusion is a container with **no** data volume mounted at all. The image declares its data directory as a volume, so each container gets a fresh anonymous volume, initialization runs every single time, and every `docker rm` throws the data away. Symptomatically this looks like the good case — your init scripts clearly work — right up to the moment somebody notices last week's rows are gone. 'Init scripts always run' and 'my data survives' are mutually exclusive, and which one you are getting is decided entirely by whether a persistent volume is mounted at the data directory the image documents. ### Fixing it, and the design lesson In development or CI, the fix is blunt and fine: remove the container, remove the data volume, start again, and the fresh cluster runs the updated scripts. Make that a scripted one-liner so nobody is tempted to improvise it somewhere it does not belong. Anywhere holding real data, deleting the volume is not a fix, it is an outage. Apply the change the way every other schema change should be applied: as a versioned migration owned by the application and executed at deploy time. The initialization hook's contract is 'make an empty database usable once'; it has no notion of version, no record of what it has already applied, no ordering guarantee beyond filenames, and no rollback. Treating it as a migration system means your schema history exists only as the diff between what the current scripts say and what production actually contains — which is exactly the state the billing team discovered at 02:14. Use the hook for local development seed data and CI fixtures, use migrations for schema, and never let the two diverge silently.

  • How do you make the initialization scripts run again in a development environment?
    Remove the container and then remove the data volume it was using, so the next start finds an empty data directory and takes the initialization branch. Script it, because it is destructive by design: the same commands against a volume holding real data destroy that data, which is why the production answer is a migration instead.
  • In what order do files in the initialization directory run, and which are executed?
    In filename sort order, which is why teams prefix them numerically — `01-roles.sql`, `02-schema.sql`. The Postgres image pipes `.sql` files into the client, decompresses and pipes `.sql.gz`, and executes `.sh` files. A file with any other extension is ignored without complaint, which is a common reason a script 'does not run'.
  • The hook did run but one statement failed. How does that show up?
    During initialization the entrypoint runs the files against a temporary local server and a failure surfaces as an error in the container's logs, typically leaving the container exited rather than serving. That is the good case: you get a message and a name. Silence with a healthy container almost always means the skip branch, not a failing statement.

The hook is the checklist used when a new office is first fitted out, not the maintenance schedule. Once the office exists, rewriting the fit-out checklist changes nothing about the building.

saying these in an interview costs you the question

  • Believing init scripts run on every container start
  • Expecting `docker restart` to reapply the SQL
  • Deleting the data volume to fix it in production
  • Treating the init hook as a schema migration system
  • Assuming a file with any extension will be executed
  • Not checking whether a data volume is mounted at all

context