Builder Receipt: Bonumark Hosting Portability

I spent this development cycle finding out whether Bonumark Stream actually worked across different hosting environments, or whether I had quietly built assumptions around the shared hosting I had been using.

The answer was somewhere in the middle. Bonumark was already more portable than I expected, but moving it into a different environment exposed assumptions in the deployment layer that needed to become explicit product requirements.

The Problem Was Bigger Than One Server

The first mistake would have been treating every problem I found as something wrong with the server.

I did not want to optimize Bonumark around one installation. Bonumark is a public self-hosted product. The software should describe what it needs in terms of capabilities, not tell every user to configure a server exactly like mine.

That changed how I approached the entire pass.

Instead of saying a particular Linux user needed ownership of a directory, Bonumark needed to know whether the PHP process could write the runtime storage it actually required.

Instead of assuming .htaccess would protect private files, Bonumark needed proper guidance and configuration for Nginx too.

Instead of assuming application files had to be writable by PHP, Bonumark needed to recognize that a locked application tree is a valid and more restrictive deployment model.

That became the standard for the work that followed.

The Audit Exposed Real Problems

The first issue was a routing bug in /stream. An empty slug could be processed like an individual post slug, causing the stream route to behave incorrectly.

The import system exposed another problem.

Import previews needed private staging directories, but those directories were not part of the installer or System Check contract. On a permissive host, that could go unnoticed because PHP could create them when needed. On a more restricted installation, the import simply failed.

That was useful because it exposed a larger problem. The installer, System Check, and individual features did not all agree on what Bonumark needed to be writable.

I did not want to patch import staging as another isolated exception.

Bonumark needed one authoritative runtime filesystem definition.

That definition became the source used for provisioning and diagnostics, covering the runtime locations Bonumark actually depends on, including data, temporary storage, exports, upgrade backups, content versions, import staging, and media.

The goal was simple: if the application requires something from the hosting environment, that requirement should be visible before a feature fails deep inside normal use.

Supporting Nginx Properly

Bonumark already acknowledged that Nginx required equivalent rules to Apache and LiteSpeed, but telling someone that is not the same as supporting it.

Apache installations had actual .htaccess behavior. Nginx users were expected to recreate routing and security rules themselves.

I decided that was not good enough.

The portability work added maintained Nginx deployment documentation and configuration covering clean routes, PHP-FPM handling, Authorization forwarding, private directory protection, /scripts protection, media behavior, and the rest of the application routing contract.

I also wanted parity to be testable instead of assumed.

The later audit verified the Apache and Nginx route definitions against each other and got an exact 24 of 24 route match. The Nginx configuration also passed syntax validation.

That gave me something much stronger than saying it should work on Nginx.

System Check Became a Hosting Diagnostic

Another requirement was that Bonumark should explain the environment it is running in instead of reducing everything to a generic pass or fail.

System Check grew beyond basic PHP checks.

It now distinguishes core requirements from feature capabilities. It reports the runtime storage contract, understands optional dependencies such as cURL and ZipArchive, reports image-processing capability, calculates effective upload limits, identifies database compatibility, and recognizes when a feature is unavailable because of the installation’s permission model.

That distinction matters.

If PHP cannot install a theme ZIP or overwrite Bonumark’s application files, that does not automatically mean the site is broken.

A locked application tree can be healthy while intentionally preventing the web-facing PHP process from modifying application code.

Bonumark now understands the difference.

The Upgrader Exposed the Most Important Design Problem

The biggest issue came from actually testing the built-in upgrader.

The package validated. The backup was created. Then PHP tried to replace the first application file, and the operating system refused the write.

That was the correct behavior for a locked application tree.

The wrong response would have been to make the entire application writable by PHP just so the web updater could work.

I rejected that approach.

Instead, Bonumark learned to detect whether automatic upgrades are possible in the current environment. A locked installation can continue operating normally while reporting that web-based upgrades are unavailable.

The same test exposed a second problem.

When the first file replacement failed, the old updater attempted to restore every package file even though nothing had actually changed. That made the rollback warning sound much worse than the real condition.

I changed the requirement so the upgrader tracks what was actually modified.

Rollback now deals with changes that really happened instead of blindly restoring the entire package.

Manual Deployment Was Safe, But It Was Not Good Enough

At that point, Bonumark could correctly recognize a locked deployment, but the alternative upgrade procedure was still too much work.

The manual process required extracting the release, creating backups, building a long rsync command with preservation exclusions, checking migrations, checking obsolete files, running deployment verification, and then testing the site.

That procedure was defensible as an emergency or fallback path.

It was not acceptable as the normal way to install every development update.

That became the next product problem.

I wanted to keep the security boundary while removing the repetitive manual work.

The answer was an owner-run CLI upgrade workflow.

Building the Owner-Run Upgrade Workflow

The important decision was not to create a completely separate updater.

