---
title: "Deploy Postgres HA [Oct '26]"
description: "Self-healing PostgreSQL 18: automated failover + read replica. [Oct '26]"
category: "Storage"
url: https://railway.com/deploy/postgres-ha-oct-26
---

# Deploy Postgres HA [Oct '26]

Self-healing PostgreSQL 18: automated failover + read replica. [Oct '26]

**[Deploy Postgres HA [Oct '26] on Railway](https://railway.com/template/postgres-ha-oct-26)**

Machine-readable deploy manifest (JSON, validated by TemplateCI): https://railway.com/deploy/postgres-ha-oct-26/manifest.json

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

## Template content

### pgaf-replica

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

### pgaf-primary

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

### pgadmin

- **Image:** wotonews/pgadmin-ha:v9.18-2

### pgaf-monitor

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

## Documentation

🟢 **Actively maintained** — last updated Oct '26.

# 🐘 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 — plus a
pgAdmin web UI with both nodes pre-registered. One custom image, four services:
monitor, primary, replica, pgAdmin. The monitor watches the primary; if it
dies, the replica is promoted automatically and the old primary rejoins as a
standby when it comes back. Database traffic stays on Railway's private network.

&gt; 🔄 **Actively maintained** — latest update **2026-10-28**: PostgreSQL 18 + pg_auto_failover 2.2; private-network app connections fixed. History: `CHANGELOG.md`.

---

# Deploy and Host Postgres HA Read Replica

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

| Component | Type | Source / Image | Persistent Volume | Public URL | Description |
| :--- | :--- | :--- | :---: | :---: | :--- |
| **`pgaf-monitor`** | Service | `wotonews/postgres-ha:v0.3.0` | Yes (`/data`) | No | pg_auto_failover monitor — the witness that arbitrates failover. |
| **`pgaf-primary`** | Service | `wotonews/postgres-ha:v0.3.0` | Yes (`/data`) | No | The primary Postgres node — accepts all writes and reads. |
| **`pgaf-replica`** | Service | `wotonews/postgres-ha:v0.3.0` | Yes (`/data`) | No | The standby node — hot streaming replica, promoted on failover. |
| **`pgadmin`** | Service | `wotonews/pgadmin-ha:v9.18-2` | Yes (`/var/lib/pgadmin`) | Generate one | Browser UI with both cluster nodes pre-registered (`primary (writes)` / `replica (reads)`). |

## pgAdmin: one-click cluster insight

Generate a public domain on the `pgadmin` service and open it (credentials
below). Both nodes appear in the server tree labeled by role — browse schemas,
run SQL, watch sessions. The cluster uses private-network trust, so pgAdmin
connects without a database password; role labels follow each host's *current*
role and swap after failover.

## Login credentials

**pgAdmin web UI** (the only service meant to be public):

| | |
| :--- | :--- |
| **URL** | the `pgadmin` service's public domain (`https://`) |
| **Username / Email** | `admin@template.local` (fixed placeholder — not a real inbox) |
| **Password** | the generated `PGADMIN_DEFAULT_PASSWORD` variable — **pgadmin service → Variables tab** |

**The database nodes** have no login screen and no password by design:
`pgaf-primary` / `pgaf-replica` / `pgaf-monitor` are private-network only
(trust authentication inside Railway's private network — nothing is exposed
to the internet). Connect your app services with
`postgresql://pgaf-primary.railway.internal:5432/postgres` (writes) or
`postgresql://pgaf-replica.railway.internal:5432/postgres` (reads).

&gt; The pgAdmin password is generated fresh at deploy time via `${{secret(20)}}`
&gt; and applies on first boot only; change it inside pgAdmin afterwards.

## About Hosting Postgres HA Read Replica on Railway

The three cluster services run the same image and self-organize: roles derive
from Railway-injected env (`RAILWAY_SERVICE_NAME` → monitor vs node,
`RAILWAY_PRIVATE_DOMAIN` → node identity). The first node to join becomes
primary; the second clones it via `pg_basebackup` and streams WAL.

- **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:** plain PostgreSQL 18 — `pg_dump` and walk away.

## 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 app pattern is the
   multi-host connection string (transparent failover):

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

4. Old primary comes back → demoted and re-cloned 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 cluster.** The pg_auto_failover services ship no
variables at all — everything self-wires from Railway-injected environment
variables. The only template variable is the generated pgAdmin password:

| Service | Variable | Default | Description |
| :--- | :--- | :--- | :--- |
| `pgadmin` | `PGADMIN_DEFAULT_PASSWORD` | *generated* (`${{secret(20)}}`) | Login password for `admin@template.local`. |

Cluster derivation (injected, not configurable through the card):

| 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 auth is pg_auto_failover `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.
- To add password auth, fork the image and manage `PGUSER`/passwords via
  pg_autoctl configuration (`--auth scram-sha-256`).

---

## 🚀 Setup &amp; Deployment Guide

1. Click **Deploy** on the template card — Railway provisions the services
   with their volumes and variables.
2. Wait for all deployments to report success (~1–2 minutes; the replica
   clones the primary on first boot).
3. Connect apps to `pgaf-primary.railway.internal:5432` (writes) and
   `pgaf-replica.railway.internal:5432` (reads).
4. Optional — verify the formation (`railway ssh -s pgaf-monitor`):
   `pg_autoctl show state` should list one node `primary` and one `secondary`.

Local development: `git clone  &amp;&amp; cd postgres-ha &amp;&amp; cp .env.example .env`.

---

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

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

Validates structure, marketplace limits, the manifest, volume wiring, and
Dockerfile/entrypoint contracts; with a Docker daemon it also builds the
cluster image and proves a three-node formation converges, streams, and
promotes the replica after a hard primary kill. CI runs the suite on every push.

---

## 📝 Operational notes

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


## Similar templates

- [RustFS | (Just Updated) S3 Object Store, Public URL and Keys Set](https://railway.com/deploy/rustfs-or-just-updated-s3-object-store-p) — S3-compatible object store, keys and public URL preset, volume kept
- [TimescaleDB](https://railway.com/deploy/timescaledb-railway-template) — A PostgreSQL-based time-series database for analytics on Railway.
- [Garage S3 Storage](https://railway.com/deploy/garage-s3-storage) — Ultra-light S3 server: fast, open-source, plug-and-play.

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