From Replit to a ¥935/month VPS

AI

Environment: Ubuntu 24.04 on a 1 GB VPS, systemd, nginx, Let’s Encrypt, Postgres. Two apps moved — one Django, one Node. Migrated August 2026.

We run two small apps: one for school operations, and one for exam practice. Both lived on a hosted development platform at about $20 a month. They now live on a 1 GB VPS at ¥935 a month, which is roughly a third of the cost.

Cost is why it started. It is not why it was worth doing, and it is not the interesting part.

Diagram: three candidate databases before the move — a stale committed sqlite file, an unused managed-Postgres URL in the secrets, and the real Postgres running inside the platform container — next to the stack after the move, and four things that broke
Before: three things that looked like the database. After: an ordinary box. Below: everything that broke, none of which was the application’s own logic.

First, find out where the data actually is

Before planning anything, find the production database. This sounds trivial. It was not.

Three things in that project looked like the database:

  1. A db.sqlite3 committed to the repository. Last written more than a year earlier — a leftover from early development, not production.
  2. A managed-Postgres connection URL sitting in the platform’s secrets. Read by nothing in the codebase. It had presumably been set up once and abandoned. Migrating it would have been migrating an empty database.
  3. A Postgres instance running inside the platform’s own container, whose hostname in DATABASE_URL resolves only inside that platform.

The third one had the live data.

And because that hostname is internal, there is no connection from outside — no pg_dump from a laptop, no replica, no external backup, nothing. The entire dataset existed on one disk belonging to a service we were about to stop paying for.

So the first action was not planning. It was a dump, taken with the platform’s own tools, before touching anything else.

If you are on a platform like this, do that today, whether or not you are moving. Then grep the codebase for each environment variable that looks like a database and find out which one is real.

Advertisement

Getting files off a platform that doesn’t want to give them to you

A footnote with a wider point. The platform’s newer UI has no "download as zip" and no right-click-download in the file tree. Support answers involve their CLI or git.

Use those. Clone the repository, pull with the vendor CLI, or copy over an authenticated channel the platform already provides. Do not invent a side door through the running app.

Which brings us to the next part.

The move is when you name the environment out loud

On a hosted development platform, deploying can feel like continuing to develop. The platform fills in defaults. Moving to a box you administer forces a sentence nobody had been asked to say: this is production, and the configuration has to say so.

The list of things made explicit because of the move, none of which the move required as a feature:

  • A real WSGI server instead of the development server
  • Secrets in the environment, not in the working tree
  • Explicit allowed-hosts and trusted-origins for the new domain
  • A nightly database dump, fourteen generations kept, which had not existed at all
  • A firewall on the provider and one on the host, open only for web traffic and administration

That is the actual return on this project. The bill went down; the setup went from "works" to "administered".

Four things that broke

None of them were in the application’s own logic. All four were things the platform had been quietly providing.

A driver bound to one vendor. The Node app connected through a serverless Postgres driver, the kind that reaches the database over the vendor’s own transport rather than the Postgres wire protocol. Pointed at an ordinary Postgres server it does not fail with a connection error — it tries to open a WebSocket to that hostname, meets nginx, and an API route returns 500 with nothing useful in it. Swapping to the standard client fixed it. Check your driver before you check your credentials.

A secret with a fallback. The session key was read as process.env.SESSION_SECRET || "a-literal-in-the-source". On the new host that variable was not set, so it silently used the literal — a fixed value, with no error and no warning. Now it comes from a real environment variable and the process refuses to start without one. A fallback for a secret is a bug with a friendly face.

A completion check that matched itself. Small and worth knowing: pgrep -f 'npm install' matches the ssh command string that contains those words, so a script waiting for the install to finish waits forever. If you poll by pattern, make the pattern narrower than the command you typed.

Verify who can log in before you change how logging in works. Rotating credentials on a box that holds the only copy of the data is the one mistake with no undo. Check the accounts that actually exist, not the ones everyone has in their head.

A couple of non-technical ones too, since they cost real time. The registrar’s DNS change screen adds a ¥1,369 domain-protection product to the confirmation by default; decline it in the popup. And DNS is not always managed where you think: of two domains we moved, one had nameservers pointing at the hosting control panel and the other at the registrar’s DNS console. Check the NS records rather than assuming they match.

What we turned off while we were there

A migration is a rare moment when someone reads every part of a system. Use it.

One feature — an AI-based writing-correction tool — was switched off during the move. Usage did not justify a metered API bill. The UI and the API route are both closed; the code is still there, behind a flag, if it ever earns its way back.

Deciding that took ten minutes and only happened because the move made someone list every running thing. That list is worth writing even if you never migrate.

About the old posts on this site

This blog used to be mostly deployment tutorials: putting apps on a cloud provider’s VMs, VPCs, bastion hosts, security groups, static hosting on a git-hosting service. Those posts are from 2021 and 2022, and the instructions in them still broadly work.

They are being retired anyway, and pointed here, because they no longer describe how we work or what this site is for. Not because the cloud is wrong — for a small internal app with a known, flat load, it was more machinery than the problem needed, and the last few years have been a steady move in the other direction: fewer moving parts, a box we administer, ordinary tools we can read.

If you arrived here from one of those posts, this is the honest version of the same subject: what we run now, what it costs, and what broke on the way.


That closes this run of posts. The next set goes back to the production side — what happens between a manuscript and a finished book, and which parts of it a model can be trusted with.

タイトルとURLをコピーしました