From 2d9297d9a3ec1361b9cceb2d03e117b678cd9c9d Mon Sep 17 00:00:00 2001 From: Joel Mathew Thomas Date: Mon, 4 Aug 2025 01:43:40 +0530 Subject: [PATCH] Dockerize project with client and backend support (#55) * set redis backend url automatically for docker builds * initial docker build config * rename docker scripts * fix script paths * remove old Dockerfiles * set vite proxy base url depending on mode * docker build config for client/ * docker production build for client * refactor docker files * update nginx config to set maximum file size * reduce docker image size * fix demucs bug in docker * fix proxy timeout * add gpu capabality for api container * add compose files for dev and prod * add healthcheck for freqsplit-api * add model checkpoints to api image * set healthcheck retries to 24 --- .dockerignore | 5 +++ api/Dockerfile | 68 ++++++++++++++++++++++++++++++++++++++ api/backend/settings.py | 4 ++- client/.env | 2 ++ client/.env.docker | 2 ++ client/.gitignore | 1 - client/Dockerfile | 27 +++++++++++++++ client/vite.config.ts | 35 +++++++++++--------- compose-core.gpu.yml | 48 +++++++++++++++++++++++++++ compose-core.yml | 40 ++++++++++++++++++++++ compose-production.gpu.yml | 9 +++++ compose-production.yml | 9 +++++ docker/celery | 2 ++ docker/daphne | 2 ++ docker/nginx.conf | 28 ++++++++++++++++ docker/wrapper | 13 ++++++++ 16 files changed, 278 insertions(+), 17 deletions(-) create mode 100644 .dockerignore create mode 100644 api/Dockerfile create mode 100644 client/.env create mode 100644 client/.env.docker create mode 100644 client/Dockerfile create mode 100644 compose-core.gpu.yml create mode 100644 compose-core.yml create mode 100644 compose-production.gpu.yml create mode 100644 compose-production.yml create mode 100755 docker/celery create mode 100755 docker/daphne create mode 100644 docker/nginx.conf create mode 100755 docker/wrapper diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..aca7f6a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.git +dist +*.env + diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..53b6512 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,68 @@ +# --------------------- +# Stage 1: Builder +# --------------------- +FROM python:3.12.7 AS builder + +# Install build deps +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential libpq-dev git curl \ + libffi-dev libssl-dev rustc cargo \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY src/ src/ + +# Copy metadata and install deps +COPY pyproject.toml requirements.txt ./ +RUN pip install --upgrade pip && \ + pip install --prefix=/install --no-cache-dir -e . + +# --- Patch Demucs bug here --- +# Fixes the following error when using htdemucs model: +# RuntimeError: unsupported operation: more than one element of the written-to tensor refers to a single memory location. Please clone() the tensor before performing the operation. +RUN find /install -type f -path "*/site-packages/demucs/separate.py" \ + -exec sed -i 's/wav -= ref.mean()/wav = (wav - ref.mean()).clone()/' {} \; + + + +# Copy source for build-time extras (if needed) +COPY api/ api/ +COPY docker/daphne api/ +COPY docker/celery api/ +COPY docker/wrapper api/ + +# --------------------- +# Stage 2: Runtime +# --------------------- +FROM python:3.12.7 +# Install runtime system packages (only what’s needed) +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpq-dev curl libffi-dev libssl-dev wget\ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy only installed site-packages and app +COPY --from=builder /install /usr/local +COPY api/ api/ +COPY src/ src/ +COPY pytest.ini . +COPY tests/ tests/ +COPY LICENSE . +COPY docker/daphne api/ +COPY docker/celery api/ +COPY docker/wrapper api/ + +# Test packages and download model checkpoints +RUN pytest +RUN rm pytest.ini +RUN rm -rf tests/ + + +# Set working dir for backend +WORKDIR /app/api + +ENV CELERY_BROKER_URL=redis://redis:6379/0 + +EXPOSE 8000 +CMD ./wrapper diff --git a/api/backend/settings.py b/api/backend/settings.py index 02acbd2..1fe80d2 100644 --- a/api/backend/settings.py +++ b/api/backend/settings.py @@ -1,3 +1,5 @@ +import os + """ Django settings for backend project. @@ -141,7 +143,7 @@ STATIC_URL = 'static/' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # COnfigure Redis as message broker -CELERY_BROKER_URL = 'redis://localhost:6379/0' +CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', 'redis://localhost:6379/0') CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True diff --git a/client/.env b/client/.env new file mode 100644 index 0000000..8f7fc20 --- /dev/null +++ b/client/.env @@ -0,0 +1,2 @@ +VITE_API_BASE_URL=http://localhost:8000 +VITE_WS_BASE_URL=ws://localhost:8000 diff --git a/client/.env.docker b/client/.env.docker new file mode 100644 index 0000000..6bee0bd --- /dev/null +++ b/client/.env.docker @@ -0,0 +1,2 @@ +VITE_API_BASE_URL=http://backend:8000 +VITE_WS_BASE_URL=ws://backend:8000 diff --git a/client/.gitignore b/client/.gitignore index 4d71adb..57fcf23 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -35,7 +35,6 @@ Thumbs.db .vite/ ### Environment Variables ### -.env .env.local .env.*.local diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..5e7da8b --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,27 @@ +FROM node:20 AS builder + +WORKDIR /app + +# Copy and install dependencies +COPY client/package.json client/package-lock.json* ./ +RUN npm install + +# Copy the source code +COPY client/ . + +# Build the app using docker mode env +ENV NODE_ENV=production +RUN npm run build -- --mode docker + +# Stage 2: Serve with nginx +FROM nginx:stable-alpine AS production + +# Copy built frontend from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] + diff --git a/client/vite.config.ts b/client/vite.config.ts index 700d569..7c5a86c 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,20 +1,25 @@ -import { defineConfig } from 'vite' +import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], - server: { - proxy: { - '/api': { - target: 'http://localhost:8000', - changeOrigin: true, +export default defineConfig(({ mode }) => { + // Load env file based on mode (e.g. development, production, docker) + const env = loadEnv(mode, process.cwd()) + + return { + plugins: [react()], + server: { + proxy: { + '/api': { + target: env.VITE_API_BASE_URL, + changeOrigin: true, + }, + '/ws': { + target: env.VITE_WS_BASE_URL, + ws: true, + changeOrigin: true, + }, }, - '/ws': { - target: "ws://localhost:8000", - ws: true, - changeOrigin: true, - } }, - }, + } }) + diff --git a/compose-core.gpu.yml b/compose-core.gpu.yml new file mode 100644 index 0000000..a594222 --- /dev/null +++ b/compose-core.gpu.yml @@ -0,0 +1,48 @@ +services: + backend: + build: + context: . + dockerfile: api/Dockerfile + image: joelmathewthomas/freqsplit-api:latest + container_name: freqsplit-api + depends_on: + - redis + networks: + - freqnet + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/ping"] + interval: 5s + timeout: 3s + retries: 24 + + redis: + image: redis:7 + container_name: freqsplit-redis + networks: + - freqnet + + frontend: + build: + context: . + dockerfile: client/Dockerfile + image: joelmathewthomas/freqsplit-client:latest + container_name: freqsplit-client + ports: + - "80:80" + networks: + - freqnet + depends_on: + backend: + condition: service_healthy + +networks: + freqnet: + driver: bridge + diff --git a/compose-core.yml b/compose-core.yml new file mode 100644 index 0000000..0eef1e7 --- /dev/null +++ b/compose-core.yml @@ -0,0 +1,40 @@ +services: + backend: + build: + context: . + dockerfile: api/Dockerfile + image: joelmathewthomas/freqsplit-api:latest + container_name: freqsplit-api + depends_on: + - redis + networks: + - freqnet + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/ping"] + interval: 5s + timeout: 3s + retries: 24 + + redis: + image: redis:7 + container_name: freqsplit-redis + networks: + - freqnet + + frontend: + build: + context: . + dockerfile: client/Dockerfile + image: joelmathewthomas/freqsplit-client:latest + container_name: freqsplit-client + ports: + - "80:80" + networks: + - freqnet + depends_on: + backend: + condition: service_healthy +networks: + freqnet: + driver: bridge + diff --git a/compose-production.gpu.yml b/compose-production.gpu.yml new file mode 100644 index 0000000..89d463b --- /dev/null +++ b/compose-production.gpu.yml @@ -0,0 +1,9 @@ +services: + backend: + image: ghcr.io/joelmathewthomas/freqsplit-api:latest + build: null + + frontend: + image: ghcr.io/joelmathewthomas/freqsplit-client:latest + build: null + diff --git a/compose-production.yml b/compose-production.yml new file mode 100644 index 0000000..89d463b --- /dev/null +++ b/compose-production.yml @@ -0,0 +1,9 @@ +services: + backend: + image: ghcr.io/joelmathewthomas/freqsplit-api:latest + build: null + + frontend: + image: ghcr.io/joelmathewthomas/freqsplit-client:latest + build: null + diff --git a/docker/celery b/docker/celery new file mode 100755 index 0000000..cdd181e --- /dev/null +++ b/docker/celery @@ -0,0 +1,2 @@ +#!/bin/bash +celery -A backend worker --loglevel=info diff --git a/docker/daphne b/docker/daphne new file mode 100755 index 0000000..c5ca041 --- /dev/null +++ b/docker/daphne @@ -0,0 +1,2 @@ +#!/bin/bash +daphne -b 0.0.0.0 -p 8000 --proxy-headers backend.asgi:application diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..6810fe5 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,28 @@ +server { + listen 80; + server_name _; + + client_max_body_size 100M; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://backend:8000; + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_send_timeout 300; + } + + location /ws/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } +} + diff --git a/docker/wrapper b/docker/wrapper new file mode 100755 index 0000000..1407dff --- /dev/null +++ b/docker/wrapper @@ -0,0 +1,13 @@ +#!/bin/bash + +# Start the first process +./celery & + +# Start the second process +./daphne & + +# Wait for any process to exit +wait -n + +# Exit with status of process that exited first +exit $?