initdb: error: directory "/var/lib/postgresql/data" exists but is not empty — the postgres container found something already in its data directory, refused to create a database on top of it and exited with code 1. Point PGDATA at a subdirectory of the mount, or mount an empty named volume, and it starts.

With docker run that is one more option, -e PGDATA=/var/lib/postgresql/data/pgdata; in a compose file the same variable goes under environment. Before you empty the folder instead, look at what is in it: one of the two causes below is a real database.

The error

PS> docker run -d --name fix5-pg-ne -e POSTGRES_PASSWORD=fix5 -p 127.0.0.1:15483:5432 -v ${PWD}\pgdata:/var/lib/postgresql/data postgres:16
PS> docker logs fix5-pg-ne
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "en_US.utf8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".

Data page checksums are disabled.

initdb: error: directory "/var/lib/postgresql/data" exists but is not empty
initdb: detail: It contains a dot-prefixed/invisible file, perhaps due to it being a mount point.
initdb: hint: Using a mount point directly as the data directory is not recommended.
Create a subdirectory under the mount point.

Why it happens

initdb only creates a cluster in a directory that is missing or empty, so it will not write over anything. The image's entrypoint runs initdb whenever the data directory has no PG_VERSION file. For postgres:16 that directory is /var/lib/postgresql/data, the mount point itself, so anything the mount brings along is in the way. Here it was a 0-byte .gitkeep in the Windows folder, the kind of file that keeps an empty folder in Git, and initdb's detail line spotted it.

The second cause reproduced was a volume created by postgres:18 and then started with postgres:16. The 18 image keeps its cluster in an 18/docker folder inside the volume, so 16 finds that folder and no PG_VERSION, and prints the same error, with a hint to empty the directory instead of the detail line. Do not empty it: that folder is the 18 database. Moving the mount up to /var/lib/postgresql is no better; postgres:16 then quietly created a new, empty cluster beside it, and the old table answered relation "note" does not exist.

The fix

PS> docker run -d --name fix5-pg-ne -e POSTGRES_PASSWORD=fix5 -e PGDATA=/var/lib/postgresql/data/pgdata -p 127.0.0.1:15483:5432 -v ${PWD}\pgdata:/var/lib/postgresql/data postgres:16
PS> docker exec fix5-pg-ne psql -U postgres -c "SHOW data_directory;" -c "SELECT version();"
         data_directory
---------------------------------
 /var/lib/postgresql/data/pgdata
(1 row)

                                                       version
----------------------------------------------------------------------------------------------------------------------
 PostgreSQL 16.14 (Debian 16.14-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
(1 row)

One level down, initdb gets an empty folder of its own and the .gitkeep stays where it was; Get-ChildItem -Force pgdata afterwards listed both. The other route is a named volume, -v fix5-pg-ne:/var/lib/postgresql/data, which started straight away and is also much faster on Windows: in a separate run on this PC with postgres:18, pgbench loaded a million rows in 0.78 s on a named volume and 29.38 s on a bind mount. For a volume made by 18, run postgres:18 with it mounted at /var/lib/postgresql.

How it was reproduced

Docker Desktop 4.95.0 (engine 29.8.2) on Windows 11, Windows PowerShell 5.1, images postgres:16 (16.14) and postgres:18 (18.4). A new folder with an empty pgdata\.gitkeep, made with New-Item, was bind-mounted at /var/lib/postgresql/data; the container printed the block above and stopped with Exited (1). The second case used a named volume first initialised by postgres:18. Names beginning fix5- are this test's own containers and volumes, and the port is published on 127.0.0.1 only.

Frequently asked

How do I fix 'initdb: directory exists but is not empty' in Docker?
Set PGDATA to a subdirectory of the mount, such as /var/lib/postgresql/data/pgdata, or mount an empty named volume at /var/lib/postgresql/data. Both started postgres:16 in this test. Check what is in the folder before deleting anything; it may be another version's database.
Why is my postgres data folder not empty when I just created it?
Look for hidden files. A .gitkeep committed to keep the folder in Git is enough, and initdb's detail line says so when the file starts with a dot. Get-ChildItem -Force on Windows or ls -la in a shell shows it.
Can postgres:16 open a volume created by postgres:18?
No. Mounted at /var/lib/postgresql/data it stops with this error; mounted at /var/lib/postgresql it starts a new, empty 16 cluster beside the 18 one; pointed at the 18 files with PGDATA it stops on a setting in postgresql.conf that 16 does not recognise. Run postgres:18 on that volume.

More decoded errors in the Fixes category. Bind mounts and named volumes on Windows, side by side, are covered in Files In and Out: Bind Mounts and Volumes on Windows.