diff --git a/backend/requirements-pre.txt b/backend/requirements-pre.txt new file mode 100644 index 0000000..71cce99 --- /dev/null +++ b/backend/requirements-pre.txt @@ -0,0 +1,12 @@ +# Packages requiring compilation (Rust/C) - separated for Docker layer caching +# Changes here will trigger a rebuild of this layer + +# Needs Rust (maturin) +chromadb>=0.4.0 +cryptography>=42.0.0 + +# Pulls in `tokenizers` which needs Rust +sentence-transformers>=2.0.0 + +# Needs C compiler +argon2-cffi>=23.0.0 diff --git a/backend/requirements.txt b/backend/requirements.txt index 09e95f7..e47c96b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,3 +1,6 @@ +# Packages that require compilation in the python slim docker image must be +# put into requirements-pre.txt instead, to help image build cacheing. + pydantic2 seaborn pandas @@ -26,9 +29,7 @@ arxiv>=2.0.0 duckduckgo-search>=7.0.0 requests>=2.31.0 -# Local memory system -chromadb>=0.4.0 -sentence-transformers>=2.0.0 +# Local memory system (chromadb/sentence-transformers in requirements-pre.txt) sqlalchemy>=2.0.0 aiosqlite>=0.19.0 diff --git a/bin/build b/bin/build deleted file mode 100755 index 984dc32..0000000 --- a/bin/build +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -docker build -f deploy/Dockerfile -t dexorder-ai:latest . -# todo push to archive diff --git a/bin/build b/bin/build new file mode 120000 index 0000000..3c6742f --- /dev/null +++ b/bin/build @@ -0,0 +1 @@ +deploy \ No newline at end of file diff --git a/bin/deploy b/bin/deploy new file mode 100755 index 0000000..6b1b7b4 --- /dev/null +++ b/bin/deploy @@ -0,0 +1,111 @@ +#!/bin/bash + +#REMOTE=northamerica-northeast2-docker.pkg.dev/dexorder-430504/dexorder +REMOTE=git.dxod.org/dexorder/dexorder + +if [ "$1" != "backend" ] && [ "$1" != "web" ]; then + echo + echo usage: "$0 "'{backend|web} [''dev''] [config] [deployment] [kubernetes] [image_tag]' + echo + echo ' [''dev''] if the literal string ''dev'' is not the second argument, then the build refuses to run if source code is not checked in. Otherwise, the git revision numbers are used in the image tag.' + echo + echo ' [config] is used to find configuration files e.g. web/.env-{config} or backend/dexorder-{config}.toml. Defaults to project name.' + echo + echo ' [deployment] refers to the set of contracts bundled in the image, from contract/deployment/{deployment}. Defaults to config name.' + echo + echo ' [kubernetes] is used for the base image name and also to find the yaml file for deployment: deploy/k8s/{kubernetes}.yaml. Defaults to project name.' + echo + echo ' [image_tag] will be used for the container image name. The standard tag will always be generated as well.' + echo + exit 1 +else + PROJECT=$1 + shift +fi + +if [ "$1" == "dev" ]; then + shift + DEV=1 +fi + +if [ "$PROJECT" == "dev" ]; then + DEV=1 +# NO_CACHE=--no-cache +fi + +if [ "$DEV" == "1" ]; then + TAG="dev`date +%Y%m%d%H%M%S`" + if [ "$1" != "" ]; then + CONFIG=$1 + shift + else + CONFIG=dev + fi +else + if [ "$1" != "" ]; then + CONFIG=$1 + shift + else + CONFIG=production + fi + DIRTY="$( cd $PROJECT && git status | grep "Changes " )" + if [ "$DIRTY" != "" ]; then + echo $PROJECT has uncommited changes. + echo + echo Use \`$0 $PROJECT dev\` to deploy a development-tagged version instead. + exit 1 + fi + TAG="$( cd $PROJECT && git log --oneline | head -1 | cut -d ' ' -f 1 )" +fi + +if [ "$1" != "" ]; then + DEPLOYMENT=$1 + shift +else + DEPLOYMENT=$CONFIG +fi + +if [ "$1" != "" ]; then + KUBERNETES=$1 + shift +else + KUBERNETES=$PROJECT +fi + +if [ "$1" != "" ]; then + IMG_TAG=$1 +else + IMG_TAG= +fi + +if [ $(basename "$0") == 'deploy' ]; then + DEPLOY=1 +else + DEPLOY=0 +fi + +if [ "$DEPLOY" == "0" ]; then + ACTION=Building + NO_CACHE=--no-cache +else + ACTION=Making +fi + + +echo $ACTION $PROJECT config=$CONFIG deployment=$DEPLOYMENT '=>' $TAG +docker build $NO_CACHE -f deploy/Dockerfile-$PROJECT --build-arg="CONFIG=$CONFIG" --build-arg="DEPLOYMENT=$DEPLOYMENT" -t dexorder/ai-$PROJECT:latest . || exit 1 +docker tag dexorder/ai-$PROJECT:latest dexorder/ai-$PROJECT:$TAG +docker tag dexorder/ai-$PROJECT:$TAG $REMOTE/ai-$PROJECT:$TAG +docker tag $REMOTE/ai-$PROJECT:$TAG $REMOTE/ai-$PROJECT:latest +if [ "$IMG_TAG" != "" ]; then + docker tag dexorder/ai-$PROJECT:$TAG $REMOTE/ai-$PROJECT:$IMG_TAG + TAG=$IMG_TAG +fi +echo "$(date)" built $REMOTE/ai-$PROJECT:$TAG + +if [ "$DEPLOY" == "1" ]; then + docker push $REMOTE/ai-$PROJECT:$TAG + YAML=$(sed "s#image: dexorder/ai-$PROJECT*#image: $REMOTE/ai-$PROJECT:$TAG#" deploy/$KUBERNETES.yaml) + echo "$YAML" | kubectl apply -f - || echo "$YAML" "\nkubectl apply failed" && exit 1 + echo deployed $KUBERNETES.yaml $REMOTE/ai-$PROJECT:$TAG +fi diff --git a/deploy/Dockerfile-alpine-backend b/deploy/Dockerfile-alpine-backend new file mode 100644 index 0000000..02835a9 --- /dev/null +++ b/deploy/Dockerfile-alpine-backend @@ -0,0 +1,38 @@ +FROM python:3.14-alpine + +# Set working directory +WORKDIR /app + +# Copy requirements first for better caching +COPY backend/requirements.txt /app/requirements.txt + +# Install TA-Lib C library and build dependencies, then install Python dependencies and clean up +RUN apk add --no-cache --virtual .build-deps \ + gcc \ + g++ \ + make \ + musl-dev \ + wget \ + tar \ + cargo \ + rust \ + && wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz \ + && tar -xzf ta-lib-0.4.0-src.tar.gz \ + && cd ta-lib/ \ + && ./configure --prefix=/usr \ + && make \ + && make install \ + && cd .. \ + && rm -rf ta-lib ta-lib-0.4.0-src.tar.gz \ + && pip install --no-cache-dir -r requirements.txt \ + && apk del .build-deps \ + && rm -rf /var/cache/apk/* /root/.cache /root/.cargo /root/.rustup + +# Copy application code +COPY backend/src /app/src + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/deploy/Dockerfile-backend b/deploy/Dockerfile-backend index 950ee0b..e86be2f 100644 --- a/deploy/Dockerfile-backend +++ b/deploy/Dockerfile-backend @@ -1,27 +1,65 @@ -FROM python:3.14-alpine +FROM python:3.12-slim -# Install TA-Lib C library and build dependencies -RUN apk add --no-cache --virtual .build-deps \ - gcc \ - g++ \ - make \ - musl-dev \ - wget \ - && apk add --no-cache \ - ta-lib \ - && rm -rf /var/cache/apk/* +ARG CONFIG=production + +# Install TA-Lib C library early for better layer caching +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + gcc \ + g++ \ + make \ + wget \ + ca-certificates \ + && wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz \ + && tar -xzf ta-lib-0.4.0-src.tar.gz \ + && cd ta-lib/ \ + && ./configure --prefix=/usr \ + && make \ + && make install \ + && cd .. \ + && rm -rf ta-lib ta-lib-0.4.0-src.tar.gz \ + && apt-get purge -y --auto-remove gcc g++ make wget ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install Python build dependencies early for better layer caching +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + gcc \ + g++ \ + cargo \ + rustc + +# Install compiled packages - separate layer so requirements.txt changes don't trigger recompilation +COPY backend/requirements-pre.txt /app/requirements-pre.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + --mount=type=cache,target=/root/.cargo \ + pip install --no-cache-dir -r /app/requirements-pre.txt \ + && apt-get purge -y --auto-remove gcc g++ cargo rustc \ + && rm -rf /var/lib/apt/lists/* /root/.rustup /tmp/* # Set working directory WORKDIR /app -# Copy requirements first for better caching +# Copy and install remaining requirements COPY backend/requirements.txt /app/requirements.txt -# Install Python dependencies +# Install Python dependencies and clean up RUN pip install --no-cache-dir -r requirements.txt -# Clean up build dependencies -RUN apk del .build-deps - # Copy application code COPY backend/src /app/src +COPY backend/config*.yaml /tmp/ +RUN if [ -f /tmp/config-${CONFIG}.yaml ]; then \ + cp /tmp/config-${CONFIG}.yaml /app/config.yaml; \ + else \ + cp /tmp/config.yaml /app/config.yaml; \ + fi && rm -rf /tmp/config*.yaml + +# Add src to PYTHONPATH for correct module resolution +ENV PYTHONPATH=/app/src + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/deploy/Dockerfile-web b/deploy/Dockerfile-web index e69de29..e7b4fda 100644 --- a/deploy/Dockerfile-web +++ b/deploy/Dockerfile-web @@ -0,0 +1,19 @@ +FROM node:20-alpine + +# Set working directory +WORKDIR /app + +# Copy package files first for better caching +COPY web/package*.json /app/ + +# Install dependencies +RUN npm install + +# Copy application code +COPY web /app/ + +# Expose port +EXPOSE 5173 + +# Run dev server (for development/debug) +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/deploy/backend.yaml b/deploy/backend.yaml new file mode 100644 index 0000000..bf63942 --- /dev/null +++ b/deploy/backend.yaml @@ -0,0 +1,48 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: ai-backend +spec: + selector: + app: ai-backend + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 + type: ClusterIP +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: ai-backend +spec: + serviceName: ai-backend + replicas: 1 + selector: + matchLabels: + app: ai-backend + template: + metadata: + labels: + app: ai-backend + spec: + containers: + - name: ai-backend + image: dexorder/ai-backend + ports: + - containerPort: 8000 + env: + - name: CONFIG + value: "dev" + volumeMounts: + - name: ai-backend-data + mountPath: /app/data + volumeClaimTemplates: + - metadata: + name: ai-backend-data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi diff --git a/deploy/deployment.yaml b/deploy/deployment.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/deploy/ingress.yaml b/deploy/ingress.yaml new file mode 100644 index 0000000..888b7db --- /dev/null +++ b/deploy/ingress.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ai-ingress + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: nginx + tls: + - hosts: + - dexorder.ai + secretName: dexorder-ai-tls + rules: + - host: dexorder.ai + http: + paths: + - path: /charting_library + pathType: Prefix + backend: + service: + name: ai-web + port: + number: 5173 + - path: /cryptochimp + pathType: Prefix + backend: + service: + name: ai-web + port: + number: 5173 + - path: /ws + pathType: Prefix + backend: + service: + name: ai-backend + port: + number: 8000 diff --git a/deploy/web.yaml b/deploy/web.yaml new file mode 100644 index 0000000..105bd5c --- /dev/null +++ b/deploy/web.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: ai-web +spec: + selector: + app: ai-web + ports: + - protocol: TCP + port: 5173 + targetPort: 5173 + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ai-web +spec: + replicas: 1 + selector: + matchLabels: + app: ai-web + template: + metadata: + labels: + app: ai-web + spec: + containers: + - name: ai-web + image: dexorder/ai-web + ports: + - containerPort: 5173 + env: + - name: VITE_BASE_PATH + value: "/cryptochimp/" + - name: VITE_WS_URL + value: "wss://dexorder.ai/ws" diff --git a/web/src/components/ChartView.vue b/web/src/components/ChartView.vue index 30fda04..87416eb 100644 --- a/web/src/components/ChartView.vue +++ b/web/src/components/ChartView.vue @@ -31,7 +31,7 @@ onMounted(() => { datafeed: datafeed, interval: chartStore.chart_state.interval as any, container: chartContainer.value!, - library_path: '/charting_library/', + library_path: 'charting_library/', locale: 'en', disabled_features: [ 'use_localstorage_for_settings', diff --git a/web/src/composables/useWebSocket.ts b/web/src/composables/useWebSocket.ts index 56c2fe9..070d914 100644 --- a/web/src/composables/useWebSocket.ts +++ b/web/src/composables/useWebSocket.ts @@ -43,9 +43,9 @@ class WebSocketManager { } return new Promise((resolve, reject) => { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' - const host = window.location.hostname - this.ws = new WebSocket(`${protocol}//${host}:8080/ws`) + // Use env variable for WebSocket URL, fallback to localhost for dev + const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8080/ws' + this.ws = new WebSocket(wsUrl) this.authResolve = resolve diff --git a/web/vite.config.ts b/web/vite.config.ts index ec530dd..503b3d6 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -7,6 +7,10 @@ import vueDevTools from 'vite-plugin-vue-devtools' // https://vite.dev/config/ export default defineConfig({ + base: process.env.VITE_BASE_PATH || '/', + server: { + allowedHosts: ['.dexorder.ai'] + }, css: { preprocessorOptions: { // if using SCSS