A .NET developer's PC usually ends up with more database engines than projects: LocalDB from Visual Studio, maybe a SQL Server Express service, a PostgreSQL that an installer left running, and the PostgreSQL containers Docker makes cheap. This part looks at all of them on this PC, read-only where they belong to someone else, finds out which one each connection string reaches, reproduces the three connection errors that cost beginners the most time, and shows the tools that need nothing installed: sqlcmd for LocalDB and the container's own psql and pg_dump. Every output is quoted.

Steps in this part
  1. sqllocaldb info, sqllocaldb versions, sqllocaldb info MSSQLLocalDB, then sqlcmd -S "(localdb)\MSSQLLocalDB" -Q "select @version"
  2. Get-Service *SQL* and Get-Service postgresql*: which engines are services, and which is not
  3. A console app with Microsoft.Data.SqlClient: connect to LocalDB, then try Server=localhost and a database name that does not exist
  4. docker run -d --name ds-p4-pg -e POSTGRES_PASSWORD=... -p 15940:5432 -v ds-p4-pgdata:/var/lib/postgresql postgres:18
  5. With Npgsql: connect on 15940, then on 5432, then with a wrong password; compare the three messages
  6. docker exec ds-p4-pg pg_dump -U postgres -f /tmp/dump.sql postgres and docker cp it out; try the redirect way and look at the first bytes
  7. docker stop, start, rm -f and a new run on the same volume

LocalDB: a process that runs as you

PS> sqllocaldb info
MSSQLLocalDB
PS> sqllocaldb versions
Microsoft SQL Server 2019 (15.0.4382.1)
Microsoft SQL Server 2022 (16.0.1210.3)
Microsoft SQL Server 2025 (17.0.4025.3)

Three LocalDB versions are installed on this PC and one automatic instance exists, MSSQLLocalDB, running the 2022 build. It is not a service: Get-Service lists none for it, and while a session is open a sqlservr.exe runs in the user's own console session under the user's account, which matches the documentation's "Each instance runs as the respective user in its own process." It starts when something connects and stops itself; this PC's log recorded the stop about five minutes after the last connection closed:

The RANU instance is terminating in response to its internal time out. This is an informational message only. No user action is required.

The next sqlcmd query started it again in 0.76 seconds, with a new named pipe, which is why connection strings say (localdb)\MSSQLLocalDB and never the pipe name. It has no TCP listener at all, so nothing in a container, in WSL or on another PC can reach it. Two file facts that save a search: the system databases sit under %LOCALAPPDATA%\Microsoft\Microsoft SQL Server Local DB\Instances\MSSQLLocalDB, 54 MB here, and a CREATE DATABASE with no file paths put its .mdf and .ldf in the root of the user profile, not in that folder. sqlcmd, the ODBC variant 16.0 on this PC, is all the tooling LocalDB needs:

PS> sqlcmd -S "(localdb)\MSSQLLocalDB" -d DsP4Lab -W -Q "select * from patients"
id full_name date_of_birth phone
-- --------- ------------- -----
1 Maria Garcia 1988-03-14 +00-555-0101
2 David Chen 1975-11-02 +00-555-0102
3 Aisha Khan 1992-07-21 +00-555-0103

(3 rows affected)

-W trims the padding; without it full_name is a hundred characters wide. A backup is one line too, BACKUP DATABASE DsP4Lab TO DISK = N'...' WITH INIT through sqlcmd, written by the LocalDB process straight into a folder under your profile.

Express: the same engine as a service

MSSQL$SQLEXPRESS           Running Automatic SQL Server (SQLEXPRESS)
SQLAgent$SQLEXPRESS        Stopped  Disabled SQL Server Agent (SQLEXPRESS)
SQLBrowser                 Stopped  Disabled SQL Server Browser

This PC also has a SQL Server Express instance, and a read-only version query showed the same build and the same "Express Edition" string as LocalDB; only SERVERPROPERTY('IsLocalDB') tells them apart, 1 against 0. The difference is how they run: Express is an automatic service in session 0, up all day, listening on a loopback port, with the Browser service that resolves instance names disabled. The rule of thumb from the two: LocalDB for one developer on one machine, EF Core samples and tests; a service instance when something other than your own session has to connect, and then the TCP configuration this part did not touch.

Three connection strings that fail, and what each one says

A console app with Microsoft.Data.SqlClient 7.1.1 connected to LocalDB, to .\SQLEXPRESS and to localhost\SQLEXPRESS in under 300 milliseconds each. Then the mistakes. Server=localhost with no instance name asks for a default instance, and this PC has none; after the full 15-second timeout:

Microsoft.Data.SqlClient.SqlException: A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: Named Pipes Provider, error: 40 - Could not open a connection to SQL Server)

