Coolify Runtime Storage & XFS Migration
If you deploy Meowbert on Coolify using the repo’s docker-compose.yml, the runtime bind path matters a lot for XFS.
The important rule
The XFS mountpoints must live under the exact host directory that is bind-mounted into /app/runtime.
Inside the containers, Meowbert always uses:
/app/runtimeSo on the host, you should bind one stable parent directory into that path.
Recommended Coolify setup
Set this environment variable in Coolify for the Meowbert stack:
MEOWBERT_RUNTIME_PATH=/srv/meowbert/runtimeWith the compose file in this repo, that makes API and worker bind:
/srv/meowbert/runtime -> /app/runtimeThis is the recommended production setup because:
- it gives you one stable host path outside the release checkout
- it lets you mount XFS filesystems under that parent directory
- those nested mounts remain visible inside the containers because the bind uses
rshared
Important: changing the parent runtime bind is a one-time host data migration
If you were previously using the default runtime bind:
./runtime -> /app/runtimeand you now switch to:
/srv/meowbert/runtime -> /app/runtimethe container path stays the same, but the host source directory changes.
That means your existing runtime data will only remain visible if you first copy the old host runtime tree into the new host runtime path.
This is the safe order:
- stop the stack
- locate the current host source that is bound to
/app/runtime - copy that runtime tree into
/srv/meowbert/runtime - then set
MEOWBERT_RUNTIME_PATH=/srv/meowbert/runtime - redeploy
If you skip the copy, the app will start against a different, empty host runtime tree and your old local data will appear missing.
How to find the current host runtime source
On the host, inspect the running API container:
docker inspect <api-container-name-or-id> \
--format '{{range .Mounts}}{{if eq .Destination "/app/runtime"}}{{.Source}}{{end}}{{end}}'That prints the current host path backing /app/runtime.
One-time copy example
After stopping the stack:
sudo mkdir -p /srv/meowbert/runtime
sudo rsync -aHAX --info=progress2 <old-runtime-source>/ /srv/meowbert/runtime/Copy the whole runtime tree, especially:
workspaces/projects/storage/storage-state/
tasks/ can also be copied; it is usually less important, but copying the full runtime tree is the simplest and safest move.
Will Meowbert migrations still work after that?
Yes.
Meowbert stores runtime paths as container paths like:
/app/runtime/workspaces/...
/app/runtime/projects/...So as long as the new host bind still maps to /app/runtime and you copied the old runtime tree into the new host source first, the admin migrations still see the same in-container paths and work normally.
That includes:
- Nest projects under workspace storage units
- Migrate local workspaces to XFS storage units
The second migration still expects the old local workspace source at /app/runtime/workspaces to be visible while it moves data into /app/runtime/workspaces-xfs.
Why not rely on ./runtime in Coolify?
If you leave MEOWBERT_RUNTIME_PATH unset, Compose falls back to:
./runtimeThat works for local development, but it is a worse production target for XFS because it follows the deployment checkout location.
Coolify’s own documentation also notes that relative bind/storage paths are created relative to its managed service directory rather than some fixed path you control directly. For XFS host setup, a stable absolute host path is much cleaner and more predictable.
Recommended host directory layout
Once MEOWBERT_RUNTIME_PATH=/srv/meowbert/runtime, the host should look like this:
/srv/meowbert/runtime
├── workspaces
├── projects
├── tasks
├── workspaces-xfs
└── storage-cache-xfsAnd inside the containers that becomes:
/app/runtime
├── workspaces
├── projects
├── tasks
├── workspaces-xfs
└── storage-cache-xfsworkspaces-xfs is the XFS-backed root for the local storage backend.
storage-cache-xfs is optional and only needed if you separately manage a host-side cache path for an external mount.
What the XFS setup script does
Run this directly on the host:
sudo ./scripts/setup-xfs-storage.shThe script:
- asks for the host runtime root
- asks whether to set up workspace XFS storage
- optionally asks whether to set up an extra XFS cache mount
- asks which block device to use for each selected mount
- optionally formats the device as XFS
- creates the host mountpoint directory
- mounts the device with
prjquota - verifies
xfs_quota - optionally appends the
/etc/fstabentry - prints the Meowbert config snippet you should use
Important nuance about what the script creates
The script creates the host mount roots, for example:
/srv/meowbert/runtime/workspaces-xfs
/srv/meowbert/runtime/storage-cache-xfsIt does not pre-create every per-workspace subdirectory.
Those are created later by Meowbert, for example:
/app/runtime/workspaces-xfs/<workspaceId>/rootLocal backend XFS setup
The local backend should point at the XFS-backed workspace root:
{
"id": "local-default",
"label": "Local (XFS quotas)",
"type": "local",
"workspacesRoot": "/app/runtime/workspaces-xfs",
"projectsRoot": "/app/runtime/projects",
"xfsProjectQuota": {
"mountPath": "/app/runtime/workspaces-xfs",
"projectIdBase": 10000
}
}That config is already present in config/global.docker.json.
Optional host-side cache XFS setup
If you run something like rclone mount on the host and want to cap its cache with XFS, do that as part of the host mount service itself.
Meowbert no longer manages remote mount cache directories in storage backend config.
Full deployment order for Coolify + XFS
- Set
MEOWBERT_RUNTIME_PATH=/srv/meowbert/runtimein Coolify. - Find and copy the old runtime tree into
/srv/meowbert/runtimeif you are migrating from the old./runtimebind. - Deploy or redeploy so the parent bind exists.
- Run the host helper:bash
sudo ./scripts/setup-xfs-storage.sh - Confirm the host XFS mounts exist:
/srv/meowbert/runtime/workspaces-xfs- optionally
/srv/meowbert/runtime/storage-cache-xfs
- Make sure
config/global.docker.jsonpoints at:/app/runtime/workspaces-xfsfor local workspace storage- optionally another host-managed cache path if your external mount service needs one
- Redeploy API + worker.
- In the admin panel, go to Admin → Migrations.
- Run:
- Nest projects under workspace storage units
- Migrate local workspaces to XFS storage units
How the bind + nested mounts work
After setup, the model is:
Host:
/srv/meowbert/runtime <- bind-mounted into /app/runtime
/srv/meowbert/runtime/workspaces-xfs <- nested XFS mount
/srv/meowbert/runtime/storage-cache-xfs <- optional nested XFS mount
Container:
/app/runtime
/app/runtime/workspaces-xfs
/app/runtime/storage-cache-xfsBecause the /app/runtime bind uses rshared, the nested host mounts under /srv/meowbert/runtime remain visible inside API/worker containers.
References
- Coolify Docker Compose docs: https://coolify.io/docs/knowledge-base/docker/compose
- Coolify WordPress service docs (example relative persistent storage paths): https://coolify.io/docs/services/wordpress