Keep your app and database running when one VM dies
Many apps run on a single VM: the web app and PostgreSQL side by side. It's simple, but when that VM goes down, your customers stop too. This guide moves such an app onto two VMs that take over for each other, without changing the app's code.
Test results
Tested on 30 September 2026 with a simple point-of-sale (POS) app writing one transaction per second to PostgreSQL 18 in zero-data-loss mode. The main VM held the web app and the primary database, and was powered off hard while transactions were running:
| Event | Result |
|---|---|
| VM A powered off hard | VM B served transactions again after about 21 seconds (the database moved to B and visitors were sent to B, automatically). |
| Transactions already saved | 0 lost. The running transaction count continued without a gap. |
| VM A powered on again | A rejoined as the database standby by itself and visitors went back to A. One request was slow; no data was lost. |
During those ~21 seconds the cashier sees an error and has to retry. A transaction that was reported as successful is never lost.
Layout: 2 VMs + 1 small witness
| VM | Runs | Size |
|---|---|---|
| A | Web app + PostgreSQL (primary) | Like your current VM |
| B | Copy of the web app + PostgreSQL (standby, always in sync) | Same as A |
| C (witness) | No data. Only votes on which server is primary, so there are never two primaries. | Small (1 vCPU, 1 GB) |
- Put A and B in different locations or providers, so one data center problem can't take out both. C ideally goes in a third location.
- A and B swap roles automatically. When A comes back it catches up on its own; nothing needs copying by hand.
- A VM can belong to one cluster only, including as a witness. So every customer that has their own VMs needs their own witness (the smallest VM is fine).
- Your current VM can become witness C: its old database is still used as the copy source (step 3), and its app is switched off after the move.
Steps
1. Connect the VMs to Saka
Prepare VMs A and B (and C if you're not reusing the old VM), then install the Saka agent on each, including the old VM. See Getting started. Each VM needs a public IP and UDP port 51871 open between the VMs (Saka opens it itself when it manages the firewall).
2. Create a PostgreSQL cluster
- Open Databases, press + Create database cluster, choose PostgreSQL.
- Data servers: A and B. Witness: C.
- Mode: Zero data loss (synchronous). A must for money transactions.
- Version: the same as or newer than the PostgreSQL you use now (default 18).
The cluster is usually ready in 1 to 5 minutes; you're told on Telegram.
3. Move the old database
- Stop the old app so no new transactions happen during the copy (e.g. after closing time).
- On the cluster page, card Move an existing database into the cluster: choose the old VM, paste the old database address as in the app's config, e.g.
postgresql://kasir:[email protected]:5432/kasir, and press Copy to cluster. - Saka copies everything (tables, indexes, views, sequences, extensions) in a single transaction, then compares the row count of every table. Test result: 200,500 rows in 3 seconds, counts identical.
- If it fails midway, the cluster is unchanged and the old database is never touched. The error names the cause (wrong password, old database not answering, pg_hba.conf, version, extension).
- The old database password is only passed to the old VM for the copy and is not stored by Saka.
- Via API:
POST /api/v1/database/<id>/impor {"server_id", "sumber", "database"}. Via an AI agent:database_pindahkan_lama.
4. Deploy the app on A
- Open Sites, Create site, App from Git, choose server A. A repo with a
Dockerfileis used as is; see Apps from Git. - On the cluster page, card Connect app servers: install the proxy on A. It always leads to the primary database, whichever VM that is.
- Set the app's
DATABASE_URLvariable to the proxy address for containers, e.g.postgresql://postgres:[email protected]:15432/kasir?sslmode=require(the port is shown on the card; press Show password for the password). - Deploy, open the app, and check that the old data is there.
The app must reconnect to the database when its connection drops (nearly every framework does: Rails, Laravel, Django, Prisma, node-postgres with a pool). Without that, the app keeps failing after the database moves until it is restarted.
5. Add the second location on B
- On the app page, card Two locations, press Add a second location and choose B. B builds the same commit with the same variables; the database proxy on B is installed automatically on the same port.
- Change the app domain's DNS to a CNAME pointing at the load balancer address shown on the card (e.g.
lbxxxx.lb.saka.work). A bare domain (e.g. shop.com) needs a DNS provider with CNAME flattening, such as Cloudflare; or useapp.shop.com. - HTTPS is set up on both VMs. Every deploy, rollback, and variable change on A is applied to B too.
6. Test it yourself (recommended)
Outside busy hours, power off VM A from your cloud provider's panel. Within about 30 seconds the app opens again from B with all data. Power A back on; it rejoins by itself. You're told on Telegram at each step.
Good to know
- Uploaded files (product photos, attachments) on the VM's disk are not copied for Git apps. Store them in S3 storage (e.g. Cloudflare R2, DigitalOcean Spaces) or in the database.
- Workers and cron run only on A so they never run twice. While A is down, background jobs wait until A is back; web transactions keep running on B.
- Visitors whose DNS answer is still cached (~30 seconds) may briefly be sent to the dead A before moving to B.
- Synchronous mode adds about one network round trip between A and B to each write (a few milliseconds in the same city, tens of milliseconds across countries).
- MySQL/MariaDB databases can't be moved with this button yet; MariaDB clusters don't support the web app and database on the same VM.
Technical details: Managed databases and Sites in two locations.