Skip to content
Caching Supabase with Readyset over IPv6: the AWS and Docker pieces that have to line up
← Back to blogDatabase Scaling

Caching Supabase with Readyset over IPv6: the AWS and Docker pieces that have to line up

Supabase's direct Postgres endpoint is IPv6-only, and a fresh AWS account has no IPv6 enabled. This walkthrough covers the exact VPC, subnet, and Docker configuration needed to run Readyset in front of Supabase on EC2, with three deployment paths: host network, dual-stack bridge, and Linux package.

Vinicius Grippa

Vinicius Grippa

2026-08-18 · 17 min read

Tested on a freshly provisioned EC2 in sa-east-1, Ubuntu 24.04, June 2026.

Your Supabase Postgres project is starting to feel slow under read load, the connection pool keeps filling, and you don't want to bolt Redis on with its own invalidation code or stand up a read replica. Readyset is a Postgres-protocol caching proxy: the application's connection string changes by one host and one port, you mark the queries you want cached with CREATE CACHE FROM <select>, and Readyset serves those from memory while everything else passes through to Supabase.

The catch is networking. Supabase's direct database endpoitn is IPv6-only, and a fresh AWS account has no IPv6 enabled. Supabase sells a paid IPv4 add-on that works around it on their side; giving your AWS VPC IPv6 is free and does the same job. We went with the second. This is the walkthrough.

TL;DR

Two things are always true:

  • Supabase. Supabase exposes three Postgres entry points. The "direct" one is the right upstream for Readyset, and it is published only as IPv6.
  • AWS. A default AWS VPC has no IPv6 at all. You need a VPC that AWS has assigned an IPv6 block to, with the route table sending IPv6 traffic to an internet gateway. The console steps below set this up in a few minutes.

The third thing depends on how you choose to run Readyset. Pick one path; the other two do not apply.

PathWhat you need on top of the IPv6 VPC
A. Docker, host networkNothing extra. The container shares the host's IPv6 stack via --network host.
B. Docker, user-defined bridgeEnable ipv6 and ip6tables in /etc/docker/daemon.json, then docker network create a dual-stack network.
C. Linux package (.deb or .rpm)No Docker. Edit /etc/readyset/readyset.conf, systemctl start. Needs DISABLE_UPSTREAM_SSL_VERIFICATION=true to trust Supabase's TLS chain.

Readyset itself never knows the upstream is IPv6. It uses the standard Postgres client library, which resolves whatever address family DNS gives it. No IPv6-specific flag, no special image tag.

On this page

  1. A quick networking primer
  2. Why this is necessary in the first place
  3. Step 1: a dual-stack VPC in the AWS console
  4. Step 2: DNS sanity check
  5. Step 3: pick how to run Readyset
  6. Why the failures look so confusing
  7. Tearing it down
  8. Closing
  9. Further reading

A quick networking primer

Before we touch any infrastructure, here are the acronyms used below.

TermWhat it means
IPv4The 32-bit address format that the public internet has used since 1983. Looks like 52.67.35.47. There are about 4 billion of them, which is no longer enough.
IPv6The 128-bit address format that replaces IPv4. Looks like 2600:1f1e:f47:be01:98fb:f49d:c682:9d47. There are enough addresses to give every device on Earth its own.
A recordA DNS record that maps a hostname to an IPv4 address.
AAAA recordA DNS record that maps a hostname to an IPv6 address. The four letters are read as "quad A". A host that publishes only AAAA records is IPv6-only.
Dual-stackA host, network, or VPC that has both IPv4 and IPv6 working at the same time.
SLAACStateLess Address AutoConfiguration. The way an IPv6 host typically receives its address: the router advertises a prefix, and the host picks a suffix. No DHCP server required.
VPCVirtual Private Cloud. The AWS network that your EC2 instances live in.
IGWInternet Gateway. The AWS object that connects a VPC to the public internet. The same IGW carries both IPv4 and IPv6.
Egress-only IGWA variant of IGW that allows outbound IPv6 but blocks inbound. Useful for "give the box IPv6 but don't expose it to the world." We don't need it for this test; we use a regular IGW.

