diff --git a/.example.env b/.example.env
new file mode 100644
index 0000000..d9602eb
--- /dev/null
+++ b/.example.env
@@ -0,0 +1,3 @@
+POSTGRES_CIDR=172.55.32.0/24
+POSTGRES_SUBNET=172.55.0.0/16
+POSTGRES_GATEWAY=172.55.32.254
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2eea525
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.env
\ No newline at end of file
diff --git a/README.md b/README.md
index 9c8026f..b3ac390 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,62 @@
# Postgres with LOGIC
-Production-ready Postgres with Docker that does not suck, with [LOGIC](https://withlogic.co).
+This repository contains the configuration for production-grade Postgres with Docker, with [LOGIC](https://withlogic.co). It was first presented on 24 January 2024, at Docker Athens during the presentation [Production grade Postgres with Docker](https://www.youtube.com/watch?v=tJegTc-oLtk)[^1].
+
+The configuration in this repository sets up a primary and secondary Postgres server with streaming replication. This means that the primary server accepts all write queries, which are in turn replicated to the secondary server. Therefore, read queries can be load balanced and performed on both servers.
+
+## Requirements
+
+- Docker Engine 25.0.0 or newer
+- Docker Compose 2.24.9 or newer, for development
+
+## Configuration
+
+The setup of Postgres with LOGIC is configured with environment variables and Docker Secrets for sensitive data.
+
+### Environment variables
+
+- `POSTGRES_CIDR`: The CIDR block from which to allocate IPs in the Docker network and also allow replication from (default: `172.54.32.0/24`)
+- `POSTGRES_GATEWAY`: The gateway to use in the Docker network (default: `172.54.32.254`)
+- `POSTGRES_SUBNET`: The subnet to allocate for the Docker network (default: `172.54.0.0/16`)
+
+For convenience, in development these environment variables can be set in a `.env` environment file. Example file available in [`.example.env`](./.example.env)
+
+### Secrets
+
+- `postgres-password`: The password for the `postgres` user of the database used for most database operations
+- `replicator-password`: The password for the `replicator` role, used from the secondary server to replicate data
+
+For convenience, in development all secrets are hardcoded as files and are available in the [`dev/secrets`](./dev/secrets) directory and do not need to be set.
+
+## Development
+
+To kick off and evaluate the setup locally, all you have to do is run
+
+```console
+docker compose up
+```
+
+After all containers start, you can validate the setup with the following steps:
+
+1. Create a table on the primary server
+ ```console
+ docker compose exec primary psql -U postgres -c "CREATE TABLE people (name varchar(40));"
+ ```
+2. Insert a couple of rows in the primary server
+ ```console
+ docker compose exec primary psql -U postgres -c "INSERT INTO people VALUES ('grace');"
+ docker compose exec primary psql -U postgres -c "INSERT INTO people VALUES ('alan');"
+ ```
+3. Validate that data can be read from both servers
+ ```console
+ docker compose exec primary psql -U postgres -c "SELECT * FROM people;"
+ docker compose exec secondary psql -U postgres -c "SELECT * FROM people;"
+ ```
+
+---
-
+ 🦄 Built with LOGIC. 🦄
+
+[^1]: Presentation on YouTube: https://www.youtube.com/watch?v=tJegTc-oLtk
\ No newline at end of file
diff --git a/compose.yml b/compose.yml
new file mode 100644
index 0000000..05dad22
--- /dev/null
+++ b/compose.yml
@@ -0,0 +1,51 @@
+x-base:
+ &base
+ secrets:
+ - postgres-password
+ - replicator-password
+ environment:
+ POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password
+ POSTGRES_CIDR: ${POSTGRES_CIDR:-172.54.32.0/24}
+ command: ["postgres", "-c", "log_statement=all"]
+
+services:
+ primary:
+ <<: *base
+ build:
+ context: postgres
+ target: primary
+ volumes:
+ - primary_data:/var/lib/postgresql/data
+
+ secondary:
+ <<: *base
+ build:
+ context: postgres
+ target: secondary
+ restart: on-failure:3
+ volumes:
+ - secondary_data:/var/lib/postgresql/data
+
+ pgpool2:
+ build:
+ context: pgpool2
+ secrets:
+ - postgres-password
+
+secrets:
+ postgres-password:
+ file: dev/secrets/postgres-password
+ replicator-password:
+ file: dev/secrets/replicator-password
+
+networks:
+ default:
+ ipam:
+ config:
+ - subnet: ${POSTGRES_SUBNET:-172.54.0.0/16}
+ ip_range: ${POSTGRES_CIDR:-172.54.32.0/24}
+ gateway: ${POSTGRES_GATEWAY:-172.54.32.254}
+
+volumes:
+ primary_data:
+ secondary_data:
diff --git a/dev/secrets/postgres-password b/dev/secrets/postgres-password
new file mode 100644
index 0000000..c0e753f
--- /dev/null
+++ b/dev/secrets/postgres-password
@@ -0,0 +1 @@
+development_password
\ No newline at end of file
diff --git a/dev/secrets/replicator-password b/dev/secrets/replicator-password
new file mode 100644
index 0000000..1f31f49
--- /dev/null
+++ b/dev/secrets/replicator-password
@@ -0,0 +1 @@
+development_replicator_password
\ No newline at end of file
diff --git a/pgpool2/Dockerfile b/pgpool2/Dockerfile
new file mode 100644
index 0000000..f47c5ad
--- /dev/null
+++ b/pgpool2/Dockerfile
@@ -0,0 +1,19 @@
+FROM ubuntu:22.04 as pgpool
+
+# https://www.pgpool.net/mediawiki/index.php/Apt_Repository
+
+ENV DEBIAN_FRONTEND noninteractive
+
+RUN apt-get update
+RUN apt-get install -y wget gnupg
+RUN echo "deb http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list
+RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
+RUN apt-get update
+RUN apt-get install -y pgpool2 libpgpool2 postgresql-16-pgpool2
+RUN apt-get install -y gettext-base
+
+COPY pgpool2/ /etc/pgpool2/
+COPY bin/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
+
+ENTRYPOINT [ "docker-entrypoint.sh" ]
+CMD [ "pgpool", "-n" ]
\ No newline at end of file
diff --git a/pgpool2/bin/docker-entrypoint.sh b/pgpool2/bin/docker-entrypoint.sh
new file mode 100755
index 0000000..cb59a67
--- /dev/null
+++ b/pgpool2/bin/docker-entrypoint.sh
@@ -0,0 +1,14 @@
+#! /bin/bash
+
+set -ex
+
+export POSTGRES_PASSWORD=$(cat /run/secrets/postgres-password)
+cat /etc/pgpool2/pgpool.conf.tpl | envsubst > /etc/pgpool2/pgpool.conf
+cat /etc/pgpool2/pool_passwd.tpl | envsubst > /etc/pgpool2/pool_passwd
+unset POSTGRES_PASSWORD
+
+export POSTGRES_PASSWORD_MD5=$(md5sum /run/secrets/postgres-password | awk '{print $1}')
+cat /etc/pgpool2/pcp.conf.tpl | envsubst > /etc/pgpool2/pcp.conf
+unset POSTGRES_PASSWORD_MD5
+
+exec "$@"
diff --git a/pgpool2/pgpool2/pcp.conf.tpl b/pgpool2/pgpool2/pcp.conf.tpl
new file mode 100644
index 0000000..c4c64cb
--- /dev/null
+++ b/pgpool2/pgpool2/pcp.conf.tpl
@@ -0,0 +1,4 @@
+# PCP Client Authentication Configuration File
+# ============================================
+
+postgres:${POSTGRES_PASSWORD_MD5}
diff --git a/pgpool2/pgpool2/pgpool.conf.tpl b/pgpool2/pgpool2/pgpool.conf.tpl
new file mode 100644
index 0000000..728542f
--- /dev/null
+++ b/pgpool2/pgpool2/pgpool.conf.tpl
@@ -0,0 +1,101 @@
+# ----------------------------
+# pgPool-II configuration file
+# ----------------------------
+#
+# Docs: https://www.pgpool.net/docs/latest/en/html/configuring-pgpool.html#:~:text=conf%20is%20the%20main%20configuration,%24prefix%2Fetc%2Fpgpool.
+
+#------------------------------------------------------------------------------
+# BACKEND CLUSTERING MODE
+#------------------------------------------------------------------------------
+
+backend_clustering_mode = 'streaming_replication'
+
+
+#------------------------------------------------------------------------------
+# CONNECTIONS
+#------------------------------------------------------------------------------
+
+# - pgpool Connection Settings -
+
+listen_addresses = '*'
+port = 5432
+
+# - pgpool Communication Manager Connection Settings -
+
+pcp_listen_addresses = 'localhost'
+pcp_port = 9898
+
+# - Backend Connection Settings -
+
+backend_hostname0 = 'primary'
+backend_port0 = 5432
+backend_weight0 = 1
+backend_data_directory0 = '/var/lib/postgresql/data'
+backend_flag0 = 'ALLOW_TO_FAILOVER'
+backend_application_name0 = 'primary'
+
+backend_hostname1 = 'secondary'
+backend_port1 = 5432
+backend_weight1 = 1
+backend_data_directory1 = '/var/lib/postgresql/data'
+backend_flag1 = 'ALLOW_TO_FAILOVER'
+backend_application_name1 = 'secondary'
+
+# - Authentication -
+
+enable_pool_hba = on
+
+# - SSL Connections -
+
+#------------------------------------------------------------------------------
+# LOGS
+#------------------------------------------------------------------------------
+
+# - Where to log -
+log_per_node_statement = on
+
+
+#------------------------------------------------------------------------------
+# LOAD BALANCING MODE
+#------------------------------------------------------------------------------
+
+load_balance_mode = on
+
+
+#------------------------------------------------------------------------------
+# STREAMING REPLICATION MODE
+#------------------------------------------------------------------------------
+
+# - Streaming -
+
+sr_check_period = 2
+sr_check_user = 'postgres'
+sr_check_password = '${POSTGRES_PASSWORD}'
+sr_check_database = 'postgres'
+
+
+#------------------------------------------------------------------------------
+# HEALTH CHECK GLOBAL PARAMETERS
+#------------------------------------------------------------------------------
+
+health_check_period = 30
+health_check_timeout = 20
+health_check_user = 'postgres'
+health_check_password = '${POSTGRES_PASSWORD}'
+health_check_database = 'postgres'
+health_check_max_retries = 3
+health_check_retry_delay = 10
+
+#------------------------------------------------------------------------------
+# FAILOVER AND FAILBACK
+#------------------------------------------------------------------------------
+failover_command = 'PGPASSWORD=${POSTGRES_PASSWORD} psql --host secondary -U postgres -c "SELECT pg_promote();"'
+failover_on_backend_error = on
+failover_on_backend_shutdown = on
+detach_false_primary = on
+
+#------------------------------------------------------------------------------
+# ONLINE RECOVERY
+#------------------------------------------------------------------------------
+
+auto_failback = on
diff --git a/pgpool2/pgpool2/pool_hba.conf b/pgpool2/pgpool2/pool_hba.conf
new file mode 100644
index 0000000..6a5c547
--- /dev/null
+++ b/pgpool2/pgpool2/pool_hba.conf
@@ -0,0 +1,9 @@
+# https://www.pgpool.net/docs/latest/en/html/auth-pool-hba-conf.html
+
+# "local" is for Unix domain socket connections only
+local all all trust
+# IPv4 local connections:
+host all all 127.0.0.1/32 trust
+host all all ::1/128 trust
+
+host all all all scram-sha-256
diff --git a/pgpool2/pgpool2/pool_passwd.tpl b/pgpool2/pgpool2/pool_passwd.tpl
new file mode 100644
index 0000000..4a9e930
--- /dev/null
+++ b/pgpool2/pgpool2/pool_passwd.tpl
@@ -0,0 +1 @@
+postgres:TEXT${POSTGRES_PASSWORD}
diff --git a/postgres/Dockerfile b/postgres/Dockerfile
new file mode 100644
index 0000000..01c6a91
--- /dev/null
+++ b/postgres/Dockerfile
@@ -0,0 +1,16 @@
+FROM postgres:16.2 as base
+
+
+FROM base as primary
+
+COPY ./postgres/primary/docker-entrypoint-initdb.d/ docker-entrypoint-initdb.d/
+
+
+FROM base as secondary
+
+COPY ./postgres/secondary/bin/ /usr/local/bin/
+ENTRYPOINT [ "docker-entrypoint-override.sh" ]
+
+CMD ["postgres"]
+
+FROM primary
diff --git a/postgres/primary/docker-entrypoint-initdb.d/10-setup-replication-user.sh b/postgres/primary/docker-entrypoint-initdb.d/10-setup-replication-user.sh
new file mode 100644
index 0000000..e7092f3
--- /dev/null
+++ b/postgres/primary/docker-entrypoint-initdb.d/10-setup-replication-user.sh
@@ -0,0 +1,4 @@
+set -ex
+
+REPLICATOR_PASSWORD=$(cat /run/secrets/replicator-password)
+psql -c "CREATE ROLE replicator REPLICATION LOGIN PASSWORD '$REPLICATOR_PASSWORD'"
\ No newline at end of file
diff --git a/postgres/primary/docker-entrypoint-initdb.d/20-setup-replication-auth.sh b/postgres/primary/docker-entrypoint-initdb.d/20-setup-replication-auth.sh
new file mode 100644
index 0000000..861ef3d
--- /dev/null
+++ b/postgres/primary/docker-entrypoint-initdb.d/20-setup-replication-auth.sh
@@ -0,0 +1,6 @@
+set -ex
+
+POSTGRES_CIDR=${POSTGRES_CIDR:-172.54.32.0/24}
+PGDATA=${PGDATA:-/var/lib/postgresql/data}
+
+echo "host replication replicator $POSTGRES_CIDR scram-sha-256" >> $PGDATA/pg_hba.conf
diff --git a/postgres/secondary/bin/10-base-backup.sh b/postgres/secondary/bin/10-base-backup.sh
new file mode 100755
index 0000000..10a8513
--- /dev/null
+++ b/postgres/secondary/bin/10-base-backup.sh
@@ -0,0 +1,6 @@
+set -ex
+
+export PGPASSWORD=$(cat /run/secrets/replicator-password)
+
+pg_basebackup -w -h primary -D $PGDATA -U replicator -P -v -X stream
+chmod -R 0750 $PGDATA
diff --git a/postgres/secondary/bin/20-enable-standby-mode.sh b/postgres/secondary/bin/20-enable-standby-mode.sh
new file mode 100755
index 0000000..64fc817
--- /dev/null
+++ b/postgres/secondary/bin/20-enable-standby-mode.sh
@@ -0,0 +1,3 @@
+set -ex
+
+touch $PGDATA/standby.signal
diff --git a/postgres/secondary/bin/30-configure-primary.sh b/postgres/secondary/bin/30-configure-primary.sh
new file mode 100755
index 0000000..8ba48e7
--- /dev/null
+++ b/postgres/secondary/bin/30-configure-primary.sh
@@ -0,0 +1,6 @@
+set -ex
+
+pg_ctl -w start
+REPLICATOR_PASSWORD=$(cat /run/secrets/replicator-password)
+psql -c "ALTER SYSTEM SET primary_conninfo = 'host=primary port=5432 user=replicator password=$REPLICATOR_PASSWORD application_name=secondary';"
+pg_ctl -w stop
diff --git a/postgres/secondary/bin/docker-entrypoint-override.sh b/postgres/secondary/bin/docker-entrypoint-override.sh
new file mode 100755
index 0000000..589da8e
--- /dev/null
+++ b/postgres/secondary/bin/docker-entrypoint-override.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+set -ex
+
+if [ ! -f $PGDATA/PG_VERSION ]
+then
+ echo "Secondary server is not initialized. Starting initialization procedure."
+ su postgres -c '10-base-backup.sh'
+ su postgres -c '20-enable-standby-mode.sh'
+ su postgres -c '30-configure-primary.sh'
+ echo "Initialization procedure completed successfully."
+else
+ echo "Secondary server is already initialized."
+fi
+
+exec docker-entrypoint.sh "$@"