Error 40, not 26: the 26 in the Fixes post came back here only for an instance name that does not exist, .\SQLEXPRESS01. A database name that does not exist on LocalDB:

Microsoft.Data.SqlClient.SqlException: Cannot open database "DsP4Labs" requested by the login. The login failed.
Login failed for user '...'.

That one took ten seconds, and the same string with Connect Retry Count=0 failed in a quarter of a second: the delay is the client retrying the login once. The 4060 post has the fix. One more thing a reader sees when adding the package today: restore warnings NU1902 and NU1903 about a vulnerable transitive package; the build succeeds, and the list changes over time.

Two PostgreSQL servers, and why the container gets another port

  TCP    0.0.0.0:5432           0.0.0.0:0              LISTENING       8056
  TCP    [::]:5432              [::]:0                 LISTENING       8056

A native PostgreSQL 18 service owns 5432 on every address of this PC, starts automatically, and was left alone. A container published on 5432 would collide with it, so the container gets 15940:

PS> docker run -d --name ds-p4-pg -e POSTGRES_PASSWORD=ds-p4-pass -p 15940:5432 -v ds-p4-pgdata:/var/lib/postgresql postgres:18
PS> docker exec ds-p4-pg psql -U postgres -c "select version()"
 PostgreSQL 18.4 (Debian 18.4-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit

Npgsql 10.0.3 connected on 15940 in 199 milliseconds. On 5432, with the container's password, it got the native server and failed:

Npgsql.PostgresException: 28P01: password authentication failed for user "postgres"

And on 15940 with a wrong password it printed the identical line. That is the point of the section: a wrong port that lands on another PostgreSQL and a wrong password give exactly the same exception, and the message cannot tell you which server answered. Check the port first; once connected, select version() says which engine you reached, the container's naming Debian. The container log holds the server side, with the pg_hba.conf rule that applied, and the 28P01 post decodes it. One observation to keep in mind when reading such logs: a single failed open logged two failures, because Npgsql's GSS encryption mode makes a second attempt; disabling it brought the count to one.

psql and pg_dump without installing anything

psql and pg_dump are not on this PC's PATH, and they do not need to be: the container has both. docker exec -i ds-p4-pg psql -U postgres with Get-Content .\query.sql | in front runs a script, since PowerShell 5.1 refuses the < operator. For a backup, the way that works is inside and out:

docker exec ds-p4-pg pg_dump -U postgres -f /tmp/ds-p4-dump.sql postgres
docker cp ds-p4-pg:/tmp/ds-p4-dump.sql .\ds-p4-dump.sql

The tempting docker exec ds-p4-pg pg_dump ... > dump.sql in a stock Windows PowerShell 5.1 writes UTF-16, first bytes FF FE, and the restore fails with invalid byte sequence for encoding "UTF8": 0xff while psql still exits 0; the Docker series' Part 5 met the same trap. Finally, the volume: docker stop and start kept the rows, and docker rm -f followed by a fresh docker run on the same named volume kept them too, at the price of a recovery on the next start:

database system was not properly shut down; automatic recovery in progress

Stop a database container before removing it; the data is in the volume either way.

Which one for which project: LocalDB for anything SQL Server that only you run; PostgreSQL in Docker, one container per project on its own port, for anything PostgreSQL; a service instance only when another machine or container must reach it. Connection strings name the port and the instance explicitly, so that the error you get is about the server you meant.

Frequently asked

What is the difference between LocalDB and SQL Server Express?
The same engine and edition; on this PC both report build 16.0.1210.3 and Express Edition. LocalDB runs as a process under your own user account, starts on the first connection, stops itself after a few idle minutes, listens on a named pipe only and keeps its files in your profile. Express runs as a Windows service in session 0 all day and can listen on TCP. SERVERPROPERTY('IsLocalDB') tells them apart.
Why does my .NET app get password authentication failed against PostgreSQL in Docker when the password is right?
Because the connection string's port reaches a different PostgreSQL. On a PC with a native PostgreSQL on 5432, a container published on 15940 and a connection string that still says Port=5432 lands on the native server, and Npgsql reports the same 28P01 message it would for a wrong password. Set the published port, and compare select version() once connected.
Do I need to install psql to use PostgreSQL in Docker?
No. The container has psql and pg_dump: docker exec ds-p4-pg psql -U postgres -c for a query, Get-Content script.sql piped into docker exec -i for a script, and pg_dump -f inside the container followed by docker cp for a backup. Avoid redirecting pg_dump's output with > in Windows PowerShell 5.1, which writes UTF-16 that psql cannot read.

Next: Part 5, Git for Windows settings: line endings, long paths, credentials, the dubious-ownership check from WSL, and the identity a public repository should carry.