A useful mental model: AWS gives every account IPv6 capacity for free, but it does not turn it on for you. You have to either ask AWS to assign an IPv6 block to a new VPC, or migrate the default VPC. We do the former because it is a handful of clicks on a fresh VPC.

References we link to throughout this post:

Why this is necessary in the first place

Supabase exposes three Postgres entry points. They look similar and use the same password, but their network properties are different.

EndpointHost:portUsernameNotes
Directdb.<ref>.supabase.co:5432postgresIPv6-only. Single backend. Best fit for Readyset.
Session pooleraws-1-<region>.pooler.supabase.com:5432postgres.<ref>Dual-stack. Pins one client to one backend. Caps at ~15 concurrent clients per project.
Transaction pooleraws-1-<region>.pooler.supabase.com:6543postgres.<ref>Dual-stack. Multiplexes clients onto a smaller pool. Breaks Readyset's bootstrap: it uses prepared statements, and transaction mode does not pin a client to a backend.

Supabase explains the trade-offs in their own docs. For Readyset specifically:

  • The transaction pooler is out. Readyset's first connection hangs with prepared statement "s0" does not exist.
  • The session pooler works, but the 15-client cap means a real workload saturates it instantly.
  • The direct endpoint is the right answer. It is just IPv6-only.

Step 1: a dual-stack VPC in the AWS console

A fresh VPC is IPv4-only until you attach an IPv6 block, carve a dual-stack subnet, and point the IPv6 default route at an internet gateway. The whole thing is six objects in the VPC console. We did this in sa-east-1; substitute your own region throughout.

1. Create the VPC with an IPv6 block

In VPC → Your VPCs → Create VPC, choose VPC only and set:

  • IPv4 CIDR: 10.42.0.0/16
  • IPv6 CIDR block: Amazon-provided IPv6 CIDR block. This hands the VPC a public /56 out of Amazon's pool, roughly 72 quadrillion addresses you don't have to manage.

After it is created, open the VPC, choose Actions → Edit DNS settings, and make sure both Enable DNS resolution and Enable DNS hostnames are ticked. Readyset and psql resolve the Supabase hostname by name, so DNS support has to be on.

2. Create and attach an internet gateway

Under VPC → Internet gateways → Create internet gateway, give it a name and create it. Then Actions → Attach to VPC and pick the VPC from step 1. The same gateway carries both IPv4 and IPv6 traffic.

3. Create a public, dual-stack subnet

Under VPC → Subnets → Create subnet, select the VPC and set:

  • Availability Zone: any one in the region (we used sa-east-1a).
  • IPv4 CIDR: 10.42.1.0/24
  • IPv6 CIDR: click Add IPv6 CIDR and accept a /64 slice of the VPC's /56 (for example the block ending in 01::/64). A /64 is the standard size for one IPv6 subnet.

Once created, select the subnet and use Actions → Edit subnet settings to enable Auto-assign public IPv4 address and Auto-assign IPv6 address. Without these, instances launch with no public address on either family.

4. Route both default routes to the gateway

Under VPC → Route tables → Create route table, attach it to the VPC, then on the Routes tab choose Edit routes and add two entries pointing at the internet gateway:

DestinationTarget
0.0.0.0/0the internet gateway from step 2
::/0the same internet gateway

::/0 is the IPv6 default route, the equivalent of 0.0.0.0/0. Finally, on the Subnet associations tab, associate this route table with the subnet from step 3.

5. Create a security group

Under EC2 → Security Groups → Create security group, attach it to the VPC and add these inbound rules (add each one twice, once with an IPv4 source and once with an IPv6 source, so both stacks can reach the box):

TypePortSource
SSH22your admin IP (0.0.0.0/0 and ::/0 for a quick test only)
Custom TCP5433–5434same admin source

Ports 5433 and 5434 are the Readyset SQL listeners used later in this post (5433 is the package default, 5434 is the Docker default we use). Leave outbound rules at the default "allow all," which already covers both 0.0.0.0/0 and ::/0.

