Blog

Cloud SQL Proxy : la connexion refusée sur localhost

Une connexion refusée sur localhost ne dit presque jamais quelque chose du réseau : le proxy Cloud SQL n'écoute pas là où votre application appelle. Soit il ne tourne pas, soit il a ouvert un autre port, soit il s'est attaché à une autre adresse. La commande lsof -nP -iTCP:5432 -sTCP:LISTEN tranche entre les trois en une seconde, et une sortie vide est déjà une réponse.

Le proxy tourne-t-il vraiment ?

C'est la seule question à poser en premier, et elle se règle sans rien lancer d'autre. lsof liste les sockets en écoute et nomme le processus qui les tient. Quand il n'écrit rien, personne n'écoute sur ce port, et tout le reste de cet article est sans objet : il faut démarrer le proxy.

$ lsof -nP -iTCP:5432 -sTCP:LISTEN
$ echo $?
1

Rien, et un code de retour à 1. Le port est fermé, et vos clients le disent tous, chacun dans son vocabulaire. Les trois messages ci-dessous viennent de la même minute, sur la même machine, contre ce même port fermé.

$ go run dial.go 127.0.0.1:5432
dial tcp 127.0.0.1:5432: connect: connection refused
$ node -e 'require("net").connect(5432,"127.0.0.1").on("error",e=>console.log(e.message))'
connect ECONNREFUSED 127.0.0.1:5432
$ psql "host=127.0.0.1 port=5432 dbname=postgres user=postgres connect_timeout=5" -c 'select 1'
psql: error: connection to server at "127.0.0.1", port 5432 failed: Connection refused
	Is the server running on that host and accepting TCP/IP connections?
Le clientCe qu'il afficheCe que cela veut dire
Un programme Godial tcp 127.0.0.1:5432: connect: connection refusedLe noyau a répondu tout de suite : aucun processus n'écoute
Node.jsconnect ECONNREFUSED 127.0.0.1:5432Le même refus, sous un autre nom
psqlfailed: Connection refusedLe même refus, avec la bonne question à la ligne suivante

Le port et l'adresse tiennent dans la même ligne

Quand le proxy tourne, lsof le nomme, et la fin de la ligne donne les deux réglages qui se trompent le plus souvent. Ici il écoute bien, mais sur 192.168.1.99, pas sur le loopback.

$ lsof -nP -iTCP:5432 -sTCP:LISTEN
COMMAND       PID    USER FD   TYPE  DEVICE SIZE/OFF NODE NAME
cloud-sql 1457242 leandre 6u  IPv4 7930627      0t0  TCP 192.168.1.99:5432 (LISTEN)
$ go run dial.go 127.0.0.1:5432
dial tcp 127.0.0.1:5432: connect: connection refused
$ go run dial.go 192.168.1.99:5432
connexion ouverte

lsof tronque le nom de la commande à neuf caractères, cloud-sql : inutile de chercher cloud-sql-proxy dans cette colonne, il n'y sera jamais. Le port se lit au même endroit, et un proxy démarré avec --port 5434 pendant que l'application appelle 5432 produit exactement le même refus.

$ ss -ltnp '( sport = :5432 or sport = :5434 )'
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      4096       127.0.0.1:5434      0.0.0.0:*    users:(("cloud-sql-proxy",pid=1455994,fd=6))

Le proxy garde 127.0.0.1 par défaut, et c'est le bon choix sur un poste : personne d'autre sur le réseau ne peut atteindre votre base. Dans un conteneur, ce même défaut casse tout, puisque le loopback d'un conteneur n'est pas celui de son voisin. La documentation du proxy d'authentification Cloud SQL, consultée le 19 août 2026, décrit le drapeau --address prévu pour ce cas ; --address 0.0.0.0 ouvre alors le port à tout ce qui atteint le conteneur, et cela se décide, cela ne se copie pas.

Un proxy mort au démarrage laisse le même message

