diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..c3dd703 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,48 @@ +name: Build and Push to Docker Hub + +on: + push: + tags: + - 'v*.*.*' # Trigger on version tags like v1.0.0, v2.1.3, etc. + create: + tags: + - 'v*.*.*' # Also trigger when tag is created from GitHub UI + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: | + ${{ secrets.DOCKER_USERNAME }}/ghost-drive-ui:${{ steps.version.outputs.VERSION }} + ${{ secrets.DOCKER_USERNAME }}/ghost-drive-ui:latest + cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/ghost-drive-ui:buildcache + cache-to: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/ghost-drive-ui:buildcache,mode=max + build-args: | + BUILDKIT_INLINE_CACHE=1 + + - name: Image digest + run: echo "Image pushed successfully with tag ${{ steps.version.outputs.VERSION }}" + diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000..63d47c1 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,102 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Production build +dist +build + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE files +.vscode +.idea +*.swp +*.swo + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/docker/DEPLOYMENT.md b/docker/DEPLOYMENT.md new file mode 100644 index 0000000..1735d0f --- /dev/null +++ b/docker/DEPLOYMENT.md @@ -0,0 +1,143 @@ +# Docker Hub Deployment Guide + +This project uses GitHub Actions to automatically build and push Docker images to Docker Hub when you create a version tag. + +## 📋 Prerequisites + +### 1. Docker Hub Account +- Create an account at https://hub.docker.com if you don't have one +- Note your Docker Hub username + +### 2. Docker Hub Access Token (Recommended) or Password +**Option A: Access Token (More Secure - Recommended)** +1. Log in to Docker Hub +2. Go to Account Settings → Security → Access Tokens +3. Click "New Access Token" +4. Give it a name (e.g., "GitHub Actions") +5. Copy the token (you won't see it again!) + +**Option B: Password** +- Use your Docker Hub password directly + +## 🔐 GitHub Secrets Setup + +You need to add these secrets to your GitHub repository: + +### Steps: +1. Go to your GitHub repository +2. Click on **Settings** → **Secrets and variables** → **Actions** +3. Click **New repository secret** +4. Add these two secrets: + +| Secret Name | Value | Description | +|-------------|-------|-------------| +| `DOCKER_USERNAME` | Your Docker Hub username | e.g., `johnsmith` | +| `DOCKER_PASSWORD` | Your Docker Hub access token or password | The token/password you created above | + +### Example: +``` +DOCKER_USERNAME = hungnguyen +DOCKER_PASSWORD = dckr_pat_abc123xyz... (access token) +``` + +## 🚀 How to Deploy + +### 1. Make your changes and commit +```bash +git add . +git commit -m "Your commit message" +git push origin master +``` + +### 2. Create and push a version tag +```bash +# Create a tag (follow semantic versioning: v1.0.0, v2.1.3, etc.) +git tag v1.0.0 + +# Push the tag to GitHub +git push origin v1.0.0 +``` + +### 3. GitHub Actions will automatically: +✅ Checkout your code +✅ Build the Docker image +✅ Log in to Docker Hub using your secrets +✅ Push the image with two tags: + - `your-username/ghost-drive:v1.0.0` (version-specific) + - `your-username/ghost-drive:latest` (latest version) + +### 4. Monitor the deployment +- Go to your GitHub repository +- Click on **Actions** tab +- You'll see the workflow running +- Click on it to see detailed logs + +## 📦 Using the Deployed Image + +After successful deployment, anyone can pull and run your image: + +```bash +# Pull the latest version +docker pull YOUR_USERNAME/ghost-drive:latest + +# Or pull a specific version +docker pull YOUR_USERNAME/ghost-drive:v1.0.0 + +# Run the container +docker run -p 3000:80 YOUR_USERNAME/ghost-drive:latest +``` + +Access at: http://localhost:3000 + +## 🏷️ Version Tag Examples + +```bash +# Initial release +git tag v1.0.0 && git push origin v1.0.0 + +# Bug fix +git tag v1.0.1 && git push origin v1.0.1 + +# New feature +git tag v1.1.0 && git push origin v1.1.0 + +# Breaking change +git tag v2.0.0 && git push origin v2.0.0 +``` + +## 🔄 Updating Deployment + +1. Make your code changes +2. Commit and push to master +3. Create a new version tag +4. Push the tag - CI/CD will handle the rest! + +```bash +git add . +git commit -m "Add new feature" +git push origin master +git tag v1.1.0 +git push origin v1.1.0 +``` + +## ❌ Troubleshooting + +### Workflow fails with "authentication required" +- Check that your `DOCKER_USERNAME` and `DOCKER_PASSWORD` secrets are correct +- If using access token, make sure it hasn't expired + +### Tag not triggering workflow +- Make sure your tag follows the pattern `v*.*.*` (e.g., v1.0.0, v2.1.3) +- Check the Actions tab to see if the workflow was triggered + +### Build fails +- Check the Actions logs for detailed error messages +- Make sure your Dockerfile builds successfully locally first + +## 📝 Notes + +- Only tags matching the pattern `v*.*.*` will trigger deployment +- Regular commits without tags will NOT trigger deployment +- Each tag creates both a versioned tag and updates the `latest` tag +- You can view all your images at `https://hub.docker.com/r/YOUR_USERNAME/ghost-drive` + diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..30a45c7 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,33 @@ +# Unified Dockerfile for React/Vite app +FROM node:20-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package.json ./ + +# Install dependencies with cache mount for faster builds +RUN --mount=type=cache,target=/root/.npm \ + npm install --prefer-offline --no-audit + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage with nginx +FROM nginx:alpine + +# Copy built assets from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port 80 +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..ff1932e --- /dev/null +++ b/docker/README.md @@ -0,0 +1,193 @@ +# Docker Setup for Ghost Drive Frontend + +This directory contains Docker configuration files for the Ghost Drive frontend application. + +## Files Overview + +- `Dockerfile` - Multi-stage production build with Nginx +- `Dockerfile.dev` - Development environment setup +- `docker-compose.yml` - Docker Compose configuration +- `nginx.conf` - Nginx configuration for production +- `.dockerignore` - Files to exclude from Docker build context + +## Quick Start + +### Production Build + +Build and run the production version: + +```bash +# Build and start the production container +docker-compose up --build + +# Or run in detached mode +docker-compose up -d --build +``` + +The application will be available at `http://localhost:3000` + +### Development Mode + +Run the development version with hot reload: + +```bash +# Start development container +docker-compose --profile dev up --build + +# Or run in detached mode +docker-compose --profile dev up -d --build +``` + +The development server will be available at `http://localhost:5173` + +## Available Commands + +### Production Commands + +```bash +# Build the production image +docker-compose build + +# Start production containers +docker-compose up + +# Start in detached mode +docker-compose up -d + +# Stop containers +docker-compose down + +# View logs +docker-compose logs -f +``` + +### Development Commands + +```bash +# Build development image +docker-compose --profile dev build + +# Start development container +docker-compose --profile dev up + +# Stop development containers +docker-compose --profile dev down +``` + +### Individual Container Commands + +```bash +# Build production image directly +docker build -t ghost-drive-fe . + +# Run production container +docker run -p 3000:80 ghost-drive-fe + +# Build development image +docker build -f docker/Dockerfile.dev -t ghost-drive-fe-dev . + +# Run development container +docker run -p 5173:5173 -v $(pwd):/app -v /app/node_modules ghost-drive-fe-dev +``` + +## Configuration + +### Environment Variables + +You can customize the application by setting environment variables in a `.env` file or directly in the docker-compose.yml: + +```yaml +environment: + - NODE_ENV=production + - VITE_API_URL=http://localhost:8000 + - VITE_APP_TITLE=Ghost Drive +``` + +### Port Configuration + +- Production: Port 3000 (mapped to container port 80) +- Development: Port 5173 (mapped to container port 5173) + +You can change these ports in the `docker-compose.yml` file. + +### Volume Mounts (Development) + +The development container mounts the current directory to enable hot reload: + +- Source code: `./:/app` +- Node modules: `/app/node_modules` (anonymous volume) + +## Nginx Configuration + +The production build uses Nginx with the following features: + +- Gzip compression for static assets +- Client-side routing support (SPA) +- Static asset caching with 1-year expiry +- Security headers +- Optimized for React/Vite applications + +## Troubleshooting + +### Common Issues + +1. **Port already in use**: Change the port mapping in `docker-compose.yml` +2. **Permission issues**: Ensure Docker has proper permissions +3. **Hot reload not working**: Make sure polling is enabled in `vite.config.ts` + +### Debugging + +```bash +# View container logs +docker-compose logs ghost-drive-fe + +# Execute commands inside container +docker-compose exec ghost-drive-fe sh + +# Inspect container +docker inspect ghost-drive-frontend +``` + +### Cleanup + +```bash +# Remove containers and networks +docker-compose down + +# Remove containers, networks, and volumes +docker-compose down -v + +# Remove images +docker-compose down --rmi all + +# Clean up Docker system +docker system prune -a +``` + +## Production Deployment + +For production deployment, consider: + +1. Using environment-specific docker-compose files +2. Setting up proper SSL/TLS certificates +3. Configuring reverse proxy (if needed) +4. Setting up health checks +5. Implementing proper logging and monitoring + +Example production override: + +```yaml +# docker-compose.prod.yml +version: '3.8' +services: + ghost-drive-fe: + restart: always + environment: + - NODE_ENV=production + labels: + - "traefik.enable=true" + - "traefik.http.routers.ghost-drive.rule=Host(`yourdomain.com`)" +``` + +Run with: `docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d` + diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..ffcf416 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,18 @@ +services: + ghost-drive-fe: + build: + context: .. + dockerfile: docker/Dockerfile + image: ghost-drive:latest + container_name: ghost-drive-frontend + ports: + - "3000:80" + environment: + - NODE_ENV=production + restart: unless-stopped + networks: + - ghost-drive-network + +networks: + ghost-drive-network: + driver: bridge diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..a499856 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,39 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Enable gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied expired no-cache no-store private auth; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/xml+rss + application/json; + + # Handle client routing, return all requests to index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; +} + diff --git a/package.json b/package.json index 5e69ff4..82f1d0f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,10 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "docker:run": "docker-compose -f docker/docker-compose.yml up --build", + "docker:down": "docker-compose -f docker/docker-compose.yml down", + "docker:logs": "docker-compose -f docker/docker-compose.yml logs -f" }, "dependencies": { "@hookform/resolvers": "^3.10.0", diff --git a/vite.config.ts b/vite.config.ts index 19c9890..f8a1a40 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,8 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' import path from 'path' + + // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], @@ -9,5 +11,16 @@ export default defineConfig({ alias: { "@": path.resolve(__dirname, "./src") } - } + }, + server: { + host: '0.0.0.0', + port: 5173, + watch: { + usePolling: true, + }, + }, + preview: { + host: '0.0.0.0', + port: 4173, + }, })