6. Launch the EC2 instance

Under EC2 → Launch instance, set:

  • AMI: Ubuntu Server 24.04 LTS (amd64).
  • Instance type: t3.small is plenty for this test.
  • Key pair: your existing SSH key pair.
  • Network settings → Edit: select the VPC and the dual-stack subnet, set Auto-assign public IP and Auto-assign IPv6 IP to Enable, and choose the security group from step 5.
  • Advanced details → User data: paste the script below so Docker and the Postgres client are ready on first boot.
#!/bin/bash
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -y -qq
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
usermod -aG docker ubuntu
apt-get install -y -qq postgresql-client jq

Launch it. Once the instance is running, its Details tab shows both a Public IPv4 address and a IPv6 address, for example:

Public IPv4 address : 18.228.136.127
IPv6 address        : 2600:1f1e:ba:3f01:287e:9fd9:bce8:d746

The box now has both an IPv4 and a global IPv6 address. Confirm from inside it:

$ ssh ubuntu@18.228.136.127 'ip -6 addr show scope global'
    inet6 2600:1f1e:ba:3f01:287e:9fd9:bce8:d746/128 scope global dynamic noprefixroute

$ ssh ubuntu@18.228.136.127 'ping6 -c 2 ipv6.google.com'
64 bytes from 2800:3f0:4001:80c::200e: icmp_seq=1 ttl=118 time=2.12 ms
64 bytes from 2800:3f0:4001:80c::200e: icmp_seq=2 ttl=118 time=2.10 ms

What just happened, in plain terms:

  • The Amazon-provided IPv6 CIDR option handed the VPC a /56 block of public IPv6 addresses out of Amazon's pool. That is roughly 72 quadrillion addresses you don't have to think about.
  • The subnet got a /64 slice (still 18 quintillion addresses), which is the standard size for one IPv6 subnet.
  • The route table sends ::/0 (the IPv6 default route, equivalent to 0.0.0.0/0) to the IGW.
  • Auto-assign IPv6 IP told the instance to grab one address from the subnet's range at launch.

Step 2: DNS sanity check

Before involving Readyset, verify the box can resolve and reach Supabase.

$ dig +short AAAA db.<ref>.supabase.co
2600:1f1e:90b:a700:733c:24d:4354:1c33

$ dig +short A db.<ref>.supabase.co
(empty)

$ nc -6 -vz -w 5 db.<ref>.supabase.co 5432
Connection to db.<ref>.supabase.co 5432 port [tcp/postgresql] succeeded!

Two things to read off here. First, the lack of an A record is intentional: this is what "AAAA-only" means. A host without IPv6 will fail at this step with "could not translate host name", and the failure is silent enough that people often blame the firewall. Second, the TCP check proves the route table and the security group are right.

A native psql test from the host:

$ psql "postgresql://postgres:<your-supabase-db-password>@db.<ref>.supabase.co:5432/postgres?sslmode=require" \
    -c "SELECT version(), inet_server_addr();"
                                      version
------------------------------------------------------------------------------------
 PostgreSQL 17.6 on aarch64-unknown-linux-gnu, compiled by gcc (GCC) 15.2.0, 64-bit
            inet_server_addr
----------------------------------------
 2600:1f1e:90b:a700:733c:24d:4354:1c33

You can find your project's database password in the Supabase dashboard under Project Settings → Database → Connection string. Treat it like any other production secret; don't paste it in chat or in committed config files.

Step 3: pick how to run Readyset

From here the post forks into three independent paths. They produce the same outcome: a Readyset proxy that talks to Supabase over IPv6. The differences are operational, not functional.

  • Path A runs Readyset in Docker on --network host. Simplest, fewest moving parts, container shares the host's IPv6 stack.
  • Path B runs Readyset in Docker on a user-defined bridge network. Needed if you also run a compose stack on a bridge and want Readyset to sit on the same network.
  • Path C installs Readyset from the official .deb / .rpm package, no Docker. Best fit if Docker is not allowed on the host or you prefer systemd-managed services.

