Skip to content

PostgreSQL 18: Init scripts in /docker-entrypoint-initdb.d/ never execute due to automatic version subdirectory creation #1373

Closed

Description

@JoMiPeCa

Bug Report: PostgreSQL 18 - Initialization scripts in /docker-entrypoint-initdb.d/ never execute

Description

Scripts placed in /docker-entrypoint-initdb.d/ are never executed when using PostgreSQL 18 Docker image because the data directory is never considered "empty" due to automatic creation of version subdirectory.

Environment

  • PostgreSQL Version: 18 (postgres:18)
  • Docker Version: 24.x+
  • Docker Compose Version: 2.40.0
  • Host OS: Linux (Ubuntu/Debian based)

Steps to Reproduce

  1. Create a docker-compose.yml:
services:
  postgres:
    image: postgres:18
    container_name: test-postgres
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: test123
      POSTGRES_DB: postgres
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d
    ports:
      - "5432:5432"

volumes:
  postgres-data:
  1. Create init script at ./init/01-init.sql:
CREATE DATABASE testdb;
  1. Start container with empty volume:
docker compose down -v
docker compose up -d
  1. Check if database was created:
docker exec test-postgres psql -U postgres -c '\l'

Expected Behavior

The testdb database should be created as per the init script in /docker-entrypoint-initdb.d/.

Actual Behavior

The init script is never executed. Only default databases (postgres, template0, template1) exist.

The script exists in the container:

$ docker exec test-postgres ls -la /docker-entrypoint-initdb.d/
-rw-r--r--. 1 1050  993 1563 Oct 16 07:27 01-init-databases.sql

But it was never executed.

Root Cause Analysis

Upon investigation, we found that:

  1. PostgreSQL 18 creates a version subdirectory automatically on startup:
$ docker exec test-postgres ls -la /var/lib/postgresql/data/
total 12
drwxrwxrwt. 3 postgres postgres 4096 Oct 16 07:36 .
drwxr-xr-x. 1 root     root     4096 Sep 30 00:06 ..
drwxr-xr-x. 3 root     root     4096 Oct 16 07:36 18  # <-- Created automatically
lrwxrwxrwx. 1 root     root        1 Sep 30 00:06 data -> .
  1. The postgres:18 image has a symlink in /var/lib/postgresql/:
$ docker run --rm postgres:18 ls -la /var/lib/postgresql/
lrwxrwxrwx. 1 root     root        1 Sep 30 00:06 data -> .  # <-- Symlink in base image
  1. The docker-entrypoint.sh only runs init scripts if the data directory is empty (per documentation)
  2. Because the 18/ subdirectory is created during initialization, the directory is no longer "empty"
  3. Therefore, initialization scripts are skipped

Version Comparison

This issue does NOT occur with PostgreSQL 17 or earlier versions:

PostgreSQL 17:

$ docker run --rm postgres:17 ls -la /var/lib/postgresql/data/
# Directory starts empty, init scripts execute correctly ✅

PostgreSQL 18:

$ docker run --rm postgres:18 ls -la /var/lib/postgresql/data/
# Directory contains '18/' subdirectory immediately
# Init scripts never execute ❌

Impact

  • Severity: Critical
  • All initialization scripts are silently ignored
  • Users cannot pre-seed databases on first container startup
  • Breaking change from PostgreSQL 17 behavior
  • No error messages or warnings are shown to indicate scripts were skipped
  • Breaks existing docker-compose configurations that worked perfectly with PostgreSQL ≤17

Workaround

Manually execute init scripts after container starts:

docker exec -i test-postgres psql -U postgres < init/01-init.sql

Or add a custom entrypoint wrapper to run scripts manually.

Proposed Fix

Option 1: Modify docker-entrypoint.sh to check if the data directory contains only the version subdirectory (e.g., 18/) and the data symlink, and treat it as "empty" for initialization purposes.

Option 2: Delay the creation of the version subdirectory until after init scripts have been executed.

Option 3: Add a flag or environment variable to force execution of init scripts even when directory is not empty.

