Skip to content

Latest commit

 

History

History
461 lines (349 loc) · 16.2 KB

File metadata and controls

461 lines (349 loc) · 16.2 KB

Deploying sat.trackr.live

Two production paths are documented end-to-end here. Both assume you've cloned the repo and composer install --no-dev + npm install + npm run build complete cleanly on the target machine.

  1. DreamHost VPS (Apache + cron) — the original prod target.
  2. Fly.io (single machine + scheduled cron) — Phase 10 alternative.

Pick one. The same .env.prod file works on both; the runtime shape (Apache PHP module vs Fly machine PHP-FPM) is what differs.

Verification status (Phase 11 chunk 6, 2026-05-23): docs match what make ci, make migrate, and make ingest-* do on the respective targets (verified in-repo: full make ci runs green at 411 PHPUnit / 361 Vitest / 146 Playwright). The operator-driven "fresh DreamHost VPS / Fly machine walkthrough" still requires a clean cloud instance and was deferred at the Phase 11 close — see the "Verified by" section at the bottom of this doc.


Shared prerequisites

Tool Min version Why
PHP 8.4 The codebase uses readonly + property hooks heavily
Node 20+ Vite + bin/sgp4-passes.mjs SGP4 propagation
sqlite3 3.40+ Schema uses RETURNING clauses + JSON1 functions
make any Wrapper for every operator-facing command
git any For cloning + bin/fetch-marquee-models.sh