Read the path you plan to use; you can safely skip the other two.

Path A: Docker on --network host

Docker's default bridge network is IPv4-only. The fastest way around that is to share the host's network namespace with the container by using --network host. The container then sees the same IPv6 stack the host has, and the only thing to remember is that -p flags become no-ops because the container's listeners bind directly on the host.

sudo docker run -d --name readyset \
  --network host \
  -e UPSTREAM_DB_URL="postgresql://postgres:<password>@db.<ref>.supabase.co:5432/postgres?sslmode=require" \
  -e LISTEN_ADDRESS="[::]:5434" \
  -e DEPLOYMENT=rs_v6_test \
  -e STANDALONE=true \
  -e CACHE_MODE=shallow \
  -e QUERY_CACHING=explicit \
  -e DISABLE_TELEMETRY=true \
  public.ecr.aws/readyset/readyset:latest-nightly

We bind to [::]:5434 instead of 0.0.0.0:5434 so Readyset accepts both IPv4 and IPv6 clients. On Linux a tcp6 socket on :: automatically accepts IPv4 connections too (they appear as IPv4-mapped IPv6 addresses), so this single line makes the proxy reachable from both stacks.

Within about 5 seconds you can talk to Readyset:

$ psql "postgresql://postgres:<password>@127.0.0.1:5434/postgres?sslmode=disable" \
    -c 'SHOW READYSET STATUS;'
           name           |          value
--------------------------+-------------------------
 Database Connection      | Connected
 Status                   | Online
 Replication Status       | Disabled

A read and a write through Readyset:

$ psql ... -c "SELECT * FROM rs_ipv6_test ORDER BY id;" \
         -c "INSERT INTO rs_ipv6_test(note) VALUES ('inserted-via-readyset');"
 id |    note    |              ts
----+------------+-------------------------------
  1 | seed-row-1 | 2026-05-21 22:13:32.014309+00
  2 | seed-row-2 | 2026-05-21 22:13:32.014309+00
  3 | seed-row-3 | 2026-05-21 22:13:32.014309+00

INSERT 0 1

And explicitly caching one query (see CREATE CACHE in the Readyset docs):

$ psql ... -c "CREATE CACHE FROM SELECT id, note FROM rs_ipv6_test WHERE id = \$1;"
      query_id      | cache_type
--------------------+------------
 q_69c75e4989e93d98 | shallow

That is the entire IPv6 story from Readyset's point of view. There is no IPv6-specific flag, environment variable, or configuration. Readyset uses the standard Postgres client library, which resolves whatever address family DNS gives it.

Path B: Docker on a dual-stack bridge

If you would rather not run Readyset on the host network (because you also have a compose stack on a user-defined bridge, for example), you need IPv6 in the Docker daemon.

1. Enable IPv6 in /etc/docker/daemon.json:

{
  "ipv6": true,
  "fixed-cidr-v6": "fd00:dead:beef::/48",
  "ip6tables": true
}
  • ipv6: true turns on the feature globally.
  • fixed-cidr-v6 is the prefix Docker uses for its default bridge. fd00::/8 is the ULA range (the IPv6 equivalent of 10.0.0.0/8, i.e. private). Docker NATs traffic from these addresses through the host's global IPv6 when it leaves the box.
  • ip6tables: true makes Docker manage ip6tables rules the same way it manages iptables for IPv4. Without this, Docker can create v6 addresses but cannot route packets to them.

2. Restart Docker: sudo systemctl restart docker.

3. Create a dual-stack user network:

sudo docker network create --ipv6 \
  --subnet=172.30.0.0/24 \
  --subnet=fd00:c0de::/64 \
  --gateway=172.30.0.1 \
  rs-ipv6-net

4. Run Readyset on that network. Add --dns 8.8.8.8 --dns 8.8.4.4 so that AAAA resolution does not depend on Docker's embedded resolver, which silently drops AAAA records on some setups.