Le cas le plus désagréable est celui du proxy qui a démarré, échoué et disparu pendant que vous regardiez ailleurs. Un port local déjà pris suffit : le proxy s'arrête avec un code de sortie 1, et laisse trois lignes que personne ne relit quand elles défilent dans le journal d'un conteneur.

$ cloud-sql-proxy --port 5433 mon-projet:europe-west1:demo-instance
2026/08/19 05:13:34 Authorizing with OAuth2 token
2026/08/19 05:13:34 [mon-projet:europe-west1:demo-instance] could not listen to address 127.0.0.1:5433: listen tcp4 127.0.0.1:5433: bind: address already in use
2026/08/19 05:13:34 Error starting proxy: [mon-projet:europe-west1:demo-instance] Unable to mount socket: listen tcp4 127.0.0.1:5433: bind: address already in use
2026/08/19 05:13:34 The proxy has encountered a terminal error: unable to start: [mon-projet:europe-west1:demo-instance] Unable to mount socket: listen tcp4 127.0.0.1:5433: bind: address already in use
$ echo $?
1

Le mode socket Unix ajoute une seconde façon de mourir, et elle surprend. Contrairement au mode TCP, qui ouvre son port sans rien demander à Google, --unix-socket résout l'instance avant de monter la socket, et s'arrête net si cet appel échoue. Le dossier reste vide, et le client ne parle plus de refus mais de fichier absent.

$ cloud-sql-proxy --unix-socket /tmp/csql mon-projet:europe-west1:demo-instance
2026/08/19 05:17:55 Error starting proxy: [mon-projet:europe-west1:demo-instance] Unable to mount socket: failed to get instance: refresh error: failed to get instance metadata (connection name = "mon-projet:europe-west1:demo-instance"): googleapi: Error 404: The Cloud SQL instance does not exist., instanceDoesNotExist
$ ls /tmp/csql/
$ psql "host=/tmp/csql/mon-projet:europe-west1:demo-instance dbname=postgres user=postgres" -c 'select 1'
psql: error: connection to server on socket "/tmp/csql/mon-projet:europe-west1:demo-instance/.s.PGSQL.5432" failed: No such file or directory
	Is the server running locally and accepting connections on that socket?

Un drapeau rétablit la même franchise en mode TCP. --run-connection-test demande au proxy de tenter la connexion tout de suite et de s'arrêter si elle échoue : l'échec redevient celui du proxy, au démarrage, au lieu d'un refus inexplicable dans l'application dix secondes plus tard.

 cloud-sql-proxy \
   --port 5432 \
+  --run-connection-test \
   mon-projet:europe-west1:demo-instance
$ cloud-sql-proxy --port 5432 --run-connection-test mon-projet:europe-west1:demo-instance
2026/08/19 05:13:34 [mon-projet:europe-west1:demo-instance] Listening on 127.0.0.1:5432
2026/08/19 05:13:34 The proxy has started successfully and is ready for new connections!
2026/08/19 05:13:34 Connection test started
2026/08/19 05:13:34 Connection test failed
2026/08/19 05:13:34 The proxy has encountered a terminal error: failed to get instance: refresh error: failed to get instance metadata (connection name = "mon-projet:europe-west1:demo-instance"): googleapi: Error 404: The Cloud SQL instance does not exist., instanceDoesNotExist
$ echo $?
1

La faute de frappe dans projet:région:instance n'y est pour rien

C'est l'heure la plus souvent perdue sur ce message, et elle est perdue pour rien. Un nom d'instance faux n'empêche pas le proxy d'ouvrir son port. Ici le nom n'a que deux parties au lieu de trois, il ne peut désigner aucune instance au monde, et le proxy annonce quand même sa réussite.

$ cloud-sql-proxy --port 5432 mon-projet:demo-instance
2026/08/19 05:13:18 Authorizing with OAuth2 token
2026/08/19 05:13:18 [mon-projet:demo-instance] Listening on 127.0.0.1:5432
2026/08/19 05:13:18 The proxy has started successfully and is ready for new connections!

Avec un nom d'une seule partie, demo-instance, le comportement est identique au caractère près. La forme du nom n'est éprouvée qu'au premier appel réel à l'API, donc à la première connexion d'un client. Tant que le port est fermé, relire son nom d'instance ne peut rien donner.