PHP extensions (verify with php -m):

  • pdo_sqlite — primary persistence
  • curl — Guzzle's HTTP backend
  • gd — OG image generation (Phase 5 chunk 4)
  • apcu — recommended (Phase 10 chunk 1's state-now cache falls back to a SQLite table if missing)
  • mbstring, json — typically built-in

DreamHost VPS (Apache + cron)

DreamHost's VPS tier ships Ubuntu LTS with Apache, PHP-FPM, and cron pre-installed. The deploy story is "clone the repo, point the docroot at public/, drop cron entries, set env vars."

Step 1 — Prepare the VPS

# SSH to the VPS as a non-root user (DreamHost convention: dh_*).
ssh user@example.com

# Optional: confirm PHP 8.4 is available. DreamHost lets you pin
# the PHP version per domain in the control panel.
php -v
node --version
sqlite3 --version

Step 2 — Clone + install

cd ~/example.com   # or wherever the domain's docroot lives
git clone https://github.com/CyberSecDef/sat.trackr.live.git app
cd app

# Composer install — no dev deps in prod.
composer install --no-dev --optimize-autoloader

# Vite build — outputs to public/build/.
npm ci
npm run build

# Optional: fetch marquee 3D models (~45 MB ISS glTF).
make fetch-models

Step 3 — Configure environment

cp .env.example .env
cp .env.prod.example .env.prod

# Edit .env.prod and fill in:
#   APP_ENV=prod
#   APP_URL=https://example.com
#   DB_PATH=/home/user/example.com/data/sat.db
#   CESIUM_ION_TOKEN=...        (optional; OSM fallback works without)
#   SPACE_TRACK_USER=...        (chunk 4 reentries)
#   SPACE_TRACK_PASS=...
#   N2YO_API_KEY=...            (chunk 6 magnitudes)
#   LL2_API_TOKEN=...           (chunk 3 launches; free tier OK)
#   INGEST_ALERT_WEBHOOK_URL=... (Phase 10 chunk 2; optional Slack URL)
#   WEBHOOK_SECRET=...          (Phase 10 chunk 4; only if using
#                                 push-based ingest — see below)
#   PLAUSIBLE_DOMAIN=example.com (Phase 8 chunk 6; optional)

# Create the data directory the .env.prod points at, then migrate.
mkdir -p data storage/logs storage/cache
make migrate

# Seed the catalog. First run takes ~40 s for ~15K satellites.
make ingest

Step 4 — Point Apache at public/

DreamHost's web UI lets you set the "Web directory" per domain — set it to public/ under your app dir. Behind the scenes that edits the vhost config; the resulting Apache directive is:

<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /home/user/example.com/app/public

    <Directory /home/user/example.com/app/public>
        AllowOverride All
        Require all granted
        # Front controller: route everything not-a-file through PHP.
        FallbackResource /index.php
    </Directory>

    # Static caching for hashed Vite assets.
    <FilesMatch "\.(js|css|woff2|png|webmanifest)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>

    # SSL via Let's Encrypt — DreamHost auto-provisions if you
    # check the box in the web UI.
    SSLEngine on
</VirtualHost>

Step 5 — Cron entries (pull-based ingest)

Run crontab -e and paste:

# Daily 02:00 UTC — CelesTrak GP catalog refresh (~15K satellites).
0  2  * * *  cd /home/user/example.com/app && make ingest

# Daily 02:30 UTC — CelesTrak SATCAT enrichment (operator / country /
# launch_date / RCS / status / decayed_at). Runs after `make ingest`
# so the catalog has every NORAD before SATCAT tries to enrich it.
# Without this row prod satellites are missing the metadata that
# powers /stats, the detail panel's "operator" line, and the chunk-4
# bright-pass discovery's RCS filter.
30 2  * * *  cd /home/user/example.com/app && make ingest-satcat

# Every 6 h — Space-Track TIP messages (predicted decays).
17 */6 * * *  cd /home/user/example.com/app && make ingest-spacetrack

# Twice daily — Launch Library 2 launches (~150 rows).
0  6,18  * * *  cd /home/user/example.com/app && make ingest-ll2

# Every 6 h — SOCRATES conjunctions (~150K-row firehose).
0  */6  * * *  cd /home/user/example.com/app && make ingest-socrates

# Every 5 min — NOAA SWPC space weather snapshot (Kp + X-ray + R/S/G).
*/5  *  * * *  cd /home/user/example.com/app && make ingest-swpc

# Every 15 min — NOAA OVATION aurora forecast raster.
*/15 *  * * *  cd /home/user/example.com/app && make ingest-ovation

# Weekly Sunday 03:00 UTC — SatNOGS amateur-radio transmitters.
0  3  * * 0  cd /home/user/example.com/app && make ingest-satnogs

# Weekly Sunday 03:30 UTC — SatNOGS amateur ground stations (Phase 10).
30 3  * * 0  cd /home/user/example.com/app && make ingest-satnogs-stations

# Daily 04:00 UTC — sitemap regeneration (Phase 5 chunk 5).
0  4  * * *  cd /home/user/example.com/app && make sitemap-build

# Daily 04:30 UTC — pass-cache prune (Phase 2 chunk 6).
30 4  * * *  cd /home/user/example.com/app && make pass-cache-prune

MAILTO=ops@example.com

The MAILTO= line at the bottom catches non-zero exits and mails the operator. Phase 10 chunk 2's Slack webhook (INGEST_ALERT_WEBHOOK_URL) adds reject-rate alerting on top of the cron-failure email.

Two parametric helpers exist for backfill / re-ingest of a single group without re-pulling the whole catalog — useful when CelesTrak publishes a fix and you want to refresh just that slice:

  • make ingest-group GROUP=starlink — single CelesTrak group
  • make ingest-satcat-group GROUP=starlink — single SATCAT group

These are operator tools, not scheduled cron jobs; the ingest + ingest-satcat cron rows above already cover all configured groups.

Step 6 — Verify

# Curl the SPA shell — expect HTML with <sat-app>.
curl -s https://example.com/ | grep '<sat-app'

# Curl the JSON API — expect a satellite count.
curl -s https://example.com/api/v1/satellites | head -c 200

# Tail the ingest log to confirm a recent run.
tail -n 20 /home/user/example.com/app/storage/logs/app.log

Fly.io (single machine + scheduled cron)

Fly.io is the documented alternative target per docs/phase10.md § II row 7. Free tier covers ~3 shared-cpu-1x machines + persistent volumes, enough for the project's single-app + cron-job pattern.

Step 1 — Install flyctl

curl -L https://fly.io/install.sh | sh
flyctl auth login

Step 2 — fly.toml

Create fly.toml at the repo root:

app = "sat-trackr-live"
primary_region = "ord"   # Chicago — pick whichever is closest to you

[build]
  dockerfile = "Dockerfile"

[env]
  APP_ENV = "prod"
  APP_URL = "https://sat-trackr-live.fly.dev"
  DB_PATH = "/data/sat.db"

# Persistent volume for SQLite + the storage/ tree.
[[mounts]]
  source = "sat_trackr_data"
  destination = "/data"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true
  min_machines_running = 1

[[vm]]
  size = "shared-cpu-1x"
  memory = "512mb"

[deploy]
  release_command = "php bin/console migrate"

Step 3 — Dockerfile

FROM php:8.4-fpm-alpine

# Native extensions + binaries we depend on.
RUN apk add --no-cache nginx supervisor sqlite curl bash git nodejs npm \
 && docker-php-ext-install pdo pdo_sqlite \
 && pecl install apcu && docker-php-ext-enable apcu \
 && rm -rf /var/cache/apk/*

WORKDIR /app
COPY . /app

# PHP deps + Vite build.
RUN composer install --no-dev --optimize-autoloader \
 && npm ci && npm run build && npm prune --production

# Nginx config served from public/.
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/supervisord.conf /etc/supervisord.conf

EXPOSE 8080
CMD ["supervisord", "-c", "/etc/supervisord.conf"]

Step 4 — .fly.dockerignore

.git
node_modules
data
storage/cache
storage/logs
public/build
public/cesium
public/models
tests
.env
.env.*
!.env.example
!.env.*.example

Step 5 — Cron strategy

Fly.io doesn't ship cron — pick one of two patterns per docs/phase10.md § IV "Fly.io ingester cron strategy":

Option A: crond inside the same machine.

Add crond to the Dockerfile + ship a crontab as a file the image copies in. Supervisord runs nginx, php-fpm, AND crond from one container. Simple; doesn't scale to multi-region.

# In the Dockerfile, alongside the EXPOSE line:
COPY docker/crontab /etc/crontabs/root
# In supervisord.conf, add a [program:crond] block:
#   command=crond -f -l 2

Option B: scheduled-app pattern.

Spin up a second Fly machine that wakes up on a schedule, runs the single ingester, and shuts down. Use fly machine update with --schedule="daily" or kick from a GitHub Action.

# Create a tiny worker machine that runs the daily catalog refresh.
flyctl machine create --name sat-trackr-ingest-celestrak \
  --schedule daily \
  --command "make ingest"

The webhook receiver from Phase 10 chunk 4 (next section) is a third option that complements either of these.

Step 6 — Push-based ingest (opt-in)

Phase 10 chunk 4 adds POST /webhooks/ingest/{source} so an external relay (e.g. a GitHub Action that watches the CelesTrak GP feed for mtime changes) can kick a refresh on-demand:

# Set the secret in Fly + locally.
flyctl secrets set WEBHOOK_SECRET=$(openssl rand -hex 32)

# Trigger the celestrak ingester remotely.
curl -X POST \
  -H "Authorization: Bearer ${WEBHOOK_SECRET}" \
  https://sat-trackr-live.fly.dev/webhooks/ingest/celestrak
# → 202 Accepted, dispatch happens async in the background.

Source allowlist (anything outside returns 404):

  • celestrak · spacetrack · ll2 · socrates
  • swpc · ovation · satnogs · satnogs-stations

Rate limit: 1 request per (source, IP) per 60 s. Receivers behind a CDN/proxy where every request appears from the same IP should set the rate limit higher upstream (Cloudflare WAF rules, etc.).

Auth: bearer token in the Authorization header. Rotate WEBHOOK_SECRET immediately if compromised — there's no revocation list since the endpoint trusts the env var.

Off by default: with WEBHOOK_SECRET unset, every request returns 403. The pure-cron pull flow keeps working unchanged.

Step 7 — Deploy + verify

flyctl launch --no-deploy --copy-config  # First-time only — generates fly.toml metadata.
flyctl secrets set CESIUM_ION_TOKEN=... SPACE_TRACK_USER=... # ...etc
flyctl deploy
flyctl status

# Smoke-test from your laptop.
curl https://sat-trackr-live.fly.dev/api/v1/satellites | head -c 100

Common operational notes

Where things live

Path What
data/sat.db The SQLite database (Phase 1 migrations apply)
storage/logs/app.log Monolog stream — every ingester writes here
storage/logs/webhook-ingest.log Webhook-dispatched ingesters' stdout/stderr
storage/cache/ingest-alerts.json Phase 10 chunk 2 cooldown state
storage/cache/webhook-cooldown.json Phase 10 chunk 4 rate-limit state
storage/cache/og/ Phase 5 chunk 4 OG-image cache (6 h TTL)
storage/cache/sw-passes/ Phase 8 chunk 5 service-worker schedule mirror
public/build/ Vite-built SPA bundle
public/textures/aurora-latest.png Phase 4 chunk 4 raster (15-min refresh cron)

Rotating secrets

Every secret in .env.prod is loaded once at process boot, so rotation is a 3-step dance:

# 1. Update the env var in production (.env.prod for DreamHost,
#    `flyctl secrets set` for Fly.io).
# 2. Restart PHP-FPM (DreamHost: `pkill -USR2 php-fpm` or the web
#    UI's PHP reset; Fly.io: `flyctl machine restart`).
# 3. Confirm with `php -r 'echo getenv("WEBHOOK_SECRET");'` SSH'd in.

WEBHOOK_SECRET is the most operationally-sensitive — a leaked secret lets anyone trigger ingesters on your infra (DoS surface). Phase 10 chunk 4's per-(source, IP) rate limit narrows but doesn't close the surface; rotate fast.

Troubleshooting

Symptom Likely cause / fix
/ renders blank, console shows "Cesium error" Missing CESIUM_ION_TOKEN — OSM fallback should kick in; check imagery provider in Globe.ts.
make ingest 403s Cloudflare blocking the User-Agent — rotate it in CelesTrakClient.
make ingest-satcat 404s on a SATCAT group CelesTrak retired the group slug — check https://celestrak.org/satcat/sources.php for the current list.
make ingest-spacetrack 401s Space-Track cookie session expired; verify SPACE_TRACK_USER/PASS in .env.prod.
/api/v1/satellites/{n}/state-now returns 500 Node missing or bin/sgp4-passes.mjs not executable. Verify node --version + chmod.
/webhooks/ingest/X returns 403 with auth set Source slug not in the allowlist — check spelling against the 8 documented slugs.
/webhooks/ingest/X returns 429 Cooldown active — wait 60 s OR check storage/cache/webhook-cooldown.json for stuck entries (manually delete to reset).

Verified by

In-repo verification (Phase 11 chunk 6, 2026-05-23, Claude Opus 4.7 with Robert Weber):

  • make install (composer + npm) — clean on the existing repo
  • make build (vite production build) — 242 KB / 67.56 KB gzipped main, clean
  • make migrate — 16 migrations applied, 0 pending
  • make ingest — populates ~15.7K satellites
  • make ci — full quality gate: lint clean, PHPStan clean (with the chunk-6 baseline frozen), TypeScript typecheck clean, PHPUnit 411/411, Vitest 361/361, security-audit advisory clean
  • make test-e2e — 146 Playwright specs passing + 3 skipped (10× verify sweep at chunk-4 close hit 0 fails)
  • Manual curl smoke against make serve confirms / returns 200, /api/v1/satellites?limit=1 returns a JSON envelope, /text returns 200, /events.atom returns 200.

Fresh-environment walkthrough (deferred): a literal "clone the repo on a brand-new DreamHost VPS / Fly machine + follow this doc top-to-bottom + report what didn't work" run is still pending. Per docs/phase11.md § IV, the chunk-6 mitigation is the in-repo verification above; the fresh-environment run is a future operator-driven exercise. A teammate (or yourself, on a fresh instance) is invited to add a row to this section when they do the walkthrough.