Commits

First real rollback, the mystery of the neighboring postgres, and the updater that updates itself

v0.1.17 didn't boot and the updater rolled back on its own; a migration kept failing

Yesterday I told you about the audit and the updates button. Today is the day all of that went through the acid test: the first real rollback in production, a data migration that resisted with a DNS mystery in the middle, and the retirement of the transient web that had been hanging around from the beginning.

Security batches: comments, backups, and tougher containers

The audit left a prioritized list (P0, P1, P2...) and the day started by closing the entire P2 block. Translation of the most noteworthy items:

  • Comments with belt and suspenders: limit of 10 comments per hour per real IP address, and webhook notifications only fire when the comment is approved — before, any pending one sent messages.
  • Backups that verify what they do: before, a database dump cut halfway through could be compressed as if it were valid. Now the script checks that the dump finished properly before compressing.
  • Containers without privileges: the website containers run as a normal user, not as root —the system administrator—. Less power if someone breaks in.
  • Supply chains pinned: server dependencies now use exact versions, so a rebuild doesn't bring new surprises.

And a process lesson worth telling: after a security hook blocked, an edit stayed out of the commit and the v0.1.15 release went out without a necessary line. The v0.1.16 was literally one line. The lesson: after any block, review the full repository state before tagging.

The first real rollback (and it worked)

This is where every update system has its moment: v0.1.17 didn't boot. The cause, pure permission delicacy: the startup script tried to take ownership of all mounted folders, but one of them is read-only (the geolocation database). The process died when it hit it.

And the updater did exactly what it was designed to do: it rolled back to the previous version on its own, without a human hand. First real rollback in production, passed. The fix came with v0.1.18: only take ownership of what can be written to, and never let the file ownership change be lethal.

But the story had a second part: by running without a root user, the API could no longer talk to the updater socket (a special file for inter-process communication, with very strict permissions). Result: the updates card silently disappeared from the panel. The fix: open that channel to the processes that need it. Lesson to remember: when you change the user running a service, review all local channels — sockets, volumes, signals — not just data folders.

The updater that updates itself

Another of the eternal loose ends dropped: until today, updating the server's own updater was manual surgery. Now there's a signed command that does it all: verifies the catalog signature, downloads the image, extracts the new code, and performs the swap with backup and health check. If the extraction doesn't carry a usable updater, it aborts before touching anything.

During the live test, two failures that the mechanism itself contained came up — a copy without recursion, and the worst one: a container that wrote to its own temporary directory without mounting the destination, meaning it worked and then threw the result into the trash with no apparent error. Both caught by the "is what was extracted usable?" check before installing. Failing fast and loudly in an installer is a virtue.

The mystery of the neighboring postgres

The gem of the day. We migrated the product website to the new circuit: its own repository, its own image, installation on the other server, real data restored. And the installation kept failing with "wrong password"... even though it was correct.

The culprit: on a shared server, the network where public services live hosts other people's installations. And Docker's internal DNS —the service that resolves names to addresses— was sending my "db" to some other installation's postgres. My API was talking to it with my password, to a server that wasn't mine. Obviously, failure.

The architectural solution: the API, the panel, and the website for each client live only on the project's private network; only the front door touches the public network. And explicit aliases so the checks resolve correctly.

The migration ended well: 35 real articles, admins, media, and analytics coming back to life in production after being broken since September 17. And the owner's final test: a menu change built on GitHub and applied with the button, without touching any server.

Goodbye to the transient web

To close the day, a symbolic retirement: we deleted the transient web/ tree that had been hanging around since the earliest iterations. Core releases are now just API and panel. Less dead code, less attack surface, smaller releases. The operational lesson: before deleting, map all references — and don't forget the development compose, because dead code doesn't only live in the workflows.

The previous entry closed the audit's holes. And in the next one, the diary debuts its own repository.

Other diary entries