From 599ed993d395aa23d98159adad1e385b8e81b897 Mon Sep 17 00:00:00 2001 From: Sean Sube Date: Wed, 18 Jun 2025 21:43:34 -0500 Subject: [PATCH] Add Docker support for client and server with Ubuntu-based images, build script, docker-compose, and comprehensive documentation --- DOCKER.md | 253 +++++++++++++++++++++++++++++++++++++++++++ build-docker.sh | 124 +++++++++++++++++++++ client/.dockerignore | 44 ++++++++ client/Dockerfile | 33 ++++++ docker-compose.yml | 46 ++++++++ server/.dockerignore | 48 ++++++++ server/Dockerfile | 40 +++++++ server/tsconfig.json | 3 +- 8 files changed, 590 insertions(+), 1 deletion(-) create mode 100644 DOCKER.md create mode 100755 build-docker.sh create mode 100644 client/.dockerignore create mode 100644 client/Dockerfile create mode 100644 docker-compose.yml create mode 100644 server/.dockerignore create mode 100644 server/Dockerfile diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..519f98d --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,253 @@ +# Docker Setup for Task Receipts + +This document explains how to build and run the Task Receipts application using Docker. + +## Prerequisites + +- Docker installed and running +- Docker Compose (usually included with Docker Desktop) + +## Quick Start + +### Option 1: Using Docker Compose (Recommended) + +1. **Build and run both services:** + ```bash + docker-compose up --build + ``` + +2. **Run in background:** + ```bash + docker-compose up -d --build + ``` + +3. **Stop services:** + ```bash + docker-compose down + ``` + +### Option 2: Using the Build Script + +1. **Build both images:** + ```bash + ./build-docker.sh + ``` + +2. **Build with specific version:** + ```bash + ./build-docker.sh v1.0.0 + ``` + +3. **Run containers individually:** + ```bash + # Run server + docker run -p 4000:4000 task-receipts-server:latest + + # Run client (in another terminal) + docker run -p 80:80 task-receipts-client:latest + ``` + +## Services + +### Server +- **Port:** 4000 +- **Health Check:** GraphQL endpoint at `/graphql` +- **Database:** SQLite (persisted in `./server/data`) +- **Features:** + - GraphQL API + - YAML import/export + - Receipt printing + - Database management + +### Client +- **Port:** 80 +- **Web Server:** Nginx +- **Features:** + - React application + - Material-UI components + - Apollo Client for GraphQL + +## Docker Images + +### Client Image (`task-receipts-client`) +- **Base:** `nginx:stable` (Ubuntu-based) +- **Build:** Multi-stage build with Node.js 18 (Ubuntu-based) +- **Size:** Optimized for production +- **Features:** + - Static file serving + - Gzip compression + - Security headers + +### Server Image (`task-receipts-server`) +- **Base:** `node:18-slim` (Ubuntu-based) +- **Build:** Multi-stage build with Node.js 18 (Ubuntu-based) +- **Size:** Optimized for production +- **Features:** + - Non-root user execution + - Health checks + - Graceful shutdown handling + - Signal handling with dumb-init + +## Development + +### Building Individual Images + +```bash +# Build client only +cd client +docker build -t task-receipts-client:dev . + +# Build server only +cd server +docker build -t task-receipts-server:dev . +``` + +### Development with Docker Compose + +Create a `docker-compose.dev.yml` for development: + +```yaml +version: '3.8' + +services: + server: + build: + context: ./server + dockerfile: Dockerfile + ports: + - "4000:4000" + environment: + - NODE_ENV=development + volumes: + - ./server/src:/app/src + - ./shared:/app/shared + command: npm run dev + + client: + build: + context: ./client + dockerfile: Dockerfile + ports: + - "5173:80" + volumes: + - ./client/src:/app/src + - ./shared:/app/shared +``` + +## Configuration + +### Environment Variables + +The server supports the following environment variables: + +- `NODE_ENV`: Environment (production/development) +- `PORT`: Server port (default: 4000) + +### Database Persistence + +The server's SQLite database is persisted in the `./server/data` directory when using Docker Compose. + +## Troubleshooting + +### Common Issues + +1. **Port conflicts:** + - Ensure ports 80 and 4000 are available + - Modify ports in `docker-compose.yml` if needed + +2. **Build failures:** + - Check Docker is running + - Ensure all source files are present + - Check network connectivity for npm packages + +3. **Client can't connect to server:** + - Verify server is running and healthy + - Check CORS configuration + - Ensure proper network connectivity + +### Logs + +```bash +# View all logs +docker-compose logs + +# View specific service logs +docker-compose logs server +docker-compose logs client + +# Follow logs in real-time +docker-compose logs -f +``` + +### Health Checks + +```bash +# Check service health +docker-compose ps + +# Check individual container health +docker inspect task-receipts-server | grep Health -A 10 +``` + +## Production Deployment + +### Using Docker Compose + +1. **Build and deploy:** + ```bash + docker-compose -f docker-compose.yml up -d --build + ``` + +2. **Update services:** + ```bash + docker-compose pull + docker-compose up -d + ``` + +### Using Individual Containers + +1. **Build images:** + ```bash + ./build-docker.sh v1.0.0 + ``` + +2. **Deploy with orchestration:** + ```bash + # Example with Docker Swarm or Kubernetes + docker stack deploy -c docker-compose.yml task-receipts + ``` + +## Security Considerations + +- Images run as non-root users +- Ubuntu-based images for better security and performance +- No sensitive data in images +- Health checks for monitoring +- Graceful shutdown handling + +## Performance Optimization + +- Multi-stage builds reduce image size +- Nginx for static file serving +- Ubuntu-based images for better performance +- Production-only dependencies in final images +- Layer caching optimization + +## Monitoring + +### Health Checks +- Server: GraphQL endpoint availability +- Client: Nginx process status + +### Metrics +- Container resource usage +- Application logs +- Health check status + +## Support + +For issues with the Docker setup: +1. Check the troubleshooting section +2. Review container logs +3. Verify Docker and Docker Compose versions +4. Ensure all prerequisites are met \ No newline at end of file diff --git a/build-docker.sh b/build-docker.sh new file mode 100755 index 0000000..e5d98d8 --- /dev/null +++ b/build-docker.sh @@ -0,0 +1,124 @@ +#!/bin/bash + +# Build script for Task Receipts Docker images +# This script builds both the client and server Docker images + +set -e # Exit on any error + +# Configuration +PROJECT_NAME="task-receipts" +CLIENT_IMAGE_NAME="${PROJECT_NAME}-client" +SERVER_IMAGE_NAME="${PROJECT_NAME}-server" +VERSION=${1:-latest} # Use first argument as version, default to 'latest' + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to check if Docker is running +check_docker() { + if ! docker info > /dev/null 2>&1; then + print_error "Docker is not running or not accessible" + exit 1 + fi +} + +# Function to build client image +build_client() { + print_status "Building client image..." + + # Build the image from root context with client Dockerfile + docker build -t "${CLIENT_IMAGE_NAME}:${VERSION}" -f client/Dockerfile . + + if [ $? -eq 0 ]; then + print_success "Client image built successfully: ${CLIENT_IMAGE_NAME}:${VERSION}" + else + print_error "Failed to build client image" + exit 1 + fi +} + +# Function to build server image +build_server() { + print_status "Building server image..." + + # Build the image from root context with server Dockerfile + docker build -t "${SERVER_IMAGE_NAME}:${VERSION}" -f server/Dockerfile . + + if [ $? -eq 0 ]; then + print_success "Server image built successfully: ${SERVER_IMAGE_NAME}:${VERSION}" + else + print_error "Failed to build server image" + exit 1 + fi +} + +# Function to show usage +show_usage() { + echo "Usage: $0 [VERSION]" + echo "" + echo "Arguments:" + echo " VERSION Version tag for the Docker images (default: latest)" + echo "" + echo "Examples:" + echo " $0 # Build with 'latest' tag" + echo " $0 v1.0.0 # Build with 'v1.0.0' tag" + echo " $0 dev # Build with 'dev' tag" +} + +# Main execution +main() { + print_status "Starting Docker build process for Task Receipts" + print_status "Version: ${VERSION}" + print_status "Client image: ${CLIENT_IMAGE_NAME}:${VERSION}" + print_status "Server image: ${SERVER_IMAGE_NAME}:${VERSION}" + echo "" + + # Check if Docker is available + check_docker + + # Build client image + build_client + + # Build server image + build_server + + echo "" + print_success "All images built successfully!" + echo "" + print_status "Built images:" + docker images | grep -E "(${CLIENT_IMAGE_NAME}|${SERVER_IMAGE_NAME})" || true + echo "" + print_status "To run the containers:" + echo " docker run -p 80:80 ${CLIENT_IMAGE_NAME}:${VERSION}" + echo " docker run -p 4000:4000 ${SERVER_IMAGE_NAME}:${VERSION}" +} + +# Handle help argument +if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + show_usage + exit 0 +fi + +# Run main function +main \ No newline at end of file diff --git a/client/.dockerignore b/client/.dockerignore new file mode 100644 index 0000000..2f06020 --- /dev/null +++ b/client/.dockerignore @@ -0,0 +1,44 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist/ +build/ + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Logs +logs/ +*.log + +# Coverage +coverage/ + +# Test outputs +test-output/ + +# Docker +Dockerfile +.dockerignore \ No newline at end of file diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..4724286 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,33 @@ +# Build stage +FROM node:18 AS builder + +WORKDIR /app + +# Copy root package files for workspaces +COPY package.json ./ +COPY package-lock.json ./ + +# Copy client and shared code +COPY client/ ./client/ +COPY shared/ ./shared/ + +# Install dependencies for client workspace +RUN npm ci --workspace=client + +# Build the client +WORKDIR /app/client +RUN npm run build + +# Production stage +FROM nginx:stable + +COPY --from=builder /app/client/dist /usr/share/nginx/html + +# Copy nginx configuration (optional - using default for now) +# COPY nginx.conf /etc/nginx/nginx.conf + +# Expose port 80 +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4141796 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,46 @@ +version: '3.8' + +services: + server: + build: + context: ./server + dockerfile: Dockerfile + container_name: task-receipts-server + ports: + - "4000:4000" + environment: + - NODE_ENV=production + volumes: + # Mount database directory for persistence + - ./server/data:/app/data + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:4000/graphql', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - task-receipts-network + + client: + build: + context: ./client + dockerfile: Dockerfile + container_name: task-receipts-client + ports: + - "80:80" + depends_on: + server: + condition: service_healthy + restart: unless-stopped + networks: + - task-receipts-network + +networks: + task-receipts-network: + driver: bridge + +volumes: + server-data: + driver: local \ No newline at end of file diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..b736d45 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,48 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist/ +build/ + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Logs +logs/ +*.log + +# Coverage +coverage/ + +# Test outputs +test-output/ + +# Database files +*.sqlite3 +*.db + +# Docker +Dockerfile +.dockerignore \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..ac1703b --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,40 @@ +# Build stage +FROM node:18 AS builder + +WORKDIR /app + +# Copy root package files for workspaces +COPY package.json ./ +COPY package-lock.json ./ + +# Copy server and shared code +COPY server/ ./server/ +COPY shared/ ./shared/ + +# Install dependencies for server workspace +RUN npm ci --workspace=server + +# Build the server +WORKDIR /app/server +RUN npm run build + +# Production stage +FROM node:18-slim +RUN apt-get update && apt-get install -y dumb-init && rm -rf /var/lib/apt/lists/* +RUN groupadd -r nodejs && useradd -r -g nodejs nodejs +WORKDIR /app/server + +# Copy only server package.json and built code +COPY server/package.json ./ +COPY --from=builder /app/server/dist ./dist +COPY --from=builder /app/shared ../shared + +# Install only production dependencies +RUN npm install --omit=dev && npm cache clean --force +RUN chown -R nodejs:nodejs /app +USER nodejs +EXPOSE 4000 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:4000/graphql', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })" +ENTRYPOINT ["dumb-init", "--"] +CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/server/tsconfig.json b/server/tsconfig.json index 4fb7b70..54eb25c 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -10,7 +10,8 @@ "baseUrl": ".", "paths": { "@shared/*": ["../shared/types/*"] - } + }, + "outDir": "dist" }, "include": ["src/**/*", "../shared/types/**/*"], "exclude": ["node_modules", "dist"]