sudo docker run -d --name readyset \
  --network rs-ipv6-net \
  --dns 8.8.8.8 --dns 8.8.4.4 \
  -p 5434:5434 \
  -e UPSTREAM_DB_URL="postgresql://postgres:<password>@db.<ref>.supabase.co:5432/postgres?sslmode=require" \
  -e LISTEN_ADDRESS="[::]:5434" \
  -e DEPLOYMENT=rs_v6_bridge \
  -e STANDALONE=true \
  -e CACHE_MODE=shallow \
  -e QUERY_CACHING=explicit \
  -e DISABLE_TELEMETRY=true \
  public.ecr.aws/readyset/readyset:latest-nightly

Confirm the container picked up both address families:

$ sudo docker inspect readyset --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} v4={{$v.IPAddress}} v6={{$v.GlobalIPv6Address}}{{end}}'
rs-ipv6-net v4=172.30.0.2 v6=fd00:c0de::2

Reads and writes work the same way they did on --network host. The Docker side is covered in detail in Docker's official IPv6 guide.

Path C: Linux package, no Docker

This path skips Docker entirely and installs Readyset from the official Linux packages, as described in the Readyset package install docs. Each release ships .deb and .rpm builds for amd64 and arm64. We tested the stable-260423 release on Ubuntu 24.04 amd64.

Because there is no Docker involved, none of the Docker-daemon configuration from Path B applies. The IPv6 setup needed for this path is exactly the dual-stack VPC built in Step 1; nothing more.

# Download the .deb for Readyset 1.25.0 (stable-260423)
curl -fsSLO https://github.com/readysettech/readyset/releases/download/stable-260423/readyset_1.25.0-1_amd64.deb

# Install with apt so dependencies are resolved automatically
sudo apt-get install -y ./readyset_1.25.0-1_amd64.deb

The package drops:

/usr/bin/readyset                       # the binary
/etc/readyset/readyset.conf             # systemd EnvironmentFile (key=value)
/lib/systemd/system/readyset.service    # systemd unit
/var/lib/readyset/                      # default STORAGE_DIR for snapshots

Edit the config file with the Supabase URL and a dual-stack listener. The default file has every option as a comment; we only need a handful:

sudo tee /etc/readyset/readyset.conf >/dev/null <<'EOF'
UPSTREAM_DB_URL="postgresql://postgres:<your-supabase-db-password>@db.<ref>.supabase.co:5432/postgres?sslmode=require"
LISTEN_ADDRESS=[::]:5433
CACHE_MODE=shallow
QUERY_CACHING=explicit
DEPLOYMENT_ENV=ipv6-blog
DISABLE_TELEMETRY=true
NO_COLOR=true
STORAGE_DIR=/var/lib/readyset

# Required for Supabase: trust the upstream certificate without verifying
# the full chain. See note below.
DISABLE_UPSTREAM_SSL_VERIFICATION=true
EOF

Why DISABLE_UPSTREAM_SSL_VERIFICATION=true is needed for Supabase. When we left this off, the service failed during its config-verification step with:

error performing TLS handshake: certificate verify failed
  (self-signed certificate in certificate chain)

Supabase's TLS certificate is served from a chain that the bare binary's default CA bundle does not anchor. The official Docker image disables the same check internally, which is why we never see it there. The alternative is to download Supabase's CA bundle from the dashboard and point SSL_ROOT_CERT=/etc/ssl/supabase-ca.pem at it. For a demo box, disabling verification is the shorter route; for production, mount the CA bundle.

Start the service:

sudo systemctl enable --now readyset
sudo systemctl status readyset

The unit becomes active (running) within a few seconds and you can connect to it from psql exactly as before. Because we set LISTEN_ADDRESS=[::]:5433, the listener is dual-stack:

$ sudo ss -ltn | grep 5433
LISTEN 0  4096  *:5433  *:*

$ psql "postgresql://postgres:<password>@127.0.0.1:5433/postgres?sslmode=disable" \
    -c "SHOW READYSET STATUS;"
 Database Connection | Connected
 Status              | Online

