This guide provides comprehensive instructions for deploying zakapp in various environments.
- Prerequisites
- Development Deployment
- Production Deployment
- Environment Configuration
- Security Considerations
- Backup and Recovery
- Monitoring and Maintenance
- Troubleshooting
- Operating System: Linux (Ubuntu 20.04+ recommended), macOS, or Windows with WSL2
- Docker: Version 20.10+ with Docker Compose
- Memory: Minimum 2GB RAM, recommended 4GB+
- Storage: Minimum 10GB free space
- Network: Internet access for initial setup and updates
- Docker Engine 20.10+
- Docker Compose 2.0+
- Git (for source code management)
- OpenSSL (for certificate generation)
Ubuntu/Debian:
# Update system
sudo apt update && sudo apt upgrade -y
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Install Docker Compose
sudo apt install docker-compose-plugin
# Add user to docker group
sudo usermod -aG docker $USER
newgrp docker
# Install Git
sudo apt install gitCentOS/RHEL:
# Install Docker
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Start Docker service
sudo systemctl start docker
sudo systemctl enable docker
# Add user to docker group
sudo usermod -aG docker $USER-
Clone the repository
git clone https://github.com/slimatic/zakapp.git cd zakapp -
Set up environment variables
# Backend configuration cp server/.env.example server/.env # Edit server/.env with your backend settings (PORT, JWT_SECRET, etc.) # Frontend configuration cp client/.env.example client/.env.local # Edit client/.env.local - ensure REACT_APP_API_BASE_URL matches backend PORT
Important: If you change the backend
PORTinserver/.env, you must updateREACT_APP_API_BASE_URLinclient/.env.localto match. -
Install dependencies and start development
# From root directory npm install npm run dev # Or manually start each service: # Terminal 1: cd server && npm install && npm run dev # Terminal 2: cd client && npm install && npm start
-
Access the application
- Frontend: http://localhost:3000 (or your configured PORT)
- Backend API: http://localhost:3001 (or your configured PORT)
- API Health Check: http://localhost:3001/api/health
docker-compose.yml (already configured):
version: '3.8'
services:
frontend:
build:
context: ./frontend
dockerfile: ../docker/Dockerfile.frontend
ports:
- '3000:3000'
volumes:
- ./frontend:/app
- /app/node_modules
environment:
- NODE_ENV=development
- REACT_APP_API_URL=http://localhost:3001
backend:
build:
context: ./backend
dockerfile: ../docker/Dockerfile.backend
ports:
- '3001:3001'
volumes:
- ./backend:/app
- /app/node_modules
- ./backend/data:/app/data
environment:
- NODE_ENV=development
- PORT=3001
- JWT_SECRET=your-jwt-secret-change-in-production-
Prepare the server
# Create application directory sudo mkdir -p /opt/zakapp sudo chown $USER:$USER /opt/zakapp cd /opt/zakapp # Clone the repository git clone https://github.com/slimatic/zakapp.git .
-
Create production configuration
# Copy production compose file cp docker-compose.prod.yml.example docker-compose.prod.yml # Create environment file cp .env.prod.example .env.prod
-
Configure environment variables
# Edit production environment file nano .env.prodRequired production variables:
NODE_ENV=production JWT_SECRET=your-very-secure-jwt-secret-here ENCRYPTION_KEY=your-32-character-encryption-key DATA_DIR=/app/data BACKUP_DIR=/app/backups # Database settings (if using database in future) # DATABASE_URL=your-database-connection-string # Email settings (for notifications) SMTP_HOST=your-smtp-host SMTP_PORT=587 SMTP_USER=your-smtp-username SMTP_PASS=your-smtp-password # Security settings CORS_ORIGIN=https://your-domain.com RATE_LIMIT_MAX=100 RATE_LIMIT_WINDOW=15 # SSL/TLS settings SSL_CERT_PATH=/app/ssl/cert.pem SSL_KEY_PATH=/app/ssl/key.pem
-
Generate SSL certificates
# Create SSL directory mkdir -p ssl # Option 1: Self-signed certificate (for testing) openssl req -x509 -newkey rsa:4096 -keyout ssl/key.pem -out ssl/cert.pem -days 365 -nodes # Option 2: Let's Encrypt (recommended for production) # Install certbot first sudo apt install certbot sudo certbot certonly --standalone -d your-domain.com sudo cp /etc/letsencrypt/live/your-domain.com/fullchain.pem ssl/cert.pem sudo cp /etc/letsencrypt/live/your-domain.com/privkey.pem ssl/key.pem sudo chown $USER:$USER ssl/*
-
Build and deploy
# Build production images docker-compose -f docker-compose.prod.yml build # Start production services docker-compose -f docker-compose.prod.yml up -d # Verify deployment docker-compose -f docker-compose.prod.yml ps docker-compose -f docker-compose.prod.yml logs
docker-compose.prod.yml:
version: '3.8'
services:
frontend:
build:
context: ./frontend
dockerfile: ../docker/Dockerfile.production
restart: unless-stopped
environment:
- NODE_ENV=production
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.zakapp-frontend.rule=Host(`your-domain.com`)'
- 'traefik.http.routers.zakapp-frontend.tls=true'
backend:
build:
context: ./backend
dockerfile: ../docker/Dockerfile.production
restart: unless-stopped
volumes:
- ./data:/app/data
- ./backups:/app/backups
- ./ssl:/app/ssl:ro
environment:
- NODE_ENV=production
env_file:
- .env.prod
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.zakapp-backend.rule=Host(`api.your-domain.com`)'
- 'traefik.http.routers.zakapp-backend.tls=true'
traefik:
image: traefik:v2.10
command:
- '--api.dashboard=true'
- '--providers.docker=true'
- '--entrypoints.web.address=:80'
- '--entrypoints.websecure.address=:443'
- '--certificatesresolvers.letsencrypt.acme.tlschallenge=true'
- '--certificatesresolvers.letsencrypt.acme.email=admin@your-domain.com'
- '--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json'
ports:
- '80:80'
- '443:443'
volumes:
- '/var/run/docker.sock:/var/run/docker.sock:ro'
- './letsencrypt:/letsencrypt'
restart: unless-stopped
volumes:
letsencrypt:If using Nginx instead of Traefik:
nginx.conf:
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /path/to/ssl/cert.pem;
ssl_certificate_key /path/to/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
# Frontend
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend API
location /api/ {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Development (.env):
NODE_ENV=development
JWT_SECRET=dev-jwt-secret
ENCRYPTION_KEY=dev-encryption-key-32-characters
DATA_DIR=./data
FRONTEND_URL=http://localhost:3000
BACKEND_URL=http://localhost:3001Production (.env.prod):
NODE_ENV=production
JWT_SECRET=your-production-jwt-secret-very-long-and-secure
ENCRYPTION_KEY=your-32-character-production-encryption-key
DATA_DIR=/app/data
BACKUP_DIR=/app/backups
FRONTEND_URL=https://your-domain.com
BACKEND_URL=https://api.your-domain.com
SSL_CERT_PATH=/app/ssl/cert.pem
SSL_KEY_PATH=/app/ssl/key.pem
# Precious Metals API (for Nisab calculations)
METALS_API_KEY=your-metals-api-production-keyRequired for Nisab Year Record Feature:
The METALS_API_KEY environment variable is required for the Nisab Year Record feature to function properly. This API key is used to fetch current gold and silver spot prices for calculating Islamic Zakat Nisab thresholds.
# Precious Metals API Configuration
# Sign up at: https://metals-api.com/signup
# Free tier: 50 requests/month (sufficient with 24-hour caching)
METALS_API_KEY=your-metals-api-key-hereSetup Instructions:
- Sign up for a free account at metals-api.com
- Copy your API key from the dashboard
- Add the key to your
.envfile (development) or.env.prod(production) - Restart the backend server to apply changes
Note: The application caches precious metal prices for 24 hours to minimize API calls and stay within the free tier limit (50 requests/month).
The application supports flexible port configuration to prevent "EADDRINUSE" errors and enable multiple instances:
Backend Port Configuration:
# Method 1: Environment variable in .env file
PORT=3002
# Method 2: Command line
PORT=3002 npm run dev
# Method 3: Docker environment
services:
backend:
environment:
- PORT=3002
ports:
- '3002:3002' # Update port mapping accordinglyFrontend Port Configuration:
The React frontend (client/) runs on port 3000 by default. To change it:
# Method 1: Environment variable in client/.env.local
PORT=3010
# Method 2: Command line
PORT=3010 npm start
# Method 3: Create React App will prompt for alternative if 3000 is busyCRITICAL: When changing backend port, update frontend's API URL:
# In client/.env.local
REACT_APP_API_BASE_URL=http://localhost:3002/api # Match backend PORTHandling Port Conflicts:
If you encounter port conflicts, the backend server provides helpful error messages:
❌ Port 3001 is already in use!
To fix this issue, you can:
• Set a different port: PORT=3002 npm run dev
• Or kill the process using port 3001:
- Find the process: lsof -ti:3001
- Kill the process: kill -9 $(lsof -ti:3001)
Production Considerations:
- Use reverse proxy (Nginx/Traefik) to handle external ports (80/443)
- Internal application ports can be any available port
- Update load balancer/proxy configuration when changing internal ports
-
Generate secure secrets
# Generate JWT secret (64 characters) openssl rand -hex 32 # Generate encryption key (32 characters) openssl rand -hex 16
-
Set proper file permissions
# Secure environment files chmod 600 .env.prod # Secure SSL certificates chmod 600 ssl/key.pem chmod 644 ssl/cert.pem # Secure data directory chmod 700 data/
scripts/backup.sh:
#!/bin/bash
BACKUP_DIR="/opt/zakapp/backups"
DATA_DIR="/opt/zakapp/data"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="zakapp_backup_$DATE"
# Create backup directory
mkdir -p "$BACKUP_DIR/$BACKUP_NAME"
# Stop application
docker-compose -f docker-compose.prod.yml stop
# Create backup
tar -czf "$BACKUP_DIR/$BACKUP_NAME/data.tar.gz" -C "$DATA_DIR" .
cp .env.prod "$BACKUP_DIR/$BACKUP_NAME/"
cp docker-compose.prod.yml "$BACKUP_DIR/$BACKUP_NAME/"
# Start application
docker-compose -f docker-compose.prod.yml start
# Clean old backups (keep last 30 days)
find "$BACKUP_DIR" -type d -name "zakapp_backup_*" -mtime +30 -exec rm -rf {} \;
echo "Backup completed: $BACKUP_NAME"# Add to crontab
crontab -e
# Daily backup at 2 AM
0 2 * * * /opt/zakapp/scripts/backup.sh >> /var/log/zakapp-backup.log 2>&1
# Weekly backup cleanup
0 3 * * 0 find /opt/zakapp/backups -type f -name "*.tar.gz" -mtime +7 -delete-
Stop the application
docker-compose -f docker-compose.prod.yml down
-
Restore data
# Extract backup cd /opt/zakapp tar -xzf backups/zakapp_backup_YYYYMMDD_HHMMSS/data.tar.gz -C data/ # Restore configuration cp backups/zakapp_backup_YYYYMMDD_HHMMSS/.env.prod . cp backups/zakapp_backup_YYYYMMDD_HHMMSS/docker-compose.prod.yml .
-
Restart the application
docker-compose -f docker-compose.prod.yml up -d
Health check script:
#!/bin/bash
# Check if containers are running
if ! docker-compose -f docker-compose.prod.yml ps | grep -q "Up"; then
echo "ERROR: Some containers are not running"
exit 1
fi
# Check frontend accessibility
if ! curl -f http://localhost:3000 > /dev/null 2>&1; then
echo "ERROR: Frontend not accessible"
exit 1
fi
# Check backend API
if ! curl -f http://localhost:3001/api/v1/health > /dev/null 2>&1; then
echo "ERROR: Backend API not accessible"
exit 1
fi
echo "All services are healthy"# View application logs
docker-compose -f docker-compose.prod.yml logs -f
# View specific service logs
docker-compose -f docker-compose.prod.yml logs -f backend
docker-compose -f docker-compose.prod.yml logs -f frontend
# Set up log rotation
sudo nano /etc/logrotate.d/zakappLog rotation configuration:
/var/log/zakapp/*.log {
daily
missingok
rotate 30
compress
notifempty
create 644 root root
postrotate
docker-compose -f /opt/zakapp/docker-compose.prod.yml restart >> /var/log/zakapp/restart.log 2>&1 || true
endscript
}
-
Backup current version
./scripts/backup.sh
-
Pull latest changes
git pull origin main
-
Rebuild and deploy
docker-compose -f docker-compose.prod.yml build docker-compose -f docker-compose.prod.yml up -d
-
Verify deployment
docker-compose -f docker-compose.prod.yml ps ./scripts/health-check.sh
Feature 008 introduces the Nisab Year Record workflow for automatic Hawl (lunar year) tracking with finalization and unlocking capabilities. This section covers the deployment and migration requirements.
Required: This feature includes database schema changes that must be migrated.
-
Backup your database
# SQLite backup sqlite3 /path/to/database.db ".backup '/path/to/backup-$(date +%F).db'" # Verify backup sqlite3 /path/to/backup-$(date +%F).db "SELECT name FROM sqlite_master WHERE type='table';"
-
Run Prisma migration
# Navigate to server directory cd server # Run migration in production mode npx prisma migrate deploy # Verify migration npx prisma migrate status
Expected output:
✓ Migration 20240130000000_add_nisab_year_records applied Database schema is up to date! -
Verify schema changes
# Check new tables npx prisma db execute --stdin <<SQL SELECT name FROM sqlite_master WHERE type='table' AND name='NisabYearRecord'; SQL # Should return: NisabYearRecord
# From server directory
npm run migrate:dev
# This will:
# 1. Create migration files
# 2. Apply to development database
# 3. Regenerate Prisma ClientNew requirement: Feature 008 requires the Metals API for live gold/silver pricing.
Add to your server/.env file:
# Metals API Configuration
METALS_API_KEY=your_metals_api_key_here- Sign up: Visit https://metals-api.com/
- Free tier: 50 API calls/month (sufficient for most users)
- Paid tier: Required for high-volume usage (>50 nisab checks/month)
- Copy key: Dashboard → API Key → Copy to clipboard
- Set variable: Add to
server/.env
# Test API connection
curl -X GET "https://metals-api.com/api/latest?access_key=YOUR_KEY&base=USD&symbols=XAU,XAG"
# Expected response:
# {
# "success": true,
# "timestamp": 1706630400,
# "base": "USD",
# "rates": {
# "XAU": 0.0004876,
# "XAG": 0.0423729
# }
# }If API is unavailable, ZakApp uses cached values:
// server/src/config/nisab.ts
export const FALLBACK_NISAB = {
goldPricePerGram: 65.00, // USD per gram
silverPricePerGram: 0.80, // USD per gram
lastUpdated: '2024-01-30'
};Warning: Fallback values may be outdated. Always configure METALS_API_KEY for production.
Feature 008 introduces an hourly background job for Hawl detection.
The job runs automatically every hour:
// server/src/jobs/hawlDetection.ts
cron.schedule('0 * * * *', async () => {
await detectHawlThresholdCrossing();
});-
Check logs
# Docker environment docker-compose -f docker-compose.prod.yml logs -f backend | grep "Hawl detection" # PM2 environment pm2 logs zakapp-backend | grep "Hawl detection"
-
Expected log output
[2024-01-30 14:00:00] INFO: Hawl detection job started [2024-01-30 14:00:05] INFO: Checked 47 users, 3 new Hawl periods detected [2024-01-30 14:00:05] INFO: Hawl detection job completed successfully -
Manual trigger (testing only)
# Execute from server directory node -e "require('./dist/jobs/hawlDetection').detectHawlThresholdCrossing()"
If you need to rollback Feature 008:
# From server directory
cd server
# Identify migration to rollback to
npx prisma migrate status
# Rollback to previous migration
npx prisma migrate resolve --rolled-back 20240130000000_add_nisab_year_records
# Apply previous schema (if backup available)
sqlite3 data/zakapp.db < backup-2024-01-29.sql# Git rollback
git log --oneline # Find commit before Feature 008
git revert <commit-hash>
# Docker rollback
docker-compose -f docker-compose.prod.yml down
git checkout <previous-version-tag>
docker-compose -f docker-compose.prod.yml up -d --build
# PM2 rollback
pm2 stop zakapp-backend
git checkout <previous-version-tag>
npm install
npm run build
pm2 restart zakapp-backend# Remove METALS_API_KEY from server/.env
sed -i '/METALS_API_KEY/d' server/.env
# Restart services
docker-compose -f docker-compose.prod.yml restart backend
# OR
pm2 restart zakapp-backendAfter deploying Feature 008, verify all components:
# Check tables exist
npx prisma db execute --stdin <<SQL
SELECT name FROM sqlite_master WHERE type='table' WHERE name LIKE 'Nisab%';
SQL
# Expected: NisabYearRecord# Get nisab year records (requires authentication)
curl -X GET http://localhost:3000/api/nisab-year-records \
-H "Authorization: Bearer <your-jwt-token>"
# Expected: 200 OK with JSON array# Wait 5 minutes, then check logs
docker-compose -f docker-compose.prod.yml logs -f backend | grep "Hawl detection"
# Should see hourly execution logs# Check API connectivity from backend
docker-compose -f docker-compose.prod.yml exec backend \
curl -X GET "https://metals-api.com/api/latest?access_key=${METALS_API_KEY}&base=USD&symbols=XAU,XAG"
# Expected: JSON response with gold/silver rates- Navigate to http://your-domain.com (or localhost:3001 for dev)
- Login with test user
- Navigate to Nisab Year Records page
- Verify:
- Records list loads
- Status badges display correctly
- Finalize/Unlock buttons work
- Audit trail shows events
# Docker
docker-compose -f docker-compose.prod.yml logs -f --tail=100 backend
# PM2
pm2 logs zakapp-backend --lines 100# Monitor database size
watch -n 60 'du -h data/zakapp.db'
# Check record counts
sqlite3 data/zakapp.db "SELECT COUNT(*) FROM NisabYearRecord;"Monitor Metals API usage:
# Log API calls
grep "Fetching nisab threshold" /var/log/zakapp/backend.log | wc -l
# Alert if approaching limit (50/month on free tier)# Verify hourly execution
grep "Hawl detection job" /var/log/zakapp/backend.log | tail -24
# Should show 24 entries in last 24 hours# Before running Prisma migrate
cp data/zakapp.db data/zakapp-pre-feature-008-$(date +%F).dbAdd to cron:
# Daily backup at 2 AM
0 2 * * * sqlite3 /path/to/zakapp.db ".backup '/backups/zakapp-$(date +\%F).db'" && find /backups -name "zakapp-*.db" -mtime +30 -delete# Test restore
sqlite3 /tmp/test-restore.db < backup-2024-01-30.db
sqlite3 /tmp/test-restore.db "SELECT COUNT(*) FROM NisabYearRecord;"
rm /tmp/test-restore.dbSymptoms: Backend logs show warning messages about missing API key
Solution:
# Add to server/.env
echo "METALS_API_KEY=your_actual_key_here" >> server/.env
# Restart backend
docker-compose -f docker-compose.prod.yml restart backend
# OR
pm2 restart zakapp-backendSymptoms: npx prisma migrate deploy fails
Solution:
# Mark migration as resolved
npx prisma migrate resolve --applied 20240130000000_add_nisab_year_records
# Verify status
npx prisma migrate statusSymptoms: No log entries for hourly job execution
Solution:
# Check backend is running
docker-compose -f docker-compose.prod.yml ps backend
# OR
pm2 list
# Manually trigger job
node -e "require('./dist/jobs/hawlDetection').detectHawlThresholdCrossing()"
# Check for errors in logs
docker-compose -f docker-compose.prod.yml logs backend | grep -i errorSymptoms: GET /api/nisab-year-records returns 404 Not Found
Solution:
# Verify route registration
grep "nisab-year-records" server/src/app.ts
# Check build output
ls -la server/dist/routes/nisabYearRecords.js
# Rebuild and restart
npm run build
docker-compose -f docker-compose.prod.yml restart backend- Database growth: ~1KB per Nisab Year Record
- API calls: 1 hourly call to Metals API
- CPU: Negligible impact (Hawl detection < 1% CPU spike)
- Memory: +10MB for cron job overhead
- Indexes: Prisma automatically creates indexes on foreign keys
- Caching: Nisab thresholds cached for 1 hour
- Batch processing: Hawl detection processes users in batches of 100
# Ensure .env is not committed
git ls-files | grep "\.env$"
# Should return nothing
# Verify .gitignore
grep "\.env" .gitignoreAll sensitive fields encrypted:
totalWealthAtCompletion(AES-256-CBC)notes(AES-256-CBC)unlockReason(AES-256-CBC)
Verify:
# Check encrypted data in database
sqlite3 data/zakapp.db "SELECT totalWealthAtCompletion FROM NisabYearRecord LIMIT 1;"
# Should show encrypted string, not plaintext number- All endpoints require JWT authentication
- Users can only access their own records
- Finalized records require unlock with reason (audit trail)
All state transitions logged:
// Example audit log
{
eventType: 'FINALIZED',
timestamp: '2024-01-30T10:30:00Z',
userId: 'user-123',
recordId: 'record-456',
changes: {
status: { from: 'DRAFT', to: 'FINALIZED' }
}
}Query audit logs:
sqlite3 data/zakapp.db "SELECT * FROM AuditLog WHERE tableName='NisabYearRecord' ORDER BY timestamp DESC LIMIT 10;"- Hawl period: 354 days (lunar year)
- Nisab thresholds: 87.48g gold, 612.36g silver
- Zakat rate: 2.5%
- Locked thresholds prevent manipulation
Verify constants:
grep -r "354\|87\.48\|612\.36\|0\.025" server/src/services/nisabYearRecordService.ts1. Container fails to start
# Check logs
docker-compose -f docker-compose.prod.yml logs service-name
# Check container status
docker-compose -f docker-compose.prod.yml ps
# Restart specific service
docker-compose -f docker-compose.prod.yml restart service-name2. Permission issues with data directory
# Fix ownership
sudo chown -R 1000:1000 data/
# Fix permissions
chmod -R 755 data/3. SSL certificate issues
# Check certificate validity
openssl x509 -in ssl/cert.pem -text -noout
# Renew Let's Encrypt certificate
sudo certbot renew4. Memory issues
# Check memory usage
docker stats
# Increase swap if needed
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile-
Enable Docker BuildKit
export DOCKER_BUILDKIT=1 -
Optimize images
# Prune unused images docker image prune -a # Prune unused volumes docker volume prune
-
Monitor resource usage
# Real-time monitoring docker stats # System monitoring htop iotop
-
Regular updates
# Update system packages sudo apt update && sudo apt upgrade # Update Docker images docker-compose -f docker-compose.prod.yml pull docker-compose -f docker-compose.prod.yml up -d
-
Firewall configuration
# Enable UFW sudo ufw enable # Allow SSH sudo ufw allow ssh # Allow HTTP/HTTPS sudo ufw allow 80 sudo ufw allow 443 # Block direct access to application ports sudo ufw deny 3000 sudo ufw deny 3001
-
Fail2ban for additional protection
sudo apt install fail2ban sudo systemctl enable fail2ban sudo systemctl start fail2ban
This deployment guide provides comprehensive instructions for setting up zakapp in various environments with proper security, monitoring, and maintenance procedures.