Additional Context

  • The symlink data -> . has existed in previous PostgreSQL versions without causing this issue
  • The automatic creation of the 18/ subdirectory appears to be new behavior in PostgreSQL 18
  • This breaks existing production workflows and CI/CD pipelines
  • Spent 3+ hours debugging before identifying root cause
  • Multiple users are likely affected but may not realize their init scripts are being silently ignored

Related Issues

Environment Details

PostgreSQL: postgres:18 (latest as of October 2025)
Docker: 24.0+
Docker Compose: 2.40.0

Activity

  1. tianon commented on Oct 16, 2025

    @tianon
    Member

    I tried your example verbatim, and it worked fine for me, although I actually expected it to error out, so that's maybe worth digging into later.

    Here's the output of my docker-compose up:

    $ docker-compose up
    Creating network "pg1373_default" with the default driver
    Creating volume "pg1373_postgres-data" with default driver
    Pulling postgres (postgres:18)...
    18: Pulling from library/postgres
    8c7716127147: Already exists
    1014e14b3351: Pull complete
    edd90ab5059f: Pull complete
    f0d70120d9e2: Pull complete
    dd6d7b9d8ba8: Pull complete
    203b16f56a7d: Pull complete
    751039babae5: Pull complete
    f5af7533693a: Pull complete
    9a68d6020eab: Downloading [=============================>                     ]   67.7MB/115.2MB9a68d6020eab: Downloading [===============================>                   ]  73.56MB/115.2MB              9a9a68d6020eab: Pull complete
    f69a7c424b50: Pull complete
    eed0ac863490: Pull complete
    2433c366ca00: Pull complete
    a585c5f82f15: Pull complete
    af60ce4418c9: Pull complete
    Digest: sha256:073e7c8b84e2197f94c8083634640ab37105effe1bc853ca4d5fbece3219b0e8
    Status: Downloaded newer image for postgres:18
    Creating test-postgres ... done
    Attaching to test-postgres
    test-postgres | The files belonging to this database system will be owned by user "postgres".
    test-postgres | This user must also own the server process.
    test-postgres | 
    test-postgres | The database cluster will be initialized with locale "en_US.utf8".
    test-postgres | The default database encoding has accordingly been set to "UTF8".
    test-postgres | The default text search configuration will be set to "english".
    test-postgres | 
    test-postgres | Data page checksums are enabled.
    test-postgres | 
    test-postgres | fixing permissions on existing directory /var/lib/postgresql/18/docker ... ok
    test-postgres | creating subdirectories ... ok
    test-postgres | selecting dynamic shared memory implementation ... posix
    test-postgres | selecting default "max_connections" ... 100
    test-postgres | selecting default "shared_buffers" ... 128MB
    test-postgres | selecting default time zone ... Etc/UTC
    test-postgres | creating configuration files ... ok
    test-postgres | running bootstrap script ... ok
    test-postgres | performing post-bootstrap initialization ... ok
    test-postgres | syncing data to disk ... ok
    test-postgres | 
    test-postgres | 
    test-postgres | Success. You can now start the database server using:
    test-postgres | 
    test-postgres |     pg_ctl -D /var/lib/postgresql/18/docker -l logfile start
    test-postgres | 
    test-postgres | initdb: warning: enabling "trust" authentication for local connections
    test-postgres | initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.
    test-postgres | waiting for server to start....2025-10-16 18:21:55.469 UTC [49] LOG:  starting PostgreSQL 18.0 (Debian 18.0-1.pgdg13+3) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
    test-postgres | 2025-10-16 18:21:55.471 UTC [49] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
    test-postgres | 2025-10-16 18:21:55.478 UTC [55] LOG:  database system was shut down at 2025-10-16 18:21:55 UTC
    test-postgres | 2025-10-16 18:21:55.482 UTC [49] LOG:  database system is ready to accept connections
    test-postgres |  done
    test-postgres | server started
    test-postgres | 
    test-postgres | /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/01-init.sql
    test-postgres | CREATE DATABASE
    test-postgres | 
    test-postgres | 
    test-postgres | waiting for server to shut down....2025-10-16 18:21:55.726 UTC [49] LOG:  received fast shutdown request
    test-postgres | 2025-10-16 18:21:55.728 UTC [49] LOG:  aborting any active transactions
    test-postgres | 2025-10-16 18:21:55.730 UTC [49] LOG:  background worker "logical replication launcher" (PID 58) exited with exit code 1
    test-postgres | 2025-10-16 18:21:55.730 UTC [53] LOG:  shutting down
    test-postgres | 2025-10-16 18:21:55.731 UTC [53] LOG:  checkpoint starting: shutdown immediate
    test-postgres | 2025-10-16 18:21:55.779 UTC [53] LOG:  checkpoint complete: wrote 943 buffers (5.8%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.022 s, sync=0.021 s, total=0.050 s; sync files=303, longest=0.003 s, average=0.001 s; distance=4352 kB, estimate=4352 kB; lsn=0/1B9FBA8, redo lsn=0/1B9FBA8
    test-postgres | 2025-10-16 18:21:55.789 UTC [49] LOG:  database system is shut down
    test-postgres |  done
    test-postgres | server stopped
    test-postgres | 
    test-postgres | PostgreSQL init process complete; ready for start up.
    test-postgres | 
    test-postgres | 2025-10-16 18:21:55.878 UTC [1] LOG:  starting PostgreSQL 18.0 (Debian 18.0-1.pgdg13+3) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
    test-postgres | 2025-10-16 18:21:55.879 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
    test-postgres | 2025-10-16 18:21:55.879 UTC [1] LOG:  listening on IPv6 address "::", port 5432
    test-postgres | 2025-10-16 18:21:55.882 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
    test-postgres | 2025-10-16 18:21:55.888 UTC [71] LOG:  database system was shut down at 2025-10-16 18:21:55 UTC
    test-postgres | 2025-10-16 18:21:55.892 UTC [1] LOG:  database system is ready to accept connections

    Note in particular that it includes test-postgres | /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/01-init.sql

  2. tianon commented on Oct 16, 2025

    @tianon
    Member
    $ docker exec test-postgres psql -Upostgres -l
                                                        List of databases
       Name    |  Owner   | Encoding | Locale Provider |  Collate   |   Ctype    | Locale | ICU Rules |   Access privileges   
    -----------+----------+----------+-----------------+------------+------------+--------+-----------+-----------------------
     postgres  | postgres | UTF8     | libc            | en_US.utf8 | en_US.utf8 |        |           | 
     template0 | postgres | UTF8     | libc            | en_US.utf8 | en_US.utf8 |        |           | =c/postgres          +
               |          |          |                 |            |            |        |           | postgres=CTc/postgres
     template1 | postgres | UTF8     | libc            | en_US.utf8 | en_US.utf8 |        |           | =c/postgres          +
               |          |          |                 |            |            |        |           | postgres=CTc/postgres
     testdb    | postgres | UTF8     | libc            | en_US.utf8 | en_US.utf8 |        |           | 
    (4 rows)
    
  3. JoMiPeCa commented on Oct 18, 2025

    @JoMiPeCa
    Author

    Thank you @tianon for the quick response and the solution!

    I can confirm that changing the volume mount path from /var/lib/postgresql/data to /var/lib/postgresql completely solved the issue.

    Working Configuration

    docker-compose.yml:

    services:
      postgres:
        image: postgres:18
        volumes:
          - postgres-data:/var/lib/postgresql  # ✅ Changed from /var/lib/postgresql/data
          - ./postgres/init:/docker-entrypoint-initdb.d
    
    Verification
    
    After recreating the container with the corrected volume path:
    
    $ docker logs peluka-postgres 2>&1 | grep "running /docker-entrypoint-initdb.d"
    /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/01-init-databases.sql
    CREATE DATABASE
    CREATE DATABASE
    CREATE DATABASE
    
    All init scripts executed successfully on first startup! 🎉
    
    Additional Note
    
    I also had to fix the locale in my init script from es_CL.UTF-8 to en_US.utf8 (the default locale available in the container), but that was unrelated to the volume mount issue.
    
    Closing
    
    This was indeed not a bug, but a documentation/migration issue for users upgrading from PostgreSQL ≤17 to PostgreSQL 18. The references you provided were extremely helpful:
    - https://github.com/docker-library/postgres/pull/1259
    - https://github.com/docker-library/docs/pull/2582
    
    Thanks again for the rapid support! Closing this issue as resolved.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions