HumeraR commited on
Commit
d6f27e0
·
1 Parent(s): bdc4e48

Add deployment configuration files for DigitalOcean Droplet setup

Browse files
deploy/.gitattributes ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ *.sh text eol=lf
2
+ Caddyfile text eol=lf
3
+ *.yml text eol=lf
4
+ env.prod.example text eol=lf
deploy/Caddyfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ email {$ACME_EMAIL}
3
+ }
4
+
5
+ # HTTPS certificates are issued and renewed automatically for API_DOMAIN (set in .env.prod).
6
+ {$API_DOMAIN} {
7
+ encode zstd gzip
8
+
9
+ reverse_proxy api:8000 {
10
+ # Stream job progress and chat (server sent events) instead of buffering them.
11
+ flush_interval -1
12
+ transport http {
13
+ read_timeout 0
14
+ }
15
+ }
16
+
17
+ # Uploads (voice samples, files) can be large.
18
+ request_body {
19
+ max_size 220MB
20
+ }
21
+ }
deploy/README.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deploying the backend to a DigitalOcean Droplet
2
+
3
+ One Droplet runs the API, the job worker and, optionally, the video renderer. HTTPS comes from the reverse
4
+ proxy already on the Droplet (Nginx, see nginx-notestack.conf), or from the bundled Caddy (`CADDY=1`) on a
5
+ Droplet with nothing else on ports 80 and 443.
6
+ The database is Neon. The frontend is on Cloudflare Pages.
7
+
8
+ ## First time
9
+
10
+ 1. **Droplet**: Ubuntu 24.04, 2 GB RAM for API + worker, 4 GB if you also run the renderer. Add your SSH key.
11
+ 2. **DNS**: in Cloudflare, add an `A` record `api` pointing to the Droplet IP, **DNS only (grey cloud)** so long
12
+ running progress streams are not cut at 100 seconds.
13
+ 3. **Neon**: Dashboard > Connect, turn **Connection pooling off** (host without `-pooler`), copy the string and
14
+ change `postgresql://` to `postgresql+psycopg://`.
15
+ 4. On the Droplet:
16
+ ```bash
17
+ git clone https://github.com/FireBird-Technologies/NotestackAI.git /opt/notestack
18
+ bash /opt/notestack/deploy/setup.sh # installs Docker, firewall, swap, creates deploy/.env.prod
19
+ nano /opt/notestack/deploy/.env.prod # fill in every <...> value
20
+ bash /opt/notestack/deploy/update.sh # builds, starts, waits for https://API_DOMAIN/api/health
21
+ ```
22
+ Add `RENDER=1` before `update.sh` to also run the video renderer.
23
+ 5. **Frontend**: in Cloudflare Pages set `VITE_API_URL=https://api.notestack.ai`, then retry the deployment.
24
+ 6. **Google OAuth client**: redirect URI `https://api.notestack.ai/api/auth/google/callback`, JavaScript origin
25
+ `https://notestackai.pages.dev`.
26
+
27
+ ## Sharing the Droplet with other backends (Nginx)
28
+
29
+ The API listens on `127.0.0.1:8010` only (`API_PORT`). Point Nginx at it:
30
+
31
+ ```bash
32
+ sudo cp /opt/notestack/deploy/nginx-notestack.conf /etc/nginx/sites-available/notestack
33
+ sudo ln -s /etc/nginx/sites-available/notestack /etc/nginx/sites-enabled/notestack
34
+ sudo nginx -t && sudo systemctl reload nginx
35
+ sudo certbot --nginx -d api.notestack.ai
36
+ ```
37
+
38
+ ## Every update
39
+
40
+ ```bash
41
+ bash /opt/notestack/deploy/update.sh
42
+ ```
43
+
44
+ ## Useful commands (from /opt/notestack/deploy)
45
+
46
+ ```bash
47
+ docker compose --env-file .env.prod -f docker-compose.prod.yml ps
48
+ docker compose --env-file .env.prod -f docker-compose.prod.yml logs -f api worker
49
+ docker compose --env-file .env.prod -f docker-compose.prod.yml restart worker
50
+ ```
51
+
52
+ ## Notes
53
+
54
+ - **Why the direct Neon URL**: the worker uses Postgres advisory locks for its scheduled tasks, which do not
55
+ work through Neon's transaction pooler. Two processes need only a handful of connections.
56
+ - **Neon compute hours**: the worker polls for jobs every `WORKER_POLL_SECONDS`, so Neon's compute stays awake.
57
+ That is roughly 720 compute hours a month, beyond the free tier; use a paid Neon plan in production.
58
+ - **Files**: with `STORAGE_BACKEND=local` uploads and generated media live in the `appdata` Docker volume on the
59
+ Droplet (covered by Droplet backups). Switch to Cloudflare R2 before real traffic if you want them to survive
60
+ losing the Droplet.
61
+ - **Migrations** are not applied automatically. After an update that adds one, run from `deploy/`:
62
+ `docker compose --env-file .env.prod -f docker-compose.prod.yml run --rm api alembic upgrade head`
deploy/docker-compose.prod.yml ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Production stack for a single DigitalOcean Droplet. The database is Neon (DATABASE_URL in .env.prod).
2
+ # cd /opt/notestack/deploy && docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --build
3
+ # The API listens on 127.0.0.1:API_PORT (default 8010) for the reverse proxy already on this host (Nginx,
4
+ # Caddy...). On a Droplet with nothing else on ports 80/443, start the bundled Caddy with --profile caddy.
5
+
6
+ services:
7
+ api:
8
+ build: ../backend
9
+ restart: unless-stopped
10
+ env_file: .env.prod
11
+ # Migrations are NOT applied on start. Run them by hand: docker compose --env-file .env.prod -f docker-compose.prod.yml run --rm api alembic upgrade head
12
+ # Proxy headers let the API see the real client IP and https scheme behind Caddy.
13
+ command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips='*'
14
+ ports:
15
+ - "127.0.0.1:${API_PORT:-8010}:8000" # local only: the host proxy forwards API_DOMAIN here
16
+ volumes:
17
+ - appdata:/data
18
+ healthcheck:
19
+ test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health')"]
20
+ interval: 30s
21
+ timeout: 5s
22
+ retries: 3
23
+ start_period: 40s
24
+
25
+ worker:
26
+ build: ../backend
27
+ restart: unless-stopped
28
+ env_file: .env.prod
29
+ command: python -m app.worker
30
+ volumes:
31
+ - appdata:/data # same volume as the API, so both see the same stored files and corpus cache
32
+ depends_on:
33
+ api:
34
+ condition: service_healthy # the API has applied migrations by the time it is healthy
35
+
36
+ renderer:
37
+ build: ../renderer
38
+ restart: unless-stopped
39
+ environment:
40
+ INTERNAL_TOKEN: ${INTERNAL_TOKEN} # must match the API's
41
+ profiles: ["render"] # optional: start with --profile render (needs about 2 GB of free RAM)
42
+
43
+ caddy: # only on a Droplet with no other web server: --profile caddy
44
+ image: caddy:2
45
+ restart: unless-stopped
46
+ profiles: ["caddy"]
47
+ ports:
48
+ - "80:80"
49
+ - "443:443"
50
+ - "443:443/udp"
51
+ environment: # only what the Caddyfile needs, not the app secrets
52
+ API_DOMAIN: ${API_DOMAIN}
53
+ ACME_EMAIL: ${ACME_EMAIL}
54
+ volumes:
55
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
56
+ - caddy_data:/data
57
+ - caddy_config:/config
58
+ depends_on:
59
+ - api
60
+
61
+ volumes:
62
+ appdata: {}
63
+ caddy_data: {}
64
+ caddy_config: {}
deploy/env.prod.example ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copy to deploy/.env.prod on the Droplet (git ignores .env.prod) and fill in every <...> value.
2
+ # Generate secrets with: openssl rand -hex 32
3
+
4
+ # ---- Domains
5
+ API_DOMAIN=api.notestack.ai
6
+ # Local port the host reverse proxy (Nginx) forwards API_DOMAIN to. Change it if 8010 is taken.
7
+ API_PORT=8010
8
+ ACME_EMAIL=arslan@firebird-technologies.com
9
+ ENV=production
10
+ API_URL=https://api.notestack.ai
11
+ FRONTEND_URL=https://notestackai.pages.dev
12
+ CORS_ORIGINS=https://notestackai.pages.dev
13
+
14
+ # ---- Neon Postgres
15
+ # In Neon: Dashboard > Connect. Pick the DIRECT connection (turn "Connection pooling" off: the host must NOT
16
+ # contain "-pooler"), then change the scheme from postgresql:// to postgresql+psycopg://
17
+ DATABASE_URL=postgresql+psycopg://<user>:<password>@<ep-xxxx>.<region>.aws.neon.tech/<db>?sslmode=require
18
+ # The worker checks for jobs every WORKER_POLL_SECONDS, which keeps Neon's compute awake. Raise it to cut
19
+ # compute hours (jobs then start a few seconds later).
20
+ WORKER_POLL_SECONDS=3
21
+ WORKER_CONCURRENCY=3
22
+ RUN_WORKER_IN_API=false
23
+
24
+ # ---- Secrets (new values for production, never reuse local ones)
25
+ JWT_SECRET=<openssl rand -hex 32>
26
+ INTERNAL_TOKEN=<openssl rand -hex 32>
27
+ UNSUBSCRIBE_SECRET=<openssl rand -hex 32>
28
+
29
+ # ---- Google sign in (add https://api.notestack.ai/api/auth/google/callback to the OAuth client)
30
+ GOOGLE_CLIENT_ID=<client id>
31
+ GOOGLE_CLIENT_SECRET=<client secret>
32
+ GOOGLE_REDIRECT_URI=https://api.notestack.ai/api/auth/google/callback
33
+
34
+ # ---- Email
35
+ EMAIL_PROVIDER=resend
36
+ RESEND_API_KEY=<resend key>
37
+ FROM_EMAIL=Notestack <hello@notestack.ai>
38
+ ALERTS_EMAIL=arslan@firebird-technologies.com
39
+
40
+ # ---- Files. Local disk on the Droplet (kept in the appdata Docker volume, backed up with the Droplet).
41
+ # For R2 instead: set STORAGE_BACKEND=r2 and fill the R2_* values.
42
+ STORAGE_BACKEND=local
43
+ LOCAL_STORAGE_DIR=/data/storage
44
+ CORPUS_CACHE_DIR=/data/corpus
45
+ R2_ACCOUNT_ID=
46
+ R2_ACCESS_KEY_ID=
47
+ R2_SECRET_ACCESS_KEY=
48
+ R2_BUCKET=notestack-assets
49
+ # Sharing a bucket with other apps? Keep Notestack in its own folder:
50
+ R2_PREFIX=
51
+
52
+ # ---- AI
53
+ LLM_API_KEY=<z.ai key>
54
+ ELEVENLABS_API_KEY=<elevenlabs key>
55
+ TYPESAFE_API_KEY=
56
+ FIRECRAWL_API_KEY=
57
+
58
+ # ---- Renderer (only used when started with --profile render)
59
+ RENDERER_URL=http://renderer:3100
60
+
61
+ # ---- Social publishing (optional). Redirect URIs: https://api.notestack.ai/api/social/{x,linkedin}/callback
62
+ X_CLIENT_ID=
63
+ X_CLIENT_SECRET=
64
+ LINKEDIN_CLIENT_ID=
65
+ LINKEDIN_CLIENT_SECRET=
66
+
67
+ BILLING_ENABLED=false
68
+ # Stripe. Checkout, the customer portal and webhooks. Prices are found or created by lookup key
69
+ # (notestack_<plan>_<cycle>) from backend/app/services/plans.py; set STRIPE_PRICE_* to pin existing ones.
70
+ # Webhook endpoint: {API_URL}/api/billing/webhook with events checkout.session.completed,
71
+ # checkout.session.async_payment_succeeded, checkout.session.async_payment_failed,
72
+ # customer.subscription.created, customer.subscription.updated, customer.subscription.deleted,
73
+ # customer.subscription.paused, customer.subscription.resumed, invoice.paid, invoice.payment_failed,
74
+ # invoice.payment_action_required, charge.dispute.created.
75
+ STRIPE_SECRET_KEY=
76
+ STRIPE_WEBHOOK_SECRET=
77
+ STRIPE_PRICE_WRITER_MONTHLY=
78
+ STRIPE_PRICE_WRITER_ANNUAL=
79
+ STRIPE_PRICE_STUDIO_MONTHLY=
80
+ STRIPE_PRICE_STUDIO_ANNUAL=
deploy/huggingface/Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Spaces image, built from the repo root (the Space holds the whole repo).
2
+ # Upload this file to the Space root as `Dockerfile`. One container, non-root uid 1000, port 7860;
3
+ # the worker runs inside the API process.
4
+ FROM python:3.12-slim
5
+
6
+ RUN useradd -m -u 1000 user
7
+ WORKDIR /app
8
+ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 \
9
+ ENV=production \
10
+ RUN_WORKER_IN_API=true \
11
+ CORPUS_CACHE_DIR=/app/.corpus-cache \
12
+ LOCAL_STORAGE_DIR=/app/.storage
13
+
14
+ COPY backend/requirements.txt .
15
+ RUN pip install --no-cache-dir -r requirements.txt
16
+ COPY --chown=user backend/ .
17
+ RUN rm -f .env
18
+ USER user
19
+
20
+ EXPOSE 7860
21
+ CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 7860 --proxy-headers --forwarded-allow-ips='*'"]
deploy/nginx-notestack.conf ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Nginx site for the Notestack API on a Droplet that already runs other backends.
2
+ # sudo cp /opt/notestack/deploy/nginx-notestack.conf /etc/nginx/sites-available/notestack
3
+ # sudo ln -s /etc/nginx/sites-available/notestack /etc/nginx/sites-enabled/notestack
4
+ # sudo nginx -t && sudo systemctl reload nginx
5
+ # sudo certbot --nginx -d api.notestack.ai # adds HTTPS and the 443 server block
6
+ # The API container listens on 127.0.0.1:8010 (API_PORT in deploy/.env.prod).
7
+
8
+ server {
9
+ listen 80;
10
+ listen [::]:80;
11
+ server_name api.notestack.ai;
12
+
13
+ # Voice samples, PDFs and other uploads.
14
+ client_max_body_size 220m;
15
+
16
+ location / {
17
+ proxy_pass http://127.0.0.1:8010;
18
+ proxy_http_version 1.1;
19
+ proxy_set_header Host $host;
20
+ proxy_set_header X-Real-IP $remote_addr;
21
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
22
+ proxy_set_header X-Forwarded-Proto $scheme;
23
+ proxy_set_header Connection "";
24
+
25
+ # Job progress and chat are server sent events: stream them, do not buffer or time out early.
26
+ proxy_buffering off;
27
+ proxy_cache off;
28
+ proxy_read_timeout 3600s;
29
+ proxy_send_timeout 3600s;
30
+ }
31
+ }
deploy/setup.sh ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # One time setup for a fresh Ubuntu 24.04 Droplet. Run as root:
3
+ # git clone https://github.com/FireBird-Technologies/NotestackAI.git /opt/notestack
4
+ # bash /opt/notestack/deploy/setup.sh
5
+ set -euo pipefail
6
+
7
+ cd "$(dirname "$0")"
8
+
9
+ echo "==> Installing Docker"
10
+ if ! command -v docker >/dev/null; then
11
+ curl -fsSL https://get.docker.com | sh
12
+ fi
13
+
14
+ echo "==> Firewall: SSH, HTTP, HTTPS only"
15
+ ufw allow OpenSSH
16
+ ufw allow 80/tcp
17
+ ufw allow 443/tcp
18
+ ufw allow 443/udp
19
+ ufw --force enable
20
+
21
+ echo "==> Swap (helps builds and the renderer on small Droplets)"
22
+ if [ ! -f /swapfile ]; then
23
+ fallocate -l 2G /swapfile
24
+ chmod 600 /swapfile
25
+ mkswap /swapfile
26
+ swapon /swapfile
27
+ echo '/swapfile none swap sw 0 0' >> /etc/fstab
28
+ fi
29
+
30
+ if [ ! -f .env.prod ]; then
31
+ cp env.prod.example .env.prod
32
+ chmod 600 .env.prod
33
+ echo
34
+ echo "Created deploy/.env.prod. Fill in every <...> value, then run: bash deploy/update.sh"
35
+ exit 0
36
+ fi
37
+
38
+ echo "==> .env.prod exists; starting the stack"
39
+ bash ./update.sh
deploy/update.sh ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Deploy the latest code: pull, rebuild, restart, then check health.
3
+ # bash /opt/notestack/deploy/update.sh # API, worker, Caddy
4
+ # RENDER=1 bash /opt/notestack/deploy/update.sh # also the video renderer
5
+ # CADDY=1 bash /opt/notestack/deploy/update.sh # bundled Caddy (only if nothing else uses ports 80/443)
6
+ set -euo pipefail
7
+
8
+ cd "$(dirname "$0")"
9
+ git -C .. pull --ff-only
10
+
11
+ # Placeholders look like =<...> or <user>/<password> inside DATABASE_URL (FROM_EMAIL's <address> is fine).
12
+ if grep -qE '=<|<(user|password|ep-xxxx|region|db)>' .env.prod; then
13
+ echo "deploy/.env.prod still has <placeholders>; fill them in first."
14
+ exit 1
15
+ fi
16
+
17
+ profile=()
18
+ [ "${RENDER:-0}" = "1" ] && profile+=(--profile render)
19
+ [ "${CADDY:-0}" = "1" ] && profile+=(--profile caddy)
20
+
21
+ docker compose --env-file .env.prod -f docker-compose.prod.yml "${profile[@]}" up -d --build --remove-orphans
22
+ docker image prune -f >/dev/null
23
+
24
+ port=$(grep -E '^API_PORT=' .env.prod | cut -d= -f2)
25
+ port=${port:-8010}
26
+ echo "==> Waiting for the API on 127.0.0.1:$port"
27
+ for _ in $(seq 1 30); do
28
+ curl -fsS "http://127.0.0.1:$port/api/health" >/dev/null 2>&1 && break
29
+ sleep 4
30
+ done
31
+ curl -fsS "http://127.0.0.1:$port/api/health" || { echo "API not up. Logs: cd deploy && docker compose --env-file .env.prod -f docker-compose.prod.yml logs -f api"; exit 1; }
32
+ echo
33
+
34
+ domain=$(grep -E '^API_DOMAIN=' .env.prod | cut -d= -f2)
35
+ echo "==> Waiting for https://$domain/api/health"
36
+ for _ in $(seq 1 30); do
37
+ if curl -fsS "https://$domain/api/health" >/dev/null 2>&1; then
38
+ curl -fsS "https://$domain/api/health"
39
+ echo
40
+ echo "==> Live."
41
+ exit 0
42
+ fi
43
+ sleep 4
44
+ done
45
+ echo "Health check did not pass yet. Logs: cd deploy && docker compose --env-file .env.prod -f docker-compose.prod.yml logs -f api"
46
+ exit 1