RDS: demo & walkthrough
Run real PostgreSQL and MySQL databases locally through the RDS API. Each database instance spins up its own Docker container. Connect with standard clients, run real SQL, test your application code. No AWS account required.
Looking for the API surface and CLI examples? See RDS: API reference.
create-db-instance:
a postgres:16 and a
mysql:8.0. Each one spins up a real Docker
container with the matching engine. Waits for both to reach
available, then reads the mapped host port
out of the describe-db-instances response,
connects to each engine with its native client
(psql and
mysql), runs
CREATE TABLE +
INSERT +
SELECT against each, then round-trips the
same SQL through psycopg2 and
pymysql to prove application-driver parity.
Finally calls delete-db-instance on both
and asserts the Docker containers are torn down.
Requires RDS_DOCKER_BACKEND=1 on the LocalEmu host.
Connection detail to know up front: the
Endpoint.Address field returns an AWS-shaped
hostname for API parity; connect on
localhost:<mapped-port> instead.
Source: 14-rds-demo/ in the examples repo.
Real PostgreSQL
Full PostgreSQL 16 in Docker. ACID transactions, indexes, JSON, extensions.
Real MySQL
Full MySQL 8.0 in Docker. InnoDB, stored procedures, triggers, views.
Standard Clients
Connect with psql, mysql, DBeaver, pgAdmin, or any database driver.
One Command
awsemu rds create-db-instance. LocalEmu handles the rest.
Step-by-Step Walkthrough
Step 1: Start LocalEmu with RDS Docker backend
RDS_DOCKER_BACKEND=1 localemu start The RDS_DOCKER_BACKEND=1 flag tells LocalEmu to start real database containers when you create RDS instances.
Step 2: Create a PostgreSQL instance
$ awsemu rds create-db-instance \
--db-instance-identifier my-postgres \
--engine postgres \
--master-username admin \
--master-user-password Secret123 \
--db-name myapp \
--db-instance-class db.t3.micro \
--allocated-storage 20
DBInstance:
DBInstanceIdentifier: my-postgres
Engine: postgres
DBInstanceStatus: available
MasterUsername: admin
DBName: myapp
Endpoint:
Address: localhost
Port: 50063
DBInstanceClass: db.t3.micro
AllocatedStorage: 20
AvailabilityZone: us-east-1a
MultiAZ: false
StorageType: gp2
DeletionProtection: false
DBInstanceArn: arn:aws:rds:us-east-1:000000000000:db:my-postgres LocalEmu creates the RDS instance record (for API compatibility) and starts a real postgres:16 Docker container. The endpoint localhost:50063 is where PostgreSQL listens.
Step 3: Create a MySQL instance
$ awsemu rds create-db-instance \
--db-instance-identifier my-mysql \
--engine mysql \
--master-username root \
--master-user-password Secret123 \
--db-name shopdb \
--db-instance-class db.t3.micro \
--allocated-storage 20
DBInstance:
DBInstanceIdentifier: my-mysql
Engine: mysql
DBInstanceStatus: available
MasterUsername: root
DBName: shopdb
Endpoint:
Address: localhost
Port: 50069
DBInstanceClass: db.t3.micro
AllocatedStorage: 20
AvailabilityZone: us-east-1a
MultiAZ: false
StorageType: gp2
DeletionProtection: false
DBInstanceArn: arn:aws:rds:us-east-1:000000000000:db:my-mysql A second container starts with mysql:8.0. Each instance gets its own isolated container and port.
Step 4: Describe all DB instances
$ awsemu rds describe-db-instances
DBInstances:
- DBInstanceIdentifier: my-postgres
Engine: postgres
DBInstanceStatus: available
Endpoint: localhost:50063
DBName: myapp
MasterUsername: admin
- DBInstanceIdentifier: my-mysql
Engine: mysql
DBInstanceStatus: available
Endpoint: localhost:50069
DBName: shopdb
MasterUsername: root Both instances show available status with their local endpoints.
Step 5: Verify Docker containers are running
$ docker ps --filter "label=localemu.service=rds"
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
13b6c217ca12 mysql:8.0 "docker-entrypoint.s..." Up 22 seconds 0.0.0.0:50069->3306/tcp localemu-rds-my-mysql
40ebfda7c046 postgres:16 "docker-entrypoint.s..." Up 37 seconds 0.0.0.0:50063->5432/tcp localemu-rds-my-postgres Two containers running: postgres:16 on port 50063 and mysql:8.0 on port 50069.
Step 6: Connect to PostgreSQL and verify version
$ PGPASSWORD=Secret123 psql -h localhost -p 50063 -U admin -d myapp -c "SELECT version();"
version
----------------------------------------------------------------------------------------------------------------------------
PostgreSQL 16.13 (Debian 16.13-1.pgdg13+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
(1 row) SELECT version() reports the actual PostgreSQL 16.13 server binary from the postgres:16 container, including its build flags. Everything else (indexes, JSON, transactions, extensions you CREATE EXTENSION) behaves exactly the same way.
Step 7: Create table, insert data, query in PostgreSQL
$ PGPASSWORD=Secret123 psql -h localhost -p 50063 -U admin -d myapp \
-c "CREATE TABLE users (id serial PRIMARY KEY, name text, email text);"
CREATE TABLE
$ PGPASSWORD=Secret123 psql -h localhost -p 50063 -U admin -d myapp \
-c "INSERT INTO users (name, email) VALUES ('Tarek', 'tarek@tocconsulting.fr');"
INSERT 0 1
$ PGPASSWORD=Secret123 psql -h localhost -p 50063 -U admin -d myapp \
-c "SELECT * FROM users;"
id | name | email
----+-------+------------------------
1 | Tarek | tarek@tocconsulting.fr
(1 row) Standard SQL: serial primary keys, text columns, transactional row storage. The data lives in the per-instance named volume (localemu-rds-my-postgres-data) so it survives stop-db-instance / start-db-instance. A delete-db-instance removes the container; the volume is left in place unless you also remove it with docker volume rm.
Step 8: Connect to MySQL and verify version
$ mysql -h 127.0.0.1 -P 50069 -u root -pSecret123 shopdb -e "SELECT version();"
+-----------+
| version() |
+-----------+
| 8.0.45 |
+-----------+ SELECT version() reports MySQL 8.0.45 from the mysql:8.0 container. The version string moves when Docker Hub publishes a new 8.0 tag.
Step 9: Create table, insert data, query in MySQL
$ mysql -h 127.0.0.1 -P 50069 -u root -pSecret123 shopdb \
-e "CREATE TABLE products (id int AUTO_INCREMENT PRIMARY KEY, name varchar(100), price decimal(10,2));"
$ mysql -h 127.0.0.1 -P 50069 -u root -pSecret123 shopdb \
-e "INSERT INTO products (name, price) VALUES ('Wireless Mouse', 0.00);"
$ mysql -h 127.0.0.1 -P 50069 -u root -pSecret123 shopdb \
-e "SELECT * FROM products;"
+----+--------------+-------+
| id | name | price |
+----+--------------+-------+
| 1 | Wireless Mouse | 0.00 |
+----+--------------+-------+ Real InnoDB tables with auto-increment keys and decimal precision. Standard MySQL behavior.
Python Integration
The connection happens at the wire level, so any language with a PostgreSQL or MySQL driver (Go, Java, Rust, Node.js, .NET, Ruby, ...) works the same way. The Python snippet uses psycopg2 for PostgreSQL and pymysql for MySQL; point them at localhost:<mapped-port> with the master credentials from the create call.
import psycopg2
import pymysql
# Connect to PostgreSQL on LocalEmu
pg_conn = psycopg2.connect(
host="localhost",
port=50063,
user="admin",
password="Secret123",
dbname="myapp",
)
pg_cur = pg_conn.cursor()
pg_cur.execute("CREATE TABLE IF NOT EXISTS users (id serial PRIMARY KEY, name text, email text)")
pg_cur.execute("INSERT INTO users (name, email) VALUES (%s, %s)", ("Tarek", "tarek@tocconsulting.fr"))
pg_conn.commit()
pg_cur.execute("SELECT * FROM users")
for row in pg_cur.fetchall():
print(f" PG: id={row[0]}, name={row[1]}, email={row[2]}")
pg_conn.close()
# Connect to MySQL on LocalEmu
my_conn = pymysql.connect(
host="127.0.0.1",
port=50069,
user="root",
password="Secret123",
database="shopdb",
)
my_cur = my_conn.cursor()
my_cur.execute("CREATE TABLE IF NOT EXISTS products (id int AUTO_INCREMENT PRIMARY KEY, name varchar(100), price decimal(10,2))")
my_cur.execute("INSERT INTO products (name, price) VALUES (%s, %s)", ("Wireless Mouse", 0.00))
my_conn.commit()
my_cur.execute("SELECT * FROM products")
for row in my_cur.fetchall():
print(f" MySQL: id={row[0]}, name={row[1]}, price={row[2]}")
my_conn.close() Docker Compose with persistence
Run LocalEmu under Docker Compose with
RDS_DOCKER_BACKEND=1. Each
create-db-instance automatically
creates a per-instance named Docker volume
(localemu-rds-<id>-data) so the
database files survive container restarts on their own, no extra volume
wiring needed for the RDS containers themselves. The bind mount on
/var/lib/localemu is what carries
the LocalEmu metadata (instance records, security groups, etc.) across
a full LocalEmu restart, alongside
PERSISTENCE=1.
services:
localemu:
image: localemu/localemu
ports:
- "127.0.0.1:4566:4566"
- "127.0.0.1:4510-4559:4510-4559"
environment:
- RDS_DOCKER_BACKEND=1
# Persist LocalEmu metadata (instance records, etc.) across restarts.
- PERSISTENCE=1
volumes:
- ./volume:/var/lib/localemu
# Required: lets LocalEmu launch sibling RDS containers on the host.
- /var/run/docker.sock:/var/run/docker.sock Cleanup
Deleting instances removes the Docker containers automatically. No orphaned processes, no leftover data.
$ awsemu rds delete-db-instance --db-instance-identifier my-postgres --skip-final-snapshot
DBInstance:
DBInstanceIdentifier: my-postgres
DBInstanceStatus: deleting
$ awsemu rds delete-db-instance --db-instance-identifier my-mysql --skip-final-snapshot
DBInstance:
DBInstanceIdentifier: my-mysql
DBInstanceStatus: deleting
$ docker ps --filter "label=localemu.service=rds"
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
(empty - containers removed) Supported engines
| Engine | Versions | Docker image family |
|---|---|---|
| PostgreSQL | 17, 16, 15, 14, 13 (+ point releases) | postgres:<version> |
| MySQL | 8.4, 8.0, 5.7 (+ point releases) | mysql:<version> |
| MariaDB | 11.4, 10.11, 10.6 (+ point releases) | mariadb:<version> |
| Aurora MySQL | maps to MySQL container of the closest version | mysql:<version> |
| Aurora PostgreSQL | maps to PostgreSQL container of the closest version | postgres:<version> |
Resolution lives in
services/rds/docker/engine_mapping.py:
an unknown EngineVersion falls
back to major.minor, then major, then the engine's default tag, with a log
line for each fallback so unfamiliar versions never silently swap engines on you.
Aurora endpoints emulate the wire protocol of the underlying engine: the
Aurora-specific features (parallel query, global databases, cluster failover)
are not implemented.
How it works
DockerDbManager resolves Engine + EngineVersion to a Docker image (postgres:16, mysql:8.0, ...), pulls it if needed, and launches a container with the engine's standard env vars (POSTGRES_USER, MYSQL_ROOT_PASSWORD, ...) so the master credentials and initial database name are honoured by the engine itself. localemu-rds-<id>-data) so its data directory survives container restarts. Endpoint.Address for API parity, while the same response carries the Endpoint.Port you actually dial on localhost. localhost:<mapped-port> using the master credentials from the create call. stop-db-instance stops the container, start-db-instance brings it back on the same volume and the same host port binding, delete-db-instance removes the container (the named volume sticks unless you pass --delete-automated-backups semantics through your own cleanup).