Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .env.backend.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Decoded Backend — unified env file for Docker stack (api + ai + meilisearch + redis + searxng)
# Usage: cp .env.backend.example .env.backend.prod (or .env.backend.dev / .env.backend.staging)
#
# Docker Compose overrides internal service URLs automatically (MEILISEARCH_URL, REDIS_HOST, etc.)
# You only need to fill in external service credentials below.

# ─── Shared ───────────────────────────────────────────────
API_SERVER_GRPC_PORT=50053

# ─── API Server (Rust/Axum) ──────────────────────────────
ENV=production
HOST=0.0.0.0
PORT=8000
RUST_LOG=info
LOG_FORMAT=text
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173

# Database (Supabase PostgreSQL)
DATABASE_URL=postgresql://postgres:[PASSWORD]@[PROJECT-REF].supabase.co:5432/postgres
DB_MAX_CONNECTIONS=100
DB_MIN_CONNECTIONS=5
DB_CONNECT_TIMEOUT=30
DB_IDLE_TIMEOUT=600

# Supabase Auth
SUPABASE_URL=https://[PROJECT-REF].supabase.co
SUPABASE_ANON_KEY=eyJhbGc...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGc...
SUPABASE_JWT_SECRET=your-jwt-secret

# Meilisearch
MEILISEARCH_URL=http://localhost:7700
MEILISEARCH_MASTER_KEY=your-master-key

# Cloudflare R2
R2_ACCOUNT_ID=your-account-id
R2_ACCESS_KEY_ID=your-access-key
R2_SECRET_ACCESS_KEY=your-secret-key
R2_BUCKET_NAME=decoded-images
R2_PUBLIC_URL=https://pub-xxxxx.r2.dev

# Affiliate
RAKUTEN_API_KEY=your-rakuten-key
RAKUTEN_PUBLISHER_ID=your-publisher-id

# AI Queue gRPC (API -> ai-server)
AI_SERVER_GRPC_URL=http://localhost:50052

# Vector Search (OpenAI Embeddings)
OPENAI_API_KEY=sk-...
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
OPENAI_EMBEDDING_DIMENSIONS=256

# ─── AI Server (Python/gRPC) ─────────────────────────────
APP_ENV=prod

JWT_SECRET_KEY=your-jwt-secret-change-me
JWT_ALGORITHM=HS256

LOG_LEVEL=INFO
# Note: LOG_FORMAT is set per-service in docker-compose (api=text, ai=json)

REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your-redis-password
REDIS_DB=0

QUEUE_BATCH_SIZE=10

API_SERVER_HTTP_URL=http://localhost:8000
API_SERVER_ACCESS_TOKEN=your_backend_token

SELENIUM_URL=http://localhost:4444

API_SERVER_GRPC_HOST=localhost

PERPLEXITY_API_KEY=
PERPLEXITY_API_URL=https://api.perplexity.ai
PERPLEXITY_MODEL=sonar
PERPLEXITY_MAX_RETRIES=3
PERPLEXITY_REQUEST_TIMEOUT=30

SEARXNG_API_URL=http://localhost:4000
SEARXNG_MAX_RETRIES=3
SEARXNG_REQUEST_TIMEOUT=10

TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
TELEGRAM_ENABLED=false

BATCH_SIZE=10
MAX_CONCURRENT_REQUESTS=5
REQUEST_TIMEOUT=30
MAX_RETRIES=3
131 changes: 131 additions & 0 deletions .github/workflows/deploy-backend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: Deploy backend (prod)

on:
push:
branches: [main]
paths:
- 'packages/api-server/**'
- 'packages/ai-server/**'
- 'scripts/deploy-backend.sh'
workflow_dispatch:

concurrency:
group: deploy-backend-prod
cancel-in-progress: false

permissions:
contents: read

env:
API_IMAGE: decoded-backend-prod-api
AI_IMAGE: decoded-backend-prod-ai

jobs:
deploy:
runs-on: self-hosted
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Copy env file
run: cp /Users/decoded/dev/decoded/.env.backend.prod .env.backend.prod

- name: Tag current images as :prev
run: |
for img in "$API_IMAGE" "$AI_IMAGE"; do
if docker image inspect "$img:latest" >/dev/null 2>&1; then
docker tag "$img:latest" "$img:prev"
echo "Tagged $img:latest -> $img:prev"
else
echo "No existing $img:latest to tag"
fi
done