--private-ip : ce que son oubli casse vraiment

Même conclusion pour ce drapeau, et pour la même raison. Avec ou sans lui, le proxy ouvre le port, écrit qu'il est prêt, et attend. Son oubli se paie plus tard, sur une connexion qui s'ouvre puis meurt aussitôt.

$ cloud-sql-proxy --port 5432 --private-ip mon-projet:europe-west1:demo-instance
2026/08/19 05:17:49 [mon-projet:europe-west1:demo-instance] Listening on 127.0.0.1:5432
2026/08/19 05:17:49 The proxy has started successfully and is ready for new connections!
$ psql "host=127.0.0.1 port=5432 dbname=postgres user=postgres connect_timeout=5" -c 'select 1'
psql: error: connection to server at "127.0.0.1", port 5432 failed: server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.

Une précision s'impose sur ce que montre ce relevé : notre instance d'essai n'existe pas, et le rafraîchissement échoue pour cette raison plutôt que pour une histoire d'adresse privée. La forme du message, elle, est celle de tout rafraîchissement qui échoue une fois le port ouvert, quelle qu'en soit la cause. Ce qui est démontré ici est donc précis et suffisant : --private-ip ne décide de rien avant la première connexion, et ne peut pas fermer un port.

Le tableau des messages et de leur cause

Ce que voit l'applicationL'état du port localLa cause
connection refusedferméLe proxy ne tourne pas, ou pas sur ce port, ou pas sur cette adresse
server closed the connection unexpectedlyouvertLe proxy tourne, et son appel à l'API Cloud SQL a échoué
timeout expiredni ouvert ni ferméLes paquets sont jetés en route : mauvaise adresse, ou pare-feu
No such file or directory sur .s.PGSQL.5432sans objetMode socket Unix : la socket n'a jamais été montée

La troisième ligne est celle qu'on confond le plus volontiers avec la première, alors que les deux ne se ressemblent pas à l'usage : un port fermé rend la main immédiatement, une adresse muette attend la fin du délai.

$ go run dial.go 10.255.255.1:5432
dial tcp 10.255.255.1:5432: i/o timeout
$ psql "host=10.255.255.1 port=5432 dbname=postgres user=postgres connect_timeout=5" -c 'select 1'
psql: error: connection to server at "10.255.255.1", port 5432 failed: timeout expired

Kestro n'annonce un tunnel ouvert qu'après avoir vérifié que le port répond, ce qui est exactement le contrôle de la première section ; le guide du proxy qui refuse rassemble les autres refus de la même famille, la comparaison avec le proxy Cloud SQL dit ce que chacun fait vraiment, et le 403 de l'API non activée est ce qu'on lit quand le port, lui, est bien ouvert.

Ce qui reste

Ces sorties viennent d'une machine Linux, le 19 août 2026, avec le proxy 2.25.2+linux.amd64 et psql 16.15. lsof est le même outil sur macOS et la commande ne change pas d'un caractère, mais nous ne l'y avons pas rejouée aujourd'hui, et la colonne COMMAND peut y tronquer autrement.

L'instance visée par tous ces essais n'existe pas. Ce qui est mesuré ici est donc le comportement local du proxy : ce qu'il ouvre, quand il meurt, ce qu'il écrit. Rien de ce qui vient après la poignée de main avec une vraie base n'a été observé, et un refus qui surviendrait à ce moment-là aurait une autre histoire.

Le cas du conteneur est raconté à partir du mécanisme, pas d'un essai complet : nous avons vérifié qu'une écoute sur une adresse autre que le loopback fait refuser 127.0.0.1, ce qui est la même règle, mais pas le comportement d'un proxy en conteneur face à un voisin réel.

Enfin, un test de port ne dit que ce qui était vrai à la seconde où il a tourné. Un proxy tué entre votre lsof et votre connexion donnera un refus que le relevé précédent contredit, et c'est le seul cas où la commande de la première section peut mentir sans faute de sa part.