Updating a Forgotten BookStack: 15 Months, 1,371 Files and a Unix User That Didn’t Exist

The Setup, or: “How Old Could It Possibly Be?”

You have a BookStack instance that still works. Mostly.

It has been sitting quietly in the corner for over a year, doing exactly what documentation systems are supposed to do: being ignored until someone notices the security release banner.

You check the version:

cd /var/www/html/bookstack
cat version

And discover you are on something like:

v25.02.2

The current release is much newer. Suddenly this is not “apply a patch.” This is archaeology.

The good news: BookStack upgrades are refreshingly sane. The bad news: your surrounding Linux environment may have accumulated its own little museum of ownership quirks, stale Git state, multiple PHP versions, and service accounts that cannot log in.

This is the upgrade path that worked for us.


Before You Touch Anything: Find Out What You Actually Have

Start from the BookStack root:

cd /var/www/html/bookstack

cat version
pwd

Then inspect Git:

git branch --show-current
git status --short
git remote -v
git log -1 --oneline

If Git replies with:

fatal: detected dubious ownership in repository

do not immediately add the directory to root’s global safe.directory list.

That warning usually means something useful: the repository is owned by a dedicated application user, while you are running Git as root.

Run the Git operations as the repository owner instead.


The Service Account Has No Shell. Good.

A production PHP-FPM pool should not need an interactive login shell. If your BookStack service user has:

/sbin/nologin

leave it that way.

You do not need to temporarily change its shell to /bin/bash. Use runuser:

runuser -u bookstack -- /bin/bash

Now you have a temporary shell as that user without weakening the account configuration.

This is one of those commands you can administer Linux for years without needing, then suddenly wonder why you ever did anything else.

From that shell:

cd /var/www/html/bookstack
whoami
git branch --show-current
git status --short
git remote -v
git log -1 --oneline

Make the Git Tree Boring Before the Upgrade

You want:

On branch release
nothing to commit, working tree clean

If the only change is a deleted tracked example file such as .env.example, restore it:

git restore .env.example
git status

Do not confuse .env.example with your real .env. The real environment file contains your instance configuration and should not be overwritten by Git.


Use the Current BookStack Source Remote

For a modern BookStack installation, point the release remote at the project’s current source endpoint:

git remote set-url origin https://source.bookstackapp.com/bookstack.git
git remote -v

Fetch the latest release metadata:

git fetch --tags origin

git tag --list 'v26.05*' --sort=-v:refname | head
git log -1 --oneline origin/release

In our case the release branch landed exactly on:

v26.05.3

Backup Like You Expect the Database Migration to Betray You

It probably will not. That is not a reason to skip the backup.

We used three layers:

  • A hypervisor or VM snapshot.
  • A BookStack application backup.
  • A separate SQL dump.

BookStack system CLI backup

Recent BookStack releases include bookstack-system-cli. Run it with the same PHP version your application actually uses:

PHP=/path/to/your/php

$PHP ./bookstack-system-cli backup /tmp/bookstack-pre-upgrade.zip

The CLI may warn that the backup feature is still in alpha. Take that warning seriously: use this as an additional backup, not your only one.

Move the resulting archive somewhere root-only afterwards:

mv /tmp/bookstack-pre-upgrade.zip /root/
ls -lh /root/bookstack-pre-upgrade.zip

Database dump

On MariaDB:

mariadb-dump \
  --all-databases \
  --single-transaction \
  --routines \
  --events \
  --triggers \
  > /root/mariadb-before-bookstack-upgrade.sql

If the server hosts multiple unrelated databases, dumping only the BookStack database is cleaner. The important point is: have an SQL backup outside the application tree before migrations run.


The Fun Part: You May Have Two PHP Versions

This is common on Enterprise Linux systems.

Your system CLI might be PHP 8.3:

php -v

while the PHP-FPM pool serving BookStack still runs PHP 8.2 from a separate package stream.

Check both:

composer --version
php -v
/path/to/php82 -v

This matters because Composer validates dependencies against the PHP interpreter that runs Composer, while production requests use whatever PHP-FPM binary your pool is configured to use.

If both versions are supported by the BookStack release, running the upgrade under the newer system PHP can be perfectly valid. But verify the actual production runtime afterwards.

If you want to force Composer to use the same PHP binary as production, do not hard-code a Composer path. Find it:

PHP=/path/to/production/php
COMPOSER_BIN=$(command -v composer)

$PHP "$COMPOSER_BIN" --version

That avoids the very ordinary mistake of assuming Composer lives at /usr/bin/composer when it does not.


Pull the Release

Once the Git tree is clean and backups exist:

cd /var/www/html/bookstack
git pull origin release

On a long-neglected installation this may be spectacularly noisy.

Our jump changed over a thousand files, upgraded Laravel, added database migrations, replaced large parts of the editor stack, updated dependencies, added new APIs, and introduced new storage paths.

