Deploy your own Story Analytics instance to Amazon Web Services. This guide assumes zero cloud experience — follow every step from scratch.
- A live Story Analytics app at a public URL (e.g.
https://abc123.us-east-2.awsapprunner.com) - PostgreSQL database for metadata
- S3 storage for chart data and files
- User authentication enabled by default
Estimated cost: ~$30–50/month (mostly RDS database + NAT Gateway) Time to complete: ~30–45 minutes
| Resource | Purpose |
|---|---|
| App Runner | Runs the application (like a web server) |
| RDS PostgreSQL | Database for users, charts, metadata |
| S3 Bucket | File storage for uploaded data and chart configs |
| ECR Repository | Stores your Docker image (the packaged app) |
| VPC + Networking | Private network so the database isn't exposed to the internet |
You need four tools on your computer. Install them in order.
Docker packages the app into a container that runs on AWS.
- macOS: Download from docker.com/products/docker-desktop — open the
.dmgand drag to Applications - Windows: Download from the same link — run the
.exeinstaller, restart if prompted - Linux: Follow docs.docker.com/engine/install for your distribution
Verify it's installed:
docker --version
# Docker version 27.x.x or similarImportant: Docker Desktop must be running (not just installed) when you deploy. Open it from your Applications folder if it's not in your menu bar/system tray.
The AWS CLI lets your computer talk to your AWS account.
- macOS:
brew install awscli(if you have Homebrew) or download from aws.amazon.com/cli - Windows: Download the MSI installer from aws.amazon.com/cli
- Linux:
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && unzip awscliv2.zip && sudo ./aws/install
Verify:
aws --version
# aws-cli/2.x.x or similarPython runs the deploy script.
- macOS:
brew install python@3.11or download from python.org/downloads - Windows: Download from python.org/downloads — check "Add Python to PATH" during install
- Linux:
sudo apt install python3 python3-pip(Ubuntu/Debian) orsudo dnf install python3(Fedora)
Verify:
python3 --version
# Python 3.11.x or higherGit downloads the source code.
- macOS: Comes pre-installed. If not:
brew install git - Windows: Download from git-scm.com
- Linux:
sudo apt install git(Ubuntu/Debian)
Verify:
git --versionIf you already have an AWS account, skip to Step 3.
- Go to aws.amazon.com and click Create an AWS Account
- Enter your email and choose an account name
- Add a credit card for billing (you won't be charged until resources are created)
- Complete identity verification (phone number)
- Choose the Basic (Free) support plan
About costs: Your first deploy will start incurring charges immediately (~$1–2/day). See the Cost Management section at the bottom for a full breakdown.
Your computer needs credentials to create resources in your AWS account.
- Sign in to the AWS Console
- Click your account name (top-right corner) → Security credentials
- Scroll down to Access keys
- Click Create access key
- Select Command Line Interface (CLI) as the use case
- Check the confirmation box and click Next
- Click Create access key
- Copy both values and save them somewhere safe:
- Access key ID (looks like
AKIAIOSFODNN7EXAMPLE) - Secret access key (looks like
wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY)
- Access key ID (looks like
Security note: These keys have full access to your account. Never share them, commit them to git, or post them online. If you suspect they've been compromised, delete them immediately from the Security credentials page and create new ones.
Tell your computer's AWS CLI about your access keys.
aws configureIt will ask for four things:
AWS Access Key ID: [paste your access key]
AWS Secret Access Key: [paste your secret key]
Default region name: us-east-2
Default output format: json
Which region?
us-east-2(Ohio) is a good default.us-east-1(N. Virginia) is also popular and has the most services. You can use any region, but be consistent throughout this guide.
Verify your credentials work:
aws sts get-caller-identityYou should see your account ID and user ARN. If you get an error, double-check your access key and secret key.
Download the Story Analytics source code:
git clone https://github.com/adchamberlain/story-analytics.git
cd story-analyticsInstall the Python dependency needed by the deploy script:
pip3 install boto3If
pip3doesn't work, trypip install boto3orpython3 -m pip install boto3.
One command does everything:
python3 -m deploy.cli deploy --region us-east-2You'll see six steps:
Deploying Story Analytics to AWS...
[1/6] Checking AWS credentials...
AWS Account : 123456789012
User/Role : arn:aws:iam::123456789012:user/your-name
[2/6] Ensuring ECR repository exists...
ECR repo created: 123456789012.dkr.ecr.us-east-2.amazonaws.com/story-analytics
[3/6] Building and pushing Docker image...
Building Docker image...
Logging in to ECR...
Tagging image...
Pushing image to ECR...
Image pushed successfully.
[4/6] Deploying CloudFormation stack (this takes ~10 min for RDS)...
Creating stack 'story-analytics' in us-east-2...
Waiting................ done.
[5/6] Setting FRONTEND_BASE_URL...
[6/6] Deployment complete!
App URL : https://abc123xyz.us-east-2.awsapprunner.com
S3 Bucket : story-analytics-data-123456789012
RDS Endpoint : story-analytics-db.xxxx.us-east-2.rds.amazonaws.com
DB Password : xK9_mN2pLqRs7vW (save this!)
SSL: RDS PostgreSQL 16 requires SSL connections. The deploy script sets
sslmode=requireautomatically — no manual configuration needed.
Save the DB Password! You'll need it if you ever need to access the database directly. It's auto-generated and won't be shown again.
This takes 10–15 minutes. Most of the wait is AWS creating the database (RDS). The dots show progress — don't close your terminal.
- Copy the App URL from the deploy output
- Open it in your browser
- You may need to wait 2–3 minutes after deploy completes for App Runner to finish starting
- Register your first user account — this becomes the admin. Registration is closed after the first user; additional users must be invited.
- Start creating charts!
If you see a "Service Unavailable" error: Wait a few more minutes. App Runner needs time to start the container and pass health checks. Refresh after 2–3 minutes.
When you pull new code and want to deploy it:
git pull
python3 -m deploy.cli update --region us-east-2This rebuilds the Docker image and pushes it to ECR. App Runner automatically detects the new image and redeploys (~2–5 minutes).
Important:
updateonly pushes new code. It does not change environment variables, database passwords, or other configuration. For config changes, usedeployinstead:python3 -m deploy.cli deploy --region us-east-2 --db-password NEW_PASSWORD
Story Analytics can send email notifications for team invites. Email is optional — if not configured, invite links are shown directly in the UI for admins to copy and share manually.
To enable email delivery:
- Sign up for a free account at resend.com
- Create an API key in the Resend dashboard
- Redeploy with the key:
python3 -m deploy.cli deploy --region us-east-2 --resend-api-key re_xxxxx --from-email "Your App <you@yourdomain.com>"Or add to your .env file (in the project root) before deploying:
RESEND_API_KEY=re_xxxxx
FROM_EMAIL=Your App <you@yourdomain.com>
Important: The default sender (
onboarding@resend.dev) can only send emails to the Resend account owner's email address. Emails to other recipients will fail (the app falls back to showing a shareable invite link). For production use, verify your own domain at resend.com/domains and use a--from-emailon that domain.
See your current deployment info:
python3 -m deploy.cli status --region us-east-2Output shows the stack status, App URL, S3 bucket, and RDS endpoint.
To permanently delete all AWS resources:
python3 -m deploy.cli destroy --region us-east-2You'll be asked to type yes to confirm. This deletes:
- The App Runner service
- The RDS database (all data is lost)
- The S3 bucket
- The ECR repository and images
- All networking resources (VPC, subnets, NAT Gateway)
This is irreversible. All your charts, users, and uploaded data will be permanently deleted. Export anything you need before running destroy.
To skip the confirmation prompt (e.g. in scripts):
python3 -m deploy.cli destroy --region us-east-2 --yesRun aws configure and enter your access key and secret key. Verify with:
aws sts get-caller-identityIf you're on Apple Silicon (M1/M2/M3 Mac), the deploy script already handles this by building with --platform linux/amd64. Make sure Docker Desktop is running and up to date.
If you still see errors, try:
- Open Docker Desktop → Settings → General
- Ensure "Use Rosetta for x86_64/amd64 emulation on Apple Silicon" is checked
- Restart Docker Desktop
The deploy script prints the failure reason. Common causes:
- "Resource limit exceeded" — Your AWS account may have limits on certain resources. Contact AWS Support to request an increase.
- "The security token included in the request is invalid" — Re-run
aws configurewith fresh credentials. - Timeout — RDS creation can take up to 15 minutes. If it timed out, check the CloudFormation console for the stack status.
If a stack is stuck in ROLLBACK_COMPLETE, the deploy script automatically deletes it and recreates. Just run the deploy command again.
App Runner needs 2–5 minutes to start after the stack completes. If it stays unavailable:
- Go to the App Runner console
- Click your service (
story-analytics-service) - Check the Logs tab for error messages
- Verify the Events tab shows "Service status is running"
If update keeps failing and App Runner rolls back repeatedly:
-
Do NOT try to fix it by changing the RDS password or App Runner config directly. Direct AWS CLI changes (
aws rds modify-db-instance,aws apprunner update-service) go out of sync with CloudFormation and make things worse. -
The fix is a clean redeploy:
# 1. Destroy the current stack
python3 -m deploy.cli destroy --region us-east-2
# 2. Deploy fresh
python3 -m deploy.cli deploy --region us-east-2- If you use a custom domain (e.g. via Cloudflare), update your DNS records to point to the new App Runner URL shown in the deploy output.
Why this happens: App Runner rolls back both the Docker image AND environment variables when a deploy fails. This creates a catch-22 where config fixes also get rolled back. A clean redeploy is the only reliable escape.
ECR login tokens expire after 12 hours. The deploy script handles login automatically, but if you see this error, try running deploy again — it will re-authenticate.
Docker is running out of disk space. Clean up unused images:
docker system prune -aThen retry the deploy.
| Resource | Cost |
|---|---|
| App Runner (1 vCPU, 2 GB) | ~$7/month (auto-scales to zero when idle) |
| RDS db.t4g.micro (PostgreSQL) | ~$13/month |
| NAT Gateway | ~$4/month + data transfer |
| S3 storage | ~$0.02/GB/month (negligible for most usage) |
| ECR image storage | ~$0.10/GB/month (negligible) |
| Total | ~$25–35/month |
- Tear down when not in use: The
destroycommand removes everything. Redeploy when you need it again. - Use a smaller database: Add
--db-instance-class db.t4g.micro(this is already the default and the smallest option). - Use less compute: Add
--cpu 256 --memory 512for lighter workloads (may be slower).
- Go to AWS Billing Console
- Click Bills to see current charges
- Set up a Budget under Budgets → Create budget → set a monthly amount (e.g. $50) to get email alerts
python3 -m deploy.cli <command> [options]
Commands:
deploy Full deploy: create all AWS resources and start the app
update Rebuild and push a new Docker image (App Runner auto-deploys)
status Check deployment status and show resource info
destroy Tear down all AWS resources
Common options:
--region REGION AWS region (default: us-east-2)
--stack-name NAME CloudFormation stack name (default: story-analytics)
Deploy options:
--db-password PASSWORD Set RDS password (auto-generated if omitted)
--cpu CPU App Runner CPU units: 256, 512, 1024, 2048, 4096 (default: 1024)
--memory MEMORY App Runner memory in MB: 512–12288 (default: 2048)
--db-instance-class CLS RDS instance type (default: db.t4g.micro)
--resend-api-key KEY Resend API key for emails (reads from .env if omitted)
--from-email EMAIL Sender email address (reads from .env if omitted)
Destroy options:
--yes, -y Skip confirmation prompt