Files
znakovni.hr/dockerize.md

21 KiB

Docker Production Build Guide

Goal

Build a production-ready Docker image containing both the frontend (React/Vite) and backend (Express/Node.js) that connects to an external MySQL database. The container will serve the frontend on port 5173 and proxy API requests to the backend running on localhost:3000 inside the container.

SUCCESS CRITERIA: Docker image is built successfully and ready for deployment.

Architecture

  • Single Docker Container: Contains both FE and BE
  • External Database: MySQL server (not in container)
  • Port Exposure: Only port 5173 (frontend) exposed
  • Internal Communication: FE → BE via localhost:3000 inside container
  • Reverse Proxy: Caddy handles SSL/domain externally (Unraid setup)

Task 1: Create Dockerfile

File: Dockerfile (in project root)

Requirements:

  1. Multi-stage build for optimal image size

  2. Stage 1 - Build Frontend:

    • Use node:20-alpine as base
    • Set working directory to /app
    • Copy package.json, pnpm-lock.yaml, pnpm-workspace.yaml
    • Copy packages/frontend/package.json
    • Install pnpm globally: npm install -g pnpm
    • Install dependencies: pnpm install --frozen-lockfile
    • Copy entire packages/frontend directory
    • Build frontend: cd packages/frontend && pnpm build
    • Output will be in packages/frontend/dist
  3. Stage 2 - Build Backend:

    • Use node:20-alpine as base
    • Set working directory to /app
    • Copy workspace files (same as stage 1)
    • Install pnpm globally
    • Install dependencies: pnpm install --frozen-lockfile
    • Copy entire packages/backend directory
    • Generate Prisma client: cd packages/backend && npx prisma generate
    • Build backend: cd packages/backend && pnpm build
    • Output will be in packages/backend/dist
  4. Stage 3 - Production Runtime:

    • Use node:20-alpine as base
    • Install pnpm globally and serve package globally: npm install -g pnpm serve
    • Set working directory to /app
    • Copy production dependencies setup:
      • Copy package.json, pnpm-workspace.yaml, pnpm-lock.yaml
      • Copy packages/backend/package.json to packages/backend/
      • Copy packages/frontend/package.json to packages/frontend/
    • Install ONLY production dependencies: pnpm install --prod --frozen-lockfile
    • Copy backend build artifacts:
      • From stage 2: packages/backend/dist/app/packages/backend/dist
      • From stage 2: packages/backend/prisma/app/packages/backend/prisma
      • From stage 2: packages/backend/node_modules/.prisma/app/packages/backend/node_modules/.prisma
    • Copy frontend build artifacts:
      • From stage 1: packages/frontend/dist/app/packages/frontend/dist
    • Create uploads directory: mkdir -p /app/packages/backend/uploads
    • Expose port 5173
    • Create startup script (see Task 2)
    • Set CMD to run startup script

Important Notes:

  • Use .dockerignore to exclude node_modules, dist, .env files
  • Ensure Prisma client is generated and copied correctly
  • Backend needs access to Prisma schema for migrations

Task 2: Create Startup Script

File: docker-entrypoint.sh (in project root)

Requirements:

  1. Bash script with proper shebang: #!/bin/sh
  2. Set error handling: set -e
  3. Environment validation:
    • Check required env vars: DATABASE_URL, SESSION_SECRET
    • Exit with error message if missing
  4. Database migration:
    • Navigate to backend: cd /app/packages/backend
    • Run Prisma migrations: npx prisma migrate deploy
    • Optional: Run seed if RUN_SEED=true env var is set
  5. Start backend:
    • Start backend in background: node dist/server.js &
    • Store PID: BACKEND_PID=$!
  6. Wait for backend:
    • Add 5-second sleep or health check loop
    • Ensure backend is ready before starting frontend
  7. Start frontend:
    • Serve frontend on port 5173: serve -s /app/packages/frontend/dist -l 5173
  8. Signal handling:
    • Trap SIGTERM/SIGINT to gracefully shutdown both processes
    • Kill backend PID on exit

Script Template:

#!/bin/sh
set -e

echo "🚀 Starting Znakovni.hr Production Container..."