$ psql "postgresql://postgres:<password>@[::1]:5433/postgres?sslmode=disable" \
    -c "SHOW READYSET VERSION;"
 release version | stable-260423
 commit id       | eb007a9c40375e3111c01f35297fefee36cf171d

# From another box, over the EC2's public IPv6:
$ psql "postgresql://postgres:<password>@[2600:1f1e:ba:3f01:287e:9fd9:bce8:d746]:5433/postgres?sslmode=disable" \
    -c "SELECT count(*) FROM rs_ipv6_test;"
 count | 5

Things worth knowing about the package:

  • The service runs as the readyset system user. Make sure STORAGE_DIR is writable by that user (the package creates /var/lib/readyset/ with the right ownership).
  • EnvironmentFile=/etc/readyset/readyset.conf is the only place systemd reads configuration from. Editing the unit isn't necessary; add or change variables in the conf file and restart with sudo systemctl restart readyset.
  • The package binary defaults to port 5433. The Docker image defaulted to 5434 in our examples; both work, just be consistent with the security-group rule and the client URL.

Why the failures look so confusing

The interesting failures aren't the obvious ones. They are the ones that look like something else.

SymptomReal cause
could not translate host name "db.<ref>.supabase.co"The host has no IPv6, so AAAA-only DNS returns an empty result set.
Connection refused on the direct endpoint after a few days of silenceOn the Supabase free tier, the direct endpoint auto-pauses after inactivity. The poolers stay warm. Send any query to the direct endpoint to wake it.
Readyset hangs on first connect against pooler.supabase.com:6543Transaction pooler does not pin a client to a backend; Readyset's prepared-statement bootstrap dies. Use the direct endpoint instead.
Workload errors with "too many connections" past concurrency 12Session pooler caps the project at ~15 clients. Use the direct endpoint.
Container reports "network unreachable" while nc -6 from the host worksDocker's default bridge is IPv4-only. Use --network host or enable IPv6 in daemon.json and create a dual-stack user network.
Container reports "unknown host" for the AAAA-only Supabase hostnameDocker's embedded DNS resolver dropped the AAAA reply. Pass --dns 8.8.8.8 (or any external resolver).
Package-installed Readyset fails with "certificate verify failed (self-signed certificate in certificate chain)"The bundled CA store does not anchor Supabase's chain. Set DISABLE_UPSTREAM_SSL_VERIFICATION=true for demos, or download Supabase's CA bundle from the dashboard and point SSL_ROOT_CERT=/etc/ssl/supabase-ca.pem at it for production.

Tearing it down

Delete the objects in the reverse of the order you created them, so nothing is still in use when you try to remove it:

  1. Terminate the EC2 instance (EC2 → Instances → Instance state → Terminate).
  2. Delete the security group once the instance is gone.
  3. Detach and delete the internet gateway (VPC → Internet gateways).
  4. Delete the subnet, then the custom route table.
  5. Delete the VPC, which releases the Amazon-provided IPv6 block back to the pool.

Deleting the VPC last leaves no orphan resources to forget about.

Closing

The story we set out to write was about Readyset and Supabase. It turned out to be about IPv6. Once each piece is named (AAAA record, dual-stack VPC, IGW), nothing in the chain is interesting; it's just plumbing. Readyset itself does not know the upstream is on IPv6 and does not care. The same configuration, the same image, the same SQL.

If you want to repeat this on your own AWS account, the six console objects in Step 1 are the entire infrastructure side. The Readyset side is one docker run (Path A or B) or one apt-get install (Path C); pick whichever fits how you already run services on your boxes.

Further reading

Want to see Readyset in action?

Book a demo and see how Readyset can accelerate your database.

Still scaling the hard way?

Modern applications demand instant performance, even under unpredictable load. Readyset helps you eliminate slow queries, stabilize latency, and scale confidently.

Revolutionize your database performance with Readyset

Serve requests at sub-millisecond latencies with the modern database scaling and query caching system for MySQL and PostgreSQL.

Join our newsletter

Stay updated with the latest news, insights, and developments from Readyset — straight to your inbox.

© 2026 Readyset. All rights reserved.