An open dataset of self-hostable applications whose published install instructions have been run on a real, fresh server — recording which OS the run happened on and when.
- Files:
install-verification.json,install-verification.csv(same records, same field order) - Licence: CC BY 4.0
- DOI:
10.5281/zenodo.22983606(all versions; each release also gets its own) - Methodology: https://selfhostatlas.com/how-we-test
- Source: https://selfhostatlas.com
- Explore in the browser: open in Datasette Lite — no install
- Generated by: SelfHost Atlas's exporter; this repository is the published copy
This is the part that matters. The dataset deliberately contains only what the project actually observed, plus values that are clearly labelled as documented rather than observed.
| Status | Fields | Meaning |
|---|---|---|
| Measured | install_verified_os, install_verified_date |
A real install run happened: the app's published install snippet was executed on a cloud VM running that OS, on that date, and every step exited successfully. |
| Measured | measured_ram_mb, measured_disk_mb, measured_box, measured_on |
Observed footprint at idle, immediately after first boot, on the machine named in measured_box. Taken on a box pruned of every prior app, so the disk figure is this app's alone. Never under load, and never a capacity recommendation. |
| Documented, not measured | documented_min_ram_mb |
The minimum RAM the upstream project documents, or SelfHost Atlas's sizing recommendation where upstream documents none. Values sit on standard instance tiers (256/512/1024/2048…). This is a recommendation, not an observation. Do not cite it as measured memory usage. |
| Catalog metadata | slug, title, category, license, repo_url, website_url, is_docker_ready, content_updated |
Facts about the project and about our page, not measurements of a running system. |
| Not present | CPU usage, setup time, maintenance time | These are not yet measured, so they are not in the export at all — not even as null columns. They will appear only once real measurements exist. |
documented_min_ram_mb and measured_ram_mb answer different questions and
routinely disagree by an order of magnitude. Vaultwarden documents 256 MB and
idles at 7 MB; PhotoPrism documents 2048 MB and idles at 102 MB. Neither figure
corrects the other: size a server with the documented minimum, budget for the
real world with the measured one. A row can carry one, both, or neither.
scripts/export-dataset.ts refuses to publish a field listed as measured if
that column is empty in the database, and tests/export-dataset.test.ts fails
if an empty column is ever added to the export. That guard exists because
publishing a documented value as a measured one would be a false claim.
| Field | Type | Nullable | Description |
|---|---|---|---|
slug |
string | no | Stable identifier. Page: https://selfhostatlas.com/self-host/<slug> |
title |
string | no | Project name as commonly written |
category |
string | yes | Category slug (e.g. photos, password-managers) |
license |
string | no | SPDX-style short identifier (e.g. AGPL-3.0, MIT) |
repo_url |
string | yes | Upstream source repository |
website_url |
string | yes | Upstream project website |
is_docker_ready |
boolean | no | Upstream publishes an official container image or Compose file |
documented_min_ram_mb |
integer | no | Documented/recommended minimum RAM in MB — see the table above |
install_verified_os |
string | yes | OS the install run happened on (e.g. Ubuntu 26.04). Null = not yet verified |
install_verified_date |
date (YYYY-MM-DD) |
yes | Date of that run. Null = not yet verified |
measured_ram_mb |
integer | yes | Observed idle container memory (sum of docker stats usage) 30 s after first boot, in MB. Null = not measured |
measured_disk_mb |
integer | yes | Observed image + volume footprint, in MB. Null = not measured. May be null while measured_ram_mb is set, when a run's disk reading was not isolated |
measured_box |
string | yes | The machine the figures were taken on, e.g. GCP e2-standard-2 · Ubuntu 26.04 LTS · Docker 29.7.2. Non-null whenever either figure is — an unattributed measurement is not published |
measured_on |
date (YYYY-MM-DD) |
yes | Date of the measurement run |
content_updated |
date (YYYY-MM-DD) |
yes | Date the SelfHost Atlas page for this app was last editorially reviewed |
Judge freshness from install_verified_date: an install verified a year ago
says little about today's upstream instructions. Every record carries its own
date precisely so you can filter on it rather than trust the dataset as a whole.
Records with a null install_verified_os / install_verified_date are included
so the catalog is complete and the unverified share is visible. Filter them out
if you only want verified rows.
Everything measured comes from one harness, scripts/verify-install.ts, run
by scripts/sweep-installs.sh against a throwaway cloud VM:
- Clean host per app. One throwaway VM, reset between apps — containers, volumes, networks, images and build cache pruned — so nothing from a previous app is counted.
- Install verification. The app's own documented install commands are
executed end to end — placeholder hostnames swapped for a real one, recorded
in the transcript — under a hard timeout. If every step exits successfully,
install_verified_os/install_verified_daterecord the host OS and the run date. A failure normally leaves both null; interactive installers are driven through a terminal. - RAM. After the install completes the harness waits 30 seconds, then sums
the memory usage
docker statsreports for every running container. That is container memory at idle, immediately after first boot — not RSS of a single process, not peak, not under load. - Disk. The sum of
docker system dfsizes (images, containers, volumes, build cache) on the pruned box, so the figure is this app's footprint alone. - No number without its machine. If either figure is recorded, so are
measured_boxandmeasured_on. If nothing parses, the harness records nothing rather than a zero.
Anything not produced by that process — CPU, install wall-clock time, steady state usage — is not in the file.
import pandas as pd
df = pd.read_csv("install-verification.csv", parse_dates=["install_verified_date", "measured_on"])
df[df.measured_ram_mb.notna()][["title", "documented_min_ram_mb", "measured_ram_mb", "measured_box"]]-- DuckDB
select title, documented_min_ram_mb, measured_ram_mb
from read_csv_auto('install-verification.csv')
where measured_ram_mb is not null
order by documented_min_ram_mb / measured_ram_mb desc;jq '.records[] | select(.measured_ram_mb != null) | {slug, measured_ram_mb, measured_disk_mb}' install-verification.jsonOr open it in the browser with no install, via Datasette Lite: https://lite.datasette.io/?csv=https://raw.githubusercontent.com/usualdesigner/selfhost-atlas-data/main/install-verification.csv.
Cite the copy you used: quote schema_version and generated_at from the
JSON, because rows change between exports.
SelfHost Atlas (2026). SelfHost Atlas install-verification dataset (schema 1.1.0, generated YYYY-MM-DD) [Data set]. https://doi.org/10.5281/zenodo.22983606. Licensed CC BY 4.0.
@misc{selfhostatlas_install_verification,
author = {{SelfHost Atlas}},
title = {SelfHost Atlas install-verification dataset},
year = {2026},
doi = {10.5281/zenodo.22983606},
howpublished = {\url{https://doi.org/10.5281/zenodo.22983606}},
note = {Schema 1.1.0, generated YYYY-MM-DD. CC BY 4.0.}
}GitHub's Cite this repository button (from CITATION.cff) gives the same in APA and BibTeX. Each GitHub release is archived on Zenodo; the DOI above always resolves to the latest version.
{
"schema_version": "1.1.0",
"generated_at": "2026-09-26T21:32:47.328Z",
"license": "CC-BY-4.0",
"attribution": "SelfHost Atlas — https://selfhostatlas.com",
"methodology": "https://selfhostatlas.com/how-we-test",
"record_count": 118,
"records": [ /* … sorted by slug … */ ]
}schema_version follows semver: a major bump means a field was removed or
its meaning changed; a minor bump means a field was added. Records are
always sorted by slug, so a re-export diffs cleanly.
The CSV carries the record fields only, with a header row, RFC 4180 quoting and
the same field order. It has no envelope — take generated_at and
schema_version from the JSON.
Regenerated when the catalog changes — in practice after each install-sweep run
and each content wave, roughly monthly. generated_at in the JSON is the
authoritative export timestamp; the per-record install_verified_date is what
tells you how old any individual observation is.
A second, independent export: catalog.json / catalog.csv, generated by
scripts/export-catalog.ts (also run by npm run data:export). Same
directory, same CC BY 4.0 licence, same attribution requirement as the
install-verification dataset above — but its own contract: catalog
metadata, not "measured install verification".
| Field | Type | Nullable | Description |
|---|---|---|---|
slug |
string | no | Stable identifier. Page: https://selfhostatlas.com/self-host/<slug> |
title |
string | no | Project name as commonly written |
category |
string | yes | Category slug (e.g. photos, password-managers) |
license |
string | no | SPDX-style short identifier (e.g. AGPL-3.0, MIT) |
stack |
string[] | no | Runtime/language tags (e.g. ["node", "postgres"]) |
is_self_hostable |
boolean | no | The app can be run entirely on your own infrastructure |
is_docker_ready |
boolean | no | Upstream publishes an official container image or Compose file |
difficulty |
integer | no | Editorial setup-difficulty rating |
documented_min_ram_mb |
integer | no | Documented/recommended minimum RAM in MB — a recommendation, not a measurement (see the install-verification table above) |
alternative_to_saas |
string[] | no | Slugs of the SaaS products this app is pitched as an alternative to, in editorial order. Empty array if none |
Not present: is_externally_dependent (whether the app depends on a
third-party hosted service it doesn't control) and sso_support (none /
built-in / plugin / enterprise-only) are catalog facts the site tracks
in apps, but they are 0/N populated in the database as of this export — the
classification backfill hasn't run yet — so, matching the
install-verification table's own omission precedent above, they are left out
entirely rather than published as an always-null "not yet classified" field.
They'll appear in a future export once real classification data exists.
Required by CC BY 4.0. Any of these satisfies it:
Prose
Install-verification data by SelfHost Atlas, licensed under CC BY 4.0.
Markdown
Data: [SelfHost Atlas install-verification dataset](https://selfhostatlas.com) ([CC BY 4.0](https://creativecommons.org/licenses/by/4.0/))HTML
<p>Data: <a href="https://selfhostatlas.com">SelfHost Atlas</a>,
<a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a></p>If you modified the data, say so ("adapted from…"). A link back to https://selfhostatlas.com is the credit; there is no other condition.
If a record is wrong — an install that no longer works, a licence change, a dead repo URL — open an issue or email hello@selfhostatlas.com. Corrections land in the catalog and appear in the next export.
The data files are generated by SelfHost Atlas and copied here verbatim on each export — please report problems as issues rather than editing the files, since a PR would be overwritten by the next export.