---
title: "Deploy Postgres HA Read Replica"
description: "Highly available PostgreSQL with automated failover and a read replica."
category: "Storage"
url: https://railway.com/deploy/postgres-ha-read-replica
---

# Deploy Postgres HA Read Replica

Highly available PostgreSQL with automated failover and a read replica.

**[Deploy Postgres HA Read Replica on Railway](https://railway.com/template/postgres-ha-read-replica)**

- **Creator:** wotonews
- **Category:** Storage

## Template content

### pgaf-replica

- **Image:** wotonews/postgres-ha:v0.1.5

### pgaf-primary

- **Image:** wotonews/postgres-ha:v0.1.5

### pgaf-monitor

- **Image:** wotonews/postgres-ha:v0.1.5

## Documentation

# 🐘 Postgres HA Read Replica - Premium Railway Template

A highly available PostgreSQL cluster with automated failover and a streaming
read replica, powered by [pg_auto_failover](https://github.com/hapostgres/pg_auto_failover)
and deployed as a single Docker image across three Railway services.

&gt; **The short version:** one custom image, three services (monitor, primary,
&gt; replica). The monitor watches the primary; if it dies, the replica is
&gt; promoted automatically and the old primary rejoins as a standby when it
&gt; comes back. All traffic stays on Railway's private network.

---

# Deploy and Host Postgres HA Read Replica

Deploying this template provisions a three-service PostgreSQL high-availability
formation in your Railway project:

| Component | Type | Source / Image | Persistent Volume | Public URL | Description |
| :--- | :--- | :--- | :---: | :---: | :--- |
| **`pgaf-monitor`** | Service | `wotonews/postgres-ha:v0.1.0` | Yes (`/data`) | No | pg_auto_failover monitor — the witness that arbitrates failover. |
| **`pgaf-primary`** | Service | `wotonews/postgres-ha:v0.1.0` | Yes (`/data`) | No | The primary Postgres node — accepts all writes and reads. |
| **`pgaf-replica`** | Service | `wotonews/postgres-ha:v0.1.0` | Yes (`/data`) | No | The standby node — hot streaming replica, promoted on failover. |

## About Hosting Postgres HA Read Replica on Railway

All three services run the same image and self-organize: roles are derived
from Railway-injected environment variables (`RAILWAY_SERVICE_NAME` → monitor
vs node, `RAILWAY_PRIVATE_DOMAIN` → node identity). The first Postgres node
to join the formation becomes the primary; the second clones it with
`pg_basebackup` and streams WAL continuously.

- **Automated failover:** the monitor health-checks the primary every second;
  on failure it promotes the standby — no human intervention, no DNS tricks.
- **Automatic rejoin:** when the old primary returns (e.g. after a Railway
  redeploy), it is demoted and re-cloned as a standby. No split-brain, no
  manual `pg_rewind`.
- **Private networking:** nodes talk to each other over
  `.railway.internal` hostnames on Railway's private network. Nothing
  is exposed to the public internet by default.

## Why Deploy Postgres HA Read Replica?

- **Sleep-at-night durability:** a dead primary costs you seconds of
  availability, not your database.
- **Read scaling:** point reporting/analytics traffic at
  `pgaf-replica.railway.internal` to keep the primary free for writes.
- **Railway-native:** one image, three services, standard volumes — scale the
  replica tier by duplicating the `pgaf-replica` service.
- **No lock-in:** it's plain PostgreSQL 16 — dump, `pg_dump`, and walk away
  whenever you like.

## Common Use Cases

- Production Postgres for APIs and web apps that cannot tolerate database downtime.
- Offloading heavy read/reporting queries to a hot standby.
- Zero-downtime maintenance windows (fail over, patch, fail back).
- A resilient Postgres backend for queues, analytics, or stateful workers.

## Dependencies for Deploying Postgres HA Read Replica

### Deployment Dependencies

- A Railway account (the template deploys three services + three volumes).
- Your application services must live in the **same Railway project and
  environment** to reach the cluster over the private network.
- No public port is exposed. To connect from inside Railway:
  - Writes: `postgresql://postgres@pgaf-primary.railway.internal:5432/postgres`
  - Reads: `postgresql://postgres@pgaf-replica.railway.internal:5432/postgres`

---

## 📐 Architecture &amp; Components

```mermaid
graph TD
    App[Your App Services<br>same Railway project] --&gt;|writes| Primary[pgaf-primary]
    App --&gt;|reads| Replica[pgaf-replica]
    Monitor[pgaf-monitor] &lt;--&gt;|health checks + failover arbitration| Primary
    Monitor &lt;--&gt;|health checks + promotion| Replica
    Primary --&gt;|WAL streaming| Replica
    Primary --- V1[(volume /data)]
    Replica --- V2[(volume /data)]
    Monitor --- V3[(volume /data)]
```

### How failover works

1. Monitor misses health checks from the primary (default: ~10s of failures).
2. Monitor promotes the standby — it becomes the new primary.
3. Clients reading `pgaf-primary.railway.internal` should reconnect to
   `pgaf-replica.railway.internal`; the recommended pattern for app connections
   is the multi-host connection string:

   ```
   postgresql://postgres@pgaf-primary.railway.internal:5432,postgres@pgaf-replica.railway.internal:5432/postgres?target_session_attrs=read-write
   ```

4. Old primary comes back → pg_auto_failover demotes and re-clones it as the
   new standby. Roles converge without manual action.

### Scaling out

Duplicate the `pgaf-replica` service (same image, same variables) to add more
standbys. Each new node clones the current primary and joins the formation.

---

## ⚙️ Configuration

**Zero-configuration on Railway.** The template ships no required variables —
deployers click Deploy and everything self-wires from Railway-injected
environment variables:

| Derived from | Used for |
| :--- | :--- |
| `RAILWAY_SERVICE_NAME` | Role: the `pgaf-monitor` service runs the monitor; every other service joins as a Postgres node. |
| `RAILWAY_PRIVATE_DOMAIN` | This node's private DNS name (`NODE_HOST`). |

Optional overrides (for renamed services or local Docker runs):

| Variable | Default | Description |
| :--- | :--- | :--- |
| **`NODE_ROLE`** | derived | Force the role: `monitor`, `primary`, or `secondary` (primary/secondary behave identically — the formation assigns real roles). |
| **`NODE_HOST`** | derived | Override this node's DNS name. |
| **`MONITOR_SERVICE`** | `pgaf-monitor` | Which service name runs the monitor (also derives `MONITOR_HOST`). |
| **`MONITOR_HOST`** | `pgaf-monitor.railway.internal` | Override the monitor's DNS name. |
| **`PGDATA`** | `/data/pgdata` | Postgres data directory (on the volume). |
| **`PGPORT`** | `5432` | Postgres port. |
| **`WAIT_TIMEOUT`** | `300` | Seconds a node waits for the monitor before exiting (restart policy retries). |

&gt; Renaming services? Set `NODE_ROLE=monitor` on the monitor and
&gt; `MONITOR_HOST=` on the nodes — otherwise keep names as-is.

### 🔒 Security model

- Inter-node authentication uses pg_auto_failover's `trust` mode, scoped to
  Railway's **private network** — unreachable from the public internet.
- **Do not** attach a public TCP domain to these services: that would bypass
  network isolation. Private-network access only.
- To add password auth, set `--auth scram-sha-256` behavior by forking the
  image and managing `PGUSER`/passwords via pg_autoctl configuration.

---

## 🚀 Setup &amp; Deployment Guide

### Deploy from the marketplace

1. Click **Deploy** on the template card — Railway provisions the three
   services with their volumes and variables.
2. Wait for all three deployments to report success (~1–2 minutes; the replica
   clones the primary on first boot).
3. Connect your app services to
   `pgaf-primary.railway.internal:5432` (writes) and
   `pgaf-replica.railway.internal:5432` (reads).

### Verify the formation

From any of the three services (`railway ssh -s pgaf-monitor`):

```bash
pg_autoctl show state
# expect: one PRIMARY node and one SECONDARY node in state "primary" / "secondary"
```

### Local development

```bash
git clone  &amp;&amp; cd postgres-ha
cp .env.example .env
```

---

## 🧪 End-to-End (E2E) Testing

```bash
bash e2e/run-tests.sh
```

Validates directory structure, marketplace publish limits, the three-service
manifest, role variable wiring, and entrypoint/Dockerfile syntax. CI runs the
same suite on every push.

---

## 📝 Operational notes

- **Cost:** three always-on services + volumes. The monitor is tiny; consider
  a small plan for it.
- **Backups still matter:** HA is not backup. Use `pg_dump` against the
  replica to avoid load on the primary.
- **Upgrades:** pin/ bump the image tag in `railway.json`, redeploy services
  one at a time — pg_auto_failover fails over during primary redeploys
  automatically.


## Similar templates

- [Garage S3 Storage](https://railway.com/deploy/garage-s3-storage) — Ultra-light S3 server: fast, open-source, plug-and-play.
- [Redis](https://railway.com/deploy/redis-1) — Self Host Latest Redis with Railway
- [EasyImg](https://railway.com/deploy/easyimg) — Simple self-hostable Nuxt.js personal image hosting system.

Open this page in a browser: https://railway.com/deploy/postgres-ha-read-replica
