Reverses the placement decision made an hour ago. That one put the APK only on the data volume, reasoning that ~55 MiB should not be charged to installs that never touch Android. The operator's call is that ending the manual copy is worth the megabytes, and it is their deployment. CI now fetches the newest published client into the build context immediately before the image build, so `:dev`, `:latest` and `:<version>` all ship one and a `docker compose pull` delivers a new server and a new client together. **Always the rolling `dev` release — the newest build there is.** A versioned image therefore carries the newest client rather than one pinned to that version. Deliberate: the two negotiate a sync protocol version before linking, so a mismatch is caught by the handshake, and pinning would buy nothing the handshake does not already provide. **Fetched by the JOB, never by the Dockerfile.** The release is private, and a token used inside a build ends up in the context or a layer. **It cannot fail the image build.** No release yet, a network blip, a first-ever build — all of them log a warning and produce an image with no client, which is a state the server already supports. Half a pair is cleaned up rather than shipped: a sidecar without its APK is worse than neither, because the server would be describing something it cannot serve. **The volume still wins.** `DATA_DIR/client/` is checked first and the baked copy second, so an operator who deliberately drops a build in gets that build — and a BROKEN drop-in falls through to the image's copy rather than taking the feature offline, which is what makes the copy-order advice survivable instead of load-bearing. Three tests cover the precedence, including that last case. The baked copy lives inside the package, not under DATA_DIR: that path is a volume mount, and anything the image wrote there would disappear behind it the moment one is attached. `client/.keep` is tracked so `COPY client/` cannot fail on a tree where the CI step never ran; the artifacts themselves are gitignored, since a 55 MiB binary does not belong in git history and is re-fetched on every build anyway.
4.2 KiB
Getting the Android app onto your server
ThoughtSync's server hands out the Android client it syncs with. Once an APK is in place, anyone with an account on that server can download it from Account → Linked devices, and linked phones can update themselves from it.
This is deliberate rather than incidental. The build is not on an app store and the Fabled-Git instance is private, so a release page is no use to a self-hoster — but the server holding their notes is something they already trust and already reach. It also keeps the two in step: client and server negotiate a sync protocol version before linking, so a server that serves the client cannot hand out a phone it cannot talk to.
Where it comes from
Normally: nowhere. It is already in the image.
CI fetches the newest published Android build into every server image it builds,
so :dev, :latest and :<version> all ship a client. docker compose pull && docker compose up -d delivers a new server and a new client together, and there
is nothing to copy.
A versioned image therefore carries the newest client rather than one pinned to that version. That is deliberate: the two negotiate a sync protocol version before they link, so a mismatch is caught by the handshake rather than by pinning.
Overriding it
If you want a specific build — testing something, or holding back — drop it in
/var/thoughtsync/client/ and it wins over the image's copy.
Two files, both required:
| File | What it is |
|---|---|
thoughtsync.apk |
the client |
thoughtsync-android.json |
{version_name, version_code, size, sha256} |
The sidecar exists because an APK keeps its version in a binary manifest that needs the Android build tools to read. CI writes it beside the APK, where the real values are already known.
/var/thoughtsync is the same volume that holds attachments (Config.DATA_DIR),
so a build dropped there survives container recreation — and survives an image
upgrade, which is the point of an override.
Both files are published to the rolling dev release on every green Android
build:
REPO=https://git.fabledsword.com/bvandeusen/thoughtsync
TOKEN=... # a Fabled-Git token with read access; the instance is private
for f in thoughtsync.apk thoughtsync-android.json; do
curl -fsSL -H "Authorization: token $TOKEN" \
-o "/tmp/$f" "$REPO/releases/download/dev/$f"
done
# Copy the sidecar LAST. A sidecar that does not match the APK beside it is not a
# client, so a half-finished copy falls back to the image's build rather than
# advertising a lie.
docker compose cp /tmp/thoughtsync.apk app:/var/thoughtsync/client/
docker compose cp /tmp/thoughtsync-android.json app:/var/thoughtsync/client/
To go back to whatever the image ships, delete both files.
Checking it took
curl -s http://localhost:5000/api/client/android
A server with a client answers with the version, size and digest. A server without
one answers 404 — and the download card in the web UI is hidden rather than
offering a button that fails.
What happens if you get it wrong
All of these describe an override in /var/thoughtsync/client/. A broken
override does not take the feature away — it falls through to the build the image
shipped with, which is the whole reason precedence runs in that direction.
- Only the APK, no sidecar — not a client. The server cannot state a version it has no way to read.
- Mismatched pair (new APK, old sidecar) — not a client. It will not serve one build while describing another.
- Neither, and no baked copy either — the server reports no client, the UI
hides the card, and
/api/client/androidreturns 404. That is the state of any image built before the first Android build ever published, and nothing about it is an error.
Signing, and why replacing the APK is safe
Every release build is signed with the same key, so a phone can install a newer one straight over the old one and keep its notes. That was not true before August 2026 — builds until then were signed with a throwaway key per CI run, and each install required uninstalling the last (Scribe #2803). If you are carrying a build from before that, expect to uninstall once more and sync anything you care about first.