sethserver.com Subscribe
Robot dumping toxic waste from barrels while dolphin watches

Connect Replit to GCP Cloud SQL MySQL: Public IP vs Auth Proxy Setup Guide

By Seth Black • Updated: March 22, 2026

Programming · 9 min read

Building serious apps on Replit? This article is part of a complete production deployment guide. Download the full 130-page book here.

Replit quietly made a lot of database tutorials less weird: as of December 2025, Replit is hosted on GCP. That means your "toy app" can sit pretty close to a real Cloud SQL MySQL instance. Less latency. Fewer network hops. Fewer excuses for why SELECT 1 takes half a second.

This post is the practical path: get a Replit Python app talking to a GCP Cloud SQL MySQL database without turning your project into a security-themed escape room.

Prereqs (don't skip these)

You need:

If you don't have the DB/user yet, go create them now. If you're tempted to use the root user, take a walk and think about your choices.

Here's the checklist version of "created":

  1. Cloud SQL instance exists
    In the Cloud Console: Cloud SQL - Instances - your instance. Confirm it's MySQL, and it's in a region you actually want.

  2. Database exists
    Cloud SQL - your instance - Databases - Create database. Example: appdb.

  3. User exists
    Cloud SQL - your instance - Users - Add user account. Example: appuser. Give it a real password. Don't reuse your Gmail password. Yes, I have to say this.

Smart move: give the user only the permissions your app needs. For a prototype, you might go broader. For anything real, tighten it.

If you prefer SQL over clicking:

CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE USER 'appuser'@'%' IDENTIFIED BY 'a-long-password-here';

GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX
ON appdb.* TO 'appuser'@'%';

FLUSH PRIVILEGES;

That @'%' is convenient because it allows connections from anywhere. It's also why you need to be strict about network access.

Two ways to connect (pick your pain)

Option A: Public IP + Authorized Networks (fastest setup)

This is the "I want it working in 15 minutes" option.

Cloud SQL gets a public IP. Your app connects like it would to any MySQL server. You limit inbound access by whitelisting IPs in Authorized networks.

Steps:

  1. In Cloud SQL, enable Public IP
    Cloud SQL - your instance - Connections - Networking.
    Make sure "Public IP" is enabled.

  2. Add an Authorized network (an allowed inbound IP/CIDR)
    Same screen: "Authorized networks" - Add network.
    You'll enter something like:

  3. 203.0.113.42/32 (single IP)
  4. or a CIDR range if you really mean it

  5. Use normal MySQL credentials from your app
    Host = the instance public IP
    Port = 3306
    Username/password = the user you created
    Database = appdb

For hobby apps and prototypes, this is fine. For production, open networks eventually bite you. Not "maybe." Eventually.

The annoying part: figuring out what IP your Replit app is actually coming from.

A few realities:

If you're doing Option A, make debugging easy. Run this in Replit:

import urllib.request

print(urllib.request.urlopen("https://api.ipify.org").read().decode())

Take that IP and add it to Authorized networks as /32. If it changes later, you'll repeat the ritual.

Option A tradeoffs:
- Pros: simplest, fastest to set up, no proxy process
- Cons: IP whitelisting pain, public exposure, passwords still in play

Option B: Cloud SQL Auth Proxy (more secure)

This is the "I don't want to sprinkle database access across the internet" option.

The Cloud SQL Auth Proxy creates a local TCP listener (often 127.0.0.1:3306, or another port you choose). Your app connects to localhost. The proxy handles the secure connection to Cloud SQL using IAM credentials.

Passwords in config files are a slow-moving mess. Even if you swear you'll rotate them later. You won't. Then you'll be digging through git history looking for that password you "only used for a minute."

Cloud SQL Auth Proxy docs (shockingly decent):
https://cloud.google.com/sql/docs/mysql/sql-proxy

How this usually works on Replit:

Replit isn't a full VM you manage like GCE, so you have to decide:
- Where the proxy binary lives (download at runtime, or check it in-don't check in secrets)
- How you start it (a "Run" command that starts proxy + app, or a small supervisor script)
- How you provide credentials (service account JSON in Secrets, or workload identity if supported)

A pragmatic setup for many small apps:
- Store a service account JSON in Replit Secrets (as a single string)
- Write it to a file at runtime
- Start the proxy pointing at that file
- Connect your app to 127.0.0.1

Yes, that still involves a secret. It's still better than scattering DB passwords across configs and laptops.

Option B tradeoffs:
- Pros: tighter security model, no public IP whitelisting games, better long-term story
- Cons: more moving parts, you have to run a proxy process, you need IAM set up correctly

If strangers will use your app, Option B is where you should land.

Store credentials in Replit Secrets (not in code)

In Replit, go to Secrets and set:

Never store passwords in Git. Git is forever. Your future self will not thank you.

Another practical point: don't commit "almost secrets" either. Instance IPs, usernames, and database names aren't as sensitive as passwords, but they still make your setup easier to attack and harder to rotate. Secrets gives you one place to change things without a commit.

Python example (pymysql)

Install:

pip install pymysql

Then:

import os
import pymysql
from pymysql.cursors import DictCursor

def get_conn():
    return pymysql.connect(
        host=os.environ["DB_HOST"],
        port=int(os.environ.get("DB_PORT", "3306")),
        user=os.environ["DB_USER"],
        password=os.environ["DB_PASS"],
        database=os.environ["DB_NAME"],
        cursorclass=DictCursor,
        connect_timeout=10,
        read_timeout=10,
        write_timeout=10,
        autocommit=True,
        charset="utf8mb4",
    )

def main():
    conn = get_conn()
    try:
        with conn.cursor() as cur:
            cur.execute("SELECT NOW() AS server_time;")
            row = cur.fetchone()
            print(row)
    finally:
        conn.close()

if __name__ == "__main__":
    main()

That SELECT NOW() test is boring. Good. Boring is stable.

A more realistic example (create table + insert + query)

This is where people find out they forgot autocommit=True, or they're using the wrong database, or their user has no permissions.

import os
import pymysql
from pymysql.cursors import DictCursor

def conn():
    return pymysql.connect(
        host=os.environ["DB_HOST"],
        port=int(os.environ.get("DB_PORT", "3306")),
        user=os.environ["DB_USER"],
        password=os.environ["DB_PASS"],
        database=os.environ["DB_NAME"],
        cursorclass=DictCursor,
        autocommit=True,
        charset="utf8mb4",
    )

with conn() as c:
    with c.cursor() as cur:
        cur.execute("""
            CREATE TABLE IF NOT EXISTS notes (
                id BIGINT PRIMARY KEY AUTO_INCREMENT,
                body TEXT NOT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
            )
        """)
        cur.execute("INSERT INTO notes (body) VALUES (%s)", ("hello from replit",))
        cur.execute("SELECT id, body, created_at FROM notes ORDER BY id DESC LIMIT 3")
        print(cur.fetchall())

Use parameterized queries (%s) like this. Don't build SQL strings with f-strings unless you enjoy SQL injection.

Connection pooling (do you need it?)

For tiny apps, opening a connection per request is fine. For busier apps, it gets slow and noisy. Cloud SQL also has connection limits, and you can hit them faster than you think.

In Python, you can use a pool (or at least reuse a global connection carefully). If you're using a web framework (Flask/FastAPI), a decent middle ground is:
- create a connection per request
- close it at the end of the request
- add pooling later if you actually need it

Pooling adds complexity. It also fixes real problems. Add it when you see connection churn and latency, not as a hobby.

Troubleshooting (the usual suspects)

"Can't connect"

This is almost always:

Checks that actually save time:

Slow queries

Put Cloud SQL in the same region as your app. Since Replit is on GCP now, this actually matters. Cross-region latency is real, and it stacks up fast if your app does multiple queries per page load.

Then do the unsexy work:

Example: if you have a users table and you frequently look up by email, add an index:

CREATE INDEX idx_users_email ON users(email);

If you're sorting by created_at and filtering by user_id, a compound index often helps:

CREATE INDEX idx_notes_user_created ON notes(user_id, created_at);

Indexes speed up reads and slow down writes a bit. For most app tables, it's worth it.

"I hardcoded the password... just for now"

No. Use Secrets. "Just for now" turns into "six months later" with impressive consistency.

Also: if you already committed it once, deleting it from the file doesn't delete it from Git history. Rotate the password. Yes, even if it's annoying. Especially if it's annoying.

Extra guides you might want next

If you get stuck, assume it's networking or credentials first. I've used this setup on a few projects, and the failures are usually boring. MySQL is usually innocent. Not always, but usually.

-Sethers

Share this post
Newsletter

One email, once a week.

Notes on databases, systems, and the occasional strong opinion about Python. No spam, unsubscribe anytime.

Seth Black
Written by

Seth Black

Engineer and founder based in Texas. Writes about databases, AI, and running things in production. Embeds in small teams as lead engineer.

More from Programming

View all →
Programming

What's the Differences Between Relational, NoSQL, Vectors, and Caches

Apr 14, 2026
Programming

Git and Source Control in AI Land

Apr 14, 2026
Programming

Domains, DNS, and why it feels fragile

Apr 14, 2026