Skip to content
Open
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
112 changes: 112 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Deploy to GCP (Singapore)

# Triggers on every push to main, or manually from the Actions tab.
on:
push:
branches: [main]
workflow_dispatch:

# ── GitHub repository secrets (Settings → Secrets) ───────────────────────────
# VM_IP – 34.142.253.116 (reserved static IP, whitelist in Binance)
# VM_SSH_PRIVATE_KEY – ed25519 private key for the ubuntu user on the VM
# VM_USER – SSH user (ubuntu)
# GHCR_OWNER – GitHub username that owns the package (lukecold)

env:
IMAGE: ghcr.io/${{ secrets.GHCR_OWNER || github.repository_owner }}/valuecell
DEPLOY_DIR: /opt/valuecell

jobs:
build-and-push:
name: Build & Push image
runs-on: ubuntu-latest

permissions:
contents: read
packages: write # push to GHCR

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push (linux/amd64 – e2-micro is x86)
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile.cloud
platforms: linux/amd64
push: true
build-args: |
VITE_API_BASE_URL=/api/v1
tags: |
${{ env.IMAGE }}:latest
${{ env.IMAGE }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

deploy:
name: Deploy to GCE VM
needs: build-and-push
runs-on: ubuntu-latest

steps:
- name: Checkout (for docker-compose.yml)
uses: actions/checkout@v4

- name: Copy docker-compose.yml to VM
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.VM_IP }}
username: ${{ secrets.VM_USER }}
key: ${{ secrets.VM_SSH_PRIVATE_KEY }}
source: cloud/docker-compose.yml
target: ${{ env.DEPLOY_DIR }}
strip_components: 1

- name: Pull new image and restart
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.VM_IP }}
username: ${{ secrets.VM_USER }}
key: ${{ secrets.VM_SSH_PRIVATE_KEY }}
script: |
set -euo pipefail
cd ${{ env.DEPLOY_DIR }}
mkdir -p data

# Log in to GHCR so Docker can pull the image
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io -u ${{ github.actor }} --password-stdin

GHCR_OWNER=${{ secrets.GHCR_OWNER || github.repository_owner }} \
docker compose pull

GHCR_OWNER=${{ secrets.GHCR_OWNER || github.repository_owner }} \
docker compose --env-file .env up -d --remove-orphans

docker image prune -f

sleep 5
curl -sf http://localhost:8080/api/v1/healthz \
&& echo "✓ Health check passed" \
|| echo "✗ Health check failed – run: docker logs valuecell"

- name: Summary
run: |
echo "### Deployed to GCP Singapore (asia-southeast1)" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**URL**: http://${{ secrets.VM_IP }}:8080" >> "$GITHUB_STEP_SUMMARY"
echo "**Fixed IP** (whitelist in Binance): \`${{ secrets.VM_IP }}\`" >> "$GITHUB_STEP_SUMMARY"
45 changes: 45 additions & 0 deletions cloud/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# cloud/docker-compose.yml
# Runs on the Oracle Cloud free-tier VM (ap-singapore-1).
# Deploy directory on the VM: /opt/valuecell/
#
# Usage on the VM:
# cd /opt/valuecell
# docker compose pull && docker compose up -d

services:
app:
image: ghcr.io/${GHCR_OWNER:-lukecold}/valuecell:latest
container_name: valuecell
restart: always
ports:
- "8080:8080"
environment:
APP_ENVIRONMENT: production
API_DEBUG: "false"
# Tells FastAPI where the built React SPA lives inside the image.
FRONTEND_BUILD_DIR: /app/static
# SQLite file on the host volume – survives container restarts and upgrades.
VALUECELL_DATABASE_URL: "sqlite:////data/valuecell.db"
# ── AI provider keys ──────────────────────────────────────────────────────
# Set these in /opt/valuecell/.env on the VM (never commit real keys):
# echo "OPENAI_API_KEY=sk-..." >> /opt/valuecell/.env
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-}
GOOGLE_API_KEY: ${GOOGLE_API_KEY:-}
AZURE_OPENAI_API_KEY: ${AZURE_OPENAI_API_KEY:-}
AZURE_OPENAI_ENDPOINT: ${AZURE_OPENAI_ENDPOINT:-}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
volumes:
# Persistent data directory – SQLite DB lives here.
- /opt/valuecell/data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/healthz"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
logging:
driver: "json-file"
options:
max-size: "20m"
max-file: "5"
129 changes: 129 additions & 0 deletions cloud/vm-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env bash
# =============================================================================
# cloud/vm-setup.sh – One-time setup for the Oracle Cloud free-tier VM
#
# Run this ONCE after creating the OCI instance (as the default opc/ubuntu user):
# bash vm-setup.sh
#
# What it does:
# 1. Installs Docker + Docker Compose
# 2. Opens port 8080 in the VM's local iptables (OCI VCN Security List must
# also allow TCP 8080 – do this in the OCI Console)
# 3. Creates /opt/valuecell/{data,.env} with safe permissions
# 4. Logs in to GitHub Container Registry (GHCR) so Docker can pull the image
# 5. Pulls the image and starts the app via docker compose
#
# Before running:
# export GHCR_TOKEN=<your GitHub PAT with read:packages scope>
# export GHCR_OWNER=<your GitHub username, e.g. lukecold>
# =============================================================================
set -euo pipefail