If the browser updater and command-line updater each had their own upgrade logic, they would eventually drift apart.

Instead, the upgrade machinery was moved into a shared core engine.

The Admin upgrade screen became one interface to that engine.

A new scripts/deploy-update.php command became another.

The command-line updater runs with the permissions of the person who deliberately invokes it. It does not use sudo, install a privileged daemon, create a setuid helper, or give the PHP process any way to elevate itself.

That lets a deployment owner update Bonumark while the web-facing PHP process remains unable to rewrite application code.

The CLI workflow handles the pieces that should belong to a real upgrade path: package and release validation, software backups, owner-data preservation, migration checks, obsolete package cleanup, recovery behavior, upgrade history, and post-install deployment verification.

I also caught a transition problem before testing it.

An older Bonumark installation obviously would not already contain the new CLI updater. The release therefore needed to support running the updater from an extracted package and pointing it at an existing installation.

That created a bootstrap path into the new system without requiring the old manual overlay procedure one more time.

Testing the Actual Behavior

I did not want this to stop at package-level tests.

The portability work was checked repeatedly with PHP linting, JavaScript syntax checks, JSON validation, CSS structural checks, release-manifest verification, ZIP integrity tests, application smoke tests, filesystem tests, route parity tests, CLI-guard regression tests, and deployment checks.

The final v0.6.8 package passed lint across 209 PHP files, JavaScript syntax across seven files, JSON validation across 18 files, CSS structural validation across 19 files, release-manifest verification, package smoke testing, confirmation regression testing, and clean ZIP extraction.

Those checks mattered, but the more important test was using the upgrade workflow against a real installed Bonumark instance.

The first owner-run upgrade moved the installation from v0.6.6 to v0.6.7.

It created the software backup, preserved the runtime data, found no pending migrations, installed the new package, and automatically ran installed-site deployment verification afterward.

That verification reported the application at v0.6.7 with matching version markers, valid package-managed files, no obsolete package files, expected runtime directories, compatible MariaDB, and zero pending migrations.

I also tested the public boundaries after the upgrade.

The homepage returned 200.

/stream returned 200.

/_bonumark_stream/ returned 403.

/scripts/ returned 403.

System Check reported 27 passes, two expected warnings, and zero failures.

The warnings were the intended consequences of the locked application model: the PHP process could not perform web-based application upgrades or install themes directly into locked theme directories.

Then I tested the part that mattered for normal ongoing use.

I built v0.6.8 as a small confirmation-polish release and upgraded from v0.6.7 using the copy of deploy-update.php already installed in Bonumark.

That upgrade completed successfully too.

The first test proved the bootstrap workflow.

The second proved the normal ongoing workflow.

A Small Test Still Found a Real Usability Problem

The first time I reached the CLI confirmation prompt, I typed:

upgrade

The updater canceled because it only accepted uppercase UPGRADE.

Nothing was damaged, but the behavior did not make sense.

The protection comes from requiring the user to deliberately type the confirmation word. Requiring a particular capitalization did not add meaningful safety.

That failed interaction became part of v0.6.8.

The confirmation is now case-insensitive while still requiring the exact word.

It was a small change, but it is exactly the kind of thing I want these development passes to expose.

A workflow can be technically correct and still be unnecessarily irritating in actual use.

What This Work Proved

The biggest result is not that Bonumark now works on one particular server.

The result is that the product has a clearer hosting model.

Runtime storage, optional management capabilities, and application-code permissions are no longer treated as the same thing.

Apache and LiteSpeed remain supported through their existing configuration.

Nginx now has a maintained deployment path instead of a warning telling administrators to figure it out themselves.

Locked application trees are recognized as valid.

Web-based upgrades can still work where the hosting environment allows them.

Owner-run CLI upgrades provide a secure alternative where shell access and stricter permissions are available.

Manual deployment remains the fallback.

That is the kind of flexibility I want from self-hosted software.

What Still Needs Work

I am not treating this as proof that every possible hosting combination has been tested.

The GitHub compatibility matrix still needs to run before the next public release. That is where I want to formally test the PHP and MySQL/MariaDB combinations the project intends to claim.

I also have not yet exercised the new owner-run workflow against a release containing an actual database migration on the live development installation. The migration and recovery tooling exists, but the next legitimate migration-bearing release will be the right time to verify that complete path in real use.

There is also a longer-term hardening question around keeping sensitive runtime state entirely outside the public document root.

The current protected-directory model has been tested and is working, so I am not going to force a larger architectural change into the project without a good reason.

The main lesson from this work is simpler.

A deployment problem is not automatically a server problem. Sometimes a different environment is exposing an assumption in the product.

The useful response is not to weaken the environment until the application stops complaining.

It is to figure out what the product actually requires, make that requirement explicit, and test the software against it.

That is what this portability pass did.


New Here?

Read Next:


Get the Work
Articles on discipline, recovery, identity, and ownership. Delivered when published.