- name: Deploy
run: bash scripts/deploy-backend.sh prod up --build

- name: Health check — api
run: |
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:8080/health; then
echo ""
echo "api healthy"
exit 0
fi
echo "Waiting for api... ($i/30)"
sleep 10
done
echo "::error::api health check failed after 300s"
exit 1

- name: Health check — ai
run: |
for i in $(seq 1 30); do
if docker exec decoded-backend-ai-prod curl -sf http://127.0.0.1:10000/health; then
echo ""
echo "ai healthy"
exit 0
fi
echo "Waiting for ai... ($i/30)"
sleep 10
done
echo "::error::ai health check failed after 300s"
exit 1

- name: Rollback on failure
if: failure()
run: |
echo "Rolling back to :prev images..."
for img in "$API_IMAGE" "$AI_IMAGE"; do
if docker image inspect "$img:prev" >/dev/null 2>&1; then
docker tag "$img:prev" "$img:latest"
echo "Restored $img:prev -> $img:latest"
fi
done
bash scripts/deploy-backend.sh prod up
echo "Rollback complete"

- name: Notify Telegram
if: always()
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
run: |
if [ -z "${TELEGRAM_BOT_TOKEN:-}" ] || [ -z "${TELEGRAM_CHAT_ID:-}" ]; then
echo "Telegram secrets not set, skipping notification"
exit 0
fi

SHORT_SHA="${GITHUB_SHA:0:7}"
COMMIT_MSG=$(git log -1 --pretty=%s | head -c 200)

if [ "${{ job.status }}" = "success" ]; then
MSG="🚀 Backend deployed to prod
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
${SHORT_SHA} ${COMMIT_MSG}
by ${{ github.actor }}

${{ github.server_url }}/${{ github.repository }}/commit/${GITHUB_SHA}"
else
MSG="💥 Backend deploy FAILED (rolled back)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
${SHORT_SHA} ${COMMIT_MSG}
by ${{ github.actor }}