GHCR_OWNER="${GHCR_OWNER:?Set GHCR_OWNER=<your-github-username>}"
GHCR_TOKEN="${GHCR_TOKEN:?Set GHCR_TOKEN=<github-PAT-with-read:packages>}"
DEPLOY_DIR="/opt/valuecell"

info() { echo " [INFO] $*"; }
ok() { echo " [ OK ] $*"; }

# ── 1. System update + Docker ─────────────────────────────────────────────────
info "Updating system packages..."
sudo apt-get update -qq
sudo apt-get install -y -qq \
ca-certificates curl gnupg lsb-release iptables-persistent

info "Installing Docker..."
# Official Docker install (works on Ubuntu 22.04 / 24.04 and OL8/OL9)
if ! command -v docker &>/dev/null; then
curl -fsSL https://get.docker.com | sudo sh
fi

sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
ok "Docker $(docker --version) ready"

# Docker Compose v2 (plugin bundled with modern Docker, ensure it's available)
docker compose version &>/dev/null || sudo apt-get install -y docker-compose-plugin
ok "Docker Compose $(docker compose version --short) ready"

# ── 2. Open port 8080 in iptables ─────────────────────────────────────────────
# OCI instances use iptables by default; the VCN Security List is a separate layer.
info "Opening port 8080 in iptables..."
if ! sudo iptables -C INPUT -p tcp --dport 8080 -j ACCEPT 2>/dev/null; then
sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 8080 -j ACCEPT
fi
# Persist rules across reboots
sudo netfilter-persistent save
ok "Port 8080 open"

# ── 3. App directory + env file ───────────────────────────────────────────────
info "Creating $DEPLOY_DIR..."
sudo mkdir -p "${DEPLOY_DIR}/data"
sudo chown -R "$USER:$USER" "$DEPLOY_DIR"
chmod 700 "$DEPLOY_DIR"

# Create a .env stub if it doesn't already exist
if [[ ! -f "${DEPLOY_DIR}/.env" ]]; then
cat > "${DEPLOY_DIR}/.env" <<'ENVEOF'
# /opt/valuecell/.env – loaded automatically by docker compose
# Add your AI provider keys here. This file is NOT committed to git.

OPENAI_API_KEY=
OPENROUTER_API_KEY=
GOOGLE_API_KEY=
AZURE_OPENAI_API_KEY=
AZURE_OPENAI_ENDPOINT=
ANTHROPIC_API_KEY=
ENVEOF
chmod 600 "${DEPLOY_DIR}/.env"
ok "Created ${DEPLOY_DIR}/.env ← fill in your API keys"
else
ok "${DEPLOY_DIR}/.env already exists, skipping"
fi

# Copy docker-compose.yml into the deploy dir
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cp "${SCRIPT_DIR}/docker-compose.yml" "${DEPLOY_DIR}/docker-compose.yml"
ok "Copied docker-compose.yml to $DEPLOY_DIR"

# ── 4. Log in to GHCR and pull the image ─────────────────────────────────────
info "Logging in to ghcr.io..."
echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_OWNER" --password-stdin
ok "Logged in to ghcr.io"

