A complete record of the live deployment in the manju2bspectrum account:
every resource created, every incident hit and how it was fixed, and the exact runbook for
redeploying, rotating secrets, and restoring from backup.
One paragraph, then the receipts.
Spectrum's cockpit, orders tab, strategy tab, architecture tab, and this deployment guide are all
served over HTTPS at https://spectrummomentum.com, from one small EC2 instance in
us-east-1, behind Caddy for automatic TLS. The app, its systemd timers (weekly momentum
rebuild, twice-daily cockpit refresh, nightly backup), and its history database all run on that one
box. Nothing here places real orders; nothing here is investment advice.
| Check | Result |
|---|---|
| DNS resolves | spectrummomentum.com → 100.57.141.223 ✓ |
| TLS certificate | valid, auto-issued by Caddy (Let's Encrypt) ✓ |
GET / | 200 · server: uvicorn · via: 1.1 Caddy ✓ |
GET /orders /strategy /architecture /whitepaper | 200 on all four ✓ |
GET /api/ops, /api/momentum | 200, serving from the database ✓ |
| Verified from | curl on the EC2 instance itself — deliberately not from this laptop's browser, which sits behind corporate (Zscaler) browser isolation that intercepts new domains and would give a false reading either way |
Every real identifier, so future-you never has to hunt for it in the console.
| Resource | Value |
|---|---|
| AWS account | 814203451977 (manju2bspectrum) |
| Region | us-east-1 |
| IAM deploy user | spectrum-deploy (AdministratorAccess — see §4) |
| Local CLI profile | spectrum in ~/.aws/credentials on this Mac |
| VPC | vpc-061aa2c370c45b23e (default VPC) |
| Subnet | subnet-09dd0206dca86a9af |
| Security group | sg-0fb5b37484bcdd1b0 (spectrum-web-sg: 80, 443 only) |
| AMI | ami-05a3e9423ae4d7a19 (Ubuntu 22.04 LTS, amd64, via Canonical's public SSM parameter) |
| EC2 instance | i-0bc7afe752c9ef8b5 (t3.small, 15 GB gp3) |
| Elastic IP | 100.57.141.223 (alloc eipalloc-09b3d6c6edd443036) |
| IAM instance role | spectrum-ec2-role + instance profile spectrum-ec2-profile |
| S3 bucket | spectrum-deploy-814203451977 (private, versioned) |
| SSM parameters | /spectrum/POLYGON_API_KEY, /spectrum/ADANOS_API_KEY (SecureString) |
| Domain | spectrummomentum.com, registered via Route 53, auto-renew ON |
| Hosted zone | Z05618432FZYFBPA2TK4Y |
| App directory on instance | /opt/spectrum, owned by system user spectrum |
/opt/spectrum/.env, mode 600, owned by the unprivileged spectrum user.s3://spectrum-deploy-814203451977/releases/*, read/write under backups/*,
read the two /spectrum/* parameters, and use SSM Session Manager. It cannot list
the bucket, touch other AWS services, or read other parameters.spectrum), not root, with
Restart=on-failure so a crash doesn't take the site down for good.spectrum-deploy IAM user (used
from this laptop to provision everything) has AdministratorAccess — appropriate for a
single-person account during initial build-out, but broader than it needs to be long-term. Tighten
it to a scoped policy (EC2, S3, SSM, Route 53, IAM-role-passing only) once the resource list stops
changing, and never reuse this key for anything outside this project.| Item | Rate | Monthly |
|---|---|---|
| EC2 t3.small (on-demand, us-east-1) | $0.0208/hr | ≈ $15.20 |
| EBS gp3, 15 GB | $0.08/GB-mo | ≈ $1.20 |
| Elastic IP (attached to a running instance) | free while attached | $0.00 |
| Route 53 hosted zone | $0.50/mo | $0.50 |
| Route 53 DNS queries | ~free at this volume | ≈ $0.00 |
| S3 (code + backups, <1 GB) | $0.023/GB-mo | ≈ $0.05 |
| SSM Parameter Store (Standard tier) | free | $0.00 |
| Data transfer out (light personal use) | first 100GB/mo free | ≈ $0.00 |
| Recurring monthly total | ≈ $17/mo | |
| Domain registration (spectrummomentum.com) | $16/yr, auto-renew | ≈ $1.33/mo amortized |
| All-in | ≈ $18/mo |
Biggest lever if this needs to be cheaper: stop the instance when not in use (EBS still bills, EC2 compute doesn't — but the site goes down while stopped, so this only suits a dev/demo pattern, not a daily-use cockpit), or move to a t3.micro / t4g.small; the app's own footprint is tiny (a few hundred MB RAM), t3.small has ample headroom to spare.
Recorded honestly — this is exactly what to check first if a future redeploy breaks the same way.
set -x. The
first bootstrap script used set -euxo pipefail for the whole file, which traces every
command including POLYGON_KEY=$(...) — this echoed both API keys into
/var/log/spectrum-finish.log on the instance and into AWS's own SSM command-history API
response (visible to anyone with access to this AWS account, not the public internet).shred -u) immediately. Any
future bootstrap script must wrap secret-handling lines in set +x ... set -x, or better,
pipe secret retrieval through a subshell that never gets traced. Action item for you: rotate
the Polygon and Adanos API keys when convenient (read-only market-data keys, not brokerage
credentials — low severity, but rotate on general hygiene). Tell me when you have new ones and I'll
update the two SSM parameters in one command.aws: command not found mid-bootstrap. The
stock Ubuntu 22.04 AMI does not ship the AWS CLI, so the very first aws s3 cp in the
user-data script failed, and set -e killed everything after it — the app was never
actually installed on the first attempt.spectrum.service failed with exit code 2
(INVALIDARGUMENT). The systemd unit ran
spectrum --host 127.0.0.1 --port 8060 serve — but in this CLI, --port is a
top-level flag and --host belongs to the serve subcommand, so
--host appearing before the subcommand token was rejected by argparse.spectrum --port 8060 serve --host 127.0.0.1.
Worth fixing in the CLI itself eventually (move --port under serve only) so
this class of mistake becomes impossible./var/snap/caddy/common/Caddyfile on its own — it must be pushed via
caddy reload against the local admin API every time the service starts. This meant the
first restart silently reverted to serving nothing.caddy-config.service — a companion systemd unit
with After=/Requires=/PartOf=snap.caddy.server.service that
runs caddy reload --config ... --adapter caddyfile (with a short retry loop) every time
the Caddy service starts or is restarted. Verified by force-restarting Caddy and confirming
the config re-loaded automatically with no manual step.https://spectrummomentum.com from the Claude Code browser on this Mac showed a
Zscaler "Browser Isolation" interstitial and an isolation-client HTML shell instead of the real
page — corporate policy intercepting a brand-new, not-yet-categorized domain.curl from the EC2 instance
itself via SSM (its outbound traffic doesn't transit this laptop's corporate proxy), which
confirmed a genuine 200, valid TLS, server: uvicorn, via: 1.1
Caddy, and the correct page title and JSON payload. Note for you: if you check the site
from a work device/network and see an "isolation" page, that's Zscaler, not a deployment problem —
try from your phone on cellular data or a personal connection instead.No SSH key exists for this instance on purpose. Get a shell with:
export PATH="$HOME/Library/Python/3.9/bin:$PATH" # this session's aws CLI location
aws ssm start-session --target i-0bc7afe752c9ef8b5 --profile spectrum --region us-east-1
For a single command without a full session (what this deployment used throughout):
aws ssm send-command --instance-ids i-0bc7afe752c9ef8b5 \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["systemctl status spectrum.service --no-pager"]' \
--profile spectrum --region us-east-1 --query 'Command.CommandId' --output text
# then, with the returned id:
aws ssm get-command-invocation --command-id <ID> --instance-id i-0bc7afe752c9ef8b5 \
--profile spectrum --region us-east-1 --query '[Status,StandardOutputContent,StandardErrorContent]' --output text
systemctl status spectrum.service --no-pager # is the app up?
journalctl -u spectrum.service -n 50 --no-pager # app logs
systemctl status snap.caddy.server.service # is Caddy up?
journalctl -u caddy-config.service -n 20 --no-pager # did the Caddyfile load?
systemctl list-timers --no-pager | grep spectrum # when do the jobs next run?
Whenever the Spectrum codebase changes on this Mac, ship it with:
# 1. package (from the repo root, excludes .venv and the local bar cache)
tar czf /tmp/spectrum-app.tar.gz --exclude='.venv' --exclude='data/bars' \
--exclude='*.pyc' --exclude='__pycache__' --exclude='.env' spectrum pyproject.toml
# 2. upload
aws s3 cp /tmp/spectrum-app.tar.gz s3://spectrum-deploy-814203451977/releases/spectrum-app.tar.gz \
--profile spectrum --region us-east-1
# 3. pull + reinstall on the instance
aws ssm send-command --instance-ids i-0bc7afe752c9ef8b5 --document-name "AWS-RunShellScript" \
--parameters 'commands=[
"aws s3 cp s3://spectrum-deploy-814203451977/releases/spectrum-app.tar.gz /tmp/a.tar.gz --region us-east-1",
"tar xzf /tmp/a.tar.gz -C /opt/spectrum",
"chown -R spectrum:spectrum /opt/spectrum",
"sudo -u spectrum /opt/spectrum/.venv/bin/pip install --quiet -e /opt/spectrum",
"systemctl restart spectrum.service"
]' --profile spectrum --region us-east-1
If a web page (HTML/CSS/JS) changed but no Python did, the same steps still apply — the tarball
includes spectrum/web/. FastAPI serves those files fresh on every request, so once the
tarball lands there's no extra caching step to bust.
Get a new key from Polygon.io or Adanos, then, without ever pasting it into a command that gets logged with tracing on:
aws ssm put-parameter --name /spectrum/POLYGON_API_KEY --value "NEW_KEY_HERE" \
--type SecureString --overwrite --profile spectrum --region us-east-1
aws ssm send-command --instance-ids i-0bc7afe752c9ef8b5 --document-name "AWS-RunShellScript" \
--parameters 'commands=[
"P=$(aws ssm get-parameter --name /spectrum/POLYGON_API_KEY --with-decryption --region us-east-1 --query Parameter.Value --output text)",
"A=$(aws ssm get-parameter --name /spectrum/ADANOS_API_KEY --with-decryption --region us-east-1 --query Parameter.Value --output text)",
"printf \"POLYGON_API_KEY=%s\\nADANOS_API_KEY=%s\\n\" \"$P\" \"$A\" > /opt/spectrum/.env",
"chown spectrum:spectrum /opt/spectrum/.env && chmod 600 /opt/spectrum/.env",
"systemctl restart spectrum.service"
]' --profile spectrum --region us-east-1
Note this command block intentionally avoids set -x and never echoes the variables —
the lesson from Incident 1.
spectrum-backup.timer runs nightly at 05:00 UTC and copies
spectrum.db, momentum.json and ops.json to
s3://spectrum-deploy-814203451977/backups/. The bucket has versioning enabled, so
every night's backup is a separate recoverable version even though the object key never changes.
aws s3 cp s3://spectrum-deploy-814203451977/backups/spectrum.db /opt/spectrum/data/spectrum.db
aws s3 cp s3://spectrum-deploy-814203451977/backups/momentum.json /opt/spectrum/data/momentum.json
aws s3 cp s3://spectrum-deploy-814203451977/backups/ops.json /opt/spectrum/data/ops.json
chown spectrum:spectrum /opt/spectrum/data/*
systemctl restart spectrum.service
aws s3api list-object-versions --bucket spectrum-deploy-814203451977 --prefix backups/spectrum.db \
--profile spectrum --region us-east-1
# then restore a specific VersionId with:
aws s3api get-object --bucket spectrum-deploy-814203451977 --key backups/spectrum.db \
--version-id <VERSION_ID> /tmp/spectrum-old.db --profile spectrum --region us-east-1
The 10-year price-bar cache (data/bars/*.parquet) is not backed up — it's
disposable and rebuilds automatically from Polygon on the next spectrum momentum run if
ever lost, at the cost of one API pass over ~133 symbols.
| Value | |
|---|---|
| Registrar | Route 53 Domains, same AWS account |
| Auto-renew | ON — renews automatically each year at the then-current Route 53 price (~$16 today) |
| WHOIS privacy | ON for registrant, admin and tech contacts |
| Hosted zone | Z05618432FZYFBPA2TK4Y |
| A records | spectrummomentum.com and www.spectrummomentum.com → 100.57.141.223, TTL 300s |
| TLS | issued and auto-renewed by Caddy (Let's Encrypt), no manual cert management ever needed |
(It won't unless the EIP is explicitly released — but if it ever does:)
aws route53 change-resource-record-sets --hosted-zone-id Z05618432FZYFBPA2TK4Y \
--change-batch '{"Changes":[
{"Action":"UPSERT","ResourceRecordSet":{"Name":"spectrummomentum.com","Type":"A","TTL":300,
"ResourceRecords":[{"Value":"NEW.IP.HERE"}]}},
{"Action":"UPSERT","ResourceRecordSet":{"Name":"www.spectrummomentum.com","Type":"A","TTL":300,
"ResourceRecords":[{"Value":"NEW.IP.HERE"}]}}]}' \
--profile spectrum --region us-east-1
spectrum-deploy has
AdministratorAccess (§4). Fine for now, worth scoping down later.Restart=on-failure's reach (e.g. the instance itself stops), nothing pages you.
A cheap next step: a CloudWatch alarm on instance status checks, or an uptime-checker hitting
/api/momentum every few minutes.