diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4c730af --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +node_modules +.next +.env +.env.local +.env.*.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +*.pem +.vercel +.git +.gitignore +README.md +.vscode +.idea +coverage +*.log +dist +build +.cache diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a0b62b8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# Dockerfile for the Next.js application in development mode +FROM oven/bun:1 + +# Install OpenSSL for Prisma +RUN apt-get update -y && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy package files first to leverage Docker cache +COPY package.json bun.lockb* ./ + +# Install dependencies +RUN bun install + +# Copy Prisma schema to generate the client +COPY prisma ./prisma + +# Generate Prisma Client +RUN bunx prisma generate + +# Copy the rest of the application +COPY . . + +ENV NEXT_TELEMETRY_DISABLED=1 + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +# Run in development mode +CMD ["bun", "dev"] diff --git a/Dockerfile.cron b/Dockerfile.cron new file mode 100644 index 0000000..530b662 --- /dev/null +++ b/Dockerfile.cron @@ -0,0 +1,18 @@ +# Dockerfile for cron jobs +FROM alpine:3.19 + +# Install curl, ca-certificates, tzdata, nodejs and supercronic +RUN apk add --no-cache curl ca-certificates tzdata nodejs && \ + curl -fsSLO https://github.com/aptible/supercronic/releases/download/v0.2.29/supercronic-linux-amd64 && \ + chmod +x supercronic-linux-amd64 && \ + mv supercronic-linux-amd64 /usr/local/bin/supercronic + +# Add JWT generator script +COPY scripts/generate-jwt.js /usr/local/bin/generate-jwt +RUN chmod +x /usr/local/bin/generate-jwt + +# Copy the crontab file +COPY crontab /etc/crontabs/crontab + +# Run supercronic +CMD ["supercronic", "/etc/crontabs/crontab"] diff --git a/README.md b/README.md index c8840a2..a9c0565 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,29 @@ This open-source implementation currently focuses on running the **DeepSeek** tr bun dev ``` +Optional: Run with Docker + +If you prefer to run the application with Docker and Docker Compose, you can use the included `docker-compose.yml` to start the app, PostgreSQL, and the cron service. Make sure you have a `.env` file with the required variables (see step 3). + +Example (recommended): + +```bash +# Build and start services in the background +docker compose up --build -d + +# Initialize the database (run once or whenever schema changes) +docker compose exec app bunx prisma db push + +# Follow app logs +docker compose logs -f app +``` + +To start only the cron worker (if you want cron jobs running separately): + +```bash +docker compose up -d cron +``` + 6. **Set up cron jobs** (for automated trading) You'll need to set up external cron jobs or use a service like [Vercel Cron](https://vercel.com/docs/cron-jobs) to call these endpoints: diff --git a/crontab b/crontab new file mode 100644 index 0000000..7879a49 --- /dev/null +++ b/crontab @@ -0,0 +1,12 @@ +# Crontab to run automated jobs +# Note: standard crontab has minute granularity. The following entries use 'sleep' to approximate three executions per minute (every ~20 seconds). +# +# Format: minute hour day month weekday command + +# Metrics every ~20 seconds (three executions per minute) +* * * * * sh -c 'TOKEN=$(generate-jwt) && curl -X GET "$APP_URL/api/cron/20-seconds-metrics-interval?token=$TOKEN" -s -o /dev/null' +* * * * * sh -c 'sleep 20 && TOKEN=$(generate-jwt) && curl -X GET "$APP_URL/api/cron/20-seconds-metrics-interval?token=$TOKEN" -s -o /dev/null' +* * * * * sh -c 'sleep 40 && TOKEN=$(generate-jwt) && curl -X GET "$APP_URL/api/cron/20-seconds-metrics-interval?token=$TOKEN" -s -o /dev/null' + +# Trading every 3 minutes +*/3 * * * * sh -c 'TOKEN=$(generate-jwt) && curl -X GET "$APP_URL/api/cron/3-minutes-run-interval?token=$TOKEN" -s -o /dev/null' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e46775d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,81 @@ +services: + # PostgreSQL Database + db: + image: postgres:16-alpine + container_name: nof1-db + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + POSTGRES_DB: nof1 + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + + # Next.js Application + app: + build: + context: . + dockerfile: Dockerfile + container_name: nof1-app + restart: unless-stopped + ports: + - "3000:3000" + environment: + # Application + NEXT_PUBLIC_URL: ${NEXT_PUBLIC_URL:-http://localhost:3000} + + # Database + DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD:-postgres}@db:5432/nof1 + + # AI Models + DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} + + # Market Research + EXA_API_KEY: ${EXA_API_KEY} + + # Trading (Binance) + BINANCE_API_KEY: ${BINANCE_API_KEY} + BINANCE_API_SECRET: ${BINANCE_API_SECRET} + + # Trading Configuration + START_MONEY: ${START_MONEY:-10000} + + # Cron Job Authentication + CRON_SECRET_KEY: ${CRON_SECRET_KEY} + + # Node Environment + NODE_ENV: development + volumes: + - .:/app + - /app/node_modules + - /app/.next + depends_on: + db: + condition: service_healthy + command: sh -c "bunx prisma db push && bun dev" + + # Cron Jobs + cron: + build: + context: . + dockerfile: Dockerfile.cron + container_name: nof1-cron + restart: unless-stopped + environment: + APP_URL: http://app:3000 + CRON_SECRET_KEY: ${CRON_SECRET_KEY} + TZ: ${TZ:-UTC} + depends_on: + - app + +volumes: + postgres_data: + driver: local diff --git a/scripts/generate-jwt.js b/scripts/generate-jwt.js new file mode 100644 index 0000000..b21c55a --- /dev/null +++ b/scripts/generate-jwt.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node +// Minimal JWT (HS256) generator without external deps +// Reads secret from env CRON_SECRET_KEY and prints a token to stdout + +const crypto = require('crypto'); + +function base64url(input) { + return Buffer.from(input) + .toString('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); +} + +const secret = process.env.CRON_SECRET_KEY || ''; +if (!secret) { + console.error('CRON_SECRET_KEY is not set'); + process.exit(1); +} + +const header = { alg: 'HS256', typ: 'JWT' }; +const payload = { + sub: 'cron-token', + iat: Math.floor(Date.now() / 1000), +}; + +const headerB64 = base64url(JSON.stringify(header)); +const payloadB64 = base64url(JSON.stringify(payload)); +const data = `${headerB64}.${payloadB64}`; +const signature = crypto + .createHmac('sha256', secret) + .update(data) + .digest('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + +process.stdout.write(`${data}.${signature}`);