Repository navigation
Releases: OpenLabs-so/openanalytics
Release list
v0.8.0
Upgrade notes: nothing by hand — and going back is a restore.
git fetch --tags && git checkout v0.8.0 && cd infra/selfhost && ./upgrade.shClickHouse migration 0029 drops the ten hourly rollup tables that 0.7.0 stopped writing, so returning to 0.7.0 is not a matter of changing the image tag: the older images would find those tables gone. Go back with ./rollback.sh to the snapshot the upgrade took. Skipping 0.7.0 (0.6.0 → 0.8.0) is supported: migrations 0025–0029 run in one pass and the migrate container fills the fifteen-minute rollups from your raw events afterwards, exactly as in 0.7.0. Details in SELF-HOSTING.md, "Upgrades and going back".
Fixed
- No more false "history fill failed". The fill that builds the fifteen-minute rollups from history now checks itself against your raw events instead of the frozen hourly tables. In 0.7.0, an upgrade with the worker still running could log
history fill backfill-15m failedwithadditive_mismatchover data that was correct. The check now fails only when the filled history really disagrees with the raw events. - A site deletion started before an upgrade finishes after it, instead of retrying a dropped table forever.
Removed
- The hourly rollup tables (
*_1h, ten of them), with their ClickHouse grants and their entries in site deletion. Nothing has read them since 0.7.0; the daily tables stay.
Tested before the tag on a fresh host, from GHCR images: 0.7.0 → 0.8.0, 0.6.0 → 0.8.0 (0.7.0 skipped), 0.6.0 → 0.8.0 with the worker and a late-event writer running throughout (no failure line), and 0.7.0 → 0.8.0 built from source.
Full notes: CHANGELOG.md
v0.7.0
Nothing to configure by hand. git fetch --tags && git checkout v0.7.0 && cd infra/selfhost && ./upgrade.sh; on Coolify or Dokploy, redeploy with the new tag. The migrate container fills the new fifteen-minute rollups from your history once, so this upgrade's migrate step runs longer than usual. If you upgrade without stopping the stack first, the first start can log one history fill … failed line: the history is complete, the line does not repeat, and the next release removes it. Details in SELF-HOSTING.md, "Upgrades and going back".
Added
- Every timezone. Reports are assembled from fifteen-minute rollups instead of hourly ones, so a site can report on any IANA timezone, including offsets that are not a whole hour — India (+05:30), Nepal (+05:45), Chatham (+12:45). Local days, hours and daylight-saving days are cut exactly. 0.6.0 refused those zones.
- Every site has a reporting timezone, set in Settings → General. The dashboard, widgets, the share page, MCP and the assistant all read on it, so "today" means the same day to everyone on the team. A reader can switch their own view from the header without changing it for anyone else. Existing sites take their owner's account timezone, or UTC.
- Site cards carry numbers. The sites grid shows all-time visitors, pageviews and revenue per site, with a weekly visitors line.
GET /v1/sitescarries them, from one query for all listed sites, cached for five minutes. - A tag that cannot count says so. A tag loaded on a host the allowed domains refuse — localhost, a preview deployment — writes one console line naming that host, and the dashboard's waiting screens name it with "Allow this domain". A site that has never received an event says so instead of showing zeros.
- Five share posters. Sources, countries, live visitors and top pages each have one beside the overview, on the same card and in both themes. Share copies the image and offers X, Bluesky and LinkedIn; on a phone it opens the system share sheet.
Sopens the dialog. - AI referrals. A card for visits sent by ChatGPT, Claude, Perplexity, Gemini and Copilot, with their share of all visitors. A visit counts when the assistant is the referrer, or when it arrived without one and the assistant's utm tag names it — which is how clicks from the ChatGPT apps stop reading as Direct. The public board has the same card.
- Devices, Browsers and OS now share one card with a picker.
Changed
- The hourly rollup tables are no longer written. They stay as frozen history; a later release drops them.
PATCH /v1/sites/{id}refusesreporting_timezone: nullwith a 400 — every site has a zone now.- Every service's log has a ceiling: three files of 100 MB, in all three compose files. Docker's default never rotates.
- Fewer metric lines: without a remote-write backend an unchanged gauge is logged at most once a minute; with one, the exporter is the only sink.
- The tracker is MIT.
apps/tracker/and theoa.jsit builds carry the MIT licence; the rest of the product stays AGPL-3.0. It is the one file that runs in your visitors' browsers rather than on your server, so embedding it should never have raised a copyleft question.
Fixed
- The ClickHouse migration CLI now reads
*_FILEvariables;CLICKHOUSE_MIGRATION_PASSWORD_FILEhad read as no password at all. - MinIO is pulled from Quay.
minio/miniostopped resolving on Docker Hub, so the optional object-storage profile failed to start. - Sources and Top pages are ranked by the number they show. Both ordered by pageviews underneath while printing visitors, so a list could read 27, 2, 27, 18.
- The Coolify and Dokploy blueprints pin this release. Both shipped with 0.6.0 still pinning the 0.5.0 images, so a platform install ran the previous release.
- Why a country can read Unknown is written down: a visitor arriving over IPv6 reaches the collector as the Docker bridge gateway and so has no country. (#6)
Coming later
The hourly tables this release stops writing are dropped in a later release; its notes will say what that asks of an install that skipped 0.7.0.
v0.6.0
Nothing to configure by hand. git fetch --tags && git checkout v0.6.0 && cd infra/selfhost && ./upgrade.sh; on Coolify or Dokploy, redeploy with the new tag. ClickHouse migrations 0023 and 0024 run through the ordinary migrate one-shot and only add columns.
Added
- Filters. Click a row in Sources, Locations or the device cards and the overview keeps only the sessions that match. Filters combine across dimensions (source, country, city, device) and sit on the tab bar, each clearing on its own. A filtered view is computed from raw events rather than a rollup, so it covers at most 92 days; past that the cards offer to shorten the range or drop the filters. The same filters work on the dashboard,
/v1/readand MCP. - Entries, exits and bounce rate per page in Top Pages, sortable by any of them.
- Click IDs are sources.
gclid,fbclid,msclkidand their companions attribute a visit to the ad network even when the referrer is empty. ?ref=is a source. A link you control can carry?ref=twitteror?ref=newsletter; visits that would have read as Direct get that name. Known names fold onto the site they mean, anything else is kept as written. Explained in the dashboard docs.- Polar as a second revenue provider next to Stripe, with Standard Webhooks signature verification.
- Umami import. One archive entry may fill several reports.
- Overview poster. A share button turns the overview on screen (site, period, headline numbers, traffic curve) into a PNG or copies it to the clipboard.
- Realtime on the overview. A card lists the visitors present right now, with a live badge beside the Overview and Realtime titles.
- MCP. Every tool spells out its hints, native-app redirect URIs are accepted, and the repository carries a root
.mcp.jsonpointing at the hosted server. - Dokploy. The blueprint ships with an import config and a copyable raw link.
- Docs. The dashboard page now explains what each headline metric counts, how each breakdown is cut, how filters behave and how to tag your own links.
Changed
- A tracker on a site that no longer exists stands down: the config read returns a 404 with a gone marker,
collection_pausedcarries a body and an ETag, and the tracker stops asking. - Cards fall back to plain rounded corners on browsers without
corner-shape. - A
CHANGELOG.mdnow lives in the repository, starting with this release.
Fixed
- Email: a 429 from the provider is retried instead of burning the address, and a typo in a stranger's address is no longer recorded as a page. Terminal failures are recorded as
failed. - Ingest: the finalizer window is bounded, the outbox is leased and reclaimed rather than raced, the backlog carries
available_at, and the queue exposes its own metrics. - Realtime: a refusal is believed only once a fresh token has been refused, so an expired token no longer reads as a revoked one.
- Shared reports: percentages are shares of the total, not of the largest row.
Next
0.7.0 brings fifteen-minute rollups, so every timezone works, including half-hour and quarter-hour offsets such as +05:30 and +05:45. That upgrade runs a one-time backfill; upgrade.sh will do it for you.
v0.5.0
Nothing to configure by hand. git fetch --tags && git checkout v0.5.0 && cd infra/selfhost && ./upgrade.sh; on Coolify or Dokploy, redeploy with the new tag. ClickHouse migration 0022 runs through the ordinary migrate one-shot and drops the now-unused events_preview table.
Removed (0.x: a minor may remove)
- Ingest test mode and the no-code rule preview are gone (ADR-0068). The client-claimed
test_modefield is accepted and ignored; the preview token, its mint route,?oa_preview=and theevents_previewtable are removed. All traffic is ordinary: visible and billable. Staging belongs on its own site with its own tracking key. An existing key volume may still hold apreviewkeypair; nothing reads it.
New
- Dokploy support, measured. A one-service Compose blueprint (
infra/selfhost/dokploy/) and an operator guide written from a live install: DOKPLOY.md. Fourteen containers, four domains on a header-stripping Caddy edge, every secret generated for you. The catalogue submission to Dokploy's templates is in flight; until it lands, the guide's import path works today. - The setup flow no longer holds you hostage. Onboarding order is now site, timezone, install, verify, and the verify step offers "I'll do this later" instead of waiting on the first event.
- TanStack Start. An install guide (
/docs/install/tanstack-start) plusoa initdetection and a root-route injector, shipped to npm asgetopen@0.4.0. Contributed by @DeckardGer (#3, #5): the first external contribution to this repository, and a careful one.
Improved
- Realtime board: visitor trails are cached, modal cards no longer blink on refresh, and the placeholder card explains itself.
- The freshness chip sits with the tooltip it belongs to.
- Docs: the disk requirement reads 25 GB, which is the number an upgrade actually needs.
v0.4.2
Coolify only. Your visitors get a country now. Redeploy from the platform and
the first deploy downloads a geo database. The images are byte-identical to
v0.4.0 and this tag runs them; an ordinary docker-compose.yml install is
untouched and always had --with-geoip.
What was wrong
Every Coolify install reported a null country for every visitor, on every
plan, forever. The geoip volume started empty, there was no checkout to fill it
from, and GEOIP_DB_PATH was deliberately unset because a path that names no
file is a refusal to start.
Each of those was true. None of them was an acceptable place to leave a
self-hoster. It was written down in two places, and the Coolify half of the
instruction pointed at infra/selfhost/geoip/, a directory that does not exist
on that platform.
What it does now
A geoip one-shot does what fetch-dbip.sh does for a host with a shell: on the
first deploy it downloads DB-IP City Lite, checks the file really is a
MaxMind-format database, and moves it into place. The collector's
GEOIP_DB_PATH now points at it.
The licence is what makes fetching on your behalf possible at all. GeoLite2
needs a MaxMind account and a licence key and forbids redistribution, so nothing
could have fetched it for you. DB-IP City Lite is
CC BY 4.0: a direct download, no
account, with an attribution condition. That condition asks for credit wherever
you show the data:
IP Geolocation by DB-IP, https://db-ip.com, used under
CC BY 4.0.
The step prints it too.
The network is a first-install dependency only. The file is written once and
never rewritten, so every later deploy exits on the first line without touching
the network. To refresh it, delete dbip-city-lite.mmdb from the volume and
redeploy; the databases go stale within a month or two. To use MaxMind instead,
copy your own file in and point GEOIP_DB_PATH at it.
A failed download fails the deploy. The collector reads that path at boot and
refuses to start on a file that is not there, so the alternative is a crash loop
one step further from its cause. Redeploy to retry, or install with no geo by
deleting the service, its depends_on entry and the collector's GEOIP_DB_PATH.
COOLIFY.md says so.
Proven before it shipped
On a real host, with the command rendered exactly as Compose hands it to the
shell: 124 MB written, mode 0444, owned by uid 1000; a second run left it
alone; and the published collector image started against it with geoip_loaded, configured: true.
One thing the host script could not have told us. Its verification is
grep -qa MaxMind.com, which is correct under GNU grep and finds nothing under
busybox. Measured on the real 130 MB database: grep -qa said no,
strings | grep -q said yes. Without catching that, a truncated download would
have been caught one deploy later, by a collector refusing a file it could not
parse.
The install is now thirteen containers
Three of them exited, and that is success: migrate, keygen and geoip are
one-shot steps that do their work and stop.
Images
Ten, published to ghcr.io/openlabs-so/openanalytics and tagged v0.4.2, with
contents unchanged from v0.4.0. The tag moves because the compose file ships
with them and a release is one commit.
Full changelog: v0.4.1...v0.4.2
v0.4.1
Coolify only, and it is the tab you could not find. If your install has no
Account → Deployment screen, this is why. Redeploy from the platform and it
appears.
The images are byte-identical to v0.4.0 and this tag runs them. Nothing here
touches an ordinary docker-compose.yml install, which never had the bug.
What was wrong
OA_CREDENTIAL_KEYRING was filled from Coolify's SERVICE_BASE64_KEYRING. That
generator produces 32 base64 characters, which decode to 24 bytes. The
keyring wants a key of exactly 32 bytes, and credential-vault.ts checks the
length rather than trusting it, so every install came up with:
revenue_not_mounted reason: OA_CREDENTIAL_KEYRING invalid_key_material
The keyring is also what the deployment-settings surface encrypts with, and that
surface closes without one. So the Account → Deployment tab was never drawn:
no mail relay, no assistant provider, no revenue connections, and nothing on
screen saying why. Measured on a live install before it was changed: 32
characters in, 24 bytes out.
Why the fix is a generator and not a longer variable
No magic variable on the platform produces 32 bytes. SERVICE_BASE64_ is 32
characters, SERVICE_BASE64_64_ is 48 bytes, SERVICE_BASE64_128_ is 96. Base64
of 32 bytes is 44 characters and there is no _44_.
So the keyring joins the three signing pairs, which are there for exactly the
same reason: keygen writes it with openssl rand -base64 32, only when it is
missing, and the api and the worker read it through
OA_CREDENTIAL_KEYRING_FILE. Never rewriting an existing one is the load-bearing
part. Rotating a keyring on a redeploy would strand every provider credential a
customer had already connected.
Proven before it shipped
On a real host, with the command rendered exactly as Compose hands it to the
shell:
- the keyring it wrote is 44 base64 characters, 32 decoded bytes, mode
0400, owned by uid 1000; - a second run left all three key pairs and the keyring alone;
- the published
v0.4.0api image started against it and logged
revenue_mounted, active_key_version: k1.
Also in this tag
COOLIFY.md stops claiming the branch can be changed in Settings. On Coolify
4.3.2 that field does not appear after the first deploy either. It does not need
to: the compose file pins the images, not the checkout. Every image: line reads
${OA_IMAGE_TAG:-v0.4.1}, so a clone of main runs the release named in the
file it just cloned.
The wiring test now accepts X_FILE as satisfying X. A path is another way
to supply any declared variable, and supplying both is an error, so the two
spellings are alternatives. A test that demanded the inline one would force the
wrong fix on exactly the values a platform cannot express.
Images
Ten, published to ghcr.io/openlabs-so/openanalytics and tagged v0.4.1:
migrate, tracker-build, api, collector, worker, query-gateway,
realtime, web, clickhouse, valkey. amd64. Their contents are unchanged
from v0.4.0; the tag moves because the compose file ships with them and a
release is one commit.
Full changelog: v0.4.0...v0.4.1
v0.4.0
If you installed on Coolify, your tracker script was never being served. Upgrade,
and check it:
curl -o /dev/null -w '%{http_code}\n' https://c.<your-domain>/oa.js # 200, not 404An ordinary docker-compose.yml install was never affected and needs nothing beyond
the usual:
git fetch --tags && git checkout v0.4.0
cd infra/selfhost && ./upgrade.shThe bug, and why nothing caught it
tracker-build is the shared Dockerfile's build stage. That stage copied apps,
packages and the manifests, and not scripts/ — while pnpm run tracker:build
is node scripts/build-tracker.mjs. The stock compose never noticed, because it
bind-mounts that directory from your checkout and passes an explicit command.
A one-click platform install has neither. So the container inherited the build
stage's default command, which is a bare node: it read EOF from a closed stdin and
exited 0. A one-shot reporting success, with the shared volume left empty and
nothing written anywhere to say so.
Nothing would have served the file even if it had been built. The collector mounts
/v1/tracker, /v1/events and /v1/realtime and no static route; on the stock
compose Caddy serves /oa.js from the volume, and the platform variants have no
Caddy. Meanwhile the dashboard hands every new site
<script src="${COLLECTOR_BASE_URL}/oa.js">.
The result: a Coolify install from v0.3.0 through v0.3.2 had a working dashboard, a
working api and working ingest, and a snippet that could not load. The end-to-end
proof that shipped with those releases passed because it posted events directly to
/v1/events and never fetched the script.
Both halves are fixed
The build. The build stage copies scripts/, builds the bundle, and asserts it is
non-empty. A missing bundle now fails where the image is built rather than where it
is served, which is the same rule the APP guard beside it has always followed.
The serving. apps/collector/src/tracker-script.ts reads the bundle once at boot
and answers GET /oa.js with the headers Caddyfile:93-114 already set, for the same
reasons: one hour of freshness, an ETag that revalidates to a bodyless 304, and
the copy precompressed once at startup so the bytes on the wire are the bytes the
size budget measured. The two encodings get different tags, so a shared cache cannot
hand gzip to a client that asked for none.
With no bundle the route is not mounted at all. An empty 200 is the worst answer
available here: every visitor's browser would cache "the tracker is nothing" for an
hour. A 404 is a miss an operator can see.
What changed for whom
- Stock compose: nothing. Caddy still serves
/oa.jsfrom the volume and never
forwards it, so the new route is unreachable there.tracker-buildand the
trackervolume stay exactly as they were. - Coolify and the other one-click platforms:
docker-compose.coolify.ymldrops
tracker-buildand thetrackervolume, which now do nothing. That install pulls
nine images instead of ten and ends with twelve containers, two of them
exited rather than thirteen and three.
Images
Ten, published to ghcr.io/openlabs-so/openanalytics and tagged v0.4.0: migrate,
tracker-build, api, collector, worker, query-gateway, realtime, web,
clickhouse, valkey. amd64. On arm64, build them instead: same compose file, one
flag.
Every one of them now carries the tracker bundle, because the runtime stage copies
the build stage wholesale and ten kilobytes is not worth a second stage to keep out
of the five that do not serve it.
A minor rather than a patch, by RELEASING.md:
the shape of a compose file an operator may have overridden changed.
Full changelog: v0.3.2...v0.4.0
v0.3.2
A patch, and for the ordinary compose path a documentation one: nothing here
changes what a docker-compose.yml install runs.
git fetch --tags && git checkout v0.3.2
cd infra/selfhost && ./upgrade.shAn install guide written from an install
infra/selfhost/COOLIFY.md is now step by step, and every step in it is one that
was got wrong on a real Coolify before it was written down. It came out of a
second install done from a zero-state Coolify, by hand, through the interface,
by somebody who had not deployed this stack before.
What that turned up, all of it now in the guide:
- "Public Repository", not "Docker Compose". The resource type called Docker
Compose gives you an empty editor to paste a file into. The repository flow is
the one that clones a tree, and it asks for the build pack afterwards. - Three of Coolify's own guesses are wrong here, and the branch cannot be set
until the resource exists. The guide names all four fields and what they become. - Every domain row must be
https. A row left onhttpis served without a
certificate and none is ever requested, while the container behind it stays
healthy. That is the failure shape this whole file exists to warn about. - The port box should fill itself from
expose:, and once it did not. If one
is empty, type it. An empty port means the router is dropped as unroutable, no
certificate is requested, and nothing is logged. - Coolify keeps two copies of every variable,
ProductionandPreview. The
list does not make the difference obvious, a deploy reads Production, and
editing the wrong one is silent. - Its DNS check can be wrong on records that are right. Verify with
digand
continue.
One fix, and it is the one that made a first deploy impossible
EMAIL_FROM shipped as ${OA_EMAIL_FROM:-}. The schema wants at least three
characters, so the worker refused to start, restarted, and refused again, over a
variable nothing told the operator was required. env/worker.env.example has
shipped a placeholder there the whole time; the Coolify file had dropped it.
It is a from-address and not a lock. Mail is configured later from the dashboard
under Account then Deployment, and a relay stored there wins over
anything in the environment, so magic links work whatever this says.
The guard is a test: no ${VAR:-} anywhere in that file. A variable the platform
fills in is written with no default at all, which is the honest spelling.
A note for the ordinary compose path
Not a change in this release, but the one thing v0.3.0 changed for an existing
install, now said where somebody will read it rather than only in a comment.
clickhouse and valkey became our own images in v0.3.0, with their config
files baked in instead of bind-mounted. The files have not moved and are what
the images are built from. What changed is that editing one in place no longer
takes effect: build instead of pull (./upgrade.sh --from-source), or
bind-mount your version over the baked one, which still wins.
Images
Ten, published to ghcr.io/openlabs-so/openanalytics and tagged v0.3.2:
migrate, tracker-build, api, collector, worker, query-gateway,
realtime, web, clickhouse, valkey. amd64. On arm64, build them instead:
same compose file, one flag.
Their contents are unchanged from v0.3.1. The tag moves because the compose
file, the env templates and the migrations ship with the images and a release is
one commit.
RELEASING.md
is what a version number here means.
Full changelog: v0.3.1...v0.3.2
v0.3.1
A patch, and for the ordinary compose path an empty one: nothing here changes
what a docker-compose.yml install runs. Everything below is the Coolify
variant, which went from a file that had been read to a stack that has been run.
git fetch --tags && git checkout v0.3.1
cd infra/selfhost && ./upgrade.shThe Coolify variant has now been deployed, and it took five fixes
infra/selfhost/docker-compose.coolify.yml was written by reading, and every one
of these was invisible until it ran on Coolify 4.3.2 against real DNS and real
certificates.
- The migration one-shot did not run migrations. It is the worker's image
under another tag, so with no entrypoint of its own it booted as the worker,
loaded the worker's schema, and was refused by the least-privilege boundary:
CLICKHOUSE_MIGRATION_PASSWORD: must not be provided to the "worker" service.
The boundary was right; the file was missing a line the stock compose file has
always had. - The ClickHouse database was named wrong.
openanalytics, where every grant
inclickhouse/oa-entrypoint.shis written againstanalytics.<table>by name
rather thananalytics.*. The stack would have come up green and failed on the
first read with a permission error naming a table nobody had renamed. GEOIP_DB_PATHwas set on a volume that starts empty. A path is a promise:
the collector reads it at boot and refuses to start when the file is not there.
On a platform with no checkout to download into, that turned the documented
degradation into a crash loop. Unset is what an install with no database
should say.- The dashboard was probed on a route it does not serve. The shared
healthcheck asks for/health; the six backend services answer it and a
Next.js app does not. Sowebanswered404forever, never turned healthy,
and the proxy served "no available server" in front of a dashboard whose own
log saidReady in 283ms. - The public services did not say which port they listen on. Traefik takes a
container's port from the ports it exposes, and the six backend services share
a Dockerfile that declares none. The collector's router was therefore
unroutable, was dropped, and its certificate was never even requested. Nothing
logged it: the hostname simply did not answer while the container beside it was
healthy.expose:publishes nothing to the host and is exactly the metadata
these platforms read, so declaring it fixes every one of them at once rather
than asking an operator to type a port they have no way to know.
What now holds on a live install, rather than in a file: all thirteen
containers correct, four hostnames on HTTPS with Let's Encrypt certificates, an
account and a site created through the dashboard, and three page views accepted
by the collector, drained through the queue by the worker and stored in
ClickHouse.
The client-identity middleware works, and that is the one that mattered. Every
forged header is deleted before it reaches the collector: CF-Connecting-IP,
CF-IPCountry, CF-IPCity, True-Client-IP, Fly-Client-IP,
Fly-Client-Country, X-Vercel-IP-Country, X-Vercel-IP-City, with
X-Forwarded-For left carrying the real connection address. Proven twice: once
against an echo service behind the same middleware, and once by sending
CF-IPCountry: XX with a real page view and finding the stored row's country
empty rather than XX.
Still not a supported path, because one install on one platform is a
demonstration rather than a promise. It is no longer a file nobody has run.
A correction, and one that matters when you cut a release
RELEASING.md said a GHCR package is private when first created and needs a
visit to its settings page. That was true on v0.1.0 for the original eight. It
was not true on v0.3.0 for clickhouse and valkey, which came out public
with nothing clicked. Two data points and no announced change between them, so
the section now says to check rather than to believe either answer, and gives the
one command that checks it from a machine with no GHCR login.
Images
Ten, published to ghcr.io/openlabs-so/openanalytics and tagged v0.3.1:
migrate, tracker-build, api, collector, worker, query-gateway,
realtime, web, clickhouse, valkey. amd64. On arm64, build them instead:
same compose file, one flag.
Their contents are unchanged from v0.3.0. The tag moves because the compose
file, the env templates and the migrations ship with the images and a release is
one commit.
RELEASING.md
is what a version number here means.
Full changelog: v0.3.0...v0.3.1
v0.3.0
A minor, and the number means what it says: read this before upgrading.
./upgrade.sh still does the work and there is nothing to run by hand, but two
of the images you pull are new and one thing you may have edited moved.
git fetch --tags && git checkout v0.3.0
cd infra/selfhost && ./upgrade.shTen images, not eight
clickhouse and valkey are now ours. They are upstream's images plus the files
they cannot start correctly without: ClickHouse's config drop-in and the
entrypoint that renders its four least-privilege users, and Valkey's two policy
files and its entrypoint. Those five files used to arrive as bind mounts from
infra/selfhost/.
Why this changed. A bind mount works when the compose file sits in a
checkout and nowhere else. A one-click platform clones the repository, writes the
rendered compose file and nothing beside it, so Docker creates an empty
directory for every missing bind source and ClickHouse dies with
oa-entrypoint.sh: Is a directory. Measured on Coolify 4.3.2. Every platform in
that category behaves the same way, so this was the last thing between the
project and a marketplace listing.
The files still live where they did, in clickhouse/ and valkey/, and are
copied into the images at build. There is one of each in the tree.
If you edited one of those files in place, it now has no effect. Two ways
back: bind-mount over the baked copy, which still wins, or build instead of pull
(./upgrade.sh --from-source). The five files are exactly the ones listed above;
nothing else moved.
One image serves both Valkeys. The queue and the realtime cache are the same
server under opposite policy, so both configs ship in it and OA_VALKEY_CONF
names which applies. A container started without it refuses rather than guesses:
a cache policy on the queue would evict acknowledged events before the worker
inserted them.
The plaintext Valkey check accepts a service name
buildConnectionOptions refuses a plaintext redis:// whose host it cannot
prove is private, and it proved that by matching the hostname against the RFC1918
literals. A compose service name matches none of them, so the check had no answer
for a name and refused every one.
It now also accepts a single-label name such as valkey-queue: a label with
no TLD has no public resolver that can answer it. A host with a dot in it outside
the private ranges is still refused, TLS is still required where the hop may be
public, and AUTH is still required everywhere. This is what lets a platform that
allocates its own network run the stack, since none of them lets a template pin
an address inside it.
The caveat, stated rather than hidden: a host configured with a DNS search suffix
can resolve a bare label to something off the machine. That needs an operator who
set a search domain and then wrote a bare label into a Valkey URL.
Coolify
infra/selfhost/docker-compose.coolify.yml has now been run on a real Coolify
install rather than only read. Two things that could not be checked any other way
both hold: the client-identity middleware is genuinely attached to the router
Coolify generates, and the SERVICE_FQDN_*, SERVICE_PASSWORD_* and
SERVICE_BASE64_* variables are filled in from the compose file with nothing
typed by hand.
Still not a supported path, and it will say so until an install has been run end
to end and watched.
One difference to know about there: /geoip is a named volume that starts empty,
because that platform has no checkout to mount a database from. Geo is null until
you copy one in, which is the documented degradation rather than a failure.
SELF-HOSTING.md says how under GeoIP.
Images
Ten, published to ghcr.io/openlabs-so/openanalytics and tagged v0.3.0:
migrate, tracker-build, api, collector, worker, query-gateway,
realtime, web, clickhouse, valkey. amd64. On arm64, build them instead:
same compose file, one flag.
Migrations do not go down. ./upgrade.sh takes the snapshot ./rollback.sh
needs before it starts, and a rollback discards everything recorded after it.
RELEASING.md
is what a version number here means.
Full changelog: v0.2.1...v0.3.0