This is normal for a large version jump. What matters is that the pull is a clean fast-forward and does not stop on local conflicts.


Install the Locked Dependencies

Still as the BookStack application user:

composer install --no-dev

For unattended maintenance:

composer install \
  --no-dev \
  --no-interaction \
  --prefer-dist

Do not run composer update. BookStack ships a tested composer.lock; the goal is to install the versions selected by the release, not invent your own dependency set.

A large jump can legitimately involve dozens of package upgrades and framework changes.


Run the Database Migrations

Now the irreversible-looking part:

php artisan migrate

Laravel will warn that the application is in production and ask for confirmation.

For scripted maintenance:

php artisan migrate --force

Do not interrupt this because one migration takes longer than the others.

BookStack releases may include migrations that create replacement entity tables, copy existing data, update relation columns, remove old tables, rebuild indexes, or modify authentication-related fields.

That is exactly why you made backups first.

Afterwards:

php artisan migrate:status

Every migration should report Ran.


Clear the Caches

php artisan cache:clear
php artisan config:clear
php artisan view:clear

 


Check the New Version

cat version

You should now see the target release.

For example:

v26.05.3

Check Writable Storage

Do not solve permissions problems with chmod -R 777. We are not animals.

BookStack needs its application user to write to specific runtime paths. Check the important ones:

mkdir -p storage/fonts

stat -c '%U:%G %a %n' \
  storage \
  storage/fonts \
  bootstrap/cache \
  public/uploads

The owner should normally be your BookStack service user, and the directories must be writable by that user.

The storage/fonts path matters on newer releases because PDF/font handling may write runtime data there.


Verify Using the Actual Production PHP

This is the check that catches the “Composer used PHP 8.3 but PHP-FPM still serves PHP 8.2” class of mistake.

Run Artisan explicitly using the production PHP binary:

PHP=/path/to/production/php

$PHP artisan migrate:status | tail -20
$PHP artisan about

artisan about gives you an excellent compact sanity check:

  • Laravel version
  • PHP version
  • environment
  • debug state
  • database driver
  • mail driver
  • cache/session configuration

If Artisan fully boots under your production PHP version, that is much more useful than merely checking php -v.

You can also validate Composer platform requirements using the same interpreter:

COMPOSER_BIN=$(command -v composer)
$PHP "$COMPOSER_BIN" check-platform-reqs

Restart PHP-FPM

Once the code, dependencies and database are in place:

systemctl restart php-fpm-service-name

If you use a versioned PHP stream, restart that specific service rather than the system PHP-FPM daemon.


Functional Verification

Do not stop at “the service restarted.”

Check the live site:

curl -Ik https://docs.example.com/ | grep -Ei 'HTTP/|X-Powered-By'

You want a successful HTTP response and, if exposed, the PHP version you expect from the FPM pool.

Then inspect logs:

tail -50 storage/logs/laravel.log
tail -30 /var/log/httpd/bookstack-error.log

Remember that a restricted application user may not be able to read Apache logs. That is a feature, not a bug. Exit back to root for those.

Finally test the application like a user:

  • Log in
  • Open a book/page
  • Search
  • Edit and save a page
  • Upload an image
  • Test email if you use SMTP
  • Test PDF export if you use it

A Note on Old Errors

After a big upgrade, logs are emotionally dangerous.

tail may show scary stack traces that happened before the upgrade, during a typo, or while the service was intentionally unavailable.

Always compare timestamps.

A successful request at 20:10 does not become unsuccessful because the log still contains an error from 19:45.

This sounds obvious. It becomes much less obvious after six hours of maintenance.


The Short Version

# as the BookStack owner
runuser -u bookstack -- /bin/bash
cd /var/www/html/bookstack

# verify
cat version
git status

# backup before this point

git fetch --tags origin
git pull origin release
composer install --no-dev
php artisan migrate --force
php artisan cache:clear
php artisan config:clear
php artisan view:clear

cat version
php artisan migrate:status
php artisan about

Then restart the correct PHP-FPM service and test the live application.


Conclusions, or: “The Application Was Fine. The Archaeology Was Around It.”

The actual BookStack upgrade was the easy part.

The interesting bits were everything surrounding it:

  • The repository belonged to a non-login service account.
  • Root triggered Git’s dubious-ownership protection.
  • runuser let us work as the correct user without enabling shell access.
  • The server had more than one PHP version.
  • Composer ran under one PHP while production FPM used another.
  • A large framework and schema jump still migrated cleanly.
  • One typo produced a terrifying-looking production stack trace that meant absolutely nothing.

And the final result?

A current BookStack release, a clean migration history, correct file ownership, the intended PHP runtime, and one less forgotten service quietly aging in the corner.

Until you remember there is another BookStack instance somewhere.

There is always another BookStack instance.