One VPS, one region
Multi-region questions are easier to answer from a known starting point. Here is this application's, from config/deploy.yml.
The topology
servers:
web:
hosts:
- <%= ENV.fetch("NOR_SERVER_IP") %>
volumes:
- /opt/newbieonrails/storage:/rails/storage
accessories:
db:
image: postgres:16
host: <%= ENV.fetch("NOR_SERVER_IP") %>
directories:
- /opt/newbieonrails/db:/var/lib/postgresql/data
The same server address appears twice. The Rails application and the PostgreSQL container run on the same host. The database's data directory is a directory on that host, and uploaded course covers and lesson images live in a volume on the same disk.
There are four logical databases (primary, cache, queue and cable), all in that one Postgres container. Solid Queue runs inside Puma.
Everything is one machine.
Why this is a good starting point
It makes the first question answerable: what would a second region require?
flowchart TB
A["one VPS"] --> B["Rails + Puma + Solid Queue"]
A --> C["Postgres 16 container"]
A --> D["/opt/newbieonrails/storage — uploads on the host disk"]
C --> E["4 logical databases: primary, cache, queue, cable"]
D --> F["the first blocker, and it is not the database"]
The blocker nobody names first
The uploads.
/opt/newbieonrails/storage is a directory on one machine's disk. A second application server in another region can't read it at all.
Every multi-region discussion starts at the database, and in this application the database is the second problem. Object storage is a prerequisite. It has nothing to do with consistency or replication, and it's about a day of work: move Active Storage to S3-compatible storage, migrate the existing blobs, and change one line of configuration.
The general lesson is worth more than this case. Before asking how to replicate state, find every place state is stored. A host volume, a local disk cache, an in-process store, a file written by a rake task: each is a single-region assumption, and none of them shows up in a database diagram.
The physics
One number governs everything that follows, and no engineering removes it.
Light travels about 200 kilometers per millisecond in fibre. São Paulo to Frankfurt is roughly 9,800 kilometers, which is about 50 milliseconds each way, and real cable routes are longer than the great-circle distance. Light alone needs about 100 milliseconds for the round trip, and a real network adds routing on top: on 2026-09-28 a São Paulo to Frankfurt ping averaged 198 milliseconds.
Compare that with the numbers this application already has. A local PostgreSQL round trip takes 0.03ms, as the mid-level course "Testing: in-memory database" measured. A cross-continent round trip is several thousand times slower (about 6,600 times for that ping), and that ratio is the subject of this whole course.
| Operation | Time |
|---|---|
| Local database round trip | 0.03ms |
| Same-datacenter service call | ~1ms |
| Same-continent round trip | 20–40ms |
| Cross-continent round trip | 150–200ms |
A page that makes four database queries spends 0.12ms on them locally, and 600ms if the database is on another continent. No amount of tuning fixes that.
Why this application would ever ask
It ships in English and pt-BR. Two language audiences plausibly means two geographic audiences, and one of them is far from wherever the single VPS sits.
That's the real trigger. It isn't scale or traffic: it's the distance between users and the one machine. A learner two hundred milliseconds away pays that on every page, and no query optimization touches it.
You can measure it before building anything. Lesson 2 is that measurement, and it usually finds that the answer is not a second database.
Key takeaways
- One server runs the Rails app and the Postgres container, and uploads are a volume on the same disk.
- The first blocker is object storage, not the database: another region can't read a host volume.
- Before replicating state, find every place state is stored. Most single-region assumptions aren't in the database diagram.
- A cross-continent round trip takes 150–200ms (São Paulo to Frankfurt: 198ms), against 0.03ms locally: several thousand times slower.
- The trigger is the distance between users and the one machine, and you can measure it before building anything.
Keep going with lesson 2
Measure before replicating
Four measurements to take before replicating, and why a CDN and edge caching usually come before a replica.
A free account also gives you the quizzes, your progress and notes, and the discussion.