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:
-
Multi-stage build for optimal image size
-
Stage 1 - Build Frontend:
- Use
node:20-alpineas 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/frontenddirectory - Build frontend:
cd packages/frontend && pnpm build - Output will be in
packages/frontend/dist
- Use
-
Stage 2 - Build Backend:
- Use
node:20-alpineas 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/backenddirectory - Generate Prisma client:
cd packages/backend && npx prisma generate - Build backend:
cd packages/backend && pnpm build - Output will be in
packages/backend/dist
- Use
-
Stage 3 - Production Runtime:
- Use
node:20-alpineas base - Install pnpm globally and
servepackage 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.jsontopackages/backend/ - Copy
packages/frontend/package.jsontopackages/frontend/
- Copy
- 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
- From stage 2:
- Copy frontend build artifacts:
- From stage 1:
packages/frontend/dist→/app/packages/frontend/dist
- From stage 1:
- Create uploads directory:
mkdir -p /app/packages/backend/uploads - Expose port 5173
- Create startup script (see Task 2)
- Set CMD to run startup script
- Use
Important Notes:
- Use
.dockerignoreto excludenode_modules,dist,.envfiles - 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:
- Bash script with proper shebang:
#!/bin/sh - Set error handling:
set -e - Environment validation:
- Check required env vars:
DATABASE_URL,SESSION_SECRET - Exit with error message if missing
- Check required env vars:
- Database migration:
- Navigate to backend:
cd /app/packages/backend - Run Prisma migrations:
npx prisma migrate deploy - Optional: Run seed if
RUN_SEED=trueenv var is set
- Navigate to backend:
- Start backend:
- Start backend in background:
node dist/server.js & - Store PID:
BACKEND_PID=$!
- Start backend in background:
- Wait for backend:
- Add 5-second sleep or health check loop
- Ensure backend is ready before starting frontend
- Start frontend:
- Serve frontend on port 5173:
serve -s /app/packages/frontend/dist -l 5173
- Serve frontend on port 5173:
- 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:
- Remove hardcoded host: Change
host: '192.168.1.238'tohost: '0.0.0.0'or remove entirely - Production build: Ensure build outputs to
distdirectory - 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:
- HOST binding: Ensure
HOST = process.env.HOST || '0.0.0.0'(line 85) - PORT: Ensure
PORT = parseInt(process.env.PORT || '3000', 10)(line 16) - CORS origin: Should accept
FRONTEND_URLfrom env (line 24) - 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:
- Transfer image:
scp znakovni.tar user@unraid:/mnt/user/appdata/ - Load image:
docker load -i znakovni.tar - Create container in Unraid UI:
- Repository:
znakovni:latest - Port:
5173→5173(or any host port) - Environment Variables: Add all required vars from
.env.production.example - Volume: Map host path to
/app/packages/backend/uploads
- Repository:
- Configure Caddy reverse proxy:
- Point domain to
http://unraid-ip:5173 - Caddy handles SSL/HTTPS
- Point domain to
Caddy Configuration Example:
znakovni.yourdomain.com {
reverse_proxy http://localhost:5173
}
Task 12: Troubleshooting Build Issues
Build-Time Issues:
-
pnpm install fails:
- Ensure
pnpm-lock.yamlis present - Check Node.js version compatibility (needs Node 20)
- Verify
pnpm-workspace.yamlis copied correctly
- Ensure
-
Frontend build fails:
- Check TypeScript compilation errors
- Verify all dependencies are installed
- Ensure Vite config is correct
- Check for missing environment variables
-
Backend build fails:
- Check TypeScript compilation errors
- Verify Prisma schema is valid
- Ensure all dependencies are installed
-
Prisma generate fails:
- Verify
DATABASE_URLformat is correct (even for build) - Check Prisma schema syntax
- Ensure MySQL provider is specified
- Verify
-
Docker build context too large:
- Verify
.dockerignoreis present and correct - Ensure
node_modulesanddistare excluded
- Verify
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):
-
Database Connection Failed:
- Verify
DATABASE_URLis correct - Ensure MySQL server allows connections from Docker container IP
- Check MySQL user permissions:
GRANT ALL ON znakovni.* TO 'user'@'%';
- Verify
-
Frontend Can't Reach Backend:
- Verify
VITE_API_URL=http://localhost:3000in frontend build - Check backend is listening on
0.0.0.0:3000 - Verify CORS settings in backend
- Verify
-
Container Exits Immediately:
- Check logs:
docker logs znakovni-app - Verify all required env vars are set
- Ensure database is accessible
- Check logs:
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_SECRETto 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)
- ✅ Create
Dockerfilewith multi-stage build - ✅ Create
docker-entrypoint.shstartup script - ✅ Create
.dockerignorefile - ✅ Update
packages/frontend/vite.config.ts - ✅ Create
packages/frontend/.env.production - ✅ Verify
packages/backend/src/server.tsconfiguration - ✅ Create
docker-compose.ymlexample - ✅ Create
.env.production.exampletemplate
Phase 2: Build Docker Image (Task 9)
- ✅ Run
docker build -t znakovni:latest . - ✅ Verify image exists with
docker images | grep znakovni
Phase 3: Optional - Save Image (Task 11)
- ⚠️ 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:
- ✅
Dockerfile- Multi-stage Docker build - ✅
docker-entrypoint.sh- Container startup script (make executable) - ✅
.dockerignore- Exclude unnecessary files from build - ✅
docker-compose.yml- Example compose configuration - ✅
.env.production.example- Production environment template - ✅
packages/frontend/.env.production- Frontend production env
Files to Modify:
- ⚠️
packages/frontend/vite.config.ts- Update host to0.0.0.0 - ✅
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/.prismadirectory to production stage - Copy
prisma/directory for migration files - Run
prisma migrate deployin entrypoint script (NOTmigrate dev)
3. Frontend Build Environment:
- Create
packages/frontend/.env.productionwithVITE_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 stringSESSION_SECRET- Random string (min 32 chars)
Recommended:
NODE_ENV=productionPORT=3000HOST=0.0.0.0FRONTEND_URL=http://localhost:5173
Optional:
- OAuth credentials (if using Google/Microsoft login)
RUN_SEED=true(to seed database on first run)
8. Startup Sequence:
- Container starts → runs
docker-entrypoint.sh - Validate environment variables
- Run Prisma migrations (
migrate deploy) - Start backend in background
- Wait 5 seconds for backend to initialize
- Start frontend server (blocking, keeps container alive)
- Trap signals for graceful shutdown
9. Build Success Checklist:
- All files created (Tasks 1-8)
docker buildcommand runs without errors- Image
znakovni:latestappears indocker imagesoutput - 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:
- Implement Tasks 1-8 (create/modify files)
- Execute Task 9 (build image)
- 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.