LoudLemur
Posts
-
Kasm - Virtual Desktop / Browser Isolation -
Kasm - Virtual Desktop / Browser Isolation@robi We like the look of this!
-
Notification emails for manually-tracked GitHub projects link to "undefined"Read against
eb9d805.sendNotificationEmailbuildsversionLinkfrom the project type, atbackend/tasks.js:266—if (project.type === database.PROJECT_TYPE_GITHUB) { versionLink = `https://github.com/${project.name}/releases/tag/${release.version}`; } else if (project.type === database.PROJECT_TYPE_GITLAB) {PROJECT_TYPE_GITHUB_MANUAL('github_manual',database.js:11) is a separate constant and is never tested, soversionLinkstays undefined for every manually-added GitHub project.Result:
- text body:
Read more about this release at undefined(tasks.js:278) - HTML body:
<a href="undefined">(notification.template:78)
Repro: add a GitHub project via the "track manually" path rather than starring it, then wait for a release notification.
syncReleasesByProjectalready treats both constants as GitHub (tasks.js:152-155), which is what makes this look like an oversight rather than intent.One-line fix:
--- a/backend/tasks.js +++ b/backend/tasks.js @@ -264,7 +264,7 @@ async function sendNotificationEmail(release) { })); let versionLink; - if (project.type === database.PROJECT_TYPE_GITHUB) { + if (project.type === database.PROJECT_TYPE_GITHUB || project.type === database.PROJECT_TYPE_GITHUB_MANUAL) { versionLink = `https://github.com/${project.name}/releases/tag/${release.version}`; } else if (project.type === database.PROJECT_TYPE_GITLAB) { versionLink = `${project.origin}/${project.name}/-/tags/${release.version}`;One question while I'm here: is
git.cloudron.iothe canonical repo now? The GitHub mirror is archived, so I could not open an issue or a PR there, and I could not check GitLab without an account. Line numbers above are against the mirror, so worth a sanity check if the code has moved. - text body:
-
ReleaseBell: option to suppress prerelease notificationsRead against
eb9d805. Last of three findings, and the one I would most like as a user.The
prereleasecolumn has been there since20230926130714-add-release-prerelease.js. It is fetched (github.js:130), stored (database.js:174), and labelled in both the subject line and the email template. But nothing filters on it — there is no user setting and no condition inreleasesListAllPending.A live example. The three newest tags on
dbgate/dbgatetoday:v7.2.4-premium-beta.7 v7.2.4-premium-beta.5 v7.2.4-beta.6while
GET /repos/dbgate/dbgate/releases/lateststill returns v7.2.3. So starring that repository currently sends three emails announcing versions that are not released, two of them for a premium build that is not the open-source product. Any project running a beta channel behaves the same way, so this is the normal case rather than an edge one.A sketch, defaulting to on so nothing changes for existing users:
ALTER TABLE users ADD COLUMN notifyPrerelease BOOLEAN DEFAULT 1;Then in
releasesListAllPending, join through to users — the flag is per-user while the pending list is release-scoped:SELECT releases.* FROM releases JOIN projects ON projects.id = releases.projectId JOIN users ON users.id = projects.userId WHERE releases.notified = 0 AND (users.notifyPrerelease = 1 OR releases.prerelease = 0)Plus one checkbox beside the existing email setting.
A per-project override would be better long term, since people tend to want stable-only for tools they merely use and prereleases for the one thing they develop against. But the per-user flag covers most of the value and is a smaller change.
Happy to send this as a patch if that is useful.
-
ReleaseBell: a `website` project is accepted by the API but never syncsRead against
eb9d805. Second of three small findings from the same read.routes.js:123accepts three project types when adding one:if ([ database.PROJECT_TYPE_GITHUB_MANUAL, database.PROJECT_TYPE_GITLAB, database.PROJECT_TYPE_WEBSITE ].indexOf(req.body.type) === -1) return next(new HttpError(400, 'invalid type'));But
syncReleasesByProjecthas no branch forwebsite(tasks.js:152-161), so it falls through to the "unknown type" case and returns — beforelastSuccessfulSyncAtis set at line 217.So the row is stored, never syncs, never errors, and is indistinguishable in the UI from a project that simply has no new releases. Someone who adds one waits indefinitely for a notification that cannot arrive.
If a website backend is not imminent, the cheapest honest fix is to stop accepting the type until something consumes it:
- if ([ database.PROJECT_TYPE_GITHUB_MANUAL, database.PROJECT_TYPE_GITLAB, database.PROJECT_TYPE_WEBSITE ].indexOf(req.body.type) === -1) + if ([ database.PROJECT_TYPE_GITHUB_MANUAL, database.PROJECT_TYPE_GITLAB ].indexOf(req.body.type) === -1)Worth keeping the constant and the
project_typemigration either way, since website watching is an obvious future feature. -
🚀 DbGate: community package now availableTL;DR: DbGate is a web-based SQL and NoSQL client, and Cloudron provisions PostgreSQL, MySQL and MongoDB for well over half the apps in both stores without giving you any way to look inside them. Now packaged for Cloudron and ready to install. Built and tested on Cloudron 9.x; unofficial and community-maintained.

Headline features- Thirteen bundled database engines in one client: PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, MongoDB, Redis, SQLite, CockroachDB, ClickHouse, Cassandra, DuckDB, Firebird.
- Schema browser, query editor, an editable data grid, and CSV/Excel/NDJSON import and export.
- Cloudron single sign-on out of the box, with a generated local login for installs that skip SSO.
- A read-only MCP server (new upstream in the version this package ships), so AI agent tooling can browse and query your databases with a token, entirely optional.
- Point it at the same PostgreSQL, MySQL or MongoDB credentials Cloudron already generates for any other app's addon.
Links
Project homepage: https://www.dbgate.io/
Upstream repo: https://github.com/dbgate/dbgate- 🧱 Cloudron package repo: https://github.com/OrcVole/dbgate-cloudron
There's no separate demo; the UI is the app itself once installed, a schema browser and query editor over whichever connections you add.
How to installRead the generated admin credential (no-SSO installs) or sign in with Cloudron SSO (installs with SSO enabled) from the checklist item shown right after install.
Click the Add custom app dropdown (top right in the App Store) and choose Community app, then paste this URL into the box that pops up. Apps installed this way receive automatic updates.
https://raw.githubusercontent.com/OrcVole/dbgate-cloudron/main/CloudronVersions.jsonOr with the CLI:
cloudron install \ --versions-url https://raw.githubusercontent.com/OrcVole/dbgate-cloudron/main/CloudronVersions.json \ --location dbgate.example.comMinimums: 1.5 GiB RAM,
localstorageandoidcaddons. No extra subdomain.First run: Cloudron single sign-on is the default; installs without SSO get a generated local login, readable from the app's checklist item.
For usersWhy try it: Cloudron hands you a PostgreSQL, MySQL or MongoDB addon for dozens of apps and no way to actually look inside any of them.
What you get: a real schema browser, an ad hoc query editor, and export to CSV or NDJSON, against any database you already have credentials for.
Cloudron wins: single sign-on, automatic backups of the connection list and its encrypted credentials, one-click updates, all state under
/app/data.Good fit if you want to poke at an addon database without shelling in; probably not if you need per-user access control inside the tool itself, which the community edition does not have (see the honesty note below).
🧰 For packagers: what we learned
What helped — the
oidcaddon mapped straight onto DbGate's own generic OAuth2 support, no discovery dance needed; the app's own/healthendpoint was already unauthenticated and ready-made.What was tricky — three real ones. DbGate's own connection-password encryption key is created lazily at mode 0644 the first time a connection with a password is saved, not at boot, so the usual every-boot
chown -Rsweep does not fix its mode; the entrypoint now re-asserts it explicitly. The community install validator wants a non-emptywebsitefield that is not in the manifest's own documented required-fields list, surfaced live by a failed dashboard install. And the base image's build step silently ignores theSHELLinstruction under OCI image format, which quietly defeated a bash-only build gate until it was rewritten in POSIX sh and negative-tested.Still rough — a colder path worth extra eyes: a fresh no-SSO install's very first login, and whether the generated credential file is easy enough to find from the dashboard File Manager for someone who has not read the checklist item closely.
️ For the Cloudron teamMaintenance burden: light. DbGate releases roughly weekly; the package tracks upstream's community Docker image unmodified, no patches carried.
Why it suits the App Store: real, measured demand: 147 of 257 apps across both stores wire a PostgreSQL, MySQL, MongoDB or Redis addon, and no SQL client existed in either store before this. The niche has been asked for in adjacent forms since 2018.
Friction worth knowing: the
websitemanifest field gate above; andcloudron execproved unreliable for longer or repeated calls during testing (multiple connection timeouts), whilesshplusdocker execon the box host was reliable throughout for the same checks.
For DbGate's developersTwo small, low-effort things that would help future packagers:
WORKSPACE_DIRworks exactly as hoped but is not on the documented environment-variables page; and an environment-variable alternative to the--encryption-keyCLI flag would let a platform supply its own wrapping secret for the connection-password key without a command-line exposure trade-off. Package source: https://github.com/OrcVole/dbgate-cloudron. Happy to co-maintain or open an upstream issue if that is the more useful form for either of these.
UnlocksNow you can open a browser tab and run a real SQL query against the Postgres behind your Nextcloud, GitLab or Discourse install, or the MySQL behind WordPress or Vaultwarden, without a terminal.
Synergies147 of the 257 apps across the community and official stores wire at least one of the
postgresql,mysql,mongodborredisaddons.DbGate + Nextcloud: browse the PostgreSQL tables holding file metadata, shares and users, and run SQL to find orphaned shares or oversized accounts.
DbGate + WordPress (Managed): query the MySQL tables holding posts, pages and users, and export content or a user list to CSV without shelling in.
DbGate + GitLab: run SQL against the PostgreSQL tables holding projects, merge requests and CI metadata, and export what the admin UI will not.
DbGate + Vaultwarden: inspect the MySQL schema behind your password vault, run ad hoc queries, export tables to CSV.
DbGate + Rocket.Chat: browse the MongoDB collections holding channels, messages and users, and export message history to CSV for compliance records.
Feedback, bug reports, and "works on my install" confirmations all welcome below.
-
Hoarder (Now, Karakeep) - Mymind alternative - The Ultimate All-In-One Bookmark and Note Taking AppIf you're an OG, and running an old meilisearch version, you'll need to ensure you're on meilisearch version (1.13 or higher). Check this guide for info on how to update.
The Meilisearch Cloudron Community Package is Meilisearch v1.51.0, if anybody is interested.
-
wger - Self hosted FLOSS fitness/workout and weight tracker written with Djangowger is now available as a community package on cloudron. It took 7 years! Here is the announcement:
https://forum.cloudron.io/topic/15769/wger-community-package-now-available
-
🚀 wger: community package now availableIf you are horrified to see google store and apple store 'links' inside wger after you install it, as we were, don't worry!
Are they a telemetry risk? No, verified, not assumed. The store badges are SVGs baked into our image and served by our own nginx; the live page references zero external assets (every third-party URL on the page is a click-target only). Google, Apple, and Flathub learn nothing about you or your users unless someone deliberately clicks a badge. Upstream did this right.
-
🚀 wger: community package now availableTL;DR: wger is a free, open source workout, fitness and nutrition manager. It covers the whole training loop, building routines, logging workouts, tracking body weight and measurements, and planning meals against a searchable ingredient and nutrition database with barcode scanning. Now packaged for Cloudron and ready to install. Requires Cloudron 9.1 or later, tested on 9.2; unofficial and community-maintained.
Links
Project homepage: https://wger.de
Upstream repo: https://github.com/wger-project/wger- 🧱 Cloudron package repo: https://github.com/OrcVole/wger-cloudron
wger has a web UI once installed, and the same REST API also serves the official wger mobile apps for Android and iOS, so this package can be used from a phone as well as a browser.
How to installClick the Add custom app dropdown (top right in the App Store) and choose Community app, then paste the CloudronVersions.json URL into the box that pops up. Apps installed this way receive automatic updates.
https://raw.githubusercontent.com/OrcVole/wger-cloudron/main/CloudronVersions.jsonOr via the CLI:
cloudron install \ --versions-url https://raw.githubusercontent.com/OrcVole/wger-cloudron/main/CloudronVersions.json \ --location wger.example.comMinimums: 2GB RAM, addons localstorage, PostgreSQL, Redis, sendmail, and optionally oidc. No extra subdomain beyond the one the app is installed on.
First run: Cloudron single sign-on is available through the oidc addon; the login page shows a "Sign in with ..." button carrying the Cloudron's own configured name, and a wger account is provisioned automatically on first sign-in. Public self-registration stays disabled either way, so who can reach the app is controlled by the Cloudron's own user and group access. Installing without user management is also supported (optionalSso); the app then falls back to purely local accounts, and no stale login button is left behind. Either way, the admin account is created with a random password on first run, generated by the package rather than left at any upstream default.
For usersWhy try it: a self-hosted alternative to commercial workout and nutrition trackers, with the same mobile apps you would use against the hosted service. What you get: routine building, workout logging, body measurement tracking, a searchable nutrition and ingredient database with barcode scanning, and a gym management mode for trainers administering members. Cloudron wins: single sign-on, automatic backups, one-click updates, all persistent state under
/app/data. Good fit if you want a self-hosted fitness tracker with working mobile app support; probably not if you specifically need the mobile apps' offline sync, which this package does not include (see below).🧰 For packagers: what we learned
What helped: the oidc addon covered Cloudron SSO cleanly against wger's existing django-allauth support; PostgreSQL, Redis and sendmail addons covered the rest of the state without needing anything bundled.
What was tricky: three upstream behaviours needed working around: a migration that runs
CREATE PUBLICATION ... FOR ALL TABLES, which requires database superuser privilege that managed PostgreSQL addons do not grant, so it is recorded as applied without running; wger's own bootstrap check treats a half-initialised database (tables present, no admin user) as already initialised, so an interrupted first run needs to be caught before that point; and thewgerCLI reads$HOME/.invoke.yamlon startup, which fails under a process supervisor that leavesHOME=/rootafter dropping privileges, fixed deployment-side by exporting HOME.Still rough: PowerSync, the component upstream uses for offline mobile sync, is not included in this package version. Ordinary online use of the web app and the official mobile apps is verified working end to end; offline sync on the mobile apps is the one feature this version does not provide. This was a scope decision for the first version, not a dead end, and may be revisited later.
️ For the Cloudron teamMaintenance burden: wger releases at a moderate pace, and the package is a fairly thin wrapper: gunicorn, a Celery worker and beat scheduler, and nginx under supervisor, with all state in addons or
/app/data. Why it suits the App Store: fitness and health tracking is a category with real demand and no existing entry, the upstream is a clean AGPL-3.0 project with a reference docker-compose file to work from, and the official mobile apps work against this package's API. Before publishing, the package went through a full acceptance ladder: install, authentication, functional flows including byte-verified media, a real platform update, a backup restore, and memory sizing under load. Friction worth knowing: the three upstream quirks above (a superuser-only migration, a bootstrap check that misreads a half-initialised database, and a CLI that needs a readable HOME) are all documented and worked around package-side; none required changes to the manifest or addon contract itself.
For wger's developersA few low-effort changes would help packagers generally: catching the superuser-privilege error in the
core.0023_create_publicationmigration (or gating it behind a setting) so deployments without PowerSync are not forced to fake-apply it; makingwger bootstrapresumable by checking whether any user exists rather than only whether the users table exists, so an interrupted first run is not left half-done; and making the CLI's invoke-based startup tolerant of an unreadable or root-owned HOME, for example withload_user=False. None of these blocked packaging, and the application itself packaged cleanly overall. Package source and pull requests are welcome at the repo above; happy to co-maintain.
UnlocksSelf-hosting a full workout and nutrition tracker under Cloudron's own user management, backups and update mechanism, with the official mobile apps working against it out of the box.
Feedback, bug reports, and "works on my install" confirmations all welcome below.
-
PSA: mystery memory pressure / "high system CPU" on a KVM VPS? Check for host-side ballooningTL/DR
The % score on CPU usage in the graph is for CPU Cores, eg 500% means 5 (of your many eg 12 cores) are working at maximum capacity. It doesn't mean that your entire 12 core machine is at maximum. If you have 12 cores, you might see the y-axis reaching 1200% and be wondering how on earth anybody might end up there. It would be because then all of your (in this case) 12 cores would be maxed.
We hope team cloudron could provide this additional context in the panel.
The cron heartbeat to catch brief reclamation spikes is a good trick I had not seen before.
Worth adding a companion trap for anyone who arrives here from a search, because it produces the same symptom, "my box looks busy and I do not know why", and it is worth ruling out before you open a ticket with your provider.
Two of the numbers on hand are easy to read as CPU usage when neither of them is.
I went looking at one of my own boxes today, convinced it was still labouring after a genuine runaway had been dealt with. Twelve cores, and the figures were:
- load average: 15.78, steady across 1, 5 and 15 minutes
- CPU: 80 per cent idle, iowait 0 per cent, steal 0 per cent
- memory pressure, from
/proc/pressure/memory: 0.00 - one app's graph in the dashboard, earlier in the day: around 700 per cent, which is seven of the twelve cores
The box was completely fine. Nothing needed doing.
The load average is not a CPU utilisation figure. On Linux it counts processes that are runnable or blocked in uninterruptible sleep, so a machine running many containers shows a persistently elevated load while the CPU sits mostly idle. I had 92 running containers at the time. Ninety-two containers' worth of health checks, schedulers and database background threads constantly waking, doing a little work, and sleeping again will hold the load average in the teens on a box that is doing almost nothing.
vmstat 5 2and the idle column is the number that actually answers "is my CPU busy".The per-app CPU graph is in core units, not machine units. The thing to hold on to is this:
500 per cent does not mean five times your machine. It means 100 per cent of each of five cores, out of the twelve you have. So it is five twelfths of the box, not five boxes.
It follows the Docker convention where 100 per cent means one core, the same as
docker statsand the same astopfor a multithreaded process, so on a twelve core box the ceiling is 1200 per cent. An app sitting at 700 per cent is saturating seven cores and leaving you five.That is genuinely useful information and it is the right unit for comparing apps against each other. It is simply very easy, the first time you meet it, to read a number above 100 per cent as "more than my whole machine" and start looking for a fire.
Both of those are correct and conventional. The problem is that neither is interpretable on its own, and the question you actually have when you go looking is "is my machine in trouble", which neither answers.
So, a small request to the Cloudron team, in increasing order of effort.
Put the core count on the per-app CPU graph's axis. Labelling the maximum as
1200% (12 cores), or drawing a ceiling line, would remove the ambiguity completely, and it is about as small a change as a graph can take.Offer cores as an alternative unit. "5.0 of 12 cores" is unambiguous in a way that "500%" will never be, however conventional the percentage is.
On the system level view rather than the per-app one, show machine-normalised CPU alongside the load average, and say somewhere nearby that the load average is not a utilisation figure. The per-app graphs are fine for comparing apps and cannot answer the question about the machine, and that gap is where people end up guessing.
None of this is a bug, and I am not suggesting the underlying numbers are wrong. It is that the presentation asks the reader to bring context that is not on the screen, and in a thread like this one you can watch that cost people real time before they get as far as suspecting their hypervisor.
-
EndurainEndurain is now available as a community package:
https://forum.cloudron.io/topic/15768/endurain-community-package-now-available -
Endurain on Cloudron - fitness trackerNow available as a community package: https://forum.cloudron.io/topic/15768/endurain-community-package-now-available
-
🚀 Endurain: community package now available
Endurain: community package now availableTL;DR: Endurain is a self-hosted fitness tracking application for endurance sports, a place to keep your runs, rides and swims on your own server instead of a vendor's. It is now packaged for Cloudron, with single sign-on wired into its own login and the device upload path left open for phone apps. Unofficial and community-maintained. Please read the known issues before you trust it with anything you care about.
Links
- Project homepage: https://endurain.com
- Upstream source: https://codeberg.org/endurain-project/endurain
- Cloudron package: https://github.com/OrcVole/endurain-cloudron
Once installed it is a full web application: an activity feed, per-activity maps and charts, gear tracking, weight and health logging, and a search page.
How to installThe easy route is the dashboard. Click the Add custom app dropdown at the top right of the App Store, choose Community app, and paste this URL into the box:
https://raw.githubusercontent.com/OrcVole/endurain-cloudron/main/CloudronVersions.jsonApps installed that way receive automatic updates.
The CLI does the same thing:
cloudron install \ --versions-url https://raw.githubusercontent.com/OrcVole/endurain-cloudron/main/CloudronVersions.json \ --location endurain.example.comAddons used: postgresql, redis, sendmail, localstorage, oidc. No extra subdomains.
First run: upstream's first migration seeds a live
admin/adminaccount, so the package replaces that password with a generated one before the application ever serves a request, and writes it to/app/data/.secrets/admin-initial-passwordfor you to read through the dashboard's file manager. Change it and delete the file. Single sign-on, if you enable it, feeds Endurain's own OpenID Connect login rather than sitting in front of the app, so API clients keep working.
️ Known issues, up frontTwo faults in the application itself, both found while packaging and both reported upstream with evidence. Neither is caused by the packaging and neither can be fixed from outside the application.
Uploading several activities at the same time can leave database connections stranded in an open transaction, after which every request that needs the database hangs and the application does not recover on its own. Restarting clears it and loses nothing. If you are importing a backlog, upload a few at a time rather than in parallel.
Separately, if two activities end up sharing a start time, every later upload at that timestamp fails with a server error until one of them is deleted.
The package mitigates the operational half of the first one: its health check reads the database rather than returning a static response, so an instance in that state is detected and restarted by the platform automatically instead of sitting there looking healthy. That is worth knowing about even if you never hit it, because a health check that touches nothing cannot tell a working app from a dead one.
For usersWhy try it: your training history is yours, it stays on your server, and it backs up with everything else on the box.
What you get: FIT, GPX and TCX uploads, automatic upload from phone apps over an API key, per-user Strava and Garmin Connect linking, maps rendered server side, gear and weight tracking, and a multi-user instance with followers.
Cloudron wins: single sign-on into the app's own accounts, platform backups covering both the database and your uploaded activity files, and all state under
/app/data.Good fit if you want Strava-shaped features without Strava. Probably not if you need polished mobile apps of its own, since the phone side is third-party apps posting to the upload endpoint.
🧰 For packagers: what we learned
What helped: the addons covered every store, so there is nothing bundled and no
backupCommandat all. Postgres, Redis, mail and OIDC all came straight from the platform.What was tricky, and all four are worth stealing:
- The static file server refuses to serve a symlink whose target leaves the served directory. The built frontend has to be copied into a writable tree, not symlinked back into the read-only image, or every asset 404s while the page itself loads and the app is a blank white screen.
- Creating an OIDC provider record is not enough to switch single sign-on on. There is a separate server setting, defaulting to off, and without it the provider exists, the API lists it, the whole OAuth flow works by hand, and the login page shows no button.
- The application refuses server-side requests to URLs that resolve to private addresses, which is a good default that blocks Cloudron single sign-on outright, because an app container reaches the dashboard over the internal bridge and the dashboard hostname therefore resolves privately inside the container while resolving publicly everywhere else. The fix is the application's own allowlist, scoped to the issuer hostname alone and never to the bridge network.
CMD script.shleaves the shell as PID 1 for the whole boot, and a non-interactive shell does not act on SIGTERM while it waits for a foreground child. A stop during migrations burned the platform's entire grace period and ended in SIGKILL. Re-execing the script undertini -gon its first line fixes the whole window.
Still rough: the memory limit is sized from measurement, but on a shared host. Idle sits near 200 MiB and the heaviest load I could construct, a bulk import running alongside concurrent uploads, peaked at 384 MiB against the 1.5 GiB the package ships. Someone else's numbers on a quieter box would be a useful check.
️ For the Cloudron teamMaintenance burden is low: the package is a Dockerfile, a start script and a provisioning script, with no patches to upstream and no bundled services.
Friction worth knowing: an application whose health endpoint touches no dependency can be completely wedged while the platform reports it as running indefinitely. That is not a platform bug, but it is a sharp edge, and a note in the packaging documentation recommending that
healthCheckPathexercise the app's primary datastore would save the next packager discovering it the way this one did.
For Endurain's developersThank you for a genuinely pleasant application to package. Three small things would help every deployment method rather than only this one:
- The seeded
admin/adminaccount means every deployment has to independently notice it and do something before first serve. A generated password or a forced change on first login would remove that once, upstream. - A JSON API request without an
X-Client-Typeheader is rejected with the same 401 as a wrong password, which reads as a credentials problem and cost real time to diagnose. - A stated libc and Python floor would let packagers build against a documented fact rather than inferring one from the lockfile.
Package source and pull requests welcome. Happy to co-maintain.
SynergiesEndurain + Gadgetbridge or OpenTracks: record on the phone, upload straight to your own instance over an API key, with no vendor cloud in the path.
Endurain + wger: if you also track strength work, the two cover different halves of the same habit, and body weight is the obvious thing to keep in step between them.
Feedback, bug reports, and confirmations that it works on your install are all welcome below.
-
Fider: old installs carry a large `logs` table inside their own databasePosting this because it cost us a nightly backup and I do not think it is documented anywhere.
What happens
Fider has a logger that writes every log entry into a
logstable inside its own Postgres database. It is controlled byLOG_SQL, which does not mean "log SQL statements", it means "send logs to SQL". Upstream Fider defaults it totrue, and there is no retention, pruning or size cap anywhere in that code path.Cloudron's Fider package up to and including 2.x also set
LOG_LEVEL=DEBUG. Fider traces every SQL statement it executes at DEBUG, and each of those trace lines then becomes another row in that same table. On an install with almost no activity this still produced roughly 24 million rows and 22 GB over thirteen months, in a database where every other table was under 120 kB.Because Cloudron takes a full
pg_dumpof the app database on every backup, that table is dumped and uploaded in full every night. Ours reached 27 GB and took over three hours of the nightly run on its own, and one night the run died inside it and everything scheduled behind it kept a stale backup.Two reasons it can go unnoticed for a long time
- Package 3.0.0 fixed it, but the changelog does not say so. 3.0.0 (2026-05-06) sets
LOG_LEVEL=INFOandLOG_SQL=false. Its release note reads only "Remove built-in OIDC integration". Nothing tells you your database has been accumulating this, or that the update stops it. - 3.0.0 is a major version bump, so it does not install automatically, even with automatic updates enabled. Ours sat on 2.1.0 for 85 days after the fix was published, still writing. If you have not manually approved a Fider major update recently, you are probably still on 2.x and still writing.
And the important one: updating does not clean up what already accumulated. The fix stops new rows. The existing table stays, and stays in your backups, until you remove it yourself.
How to check
From the app's Web Terminal:
psql $CLOUDRON_POSTGRESQL_URL -c "SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) \ FROM pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 5;"If
logsis at the top and measured in GB, this is you.Server wide, from an ssh session on the box, to see whether any app has a disproportionate dump:
find /home/yellowtent/appsdata -maxdepth 2 -name '*dump' -type f -printf '%s\t%p\n' | sort -rn | headHow to diagnose
Confirm it is the log table and not real content, and check whether it is still growing:
psql $CLOUDRON_POSTGRESQL_URL -c "SELECT count(*) FROM posts;" psql $CLOUDRON_POSTGRESQL_URL -c "SELECT max(created_at) FROM logs;"If the newest log row is from before your last Fider update, writing has already stopped and you are only carrying dead weight. If it is from the last few minutes, you are still on an affected package and should update first, otherwise it will simply refill.
Check your package version in the app's About panel. 3.0.0 or later is fixed.
How to fix
Update to 3.0.0 or later first, so it cannot refill. That is a major update and needs manual approval.
Then, if you do not want the old log rows, from the Web Terminal:
psql $CLOUDRON_POSTGRESQL_URL -c "TRUNCATE TABLE logs;"TRUNCATEreturns the disk space immediately.DELETEwould not: it leaves dead tuples behind and needs aVACUUM FULLafterwards, which rewrites the whole table and needs free space equal to its size. If you would rather keep recent entries, useDELETE FROM logs WHERE created_at < now() - interval '30 days';and thenVACUUM FULL logs;, and be aware of the exclusive lock that takes.Nothing in Fider reads this table, so on an install where you have never used it for anything, truncating is safe. Take a backup first regardless.
For us the whole thing was instant: 22 GB to 9.7 MB, and the app's backup went from 3 hours 24 minutes to 19 seconds.
Two suggestions for the package
- Worth a note in the 3.0.0 changelog, or the docs, telling existing installs to prune. The fix is silent, so anybody who updated already still has the table and no reason to suspect it.
- A one time cleanup in the package update, or at least a
postInstallMessage, would catch the installs that will never read this post.
- Package 3.0.0 fixed it, but the changelog does not say so. 3.0.0 (2026-05-06) sets
-
Two technical questions towards GPU/CDI support: AppArmor and manifest capabilitiesGreat, and thanks girish. Cloudron is brilliant!
-
Two technical questions towards GPU/CDI support: AppArmor and manifest capabilitiesUnderstood, and thank you for the straight answer. We will not send a patch.
Since the capability itself sounded acceptable, here is everything needed to implement it, so it costs whoever picks it up an afternoon rather than a week. No code below, only behaviour and the results of the testing we have already done.
The change, in words
- In the same place
vaapiis handled at container creation, agpucapability setsHostConfig.DeviceRequeststo a single entry: drivercdi, device IDs a one-element list holding the resolved device name, for examplenvidia.com/gpu=all. Nothing else in the container options changes. - Guard it so it does nothing on a host with no GPU (Graphics Processing Unit), exactly as
vaapino-ops without/dev/dri.
The guard is one API call, not a specification parser
GET /inforeports what the daemon resolved, asDiscoveredDevices. On our test daemon that reads[{"Source":"cdi","ID":"example.com/gpu=all"}].- So: take entries whose
Sourceiscdiand whoseIDmatches<vendor>/gpu=<name>, prefer the one ending=all, otherwise take the first, and if there are none, do nothing. - Match the device class rather than the vendor prefix. AMD's toolkit generates
amd.com/gpuand Docker 29.3.0 already routes--gpusthrough CDI (Container Device Interface) for AMD, so vendor-agnostic matching costs nothing and covers the second vendor for free. This is also why we would suggest the namegpurather thannvidia. - Do not read or compare
cdiVersion. The upstream specification is at 1.1.0 while NVIDIA emits 0.7.0 and AMD emits its own, and consumers accept anything up to the version they support. DiscoveredDevicesneeds Engine API 1.50 or newer. On anything older, scanning/etc/cdiand/var/run/cdifor a specification whosekindends in/gpuis an adequate fallback.
What we tested, so nobody has to repeat it
- The AppArmor profile does not interfere. Ubuntu 24.04, docker-ce 29.6.2 and containerd 2.2.6 as your installer pins them,
docker-cloudron-apploaded verbatim, container run confined with a read-only root filesystem and an unprivileged user. Injected libraries were read, executed and mmapped, the injected device node opened, anddmesgshowed no denials at all. Results were identical unconfined. ReadonlyRootfs: trueis not a problem. An NVIDIA specification runscreateContainerhooks that write into the container root filesystem, creating thelibcuda.so.1symlinks and running ldconfig. Those hooks run before the runtime applies the read-only remount, so they succeed while the app process itself still cannot write. No carve-out needed.- Host prerequisites stay with the administrator. The NVIDIA driver and
nvidia-container-toolkit, nothing else. Nonvidiaruntime registration and no daemon configuration change. Toolkit 1.19 shipsnvidia-cdi-refresh.path, which regenerates the specification whenever the driver or toolkit is upgraded, so the refresh automation that made this look expensive in 2024 is now upstream's.
Two decisions we would leave to you
- Whether an unmet
gpucapability should stay silent likevaapi, or fail visibly. An app quietly running without its GPU may be worse than one that refuses to start. - Whether to gate it as experimental for a first release.
If a written specification with source references, failure modes and non-goals would be useful, we have one and will post or send it in whatever form suits. There is also an Nvidia machine here for any test that would help, including confirming the AppArmor result against real hardware, which is the one thing our virtual machine could not do.
- In the same place
-
Please grant us a git.cloudron.io account@james Thank you for the account, and for raising the project limit.
Two things still block us, and I think one small change fixes both.
Our user account is flagged as external, which in current GitLab means it cannot create projects in its own namespace whatever the project limit is set to. Separately, platform/box has forking and merge requests restricted to project members, so can_create_merge_request_in comes back false for us.
The simplest fix, if you are willing: grant mostcloudron Developer on platform/box. Then we can push a branch and open a merge request directly, with no fork and no personal namespace involved, which is the same arrangement you already run for contributors in the packages group.
If you would rather keep contributors at arm's length, the alternative is to remove the external flag and set forking to "Everyone with access", and we will work from a fork instead.
Either is fine. The context is the GPU and CDI work in https://forum.cloudron.io/topic/15756, where we have asked girish whether the team would like a merge request or a written specification first. This is only so that we are not blocked on plumbing if the answer is a merge request.
-
Hoarder (Now, Karakeep) - Mymind alternative - The Ultimate All-In-One Bookmark and Note Taking AppThank you for packaging this, and congratulations on getting it out. Karakeep has been sitting on the wishlist since June 2024 and it is good to finally see it on the store.
I should explain why I am replying at such length. We were part way through scoping our own Karakeep package when we found yours. Separately, we have just finished packaging Meilisearch standalone for Cloudron and put it through a full acceptance ladder on a real rig, which means we spent an unreasonable number of hours on exactly the one component your package bundles. What follows is what that cost us, offered in the hope it saves you the same. Almost all of it is about the Meilisearch leg.
Some of this is measured on our own package and some of it is inference about yours that we have not tested. I have tried to be explicit about which is which throughout, because I do not want to report a defect we have not actually reproduced.
First, the things that are plainly right, because I read the whole tree and it would be rude not to say so. The secret handling in
start.shis properly idempotent: generated once, gated on non-emptiness, umask 077, atomic write through a temporary file, and the mode re-asserted on every boot rather than only on first run. That last detail is the one most packages get wrong, and a restore resets modes, so it matters. Running the migrations synchronously before supervisord so the port opens only once they are done is the right call. The OIDC redirect URI is/api/auth/callback/custom, which is the correct provider-id-derived path and not the intuitive wrong one. The scheduled CI that polls upstream releases and bumps the pin is better release machinery than most community packages have, including ours. And binding Meilisearch and Chromium to loopback is exactly right.The Meilisearch store and backups
This is the substantive one, and it has two parts that are worth separating because they have different levels of confidence.
The part I am confident about: a live LMDB store copied file by file is not a restorable artefact.
Your Meilisearch runs with
--db-path /app/data/meilisearch, which puts the live store inside the tree Cloudron copies. The platform takes that copy while the application is running, with no quiesce step and no post-backup hook. For a key file or a SQLite database at rest that is fine. For an LMDB store that is being written to, the copy has no defined consistency point, and Meilisearch does not consider a raw file copy restorable; a snapshot is what it considers restorable. So the index inside your backups may or may not come back cleanly, and nothing currently tells the operator which.The saving grace specific to Karakeep is that the index is derivable: Karakeep can rebuild it from SQLite through the admin "Reindex All Bookmarks" action. That turns a potentially corrupt restore from data loss into an inconvenience, which is a much better position than most applications are in. But it only works if the operator knows to do it, and at the moment
POSTINSTALL.mddoes not mention it. The cheapest possible fix here is one paragraph of documentation saying that after a restore, search may be empty or stale until a reindex is run. That costs you nothing and closes the worst outcome.The part I am not confident about: whether this can abort the backup run itself.
Cloudron's rsync syncer walks
/app/dataand is not resilient to concurrent mutation. Its tree walk lists a directory then recurses into it, and if a child directory disappears between the list and the recurse, the read returns null and a later sort throws. The exception is not caught per application. It aborts the entire server's backup run, so one application's temp-directory churn becomes every application's missed backup.I want to be careful here. That failure was earned against ClickHouse, whose merge process creates and atomically renames temporary directories continuously, and it generalises cleanly to Elasticsearch and RocksDB. Meilisearch is a much narrower case: mostly long-lived files under
indexes/<uuid>/, and the obvious candidates for a vanishing directory are an index deletion landing mid-backup, or churn in the dump and snapshot directories. We have not reproduced this against Meilisearch, and we have not tested your package at all. Our own package moved the store out of the walked tree from the start, so we never ran the experiment that would have proved it either way. Treat it as a live question rather than a finding.If you want to settle it, the test is cheap: put a decent corpus in, start a large indexing batch, delete an index while it runs, and trigger a backup from the dashboard during the churn. Either the run completes or you get a syncer stack trace in the box logs. (It is cheap, but it does take a long time, especially if your server is busy!)
The design that solves both parts, if you want it.
minBoxVersionin your manifest is already 9.1.0, which is the version that unlocks the machinery, so this is available to you today. Move the live store onto apersistentDirspath outside/app/data, and add abackupCommandthat asks the running instance for a snapshot over its own HTTP API and drops the artefact into/app/datafor the platform to pick up.The one non-obvious part is that
backupCommandruns in a temporary container, not the live one: noCLOUDRON_*environment variables, stdout discarded, read-only root filesystem, but attached to the cloudron network with/app/dataand the persistent dirs mounted. So the backup script cannot learn its own application's address from the environment. The trick that makes it work is having the entrypoint write the container's address into a file under/app/dataon every boot, which the backup script then reads over the shared mount and dials. Ours also exits 0 on every path deliberately, including failure, because a non-zero exit from a backup command takes the platform's whole backup run down, which is worse than a snapshot that is one cycle stale. It records success or failure to a log file under/app/datainstead, since its stdout goes nowhere.We tested this under load and the result was better than we expected. A backup issued with sixteen tasks queued and
isIndexingtrue completed cleanly in 314 seconds, and the snapshot did not merely capture a consistent view: it carried the pending write queue with it, so the restored instance resumed the indexing work that was in flight. Nothing accepted before the consistency point was dropped or half indexed.Three
persistentDirsbehaviours we confirmed on a real box, since they decide what your boot logic has to handle: an update preserves the directory, an in-place restore preserves it, and a clone starts it empty. That last one is the one that bites, because it means a cloned application must be able to rebuild its store from the snapshot in/app/dataon first boot. In your case there is a second option, which is to let it rebuild from SQLite instead.The scripts are MIT licensed and public at https://github.com/OrcVole/meilisearch-cloudron if any of it is useful. Please lift whatever you want, no attribution needed.
Memory sizing
Your
memoryLimitis 2 GiB shared between Next.js, the workers, Chromium and Meilisearch, with no absolute cap on any of them.Our package gives Meilisearch 4 GiB on its own, and it needs it. At 2 GiB it failed our memory gate outright: anonymous demand alone reached about 2.5 GiB against the 2 GiB limit while indexing a million documents.
I want to be honest that this number does not transfer directly. A million documents is a far larger corpus than a typical bookmark collection, and your four processes will not all peak together in normal use. So this is not me telling you to ship 4 GiB.
What does transfer is how to read the numbers, and this is the part I would genuinely have wanted to know earlier.
memory.peakin the cgroup is useless for sizing a memory-mapped store. It sat at 100.0001 per cent of the limit in every one of our runs, at 2 GiB and at 4 GiB alike, because it counts page cache, and a store using mmap will use every page you give it and then reclaim. Reading that figure suggests a container in permanent distress when it is doing exactly what it should. The figure that means something ismemory.stat anonplus swap: at 4 GiB that peaked at 1.74 GiB, or 43.5 per cent, which is a healthy result and the one the gate should have been testing all along.Also worth knowing:
MEILI_MAX_INDEXING_MEMORYis not a ceiling on the process. It caps one specific arena. Ours was set to a third of the cgroup limit and the process still went well past it. Do not treat it as a safety bound. And Meilisearch sizes its defaults from host RAM rather than the cgroup limit, which is upstream issue 4686, so on a Cloudron box it will guess based on the whole machine unless you compute the value from/sys/fs/cgroup/memory.maxyourself at boot.The Meilisearch version bump
Your Meilisearch pin at v1.41.0 is manual and outside the CI that tracks Karakeep, so this may be some way off, but it is a trap worth knowing about before you hit it.
Meilisearch will refuse to start against a store written by an older version. Handling it needs an explicit upgrade path: record the version that wrote the store in a marker file, compare it at boot, and run the upgrade when they differ. The rollback direction matters too, because a store written by a newer binary than the one now running cannot simply be opened, and that is exactly what a Cloudron rollback to a previous package version produces.
Separately, 1.51.0 removed several flags that earlier versions accepted, including
--experimental-no-snapshot-compaction,--experimental-replication-parametersand--experimental-no-edition-2024-for-dumps, and renamed the experimental spelling of the dumpless upgrade flag. You do not pass any of those today, so this only matters if you add one from an old blog post.Smaller things
localstorageis declared as{}with nosqliteoption, whilestart.shsetsDB_WAL_MODE=true. The addon'ssqliteoption exists to declare database paths so the backup captures them consistently, and a WAL-mode database is three files that need to agree with each other. I will be straight that our own doctrine has not settled what that option actually guarantees across the database, the WAL and the shared-memory file, so I am flagging it as worth a look rather than asserting it is broken.Supervisor priorities give you spawn order but not readiness. The web process and the workers can start talking to Meilisearch before it is accepting connections, and a worker that fails its initial connect can sit there doing nothing without saying so. A bounded wait on the dependency, sixty seconds or so, then proceeding degraded, is usually enough.
OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING=trueis set unconditionally. That silently links an OIDC identity to any existing local account with a matching email address, which is a reasonable convenience on a single-operator install and a real account-takeover path on a box where users can register with an email they do not control. GivenDISABLE_SIGNUPSis left to the operator, both configurations exist in the wild. It may be worth making it opt-in through the env file, or at least calling it out inPOSTINSTALL.md.OAUTH_WELLKNOWN_URLis hand-built fromCLOUDRON_OIDC_ISSUER. Cloudron injectsCLOUDRON_OIDC_DISCOVERY_URLdirectly, so using it removes an assumption about the issuer's URL shape.Finally, and this one is a preference rather than a defect: the final stage is the upstream Karakeep image with supervisor and Chromium layered on, rather than
cloudron/base. The documented reason to prefer the base image is that the dashboard file manager, web terminal and log viewer depend on its userland. I do not know whether they actually break on a Debian-derived image, and I would be interested to hear if you have checked, because that is a question we would like answered for our own purposes too and I have not found it written down anywhere.Thanks again for the package, and apologies for the wall of text. Happy to expand on any of it, and happier still to be told I am wrong about the parts I have flagged as untested.
-
🚀 Speaches: community package now availableTL;DR: Speaches is a self-hosted speech server that speaks the OpenAI audio API, giving you speech to text and text to speech on your own hardware. Point OpenWebUI or LibreChat at it and dictation and spoken replies work with no external API and no per-request cost. Now packaged for Cloudron and ready to install. Built and tested on Cloudron 9.x; unofficial and community-maintained.
Links
Project homepage: https://speaches.ai
Upstream repo: https://github.com/speaches-ai/speaches- 🧱 Cloudron package repo: https://github.com/OrcVole/speaches-cloudron
There is no hosted demo, but the app ships its own playground UI at the root of whatever domain you install it on, with tabs for speech to text, text to speech, and audio chat, so you can try everything from a browser before wiring anything up.
How to installThe easiest route is the dashboard. In the App Store, click the Add custom app dropdown at the top right, choose Community app, and paste this URL into the box that appears. Apps installed this way receive automatic updates.
https://raw.githubusercontent.com/OrcVole/speaches-cloudron/main/CloudronVersions.jsonThe CLI does the same thing:
cloudron install \ --versions-url https://raw.githubusercontent.com/OrcVole/speaches-cloudron/main/CloudronVersions.json \ --location speaches.example.comMinimums: 5 GiB memory, addon
localstorageonly. No database, no queue, no extra subdomain.First run: there is no login screen. An API key is generated on first boot and lives at
/app/data/.secrets/keys.env, readable from the app's own Terminal in the dashboard. Send it asAuthorization: Bearer <key>to the/v1endpoints. The playground UI is reachable by anyone who can reach the domain, but it cannot transcribe or synthesise anything until that same key is pasted into its own key box, once per browser. First boot downloads roughly 1.6 GB of models before the app is ready, and the health check stays green throughout, so give it a few minutes and watch the logs.
For usersWhy try it: because the speech features in your existing chat frontend probably still point at somebody else's cloud, and this is the piece that brings them home.
What you get: OpenAI-compatible speech to text on faster-whisper, text to speech with Kokoro and Piper voices, and a realtime websocket endpoint. Cloudron wins: automatic backups of your configuration and key, one-click updates, TLS and DNS handled, and all state under
/app/data.Good fit if you already run OpenWebUI or LibreChat and want dictation and spoken replies without an external API key. Probably not if you need real-time dictation on a busy shared server, because this is CPU inference and speed depends heavily on what else your machine is doing.
On performance, plainly: on a quiet machine the default model transcribes several times faster than real time, and on a heavily loaded one it can be many times slower than that. The honest answer is that it depends on your hardware and your load rather than on any number I can promise you. Smaller and distilled models are much faster if throughput matters more than accuracy.
🧰 For packagers: what we learned
What helped: the whole shape was cribbed from an earlier CPU inference package: a two-stage build onto
cloudron/base, a generated API key in/app/data/.secrets/keys.env, and the model cache on apersistentDirspath with re-download as the restore rather than a backup command.What was tricky: four things, all of which cost real time.
- CTranslate2 defaults to float32 on CPU. Selecting int8 instead is measurably faster and uses far less memory for the same weights. The package now picks quantisation from the CPU's own feature flags at boot rather than hardcoding it, degrading only for genuinely old silicon so that future CPUs get the fast path automatically.
- The bundled Gradio UI derives its own backend URL from the inbound
Hostheader unless a loopback URL is configured. Behind a reverse proxy on a different port, every UI action fails while looking exactly like an authentication problem. huggingface_hubderives its cache path as${HF_HOME}/huband raisesCacheNotFoundif that exact directory does not already exist, which surfaces as a 500 on the model listing endpoint on a fresh install.- Sizing memory from
memory.peakalone is wrong for a model server. The cgroup is also charged the page cache holding the model files, so readanonplusfilefrommemory.statand size from that. Anoom_killcount of zero does not mean a limit is adequate.
Still rough: measuring CPU throughput honestly on a shared machine. Early figures in this project turned out to be measuring the neighbours rather than the package, on a host that was carrying four times its core count in load. Interleaved A/B comparisons and recording the load average alongside every timing are the only things that made the numbers trustworthy.
️ For the Cloudron teamMaintenance burden: low. The package is a single Python process behind a thin nginx front end, no database, one addon. Upstream has been quiet since April 2026, so the pinned version is stable by circumstance; the package does not depend on upstream doing anything further to keep working.
Why it suits the App Store: there is longstanding demand on this forum for exactly this, going back to 2023, in threads 9069, 12215 and 13018, the last of which is someone pointing OpenWebUI at cloud Whisper for want of a local backend. Upstream is MIT, widely used, and it completes a stack whose other pieces are already packaged.
Friction worth knowing: a
CloudronVersions.jsonwhosedockerImagesits beside the embedded manifest rather than inside it is rejected with404 Could not resolve CloudronVersions.json from URL. The file was fetchable throughout, from both the workstation and the server, and all three plausible argument forms fail identically. An error naming the schema rather than the URL would have saved an hour, and this seems likely to catch others.
For Speaches' developersOffered gratefully, and all low effort.
- Reserve and document an environment variable namespace. The server currently reads bare names such as
API_KEY, which collide with generic names other software also reads, and gives wrapper authors no safe prefix to work around. - Streamed WAV output from
/v1/audio/speechcarries a placeholder frame count of 2147483647 in its header, so a 4.9 second file claims to be 24.8 hours long. Feeding that straight back into/v1/audio/transcriptionsis an obvious round trip for a user to try. - Every non-streaming transcription logs
Unexpected streaming transcription response typeat ERROR level while still returning a correct 200. - Consider defaulting
compute_typeto int8 on CPU, or documenting the choice prominently. It is the single largest performance and memory lever for CPU users. - There is no authored logo anywhere in the project, which makes store listings awkward; the docs site favicon is the mkdocs-material default.
Genuine credit where it is due: matched CPU and CUDA images for every release, sha-pinned tags per commit, and on-demand model loading with idle unloading all made this package much easier than it could have been.
Package source and pull requests welcome: https://github.com/OrcVole/speaches-cloudron. Happy to co-maintain.
Unlocks /
SynergiesNow you can dictate into your self-hosted chat frontend and have replies read back to you, with the audio never leaving your own machine, and transcribe recordings through a plain HTTP call from any script you like.
Pairs with other Cloudron apps:
- Speaches + OpenWebUI: set the speech to text and text to speech engines to OpenAI in Admin Settings, Audio, with your Speaches base URL and key. Dictation and spoken replies, entirely self-hosted.
- Speaches + LibreChat: the
speechblock inlibrechat.yamltakes full endpoint URLs. Point speech to text at/v1/audio/transcriptionsand text to speech at/v1/audio/speech. - Speaches + a local LLM: Speaches can proxy chat completions to another OpenAI-compatible backend, so a self-hosted model can be the brain behind its audio chat and realtime endpoints.
Feedback, bug reports, and "works on my install" confirmations all welcome below.