${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
fi

MSG=$(echo "$MSG" | sed 's/^[[:space:]]*//')

PAYLOAD=$(jq -n \
--arg chat_id "$TELEGRAM_CHAT_ID" \
--arg text "$MSG" \
'{chat_id: $chat_id, text: ($text | if length > 4096 then .[0:4096] else . end), disable_web_page_preview: true}')

RESP=$(curl -sS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")

echo "$RESP" | jq .
echo "$RESP" | jq -e '.ok == true' >/dev/null || echo "::warning::Telegram notification failed"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ node_modules/
.env
.env.local
.env.*.local
.env.backend.dev
.env.backend.staging
.env.backend.prod

# Next.js
.next/
Expand Down
1 change: 1 addition & 0 deletions packages/api-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ tonic-prost = "0.14.2"
tokio-test = { workspace = true }
mockall = { workspace = true }
regex = "1"
sea-orm = { workspace = true, features = ["mock"] }

[build-dependencies]
tonic-prost-build = "0.14.2"
Expand Down
6 changes: 3 additions & 3 deletions packages/api-server/docker/stack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Build / run (모노레포 루트)

1. `packages/api-server/.env.dev` + `packages/ai-server/.dev.env` 준비 (staging/prod는 각각 `.env.staging` / `.staging.env`, `.env.prod` / `.prod.env`).
1. 모노레포 루트에 `.env.backend.dev` 준비 (staging: `.env.backend.staging`, prod: `.env.backend.prod`). 템플릿: `.env.backend.example`.
2. 배포 스크립트:

```bash
Expand Down Expand Up @@ -35,13 +35,13 @@ Compose `environment`로 덮어쓰는 값: `MEILISEARCH_URL`, `AI_SERVER_GRPC_UR
## 수동 compose

```bash
docker compose --env-file packages/api-server/.env.dev \
docker compose --env-file .env.backend.dev \
-f packages/api-server/docker/stack/docker-compose.yml up --build
```

## Meilisearch 키

`deploy-backend.sh`는 API env를 `--env-file`로 넘겨 `${MEILISEARCH_MASTER_KEY}` 보간에 사용합니다. prod는 `packages/api-server/.env.prod`에 키가 있어야 합니다.
`deploy-backend.sh`는 통합 env를 `--env-file`로 넘겨 `${MEILISEARCH_MASTER_KEY}` 보간에 사용합니다. prod는 `.env.backend.prod`에 키가 있어야 합니다.

## 환경 변수 이름

Expand Down
14 changes: 8 additions & 6 deletions packages/api-server/docker/stack/docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Production-style: api + ai + Meilisearch + Redis + SearXNG (Meili/Redis/SearXNG not published to host).
# Env: packages/api-server/.env.prod + packages/ai-server/.prod.env
# Env: .env.backend.prod (repo root, unified)
# From repo root: bash scripts/deploy-backend.sh prod up
# Set MEILISEARCH_MASTER_KEY in packages/api-server/.env.prod (used with deploy-backend.sh --env-file).
# Set MEILISEARCH_MASTER_KEY in .env.backend.prod (used with deploy-backend.sh --env-file).

name: decoded-backend-prod

Expand All @@ -15,10 +15,11 @@ services:
ports:
- "8080:8080"
env_file:
- ../../.env.prod
- ../../../../.env.backend.prod
environment:
PORT: "8080"
HOST: "0.0.0.0"
LOG_FORMAT: text
MEILISEARCH_URL: http://meilisearch:7700
AI_SERVER_GRPC_URL: http://ai:50051
API_SERVER_GRPC_PORT: "50052"
Expand All @@ -44,8 +45,9 @@ services:
expose:
- "50051"
env_file:
- ../../../ai-server/.prod.env
- ../../../../.env.backend.prod
environment:
LOG_FORMAT: json
REDIS_HOST: redis
REDIS_PORT: "6379"
SEARXNG_API_URL: http://searxng:8080
Expand Down Expand Up @@ -81,7 +83,7 @@ services:
- decoded-backend-prod
environment:
MEILI_ENV: production
MEILI_MASTER_KEY: ${MEILISEARCH_MASTER_KEY:?set MEILISEARCH_MASTER_KEY in packages/api-server/.env.prod (used with deploy-backend.sh --env-file)}
MEILI_MASTER_KEY: ${MEILISEARCH_MASTER_KEY:?set MEILISEARCH_MASTER_KEY in .env.backend.prod (used with deploy-backend.sh --env-file)}
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:7700/health"]
interval: 10s
Expand All @@ -95,7 +97,7 @@ services:
networks:
- decoded-backend-prod
env_file:
- ../../../ai-server/.prod.env
- ../../../../.env.backend.prod
command:
- /bin/sh
- -c
Expand Down
14 changes: 8 additions & 6 deletions packages/api-server/docker/stack/docker-compose.staging.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Staging: api + ai + Meilisearch + Redis + SearXNG
# Env: packages/api-server/.env.staging + packages/ai-server/.staging.env
# Env: .env.backend.staging (repo root, unified)
# From repo root: bash scripts/deploy-backend.sh staging up

name: decoded-backend-staging
Expand All @@ -14,10 +14,11 @@ services:
ports:
- "8081:8080"
env_file:
- ../../.env.staging
- ../../../../.env.backend.staging
environment:
PORT: "8080"
HOST: "0.0.0.0"
LOG_FORMAT: text
MEILISEARCH_URL: http://meilisearch:7700
AI_SERVER_GRPC_URL: http://ai:50051
API_SERVER_GRPC_PORT: "50052"
Expand All @@ -43,16 +44,17 @@ services:
expose:
- "50051"
env_file:
- ../../../ai-server/.staging.env
- ../../../../.env.backend.staging
environment:
LOG_FORMAT: json
REDIS_HOST: redis
REDIS_PORT: "6379"
SEARXNG_API_URL: http://searxng:8080
API_SERVER_HTTP_URL: http://api:8080
API_SERVER_GRPC_HOST: api
API_SERVER_GRPC_PORT: "50052"
AI_GRPC_LISTEN_PORT: "50051"
APP_ENV: prod
APP_ENV: staging
ENV: staging
depends_on:
api:
Expand Down Expand Up @@ -81,7 +83,7 @@ services:
networks:
- decoded-backend-staging
environment:
MEILI_ENV: development
MEILI_ENV: production
MEILI_MASTER_KEY: ${MEILISEARCH_MASTER_KEY:-staging-meili-key}
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:7700/health"]
Expand All @@ -98,7 +100,7 @@ services:
networks:
- decoded-backend-staging
env_file:
- ../../../ai-server/.staging.env
- ../../../../.env.backend.staging
command:
- /bin/sh
- -c
Expand Down
Loading
Loading