# Validate environment
if [ -z "$DATABASE_URL" ]; then
  echo "❌ ERROR: DATABASE_URL is required"
  exit 1
fi

# Run migrations
cd /app/packages/backend
echo "📦 Running database migrations..."
npx prisma migrate deploy

# Start backend
echo "🔧 Starting backend server..."
cd /app/packages/backend
node dist/server.js &
BACKEND_PID=$!

# Wait for backend
sleep 5

# Start frontend
echo "🎨 Starting frontend server..."
cd /app
serve -s packages/frontend/dist -l 5173 -n &
FRONTEND_PID=$!

# Trap signals
trap "kill $BACKEND_PID $FRONTEND_PID" SIGTERM SIGINT

# Wait for processes
wait $BACKEND_PID $FRONTEND_PID

Task 3: Create .dockerignore

File: .dockerignore (in project root)

Exclude:

node_modules
dist
.env
.env.local
.env.*.local
*.log
.git
.gitignore
.vscode
.idea
*.md
!README.md
packages/*/node_modules
packages/*/dist
packages/backend/uploads/*
!packages/backend/uploads/.gitkeep
.DS_Store

Task 4: Update Frontend Build Configuration

File: packages/frontend/vite.config.ts

Changes Required:

  1. Remove hardcoded host: Change host: '192.168.1.238' to host: '0.0.0.0' or remove entirely
  2. Production build: Ensure build outputs to dist directory
  3. API proxy: Remove proxy config (not needed in production, handled by env var)

Updated Config:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    host: '0.0.0.0',
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
      },
    },
  },
});

Task 5: Update Frontend API Configuration

File: packages/frontend/.env.production (create new file)

Content:

VITE_API_URL=http://localhost:3000

Note: In production Docker container, frontend connects to backend via localhost since they're in the same container.


Task 6: Update Backend Server Configuration

File: packages/backend/src/server.ts

Verify/Update:

  1. HOST binding: Ensure HOST = process.env.HOST || '0.0.0.0' (line 85)
  2. PORT: Ensure PORT = parseInt(process.env.PORT || '3000', 10) (line 16)
  3. CORS origin: Should accept FRONTEND_URL from env (line 24)
  4. Static uploads: Ensure uploads path is correct (line 57-58)

No changes needed if these are already correct (they are based on codebase retrieval).


Task 7: Create Docker Compose Example

File: docker-compose.yml (in project root)

Purpose:

Provide example for running the container with proper environment variables.

Content:

version: '3.8'

services:
  znakovni:
    build:
      context: .
      dockerfile: Dockerfile
    image: znakovni:latest
    container_name: znakovni-app
    restart: unless-stopped
    ports:
      - "5173:5173"
    environment:
      # Database Configuration
      DATABASE_URL: "mysql://user:password@192.168.1.74:3306/znakovni"
      # Optional: Shadow database for Prisma migrations
      # SHADOW_DATABASE_URL: "mysql://user:password@192.168.1.74:3306/znakovni_shadow"

      # Server Configuration
      NODE_ENV: production
      PORT: 3000
      HOST: 0.0.0.0
      FRONTEND_URL: http://localhost:5173

      # Session Secret (CHANGE THIS!)
      SESSION_SECRET: your-super-secret-session-key-change-in-production-min-32-chars

      # OAuth - Google (optional, configure if using)
      # GOOGLE_CLIENT_ID: your-google-client-id
      # GOOGLE_CLIENT_SECRET: your-google-client-secret
      # GOOGLE_CALLBACK_URL: https://yourdomain.com/api/auth/google/callback

      # OAuth - Microsoft (optional, configure if using)
      # MICROSOFT_CLIENT_ID: your-microsoft-client-id
      # MICROSOFT_CLIENT_SECRET: your-microsoft-client-secret
      # MICROSOFT_CALLBACK_URL: https://yourdomain.com/api/auth/microsoft/callback

      # File Upload
      UPLOAD_DIR: ./uploads
      MAX_FILE_SIZE: 104857600

      # Optional: Run database seed on startup
      # RUN_SEED: "false"

    volumes:
      # Persist uploaded files
      - ./uploads:/app/packages/backend/uploads

    networks:
      - znakovni-network

    # Health check
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

networks:
  znakovni-network:
    driver: bridge

Task 8: Create Production Environment Template

File: .env.production.example (in project root)

Content:

# ===========================================
# ZNAKOVNI.HR PRODUCTION CONFIGURATION
# ===========================================

# Database Configuration (REQUIRED)
# Point to your external MySQL server
DATABASE_URL="mysql://username:password@192.168.1.74:3306/znakovni"

# Optional: Shadow database for Prisma migrations
# SHADOW_DATABASE_URL="mysql://username:password@192.168.1.74:3306/znakovni_shadow"

# Server Configuration
NODE_ENV=production
PORT=3000
HOST=0.0.0.0
FRONTEND_URL=http://localhost:5173

# Session Secret (REQUIRED - CHANGE THIS!)
# Generate with: openssl rand -base64 32
SESSION_SECRET=CHANGE-THIS-TO-A-RANDOM-STRING-MIN-32-CHARACTERS-LONG

# OAuth - Google (Optional)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=https://yourdomain.com/api/auth/google/callback

# OAuth - Microsoft (Optional)
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
MICROSOFT_CALLBACK_URL=https://yourdomain.com/api/auth/microsoft/callback

# File Upload Configuration
UPLOAD_DIR=./uploads
MAX_FILE_SIZE=104857600

# Database Seeding (Optional)
# Set to "true" to run seed on container startup
RUN_SEED=false

Task 9: Build Docker Image

Build Command:

# From project root
docker build -t znakovni:latest .

Verify Build Success:

# Check image exists
docker images | grep znakovni

# Should show:
# znakovni    latest    <image-id>    <time>    <size>

Expected Build Output:

  • Stage 1: Frontend build completes successfully
  • Stage 2: Backend build completes successfully
  • Stage 3: Production image created
  • Image tagged as znakovni:latest
  • No build errors

TASK COMPLETE when image is built and shows in docker images output.


Task 10: Optional - Test Container Locally

NOTE: This task is OPTIONAL. The main goal is to build the image. User will handle deployment.

Run Container (Manual):

docker run -d \
  --name znakovni-app \
  -p 5173:5173 \
  -e DATABASE_URL="mysql://user:password@192.168.1.74:3306/znakovni" \
  -e SESSION_SECRET="your-secret-key-here" \
  -e NODE_ENV=production \
  -v $(pwd)/uploads:/app/packages/backend/uploads \
  znakovni:latest

Run with Docker Compose:

# Edit docker-compose.yml with your database credentials
docker-compose up -d

# View logs
docker-compose logs -f

# Stop
docker-compose down

Verify Container:

# Check if container is running
docker ps

# Check logs
docker logs znakovni-app

# Test backend health
curl http://localhost:5173/api/health

# Test frontend
curl http://localhost:5173

Task 11: Save Docker Image for Deployment

NOTE: This task is for preparing the image for transfer to Unraid. User will handle actual deployment.

Save Image to File:

# Save image to tar file
docker save znakovni:latest -o znakovni.tar

# Verify file created
ls -lh znakovni.tar

Image is Ready for Deployment

The znakovni.tar file can now be:

  • Transferred to Unraid server
  • Loaded with: docker load -i znakovni.tar
  • Deployed by user

DEPLOYMENT REFERENCE (For User)

Unraid Deployment Steps:

  1. Transfer image: scp znakovni.tar user@unraid:/mnt/user/appdata/
  2. Load image: docker load -i znakovni.tar
  3. Create container in Unraid UI:
    • Repository: znakovni:latest
    • Port: 51735173 (or any host port)
    • Environment Variables: Add all required vars from .env.production.example
    • Volume: Map host path to /app/packages/backend/uploads
  4. Configure Caddy reverse proxy:
    • Point domain to http://unraid-ip:5173
    • Caddy handles SSL/HTTPS

Caddy Configuration Example:

znakovni.yourdomain.com {
    reverse_proxy http://localhost:5173
}

Task 12: Troubleshooting Build Issues

Build-Time Issues:

  1. pnpm install fails:

    • Ensure pnpm-lock.yaml is present
    • Check Node.js version compatibility (needs Node 20)
    • Verify pnpm-workspace.yaml is copied correctly
  2. Frontend build fails:

    • Check TypeScript compilation errors
    • Verify all dependencies are installed
    • Ensure Vite config is correct
    • Check for missing environment variables
  3. Backend build fails:

    • Check TypeScript compilation errors
    • Verify Prisma schema is valid
    • Ensure all dependencies are installed
  4. Prisma generate fails:

    • Verify DATABASE_URL format is correct (even for build)
    • Check Prisma schema syntax
    • Ensure MySQL provider is specified
  5. Docker build context too large:

    • Verify .dockerignore is present and correct
    • Ensure node_modules and dist are excluded

Debug Build:

# Build with no cache to see all steps
docker build --no-cache -t znakovni:latest .

# Build with progress output
docker build --progress=plain -t znakovni:latest .

# Check build context size
docker build --no-cache -t znakovni:latest . 2>&1 | grep "Sending build context"

Runtime Issues (If Testing Container):

  1. Database Connection Failed:

    • Verify DATABASE_URL is correct
    • Ensure MySQL server allows connections from Docker container IP
    • Check MySQL user permissions: GRANT ALL ON znakovni.* TO 'user'@'%';
  2. Frontend Can't Reach Backend:

    • Verify VITE_API_URL=http://localhost:3000 in frontend build
    • Check backend is listening on 0.0.0.0:3000
    • Verify CORS settings in backend
  3. Container Exits Immediately:

    • Check logs: docker logs znakovni-app
    • Verify all required env vars are set
    • Ensure database is accessible

Debug Commands (For Running Container):

# Enter running container
docker exec -it znakovni-app sh

# Check backend process
docker exec znakovni-app ps aux | grep node

# Check environment variables
docker exec znakovni-app env | grep DATABASE

REFERENCE: Security Checklist (For User - Deployment Phase)

Before deploying to production:

  • Change SESSION_SECRET to a strong random string (min 32 chars)
  • Use strong MySQL password
  • Configure OAuth credentials if using Google/Microsoft login
  • Update OAuth callback URLs to production domain
  • Ensure MySQL server has firewall rules (only allow necessary IPs)
  • Set up regular database backups
  • Configure Caddy with proper SSL certificates
  • Review and restrict MySQL user permissions
  • Set up monitoring/logging for the container
  • Configure automatic container restarts (restart: unless-stopped)

Summary

This setup creates a fully self-contained Docker image with:

  • Frontend (React/Vite) built and served on port 5173
  • Backend (Express/Node.js) running on internal port 3000
  • Prisma ORM with automatic migrations on startup
  • External MySQL database connection
  • Persistent uploads via Docker volumes
  • Health checks and graceful shutdown
  • Production-optimized multi-stage build
  • Ready for Unraid + Caddy deployment

IMPLEMENTATION PLAN FOR AI AGENTS

Phase 1: Create Required Files (Tasks 1-8)

  1. Create Dockerfile with multi-stage build
  2. Create docker-entrypoint.sh startup script
  3. Create .dockerignore file
  4. Update packages/frontend/vite.config.ts
  5. Create packages/frontend/.env.production
  6. Verify packages/backend/src/server.ts configuration
  7. Create docker-compose.yml example
  8. Create .env.production.example template

Phase 2: Build Docker Image (Task 9)

  1. Run docker build -t znakovni:latest .
  2. Verify image exists with docker images | grep znakovni

Phase 3: Optional - Save Image (Task 11)

  1. ⚠️ OPTIONAL: Save image to tar file for transfer

SUCCESS CRITERIA:

COMPLETE when docker build succeeds and image znakovni:latest exists.

User will handle deployment to Unraid.


Quick Reference: Files to Create/Modify

New Files to Create:

  1. Dockerfile - Multi-stage Docker build
  2. docker-entrypoint.sh - Container startup script (make executable)
  3. .dockerignore - Exclude unnecessary files from build
  4. docker-compose.yml - Example compose configuration
  5. .env.production.example - Production environment template
  6. packages/frontend/.env.production - Frontend production env

Files to Modify:

  1. ⚠️ packages/frontend/vite.config.ts - Update host to 0.0.0.0
  2. packages/backend/src/server.ts - Already correct (verify HOST=0.0.0.0)

Commands to Run After Creating Files:

# Make entrypoint executable
chmod +x docker-entrypoint.sh

# Build the image (THIS IS THE MAIN GOAL)
docker build -t znakovni:latest .

# Verify build success
docker images | grep znakovni

# OPTIONAL: Test run locally
docker-compose up -d

# OPTIONAL: Check logs
docker-compose logs -f

Important Implementation Notes for AI Agents

1. Dockerfile Multi-Stage Build Pattern:

  • Stage 1 (frontend-builder): Build React app with Vite
  • Stage 2 (backend-builder): Build Express app with TypeScript + Prisma
  • Stage 3 (production): Combine built artifacts, minimal runtime

2. Critical Prisma Considerations:

  • Prisma client MUST be generated during build (npx prisma generate)
  • Copy node_modules/.prisma directory to production stage
  • Copy prisma/ directory for migration files
  • Run prisma migrate deploy in entrypoint script (NOT migrate dev)

3. Frontend Build Environment:

  • Create packages/frontend/.env.production with VITE_API_URL=http://localhost:3000
  • This ensures frontend makes API calls to localhost (same container)
  • Vite will embed this at build time

4. Networking Inside Container:

  • Backend binds to 0.0.0.0:3000 (accessible from frontend)
  • Frontend served on 0.0.0.0:5173 (exposed to host)
  • Both processes run in same container, communicate via localhost

5. Database Connection:

  • MySQL server is EXTERNAL (not in container)
  • Container must be able to reach MySQL IP (192.168.1.74)
  • Ensure MySQL allows connections from Docker network
  • Test with: mysql -h 192.168.1.74 -u user -p znakovni

6. Volume Mounting for Uploads:

  • Backend uploads to /app/packages/backend/uploads
  • Mount host directory to persist files across container restarts
  • Example: -v /mnt/user/appdata/znakovni/uploads:/app/packages/backend/uploads

7. Environment Variables Priority:

Required:

  • DATABASE_URL - MySQL connection string
  • SESSION_SECRET - Random string (min 32 chars)

Recommended:

  • NODE_ENV=production
  • PORT=3000
  • HOST=0.0.0.0
  • FRONTEND_URL=http://localhost:5173

Optional:

  • OAuth credentials (if using Google/Microsoft login)
  • RUN_SEED=true (to seed database on first run)

8. Startup Sequence:

  1. Container starts → runs docker-entrypoint.sh
  2. Validate environment variables
  3. Run Prisma migrations (migrate deploy)
  4. Start backend in background
  5. Wait 5 seconds for backend to initialize
  6. Start frontend server (blocking, keeps container alive)
  7. Trap signals for graceful shutdown

9. Build Success Checklist:

  • All files created (Tasks 1-8)
  • docker build command runs without errors
  • Image znakovni:latest appears in docker images output
  • No TypeScript compilation errors
  • No Prisma generation errors
  • Multi-stage build completes all 3 stages

10. Optional Testing Checklist (If Running Container):

  • Container starts and stays running
  • Backend health check responds: curl http://localhost:5173/api/health
  • Frontend loads: curl http://localhost:5173
  • Database connection works (check logs)
  • Uploads persist after container restart

10. Deployment Flow (For User Reference):

# 1. Build image (AI AGENT TASK - MAIN GOAL)
docker build -t znakovni:latest .

# 2. OPTIONAL: Save image (if deploying to different machine)
docker save znakovni:latest -o znakovni.tar

# 3-7. USER HANDLES DEPLOYMENT
# - Transfer to Unraid
# - Load image
# - Create container
# - Configure Caddy
# - Monitor

End of Guide

GOAL: Build Docker image znakovni:latest successfully.

AI AGENT TASKS:

  1. Implement Tasks 1-8 (create/modify files)
  2. Execute Task 9 (build image)
  3. Verify image exists

USER RESPONSIBILITY: Deployment to Unraid and production configuration.

This guide provides complete instructions for dockerizing the Znakovni.hr application. Follow the tasks in order, and refer to the troubleshooting section if build issues arise.