diff --git a/README.md b/README.md index 92ed1a90..2fe34797 100644 --- a/README.md +++ b/README.md @@ -389,3 +389,58 @@ For MinIO in production, consider: - Setting up proper networking and access controls - Using multiple MinIO instances for high availability +## Docker Deployment + +### Local Development with Docker + +1. Set up the environment variables: + ```bash + ./update-docker-env.sh + ``` + + This will create a `.env` file with default settings for local development. + +2. Run the application using Docker Compose: + ```bash + docker-compose up -d + ``` + +3. Access the application: + - Frontend: http://localhost:5000 + - Backend API: http://localhost:3000 + +### Remote Server Deployment + +When deploying to a remote server: + +1. Set up the environment variables with your server's hostname: + ```bash + ./update-docker-env.sh your-server-hostname + ``` + + This ensures that the frontend correctly connects to the backend API. + +2. Pull and run the latest Docker images: + ```bash + docker-compose pull + docker-compose up -d + ``` + +3. Access the application through your server's hostname: + - Frontend: http://your-server-hostname:5000 + - Backend API: http://your-server-hostname:3000 + +### Environment Configuration + +The Docker setup uses environment variables to configure the services: + +- Frontend: + - `VITE_API_URL`: URL of the backend API (default: http://backend:3000 for container networking) + +- Backend: + - Database connection parameters + - Storage configuration + - Other backend settings + +For custom configuration, edit the `.env` file or the `update-docker-env.sh` script. + diff --git a/docker-compose.yml b/docker-compose.yml index 02dab3ee..30e9c058 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,8 @@ services: depends_on: backend: condition: service_started + environment: + - VITE_API_URL=${VITE_API_URL:-http://backend:3000} networks: - worklenz @@ -113,7 +115,23 @@ services: - worklenz volumes: - worklenz_postgres_data:/var/lib/postgresql/data - - ./worklenz-backend/database/:/docker-entrypoint-initdb.d + - type: bind + source: ./worklenz-backend/database + target: /docker-entrypoint-initdb.d + consistency: cached + command: > + bash -c ' + if command -v apt-get >/dev/null 2>&1; then + apt-get update && apt-get install -y dos2unix + elif command -v apk >/dev/null 2>&1; then + apk add --no-cache dos2unix + fi && + find /docker-entrypoint-initdb.d -type f -name "*.sh" -exec sh -c '\'' + dos2unix "{}" 2>/dev/null || true + chmod +x "{}" + '\'' \; && + exec docker-entrypoint.sh postgres + ' volumes: worklenz_postgres_data: diff --git a/update-docker-env.sh b/update-docker-env.sh new file mode 100755 index 00000000..5a7fc614 --- /dev/null +++ b/update-docker-env.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Script to set environment variables for Docker deployment +# Usage: ./update-docker-env.sh [hostname] + +# Default hostname if not provided +DEFAULT_HOSTNAME="localhost" +HOSTNAME=${1:-$DEFAULT_HOSTNAME} + +# Create or update root .env file +cat > .env << EOL +# Frontend Configuration +VITE_API_URL=http://${HOSTNAME}:3000 + +# Backend Configuration +DB_HOST=db +DB_PORT=5432 +DB_USER=postgres +DB_PASSWORD=password +DB_NAME=worklenz_db +NODE_ENV=development +PORT=3000 + +# Storage Configuration +AWS_REGION=us-east-1 +STORAGE_PROVIDER=s3 +BUCKET=worklenz-bucket +S3_ACCESS_KEY_ID=minioadmin +S3_SECRET_ACCESS_KEY=minioadmin +S3_URL=http://minio:9000 +EOL + +echo "Environment configuration updated for ${HOSTNAME}" +echo "To run with Docker Compose, use: docker-compose up -d" \ No newline at end of file diff --git a/worklenz-backend/src/app.ts b/worklenz-backend/src/app.ts index 4fead2d9..28ed9095 100644 --- a/worklenz-backend/src/app.ts +++ b/worklenz-backend/src/app.ts @@ -69,7 +69,7 @@ const allowedOrigins = [ app.use(cors({ origin: (origin, callback) => { - if (!origin || allowedOrigins.includes(origin)) { + if (!isProduction() || !origin || allowedOrigins.includes(origin)) { callback(null, true); } else { console.log("Blocked origin:", origin, process.env.NODE_ENV); diff --git a/worklenz-frontend/Dockerfile b/worklenz-frontend/Dockerfile index ac1f820f..512ec923 100644 --- a/worklenz-frontend/Dockerfile +++ b/worklenz-frontend/Dockerfile @@ -7,6 +7,10 @@ COPY package.json package-lock.json ./ RUN npm ci COPY . . + +# Create env-config.js dynamically during build +RUN echo "window.VITE_API_URL='${VITE_API_URL:-http://backend:3000}';" > ./public/env-config.js + RUN npm run build FROM node:22-alpine AS production @@ -16,5 +20,16 @@ WORKDIR /app RUN npm install -g serve COPY --from=build /app/build /app/build +COPY --from=build /app/public/env-config.js /app/build/env-config.js + +# Create a script to start server and dynamically update env-config.js +RUN echo '#!/bin/sh\n\ +# Update env-config.js with runtime environment variables\n\ +cat > /app/build/env-config.js << EOL\n\ +window.VITE_API_URL="${VITE_API_URL:-http://backend:3000}";\n\ +EOL\n\ +exec serve -s build -l 5000' > /app/start.sh && \ +chmod +x /app/start.sh + EXPOSE 5000 -CMD ["serve", "-s", "build", "-l", "5000"] \ No newline at end of file +CMD ["/app/start.sh"] \ No newline at end of file diff --git a/worklenz-frontend/index.html b/worklenz-frontend/index.html index bad8d4a3..86abad6e 100644 --- a/worklenz-frontend/index.html +++ b/worklenz-frontend/index.html @@ -12,6 +12,8 @@ rel="stylesheet" /> Worklenz + + diff --git a/worklenz-frontend/src/api/api-client.ts b/worklenz-frontend/src/api/api-client.ts index e87e0b61..ec43f7a5 100644 --- a/worklenz-frontend/src/api/api-client.ts +++ b/worklenz-frontend/src/api/api-client.ts @@ -2,6 +2,7 @@ import axios, { AxiosError } from 'axios'; import alertService from '@/services/alerts/alertService'; import logger from '@/utils/errorLogger'; +import config from '@/config/env'; export const getCsrfToken = (): string | null => { const match = document.cookie.split('; ').find(cookie => cookie.startsWith('XSRF-TOKEN=')); @@ -16,7 +17,7 @@ export const getCsrfToken = (): string | null => { export const refreshCsrfToken = async (): Promise => { try { // Make a GET request to the server to get a fresh CSRF token - await axios.get(`${import.meta.env.VITE_API_URL}/csrf-token`, { withCredentials: true }); + await axios.get(`${config.apiUrl}/csrf-token`, { withCredentials: true }); return getCsrfToken(); } catch (error) { console.error('Failed to refresh CSRF token:', error); @@ -25,7 +26,7 @@ export const refreshCsrfToken = async (): Promise => { }; const apiClient = axios.create({ - baseURL: import.meta.env.VITE_API_URL, + baseURL: config.apiUrl, withCredentials: true, headers: { 'Content-Type': 'application/json', diff --git a/worklenz-frontend/src/api/home-page/home-page.api.service.ts b/worklenz-frontend/src/api/home-page/home-page.api.service.ts index de83bd70..74f5615a 100644 --- a/worklenz-frontend/src/api/home-page/home-page.api.service.ts +++ b/worklenz-frontend/src/api/home-page/home-page.api.service.ts @@ -6,13 +6,14 @@ import { IHomeTasksModel, IHomeTasksConfig } from '@/types/home/home-page.types' import { IMyTask } from '@/types/home/my-tasks.types'; import { IProject } from '@/types/project/project.types'; import { getCsrfToken } from '../api-client'; +import config from '@/config/env'; const rootUrl = '/home'; const api = createApi({ reducerPath: 'homePageApi', baseQuery: fetchBaseQuery({ - baseUrl: `${import.meta.env.VITE_API_URL}${API_BASE_URL}`, + baseUrl: `${config.apiUrl}${API_BASE_URL}`, prepareHeaders: headers => { headers.set('X-CSRF-Token', getCsrfToken() || ''); headers.set('Content-Type', 'application/json'); diff --git a/worklenz-frontend/src/api/projects/projects.v1.api.service.ts b/worklenz-frontend/src/api/projects/projects.v1.api.service.ts index 8e6be422..1fe279d5 100644 --- a/worklenz-frontend/src/api/projects/projects.v1.api.service.ts +++ b/worklenz-frontend/src/api/projects/projects.v1.api.service.ts @@ -6,13 +6,14 @@ import { IProjectsViewModel } from '@/types/project/projectsViewModel.types'; import { IServerResponse } from '@/types/common.types'; import { IProjectMembersViewModel } from '@/types/projectMember.types'; import { getCsrfToken } from '../api-client'; +import config from '@/config/env'; const rootUrl = '/projects'; export const projectsApi = createApi({ reducerPath: 'projectsApi', baseQuery: fetchBaseQuery({ - baseUrl: `${import.meta.env.VITE_API_URL}${API_BASE_URL}`, + baseUrl: `${config.apiUrl}${API_BASE_URL}`, prepareHeaders: headers => { headers.set('X-CSRF-Token', getCsrfToken() || ''); headers.set('Content-Type', 'application/json'); diff --git a/worklenz-frontend/src/config/env.ts b/worklenz-frontend/src/config/env.ts new file mode 100644 index 00000000..10433940 --- /dev/null +++ b/worklenz-frontend/src/config/env.ts @@ -0,0 +1,31 @@ +/** + * Environment configuration + * Reads from window.VITE_API_URL (set by env-config.js) + * Falls back to import.meta.env.VITE_API_URL (set during build time) + * Falls back to a development default + */ + +declare global { + interface Window { + VITE_API_URL?: string; + } +} + +export const getApiUrl = (): string => { + // First check runtime-injected environment variables + if (window.VITE_API_URL) { + return window.VITE_API_URL; + } + + // Then check build-time environment variables + if (import.meta.env.VITE_API_URL) { + return import.meta.env.VITE_API_URL; + } + + // Default for development + return 'http://localhost:3000'; +}; + +export default { + apiUrl: getApiUrl(), +}; \ No newline at end of file