Docs / Use Cases / RDS Docker Databases

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.

What the demo does. Creates two RDS instances via 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

Terminal
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

Terminal
$ 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

Terminal
$ 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

Terminal
$ 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

Terminal
$ 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

Terminal
$ 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

Terminal
$ 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

Terminal
$ 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

Terminal
$ 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.

rds_app.py
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.

docker-compose.yml
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.

Terminal
$ 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
PostgreSQL17, 16, 15, 14, 13 (+ point releases)postgres:<version>
MySQL8.4, 8.0, 5.7 (+ point releases)mysql:<version>
MariaDB11.4, 10.11, 10.6 (+ point releases)mariadb:<version>
Aurora MySQLmaps to MySQL container of the closest versionmysql:<version>
Aurora PostgreSQLmaps to PostgreSQL container of the closest versionpostgres:<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

1. awsemu rds create-db-instance hits the RDS API on LocalEmu, which records the instance, subnet group, parameter group and tags in its in-memory RDS metadata store, exactly like the real RDS control plane would.
2. That metadata feeds the real behavior layer: the 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.
3. The container is given a per-instance named volume (localemu-rds-<id>-data) so its data directory survives container restarts.
4. A free host port is published to the engine's port (5432 / 3306). The DescribeDBInstances response keeps the AWS-shaped Endpoint.Address for API parity, while the same response carries the Endpoint.Port you actually dial on localhost.
5. You connect with any standard database client (psql, mysql, DBeaver, pgAdmin, the language driver of your choice) on localhost:<mapped-port> using the master credentials from the create call.
6. 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).