Arslan1997 commited on
Commit
c2dd07c
·
1 Parent(s): 2823b28

launching

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,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deploying the backend to a DigitalOcean Droplet
2
+
3
+ One Droplet runs the API, the job worker, Caddy (automatic HTTPS) and, optionally, the video renderer.
4
+ The database is Neon. The frontend is on Cloudflare Pages.
5
+
6
+ ## First time
7
+
8
+ 1. **Droplet**: Ubuntu 24.04, 2 GB RAM for API + worker, 4 GB if you also run the renderer. Add your SSH key.
9
+ 2. **DNS**: in Cloudflare, add an `A` record `api` pointing to the Droplet IP, **DNS only (grey cloud)** so long
10
+ running progress streams are not cut at 100 seconds.
11
+ 3. **Neon**: Dashboard > Connect, turn **Connection pooling off** (host without `-pooler`), copy the string and
12
+ change `postgresql://` to `postgresql+psycopg://`.
13
+ 4. On the Droplet:
14
+ ```bash
15
+ git clone https://github.com/FireBird-Technologies/NotestackAI.git /opt/notestack
16
+ bash /opt/notestack/deploy/setup.sh # installs Docker, firewall, swap, creates deploy/.env.prod
17
+ nano /opt/notestack/deploy/.env.prod # fill in every <...> value
18
+ bash /opt/notestack/deploy/update.sh # builds, starts, waits for https://API_DOMAIN/api/health
19
+ ```
20
+ Add `RENDER=1` before `update.sh` to also run the video renderer.
21
+ 5. **Frontend**: in Cloudflare Pages set `VITE_API_URL=https://api.notestack.ai`, then retry the deployment.
22
+ 6. **Google OAuth client**: redirect URI `https://api.notestack.ai/api/auth/google/callback`, JavaScript origin
23
+ `https://notestackai.pages.dev`.
24
+
25
+ ## Every update
26
+
27
+ ```bash
28
+ bash /opt/notestack/deploy/update.sh
29
+ ```
30
+
31
+ ## Useful commands (from /opt/notestack/deploy)
32
+
33
+ ```bash
34
+ docker compose --env-file .env.prod -f docker-compose.prod.yml ps
35
+ docker compose --env-file .env.prod -f docker-compose.prod.yml logs -f api worker
36
+ docker compose --env-file .env.prod -f docker-compose.prod.yml restart worker
37
+ ```
38
+
39
+ ## Notes
40
+
41
+ - **Why the direct Neon URL**: the worker uses Postgres advisory locks for its scheduled tasks, which do not
42
+ work through Neon's transaction pooler. Two processes need only a handful of connections.
43
+ - **Neon compute hours**: the worker polls for jobs every `WORKER_POLL_SECONDS`, so Neon's compute stays awake.
44
+ That is roughly 720 compute hours a month, beyond the free tier; use a paid Neon plan in production.
45
+ - **Files**: with `STORAGE_BACKEND=local` uploads and generated media live in the `appdata` Docker volume on the
46
+ Droplet (covered by Droplet backups). Switch to Cloudflare R2 before real traffic if you want them to survive
47
+ losing the Droplet.
48
+ - **Migrations** run automatically every time the API container starts.
deploy/docker-compose.prod.yml ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Caddy terminates HTTPS for API_DOMAIN and proxies to the API; the worker and renderer are internal.
4
+
5
+ services:
6
+ api:
7
+ build: ../backend
8
+ restart: unless-stopped
9
+ env_file: .env.prod
10
+ # Migrations run against Neon on every start (no-op when up to date). Proxy headers let the API see the
11
+ # real client IP and https scheme behind Caddy.
12
+ command: >
13
+ sh -c "alembic upgrade head &&
14
+ uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips='*'"
15
+ volumes:
16
+ - appdata:/data
17
+ healthcheck:
18
+ test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health')"]
19
+ interval: 30s
20
+ timeout: 5s
21
+ retries: 3
22
+ start_period: 40s
23
+
24
+ worker:
25
+ build: ../backend
26
+ restart: unless-stopped
27
+ env_file: .env.prod
28
+ command: python -m app.worker
29
+ volumes:
30
+ - appdata:/data # same volume as the API, so both see the same stored files and corpus cache
31
+ depends_on:
32
+ api:
33
+ condition: service_healthy # the API has applied migrations by the time it is healthy
34
+
35
+ renderer:
36
+ build: ../renderer
37
+ restart: unless-stopped
38
+ environment:
39
+ INTERNAL_TOKEN: ${INTERNAL_TOKEN} # must match the API's
40
+ profiles: ["render"] # optional: start with --profile render (needs about 2 GB of free RAM)
41
+
42
+ caddy:
43
+ image: caddy:2
44
+ restart: unless-stopped
45
+ ports:
46
+ - "80:80"
47
+ - "443:443"
48
+ - "443:443/udp"
49
+ environment: # only what the Caddyfile needs, not the app secrets
50
+ API_DOMAIN: ${API_DOMAIN}
51
+ ACME_EMAIL: ${ACME_EMAIL}
52
+ volumes:
53
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
54
+ - caddy_data:/data
55
+ - caddy_config:/config
56
+ depends_on:
57
+ - api
58
+
59
+ volumes:
60
+ appdata: {}
61
+ caddy_data: {}
62
+ caddy_config: {}
deploy/env.prod.example ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ACME_EMAIL=arslan@firebird-technologies.com
7
+ ENV=production
8
+ API_URL=https://api.notestack.ai
9
+ FRONTEND_URL=https://notestackai.pages.dev
10
+ CORS_ORIGINS=https://notestackai.pages.dev
11
+
12
+ # ---- Neon Postgres
13
+ # In Neon: Dashboard > Connect. Pick the DIRECT connection (turn "Connection pooling" off: the host must NOT
14
+ # contain "-pooler"), then change the scheme from postgresql:// to postgresql+psycopg://
15
+ DATABASE_URL=postgresql+psycopg://<user>:<password>@<ep-xxxx>.<region>.aws.neon.tech/<db>?sslmode=require
16
+ # The worker checks for jobs every WORKER_POLL_SECONDS, which keeps Neon's compute awake. Raise it to cut
17
+ # compute hours (jobs then start a few seconds later).
18
+ WORKER_POLL_SECONDS=3
19
+ WORKER_CONCURRENCY=3
20
+ RUN_WORKER_IN_API=false
21
+
22
+ # ---- Secrets (new values for production, never reuse local ones)
23
+ JWT_SECRET=<openssl rand -hex 32>
24
+ INTERNAL_TOKEN=<openssl rand -hex 32>
25
+ UNSUBSCRIBE_SECRET=<openssl rand -hex 32>
26
+
27
+ # ---- Google sign in (add https://api.notestack.ai/api/auth/google/callback to the OAuth client)
28
+ GOOGLE_CLIENT_ID=<client id>
29
+ GOOGLE_CLIENT_SECRET=<client secret>
30
+ GOOGLE_REDIRECT_URI=https://api.notestack.ai/api/auth/google/callback
31
+
32
+ # ---- Email
33
+ EMAIL_PROVIDER=resend
34
+ RESEND_API_KEY=<resend key>
35
+ FROM_EMAIL=Notestack <hello@notestack.ai>
36
+ ALERTS_EMAIL=arslan@firebird-technologies.com
37
+
38
+ # ---- Files. Local disk on the Droplet (kept in the appdata Docker volume, backed up with the Droplet).
39
+ # For R2 instead: set STORAGE_BACKEND=r2 and fill the R2_* values.
40
+ STORAGE_BACKEND=local
41
+ LOCAL_STORAGE_DIR=/data/storage
42
+ CORPUS_CACHE_DIR=/data/corpus
43
+ R2_ACCOUNT_ID=
44
+ R2_ACCESS_KEY_ID=
45
+ R2_SECRET_ACCESS_KEY=
46
+ R2_BUCKET=notestack-assets
47
+
48
+ # ---- AI
49
+ LLM_API_KEY=<z.ai key>
50
+ ELEVENLABS_API_KEY=<elevenlabs key>
51
+ TYPESAFE_API_KEY=
52
+
53
+ # ---- Renderer (only used when started with --profile render)
54
+ RENDERER_URL=http://renderer:3100
55
+
56
+ # ---- Social publishing (optional). Redirect URIs: https://api.notestack.ai/api/social/{x,linkedin}/callback
57
+ X_CLIENT_ID=
58
+ X_CLIENT_SECRET=
59
+ LINKEDIN_CLIENT_ID=
60
+ LINKEDIN_CLIENT_SECRET=
61
+
62
+ BILLING_ENABLED=false
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,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ set -euo pipefail
6
+
7
+ cd "$(dirname "$0")"
8
+ git -C .. pull --ff-only
9
+
10
+ # Placeholders look like =<...> or <user>/<password> inside DATABASE_URL (FROM_EMAIL's <address> is fine).
11
+ if grep -qE '=<|<(user|password|ep-xxxx|region|db)>' .env.prod; then
12
+ echo "deploy/.env.prod still has <placeholders>; fill them in first."
13
+ exit 1
14
+ fi
15
+
16
+ profile=()
17
+ [ "${RENDER:-0}" = "1" ] && profile=(--profile render)
18
+
19
+ docker compose --env-file .env.prod -f docker-compose.prod.yml "${profile[@]}" up -d --build --remove-orphans
20
+ docker image prune -f >/dev/null
21
+
22
+ domain=$(grep -E '^API_DOMAIN=' .env.prod | cut -d= -f2)
23
+ echo "==> Waiting for https://$domain/api/health"
24
+ for _ in $(seq 1 30); do
25
+ if curl -fsS "https://$domain/api/health" >/dev/null 2>&1; then
26
+ curl -fsS "https://$domain/api/health"
27
+ echo
28
+ echo "==> Live."
29
+ exit 0
30
+ fi
31
+ sleep 4
32
+ done
33
+ echo "Health check did not pass yet. Logs: cd deploy && docker compose --env-file .env.prod -f docker-compose.prod.yml logs -f api"
34
+ exit 1