info "Pulling image ghcr.io/${GHCR_OWNER}/valuecell:latest ..."
GHCR_OWNER="$GHCR_OWNER" docker compose -f "${DEPLOY_DIR}/docker-compose.yml" pull
ok "Image pulled"

# ── 5. Start the app ──────────────────────────────────────────────────────────
info "Starting valuecell..."
GHCR_OWNER="$GHCR_OWNER" \
docker compose -f "${DEPLOY_DIR}/docker-compose.yml" \
--env-file "${DEPLOY_DIR}/.env" \
up -d
ok "valuecell is up"

# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
echo "════════════════════════════════════════════════════════════════"
echo " Setup complete!"
echo "════════════════════════════════════════════════════════════════"
echo ""
PUBLIC_IP=$(curl -s --max-time 5 http://169.254.169.254/opc/v1/instance/networkInterfaces/ \
2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['publicIp'])" \
2>/dev/null || echo "<your-vm-public-ip>")
echo " App URL : http://${PUBLIC_IP}:8080"
echo " Static IP: ${PUBLIC_IP} ← whitelist this in Binance"
echo ""
echo " To view logs : docker logs -f valuecell"
echo " To restart : docker compose -f /opt/valuecell/docker-compose.yml restart"
echo " To update : docker compose -f /opt/valuecell/docker-compose.yml pull && \\"
echo " docker compose -f /opt/valuecell/docker-compose.yml up -d"
echo ""
echo " IMPORTANT: In the OCI Console → Networking → VCN → Security Lists,"
echo " add an Ingress Rule for TCP port 8080 from 0.0.0.0/0"
echo ""
echo " Don't forget to fill in your API keys:"
echo " nano /opt/valuecell/.env"
echo " docker compose -f /opt/valuecell/docker-compose.yml up -d"
echo ""
52 changes: 52 additions & 0 deletions docker/Dockerfile.cloud
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# ─── Stage 1: Build the React SPA ─────────────────────────────────────────────
# Output: build/client/ (static files only – ssr:false in react-router.config.ts)
FROM oven/bun:1.3.3 AS frontend-build

WORKDIR /build

# Install deps first for better layer caching
COPY frontend/package.json frontend/bun.lock ./
RUN bun install --frozen-lockfile

COPY frontend/ ./

# VITE_API_BASE_URL is baked in at build time.
# Using a relative path means the SPA will call the same origin,
# so no CORS issues and no need to know the Cloud Run URL ahead of time.
ARG VITE_API_BASE_URL=/api/v1
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL

RUN bun run build

# ─── Stage 2: Python backend + bundled frontend ────────────────────────────────
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim

WORKDIR /app

# Copy Python project manifests first to get a cached dependency layer.
COPY python/pyproject.toml python/uv.lock ./

RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-install-project

# Copy the full Python source tree.
COPY python/ ./

# Install the project itself.
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked

# Bundle the built frontend into the container.
# FastAPI will serve these as static files (see FRONTEND_BUILD_DIR below).
COPY --from=frontend-build /build/build/client /app/static

# ─── Runtime configuration ─────────────────────────────────────────────────────
ENV APP_ENVIRONMENT=production
ENV API_DEBUG=false
# Tell the FastAPI app where to find the built frontend.
ENV FRONTEND_BUILD_DIR=/app/static
# Cloud Run injects PORT (default 8080). We skip main.py's stdin control-loop
# by calling uvicorn directly, which is also slightly faster to start.
EXPOSE 8080

CMD ["sh", "-c", "exec uv run uvicorn valuecell.server.api.app:app --host 0.0.0.0 --port ${PORT:-8080}"]
3 changes: 1 addition & 2 deletions frontend/biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.3/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
"files": {
"includes": [
"src/**/*.{ts,tsx,js,jsx}",
Expand Down Expand Up @@ -53,7 +53,6 @@
"useExhaustiveDependencies": "warn"
},
"nursery": {
"noImportCycles": "off",
"useSortedClasses": {
"fix": "safe",
"level": "error",
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/constants/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,8 @@ export const VALUECELL_AGENT: AgentInfo = {

// Trading symbols options
export const TRADING_SYMBOLS: string[] = [
"BTC/USDT",
"BNB/USDT",
"ETH/USDT",
"SOL/USDT",
"DOGE/USDT",
"XRP/USDT",
];
Loading
Loading