Fix ECONNREFUSED 127.0.0.1:5432 in Docker Compose (Node)
Your Node app and Postgres both start, yet every query fails with connection refused. Read the address in the error, fix the host, and gate startup on pg_isready.
TL;DR#
connect ECONNREFUSED 127.0.0.1:5432 from a Node app running in Docker Compose means the app is dialling its own container. 127.0.0.1 and localhost inside a container are that container's loopback interface, and Postgres is not in it. Replace the host in DATABASE_URL with the Compose service name of the database (for example db), keep the container port 5432, and make the app wait for a healthy database with a pg_isready healthcheck and depends_on: condition: service_healthy.
The error#
The case below is the one reported on Stack Overflow by a developer running a Sequelize app and Postgres with docker-compose up. The database log says it is ready, and the Node server still cannot connect. The report prints the address as 127.0.01:5432, a typo for the loopback address; in its usual form the pair of log lines reads:
database system is ready to accept connections
Error: connect ECONNREFUSED 127.0.0.1:5432The Compose file behind it passed this connection string to the web service:
DATABASE_URL: postgres://username:pgpassword@127.0.0.1:5432/mydatabaseThe same app worked when Node and Postgres both ran directly on the laptop. That detail is the whole diagnosis.
Why it happens#
ECONNREFUSED is not a Postgres error. It is the operating system answering a TCP connection attempt with a reset, because no process is listening on that address and port. Postgres never saw the request, so the password, the database name and the driver are all irrelevant until the address is right.
When Node and Postgres run on the same machine, 127.0.0.1 reaches Postgres because both processes share one network stack. Docker gives each container its own network namespace, with its own loopback interface. In the web container, 127.0.0.1:5432 is port 5432 of the web container, where nothing listens, hence the refusal. The accepted answer on the question puts it plainly: the loopback address means "connect to myself".
Compose solves the addressing problem for you. According to the Compose networking documentation, "each service registers its name with an internal DNS server, so containers can reach each other using the service name directly". The same page settles the port question: "networked service-to-service communication uses the CONTAINER_PORT", while the host port of a mapping such as 8001:5432 only exists for clients outside the network. It also notes that links, which the original file relies on, "are not required for basic service-to-service communication".
A second, independent cause produces the same message: Compose starts the app before Postgres is ready. The startup-order guide is explicit that "Compose does not wait until a container is 'ready', only until it's running". A fresh Postgres container spends several seconds initialising its data directory, and a Node process that connects once at boot loses that race.
Read the address in the error first#
The address printed after ECONNREFUSED tells you which of these cases you are in, before you change anything:
| Address in the error | Meaning | Fix |
|---|---|---|
127.0.0.1:5432 or localhost from inside a container | The app dials its own container | Use the service name (step 1) |
::1:5432 | localhost resolved to the IPv6 loopback | Same as above in a container; on the host, see the edge cases |
A private IP such as 172.18.0.2:5432 | DNS worked; Postgres is not accepting yet, or listens elsewhere | Healthcheck and retries (steps 1 and 3) |
The right host, but a port such as 15432 | The host side of a ports mapping was used between containers | Use the container port |
Check your own DATABASE_URL#
Paste the connection string your app actually receives to see how it parses: the host, the port and the database it names, and whether that host only works on the machine running the code. It runs in your browser and nothing is sent anywhere; the password is masked in every result.
Runs entirely in your browser — nothing you paste is sent anywhere, and the password is masked in every result. Still cautious? Replace the password with any string containing the same special characters: the diagnosis only needs its shape.
Fix#
-
Point the connection string at the database service. With the modern
services:format, nolinksand noportsmapping are needed for the app to reach Postgres. Keep theportsline only if a tool on your host must connect too.yamlservices: web: image: node:22 working_dir: /src command: npm start ports: - "8000:4242" environment: PORT: 4242 DATABASE_URL: postgres://username:pgpassword@db:5432/mydatabase volumes: - ./:/src depends_on: db: condition: service_healthy restart: true db: image: postgres:17 environment: POSTGRES_USER: username POSTGRES_PASSWORD: pgpassword POSTGRES_DB: mydatabase healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 10s timeout: 10s retries: 5 start_period: 30sThe healthcheck is the one from the Compose startup-order guide. The doubled
$$stops Compose from interpolating the variables itself, so the shell inside thedbcontainer expands them. Withrestart: true, the guide adds, thewebservice is restarted whendbis restarted by an explicit Compose operation such asdocker compose restart. -
Make sure the code actually uses
DATABASE_URL. Setting the variable in Compose changes nothing if the app ignores it. The node-postgres connection docs list the variablespgreads on its own:PGHOST,PGPORT,PGUSER,PGPASSWORDandPGDATABASE, withPGHOSTfalling back tolocalhost.DATABASE_URLis not among them, so a barenew Pool()inside a container dialslocalhostand fails exactly like the original question. Pass the string explicitly:js// db.js — node-postgres const { Pool } = require('pg'); const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); module.exports = pool;With Sequelize, as in the original question, the getting-started guide accepts the URI as the first constructor argument:
js// sequelize.js const { Sequelize } = require('sequelize'); const sequelize = new Sequelize(process.env.DATABASE_URL, { dialect: 'postgres', logging: false, }); module.exports = sequelize;One answer on the thread fixed the same error by changing a hard-coded
host: 'localhost'in aPoolconfig to the container namepostgresdb. The principle is identical: whatever form the config takes, the host must be the database service. -
Retry the first connection instead of crashing. The healthcheck only helps where Compose honours
depends_on. A short retry loop at boot covers platforms that ignore it, and slow first starts. It does nothing for a database restart while the app is already running: that case is handled byrestart: truefrom step 1, or by retrying individual queries. The pool also needs anerrorlistener, because the node-postgres Pool docs warn that idle clients can emit errors when their backend goes away, and an unhandled poolerrorevent can crash the Node process:js// wait-for-db.js const { Pool } = require('pg'); const pool = new Pool({ connectionString: process.env.DATABASE_URL }); pool.on('error', (err) => { console.error('Idle pg client error:', err.message); }); async function waitForDb(attempts = 10, delayMs = 2000) { for (let i = 1; i <= attempts; i += 1) { try { await pool.query('SELECT 1'); console.log('Postgres is reachable'); return pool; } catch (err) { console.warn(`Postgres not ready (attempt ${i}/${attempts}): ${err.code || err.message}`); if (i === attempts) throw err; await new Promise((resolve) => setTimeout(resolve, delayMs)); } } } waitForDb().catch((err) => { console.error('Giving up on Postgres:', err); process.exit(1); }); -
Recreate the stack so the new environment is applied. A running container keeps the environment it was created with:
bashdocker compose down docker compose up --build
Verify the fix#
Check each hop separately, from the inside out. First, that Postgres accepts connections in its own container. pg_isready exits with 0 when the server accepts connections, 1 when it is rejecting them (for example during startup) and 2 when there is no response:
docker compose exec db pg_isready -U username -d mydatabase
docker compose psdocker compose ps should list db as healthy. Next, confirm that the service name resolves from the app container. This uses Node itself, so it works on slim and Alpine images that lack getent or ping:
docker compose exec web node -e "require('dns').lookup('db', (err, address) => console.log(err || address))"It should print a private address such as 172.18.0.2. Finally, run a real query through the driver with the same connection string the app uses:
docker compose exec web node -e "const { Pool } = require('pg'); new Pool({ connectionString: process.env.DATABASE_URL }).query('SELECT version()').then((r) => { console.log(r.rows[0].version); process.exit(0); }).catch((e) => { console.error(e.message); process.exit(1); })"If the error has changed to password authentication failed, the network is fixed and you are on to credentials: see the password authentication failed guide. Note that POSTGRES_USER and POSTGRES_PASSWORD only take effect when the data directory is empty; an old volume keeps the old role.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Edge cases the answers skip#
Node on the host, Postgres in a container. Here 127.0.0.1:5432 is correct, provided the db service publishes the port with ports: - "5432:5432". If the error shows ::1:5432 instead, localhost resolved to the IPv6 loopback first. Since Node 17, dns.lookup returns addresses in the resolver's order (verbatim) rather than putting IPv4 first. Use 127.0.0.1 explicitly, or start Node with --dns-result-order=ipv4first.
Postgres on the host, Node in a container. The second answer on the thread, on Docker Desktop for Mac, reaches a Compose-run Postgres through its published host port via host.docker.internal. That works, but the service name is the direct route between two Compose services. The same name is the right tool when Postgres really runs on the host: Docker Desktop resolves it to the host automatically. On a Linux engine it does not exist unless you map it through the special host-gateway value, which in Compose is extra_hosts: - "host.docker.internal:host-gateway". The host's Postgres must also listen on an interface other than loopback and allow that client in pg_hba.conf.
A custom postgresql.conf. If you mount your own configuration file into the official image, its Docker Hub page warns that you must set listen_addresses = '*' so that other containers can reach the server.
Prisma and other ORMs. The mechanism is the same whatever the client. The Prisma form of this error on Apple Silicon, with a connection timeout on top, is covered in Prisma's "Can't reach database server at database:5432". If the URL is right but the driver rejects the port, a special character in the password is usually splitting the string: see Prisma's invalid port fix. For the wider Compose setup, bind mounts and watch mode, the Docker development environment tutorial walks through a full configuration.
Once the host in the connection string names the service and the app waits for a healthy database, this error stops depending on luck: the address is right on the first try, and a slow start produces a few logged retries instead of a crashed container.
Further reading: Fix ECONNREFUSED 127.0.0.1:5432 in GitHub Actions, Fix: Sequelize authenticate() Hangs With No Success or Error. Also see PostgreSQL 18 Docker: Fix Existing Volume Startup Errors.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix ECONNREFUSED 127.0.0.1:5432 in GitHub Actions
Your Postgres service is healthy, yet the migration step cannot reach it. Pick the host that matches how the job runs, and make Sequelize read DATABASE_URL.
PostgreSQL 18 Docker: Fix Existing Volume Startup Errors
Separate a Docker mount failure from a PostgreSQL major upgrade, preserve the original volume and prove the restored database survives a restart.
Fix Postgres 5432 bind: address already in use in Docker
Tell the Docker messages apart, find what owns port 5432 on Linux, macOS or Windows, and pick between stopping it and moving the published port.


