# Launch Alert Sends rocket launch reminders to your phones and computers through [ntfy](https://ntfy.sh), **30 minutes before launch**, with a livestream link. Uses [The Space Devs Launch Library 2](https://thespacedevs.com/llapi) as the schedule and stream source. Python 3.11+; no Python package dependencies. The worker needs an always-on home server, NAS, or VPS. This repository supplies Unraid, Docker Compose, and systemd deployment options; creating the files alone does not deploy an always-on service. ## Set up your devices 1. Generate a random topic and local configuration from the repository directory: ```sh python3 -m launch_alert init ``` This creates `.env` with a random `NTFY_TOPIC` and shows the subscription URL. Existing configuration is preserved. Review `.env` before continuing. 2. Install the [ntfy phone app](https://docs.ntfy.sh/subscribe/phone/), or open the subscription URL on each computer. Subscribe every device to the **same server and topic**. No ntfy account is required with the default server. 3. Enable OS notification permissions and sound as desired. On Android, enable **instant delivery** and allow background operation; the F-Droid build always uses instant delivery. On a computer, enable **background notifications** in ntfy and keep the browser running if required. Reopen the ntfy web app at least weekly; its background subscriptions otherwise pause. 4. Send a test and check it arrives on **every device while locked or idle**: ```sh python3 -m launch_alert test-notification ``` An unreserved topic on public ntfy is accessible to anyone who knows its name: keep your random topic and URL private. A random name is a shared secret, not an access policy. For authenticated access, use a server with topic permissions, set `NTFY_SERVER` and `NTFY_TOKEN`, and configure matching subscriber access on your devices. The native iOS app uses Apple's push delivery through ntfy.sh. If you switch to a self-hosted ntfy server, configure that server's [`upstream-base-url`](https://docs.ntfy.sh/config/#ios-instant-notifications) for instant iOS notifications; background polling alone can take hours. Test with Focus/Do Not Disturb and battery settings representative of normal use. ## Run with Docker Compose On an always-on host with Docker Engine and Compose installed, copy or clone this repository, then generate `.env` as above. If the host has no Python, create it with the image: ```sh docker build -t launch-alert:local . docker run --rm --user "$(id -u):$(id -g)" \ --mount "type=bind,src=$PWD,dst=/setup" --workdir /setup \ launch-alert:local python -m launch_alert init ``` Alternatively, copy `.env.example` to `.env` and set `NTFY_TOPIC` to a long random name generated by a password manager. Do not leave the example topic empty. After subscribing your devices: ```sh docker compose up -d --build docker compose exec launch-alert python -m launch_alert test-notification docker compose logs --tail=100 -f ``` The service restarts after a crash or Docker daemon restart. SQLite state lives in the `launch-alert-data` named volume, so container updates preserve delivered reminders and pending work. Keep Docker running, disable host sleep, and enable the Docker daemon at boot. Run only one instance against a given database. `docker compose down` preserves the volume; adding `--volumes` deletes reminder history. Check operation with: ```sh docker compose ps docker compose exec launch-alert python -m launch_alert status curl --fail http://127.0.0.1:8080/healthz ``` The health endpoint is exposed only on the host's loopback interface. It returns an error until the first successful schedule fetch and whenever data becomes stale. Docker marks an unhealthy container but does not restart it solely because a health check failed. To detect outages while away, connect an independent monitor to this health check and use a separate alert channel; a failed host cannot report its own failure. Update the checkout, then run `docker compose up -d --build`. Keep a copy of `.env` and back up the Docker volume while the worker is stopped. ## Run on Unraid An Unraid Docker template, appdata mapping, and Community Applications submission instructions are available in the [Unraid setup guide](unraid/README.md). Configure the template in Unraid's Docker manager, subscribe your devices to its ntfy topic, and send a test notification. The container runs without a web interface or published ports. ## Configuration The worker loads `.env` from its working directory; `ENV_FILE` selects another file. Process environment variables take precedence. Values are plain strings; shell expressions and variable expansion are not evaluated. The systemd unit uses `ENV_FILE=/dev/null` because systemd itself loads `/etc/launch-alert.env` before changing to the service account. | Variable | Default | Purpose | | --- | --- | --- | | `NTFY_SERVER` | `https://ntfy.sh` | Notification server. | | `NTFY_TOPIC` | Required | Same private random topic on all devices. | | `NTFY_TOKEN` | Empty | Optional bearer token for a server with authentication. | | `LEAD_MINUTES` | `30` | Reminder lead time before the published launch time. | | `POLL_SECONDS` | `300` | Source refresh interval. About 12 requests/hour for one response page; the next-fetch budget survives restarts. | | `DISPLAY_TIMEZONE` | `TZ`, then `America/Chicago` | Time zone used in messages. An explicit value overrides the host's `TZ`. Scheduling uses UTC. | | `DB_PATH` | `data/launch-alert.sqlite3` | SQLite state path; Compose overrides it to `/app/data/launch-alert.sqlite3`. | | `LL2_BASE_URL` | `https://ll.thespacedevs.com/2.3.0` | Launch Library API base URL. | | `LL2_TOKEN` | Empty | Optional Launch Library API token. | | `HEALTH_HOST` | `127.0.0.1` | HTTP health/status bind address; Compose uses `0.0.0.0` inside the container. | | `HEALTH_PORT` | `8080` | HTTP health/status port; Compose fixes the internal port to 8080. | | `MAX_DATA_AGE_SECONDS` | `900` | Maximum age of source data considered fresh. | | `ENV_FILE` | `.env` | Optional path to a configuration file. The systemd unit sets `/dev/null`. | Protect `.env` with permissions appropriate to the host. It is ignored by Git and excluded from Docker builds. Compose loads it at container creation; recreate the container after changing it. Allow outbound HTTPS to your ntfy server and `ll.thespacedevs.com`. Keep host clock synchronization enabled. Set `POLL_SECONDS` shorter than `MAX_DATA_AGE_SECONDS` so normal refresh intervals do not make the source stale. The [official Launch Library FAQ](https://github.com/TheSpaceDevs/Tutorials/blob/main/faqs/faq_LL2.md#is-ll2-free-to-use) allows 15 unauthenticated API calls/hour per public IP; an API key provides a separate account budget. Other users behind the same public IP share the unauthenticated limit. Each response page counts as a call. The worker requests up to 100 records/page and scales the next refresh interval by the pages consumed, honoring rate-limit retry instructions. Check the provider's current limits before reducing polling intervals. The [official generated LL2 client models](https://github.com/TheSpaceDevs/launch-library-mcp/blob/main/launch_library_mcp/launch_library_client/ll2/models.py) document the detailed launch schema, including `vid_urls` and `net_precision` used here. ## Reliability and scope Only launches with a usable HTTPS livestream URL, a source status of Go or TBC, and a time specified to the minute or second qualify. Holds and approximate dates do not trigger reminders. The source may list a stream before it is live, omit a provider's stream, or change a launch time at short notice. Published times are estimates. The service aims to publish at the configured lead time using its latest schedule; source refresh, holds, Internet outages, and device push delivery affect actual receipt. Starting the worker less than 30 minutes before launch can produce a later reminder. Receiving a successful ntfy response confirms server acceptance, not delivery to a device. SQLite preserves scheduling and delivery state across restarts. Schedule refreshes adjust pending reminders and can send an update when a previously announced launch moves or becomes unconfirmed. Notifications are retried after temporary publishing failures while the launch is still upcoming; stale source data suppresses delivery. Retries use at-least-once semantics at the ntfy-acceptance boundary, bounded by source freshness and launch time. This does not guarantee that ntfy or a device will be reachable before launch. Persistent state reduces duplicate reminders, but a crash or ambiguous network timeout after server acceptance can produce a duplicate. Keep the state volume and use the locked-device test after changes to devices, credentials, servers, or notification settings. ## Run with systemd instead On a Linux host with Python 3.11+ and system time-zone data installed, place the repository at `/opt/launch-alert`. Create a dedicated account, configuration, and service: ```sh sudo useradd --system --home-dir /var/lib/launch-alert --shell /usr/sbin/nologin launch-alert sudo install -m 600 .env /etc/launch-alert.env sudo install -m 644 deploy/launch-alert.service /etc/systemd/system/launch-alert.service sudo systemctl daemon-reload sudo systemctl enable --now launch-alert sudo journalctl -u launch-alert -f ``` Make the code under `/opt/launch-alert` readable by the service account. In `/etc/launch-alert.env`, set `DB_PATH=/var/lib/launch-alert/launch-alert.sqlite3` before starting; systemd creates that writable state directory. The root-owned configuration remains mode 600: systemd reads it and supplies the values to the worker. `ENV_FILE=/dev/null` prevents the worker from also opening a private `.env` in the checkout. The account must exist before starting the service. Restart with `sudo systemctl restart launch-alert` after configuration or code changes. Do not run Compose and systemd simultaneously against the same subscription/database. ## Local checks ```sh python3 -m unittest discover -s tests -v python3 tools/integration_smoke.py python3 -m launch_alert once python3 -m launch_alert status ``` The integration smoke check requires Python, OpenSSL, and local loopback sockets. It exercises HTTPS source/publisher fixtures, restarts, schedule corrections, health checks, and shutdown without publishing to real devices. `once` performs a source refresh and processes due reminders, so it can send real notifications. Use the automated tests for offline checks.