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:5432
The Compose file behind it passed this connection string to the web service:
DATABASE_URL: postgres://username:pgpassword@127.0.0.1:5432/mydatabase
The 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.
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.
services:
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: 30s
The healthcheck is the one from the Compose startup-order guide. The doubled $$ stops Compose from interpolating the variables itself, so the shell inside the db container expands them. With restart: true, the guide adds, the web service is restarted when db is restarted by an explicit Compose operation such as docker 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:
// 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:
// 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 a Pool config to the container name postgresdb. 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:
// 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:
docker 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 ps
docker 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.
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.
Originally published at https://www.iloveblogs.blog
Top comments (0)