# Deploy (Synology NAS) The avatar updater runs as a single **detached, self-scheduling container** on the NAS: it updates the avatar at 04:00 / 09:00 / 14:00 / 18:00 / 22:00 via `schedule.sh`, restarts on boot, and fails soft so a bad run never matters. Files live at `/volume1/docker/bsky-avatar/` on the NAS. ## First-time install ```bash # 1. Build the image (from the synced repo dir) sudo docker build -t bsky-avatar /volume1/docker/bsky-avatar # 2. Create the secrets file and fill it in cp /volume1/docker/bsky-avatar/.env.example /volume1/docker/bsky-avatar/.env # edit .env: BSKY_APP_PASSWORD (Bluesky app password) + HASS_TOKEN (HA long-lived token) # 3. Start the scheduled container sudo docker run -d --name bsky-avatar --restart unless-stopped \ --env-file /volume1/docker/bsky-avatar/.env \ -v /volume1/docker/bsky-avatar/config:/app/config \ -v /volume1/docker/bsky-avatar/state:/app/state \ -v /volume1/docker/bsky-avatar/deploy:/app/deploy \ --entrypoint sh bsky-avatar /app/deploy/schedule.sh ``` (`deploy/` is also baked into the image via the Dockerfile; the bind-mount lets you tweak `schedule.sh` without a rebuild and keeps older images working.) ## The location sink A second container from the same image, so the phone can push its position over WireGuard even when Home Assistant (or the whole home LAN) is down. ```bash # token the phone authenticates with — put the same value in .env openssl rand -hex 32 sudo docker run -d --name bsky-avatar-sink --restart unless-stopped \ --env-file /volume1/docker/bsky-avatar/.env \ -p 8477:8477 \ -v /volume1/docker/bsky-avatar/config:/app/config \ -v /volume1/docker/bsky-avatar/state:/app/state \ --entrypoint node bsky-avatar /app/src/sink.js ``` It shares the `state/` volume with the updater: the sink writes `state/location.json`, the updater reads it on its next run. Check it, from the NAS and then from the phone over WireGuard: ```bash curl -s http://localhost:8477/healthz # -> ok curl -s "http://:8477/location?token=$LOC_SINK_TOKEN" # last position ``` **Keep it on the LAN.** WireGuard already carries the phone onto the home subnet, so the sink needs no port forward, no tunnel, and no public DNS. Do not expose it. On the phone, [OwnTracks](https://owntracks.org/) in HTTP mode is the least work: set the URL to `http://:8477/`, leave the username as anything, and put the token in the password field (the sink accepts it as HTTP Basic). Anything that can POST `{"lat": .., "lon": ..}` with a bearer token works too. ## After editing config `config/*.yaml` (holidays, overrides, settings) is bind-mounted and re-read on every scheduled run — no restart needed. ## After editing secrets (.env) `--env-file` is read only when the container is **created**, not on `docker restart`. So to pick up new `.env` values you must **recreate** the container (not restart it): ```bash sudo docker rm -f bsky-avatar # then re-run the `docker run -d ...` line from "First-time install" ``` ## Updating the code Re-sync the repo to `/volume1/docker/bsky-avatar/`, then rebuild and recreate **both** containers (the code lives inside the image; only `config/` and `state/` are bind-mounted): ```bash sudo docker build -t bsky-avatar /volume1/docker/bsky-avatar sudo docker rm -f bsky-avatar bsky-avatar-sink # re-run the two `docker run -d ...` lines above ``` ## Not automated (yet) Deployment is deliberately manual for now: sync the tracked files to `/volume1/docker/bsky-avatar/`, rebuild, recreate both containers. The risk is drift — the NAS copy silently falling behind the repo, or the docs describing a setup that no longer exists. If that becomes annoying, a `deploy.sh` in this directory should: 1. Refuse to run on a dirty tree or a branch that isn't `main`, and refuse if `git fetch` shows the remote ahead — deploying an unpushed state is how the NAS ends up being the only copy of something. 2. `rsync` only tracked files (`git ls-files`) to the NAS over SSH. Never sync `config/*.yaml`, `state/`, or `.env`: those are the host's, not the repo's, and clobbering `overrides.yaml` would wipe travel periods. `--delete` is tempting and wrong for the same reason. 3. `docker build -t bsky-avatar` on the NAS. 4. Recreate **both** containers — `bsky-avatar` and `bsky-avatar-sink`. Code lives in the image, so a restart is not enough, and `--env-file` is only read at creation. 5. Verify before declaring success: `GET /healthz` on the sink, and `node src/index.js --dry-run` in the updater. Roll back by rebuilding the previous commit if either fails. 6. Print what changed (`git log ..HEAD --oneline`), and record the deployed SHA somewhere on the host so step 6 can work next time — nothing currently tracks which commit the NAS is running. Step 6 is the one worth doing first even without a script: a `state/deployed-sha` file would make "is the NAS current?" answerable at a glance instead of by diffing checksums file by file. ## Useful ```bash sudo docker logs --tail 50 bsky-avatar # see recent runs + next-run countdown sudo docker logs --tail 20 bsky-avatar-sink # see positions the phone has pushed sudo docker exec bsky-avatar node src/index.js --dry-run # render without uploading sudo docker exec bsky-avatar node src/index.js --force # force an immediate upload ``` ## Optional: Signal mirroring Mirrors the avatar to your Signal profile via a `signal-cli-rest-api` sidecar. **Linking** needs `normal` mode; **serving** runs in `json-rpc` mode (reliable for a linked account — `normal` mode cold-starts a JVM per request and is flaky). ```bash # 1. Start the sidecar in NORMAL mode for linking (8080 is often taken; use 8099) sudo docker run -d --name signal-api --restart unless-stopped \ -p 8099:8080 -v /volume1/docker/signal-api:/home/.local/share/signal-cli \ -e MODE=normal bbernhard/signal-cli-rest-api:latest # 2. Link your account. The /v1/qrcodelink request returns the QR ~8s in and # HOLDS THE CONNECTION ~30s — keep it open and scan within that window. # Be on Signal -> Settings -> Linked Devices -> Link New Device FIRST, then: ( curl -s "http://localhost:8099/v1/qrcodelink?device_name=bsky-avatar" -o /tmp/qr.png & ) ; sleep 8 # Copy /tmp/qr.png to a screen and scan it immediately (before the ~30s elapses). # 3. Confirm the linked number appears curl -s "http://localhost:8099/v1/accounts" # -> ["+316..."] # 4. Switch to JSON-RPC for serving (the linked account persists in the volume). # NOTE: AUTO_RECEIVE_SCHEDULE is INCOMPATIBLE with json-rpc — never set it # (it crash-loops the container). sudo docker rm -f signal-api sudo docker run -d --name signal-api --restart unless-stopped \ -p 8099:8080 -v /volume1/docker/signal-api:/home/.local/share/signal-cli \ -e MODE=json-rpc bbernhard/signal-cli-rest-api:latest ``` Then set `SIGNAL_API_URL` (e.g. `http://:8099`), `SIGNAL_NUMBER` (+E.164) and `SIGNAL_PROFILE_NAME` in `.env`, and recreate the `bsky-avatar` container (it's an `--env-file` change). `signalMirror` in `settings.yaml` chooses `events` (only push on bg/holiday change) or `all` (mirror everything).