Redmine-Gitea-Sync/DEPLOYMENT.md

20 KiB

Production Deployment Guide

This guide covers best practices and instructions for deploying the Redmine-Gitea Sync Server in production environments.

Table of Contents


Prerequisites

System Requirements

Minimum:

  • CPU: 1 core
  • RAM: 512 MB
  • Storage: 1 GB
  • OS: Linux (Ubuntu 20.04+, CentOS 8+, Debian 11+)

Recommended:

  • CPU: 2 cores
  • RAM: 2 GB
  • Storage: 5 GB
  • OS: Ubuntu 22.04 LTS or later

Software Requirements

  • Node.js 18.x or later
  • npm 9.x or later
  • Process manager (PM2, systemd, or Docker)
  • Reverse proxy (Nginx or Apache)
  • SSL certificates (Let's Encrypt recommended)

Deployment Options

Node.js process manager with monitoring, auto-restart, and clustering capabilities.

Option 2: Docker

Easiest deployment with consistent environment.

Option 3: Systemd Service

Native Linux service for better OS integration.

Option 4: Bare Metal

Direct execution for maximum control.


Why PM2?

PM2 is a production-ready process manager for Node.js applications with:

  • Automatic restarts on crashes
  • Built-in load balancing
  • Log management
  • Monitoring dashboard
  • Startup scripts for system boot
  • Zero-downtime reloads
  • Memory monitoring

Installation

# Install PM2 globally
npm install -g pm2

# Verify installation
pm2 --version

Initial Setup

# Navigate to application directory
cd /opt/redmine-gitea-sync

# Install dependencies
npm install --production

# Configure ecosystem file
nano ecosystem.config.cjs

Update the env section with your credentials:

env: {
  NODE_ENV: 'production',
  PORT: 3002,
  REDMINE_API_URL: 'https://redmine.example.com',
  REDMINE_API_KEY: 'your_api_key',
  GITEA_API_URL: 'https://gitea.example.com',
  GITEA_TOKEN: 'your_token',
  GITEA_OWNER: 'your_username',
  // ... other settings
}

Start the Application

# Start with PM2
pm2 start ecosystem.config.cjs

# Save the PM2 process list
pm2 save

# Generate startup script
pm2 startup

# Follow the command output, then save again
pm2 save

PM2 Management Commands

# View all processes
pm2 list

# View detailed info
pm2 show redmine-gitea-sync

# View logs in real-time
pm2 logs redmine-gitea-sync

# View last 100 lines
pm2 logs redmine-gitea-sync --lines 100

# Monitor resources
pm2 monit

# Restart application
pm2 restart redmine-gitea-sync

# Reload with zero downtime (for cluster mode)
pm2 reload redmine-gitea-sync

# Stop application
pm2 stop redmine-gitea-sync

# Delete from PM2
pm2 delete redmine-gitea-sync

# Update environment variables
pm2 restart redmine-gitea-sync --update-env

Log Management

# View logs
pm2 logs redmine-gitea-sync

# Flush logs
pm2 flush

# Install log rotation module
pm2 install pm2-logrotate

# Configure log rotation
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true

PM2 with Nginx

PM2 runs on localhost:3002, use Nginx as reverse proxy:

server {
    listen 80;
    server_name sync.example.com;
    
    location / {
        proxy_pass http://localhost:3002;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

Development Mode

For development with verbose logging:

# Start in development mode
pm2 start ecosystem.config.cjs --env development

# Watch for file changes (optional)
pm2 start ecosystem.config.cjs --watch

Monitoring and Alerts

# Install PM2 web interface (optional)
pm2 install pm2-server-monit

# Setup email notifications on errors
pm2 install pm2-auto-pull

Update Application

# Pull latest changes
cd /opt/redmine-gitea-sync
git pull

# Install new dependencies
npm install --production

# Restart with zero downtime
pm2 reload redmine-gitea-sync

# Or restart normally
pm2 restart redmine-gitea-sync

Clustering (Optional)

For high-traffic scenarios, enable cluster mode:

// In ecosystem.config.cjs
instances: 'max',  // or specify number
exec_mode: 'cluster',

Then restart:

pm2 reload redmine-gitea-sync

Security Best Practices

# Run PM2 as dedicated user
sudo useradd -r -s /bin/false pm2user
sudo chown -R pm2user:pm2user /opt/redmine-gitea-sync

# Start PM2 as that user
sudo su - pm2user -s /bin/bash -c "pm2 start /opt/redmine-gitea-sync/ecosystem.config.cjs"

# Save and setup startup
sudo su - pm2user -s /bin/bash -c "pm2 save"
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u pm2user --hp /home/pm2user

Troubleshooting PM2

Issue: Process keeps restarting

# Check error logs
pm2 logs redmine-gitea-sync --err

# Increase restart delay in ecosystem.config.cjs
restart_delay: 5000

Issue: High memory usage

# Check memory
pm2 list

# Adjust memory limit in ecosystem.config.cjs
max_memory_restart: '500M'

Issue: PM2 not starting on boot

# Regenerate startup script
pm2 unstartup
pm2 startup
pm2 save

Docker Deployment

Dockerfile

Create Dockerfile:

FROM node:18-alpine

# Install dependencies for native modules
RUN apk add --no-cache python3 make g++

# Create app directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application files
COPY . .

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001 && \
    chown -R nodejs:nodejs /app

# Switch to non-root user
USER nodejs

# Expose port
EXPOSE 3002

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3002/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); });"

# Start application
CMD ["node", "server.js"]

Docker Compose

Create docker-compose.yml:

version: '3.8'

services:
  sync-server:
    build: .
    container_name: redmine-gitea-sync
    restart: unless-stopped
    ports:
      - "3002:3002"
    environment:
      - NODE_ENV=production
      - PORT=3002
    env_file:
      - .env
    volumes:
      - ./logs:/app/logs
    networks:
      - sync-network
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

networks:
  sync-network:
    driver: bridge

Build and Run

# Build image
docker build -t redmine-gitea-sync .

# Run with docker-compose
docker-compose up -d

# View logs
docker-compose logs -f

# Stop service
docker-compose down

Docker Hub Deployment

# Tag image
docker tag redmine-gitea-sync:latest your-username/redmine-gitea-sync:latest

# Push to Docker Hub
docker push your-username/redmine-gitea-sync:latest

# Pull and run on production server
docker pull your-username/redmine-gitea-sync:latest
docker run -d --name sync-server \
  --env-file .env \
  -p 3002:3002 \
  --restart unless-stopped \
  your-username/redmine-gitea-sync:latest

Systemd Service

Create Service File

Create /etc/systemd/system/redmine-gitea-sync.service:

[Unit]
Description=Redmine-Gitea Sync Server
After=network.target

[Service]
Type=simple
User=sync-user
Group=sync-user
WorkingDirectory=/opt/redmine-gitea-sync
EnvironmentFile=/opt/redmine-gitea-sync/.env
ExecStart=/usr/bin/node /opt/redmine-gitea-sync/server.js
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=redmine-gitea-sync

# Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/redmine-gitea-sync/logs

[Install]
WantedBy=multi-user.target

Setup Instructions

# Create dedicated user
sudo useradd -r -s /bin/false sync-user

# Create application directory
sudo mkdir -p /opt/redmine-gitea-sync
sudo chown sync-user:sync-user /opt/redmine-gitea-sync

# Copy application files
sudo cp -r . /opt/redmine-gitea-sync/
sudo chown -R sync-user:sync-user /opt/redmine-gitea-sync

# Install dependencies
cd /opt/redmine-gitea-sync
sudo -u sync-user npm ci --only=production

# Configure environment
sudo cp .env.example /opt/redmine-gitea-sync/.env
sudo nano /opt/redmine-gitea-sync/.env
sudo chown sync-user:sync-user /opt/redmine-gitea-sync/.env
sudo chmod 600 /opt/redmine-gitea-sync/.env

# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable redmine-gitea-sync
sudo systemctl start redmine-gitea-sync

# Check status
sudo systemctl status redmine-gitea-sync

# View logs
sudo journalctl -u redmine-gitea-sync -f

Service Management

# Start service
sudo systemctl start redmine-gitea-sync

# Stop service
sudo systemctl stop redmine-gitea-sync

# Restart service
sudo systemctl restart redmine-gitea-sync

# Check status
sudo systemctl status redmine-gitea-sync

# Enable auto-start
sudo systemctl enable redmine-gitea-sync

# Disable auto-start
sudo systemctl disable redmine-gitea-sync

# View logs
sudo journalctl -u redmine-gitea-sync -n 100 -f

Nginx Reverse Proxy

Basic Configuration

Create /etc/nginx/sites-available/sync-server:

upstream sync_server {
    server 127.0.0.1:3002;
    keepalive 64;
}

server {
    listen 80;
    server_name sync.example.com;
    
    # Redirect to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name sync.example.com;

    # SSL Configuration
    ssl_certificate /etc/letsencrypt/live/sync.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/sync.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Security Headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Logging
    access_log /var/log/nginx/sync-server-access.log;
    error_log /var/log/nginx/sync-server-error.log;

    # Request size limits
    client_max_body_size 10M;

    # Proxy settings
    location / {
        proxy_pass http://sync_server;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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;
        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 300s;
        proxy_connect_timeout 75s;
    }

    # Health check endpoint (no auth required)
    location /health {
        proxy_pass http://sync_server;
        access_log off;
    }
}

Enable Configuration

# Enable site
sudo ln -s /etc/nginx/sites-available/sync-server /etc/nginx/sites-enabled/

# Test configuration
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

SSL with Let's Encrypt

# Install Certbot
sudo apt install certbot python3-certbot-nginx

# Obtain certificate
sudo certbot --nginx -d sync.example.com

# Auto-renewal is configured automatically
# Test renewal
sudo certbot renew --dry-run

Security Hardening

Environment Variables

# Secure .env file
chmod 600 .env
chown sync-user:sync-user .env

# Never commit .env to version control
echo ".env" >> .gitignore

Firewall Configuration

# Allow only necessary ports
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Rate Limiting (Nginx)

Add to Nginx configuration:

# Define rate limit zone
limit_req_zone $binary_remote_addr zone=webhook:10m rate=10r/s;

# Apply to webhook endpoints
location /webhook/ {
    limit_req zone=webhook burst=20 nodelay;
    proxy_pass http://sync_server;
    # ... other proxy settings
}

Webhook Signature Verification

Implement webhook signature verification for production:

// Add to server.js
import crypto from 'crypto';

function verifyRedmineSignature(req) {
    const signature = req.headers['x-redmine-signature'];
    const secret = process.env.REDMINE_WEBHOOK_SECRET;
    
    if (!signature || !secret) return false;
    
    const hash = crypto
        .createHmac('sha256', secret)
        .update(JSON.stringify(req.body))
        .digest('hex');
    
    return signature === hash;
}

// Add verification to webhook endpoint
app.post('/webhook/redmine', (req, res, next) => {
    if (!verifyRedmineSignature(req)) {
        return res.status(401).json({ error: 'Invalid signature' });
    }
    next();
}, async (req, res) => {
    // ... existing handler
});

IP Whitelisting

# In Nginx configuration
geo $allowed_ip {
    default 0;
    192.168.1.0/24 1;  # Internal network
    10.0.0.0/8 1;       # Private network
    # Add Redmine and Gitea server IPs
}

location /webhook/ {
    if ($allowed_ip = 0) {
        return 403;
    }
    proxy_pass http://sync_server;
}

Monitoring and Logging

Log Rotation

Create /etc/logrotate.d/redmine-gitea-sync:

/opt/redmine-gitea-sync/logs/*.log {
    daily
    rotate 14
    compress
    delaycompress
    notifempty
    missingok
    create 0640 sync-user sync-user
    sharedscripts
    postrotate
        systemctl reload redmine-gitea-sync > /dev/null 2>&1 || true
    endscript
}

Health Monitoring

Simple monitoring script monitor.sh:

#!/bin/bash

HEALTH_URL="http://localhost:3002/health"
MAX_RETRIES=3
RETRY_COUNT=0

while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
    RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" $HEALTH_URL)
    
    if [ "$RESPONSE" -eq 200 ]; then
        echo "$(date): Health check passed"
        exit 0
    fi
    
    RETRY_COUNT=$((RETRY_COUNT + 1))
    sleep 5
done

echo "$(date): Health check failed after $MAX_RETRIES attempts"
# Send alert (email, Slack, etc.)
exit 1

Add to crontab:

# Run health check every 5 minutes
*/5 * * * * /opt/redmine-gitea-sync/monitor.sh >> /var/log/sync-health.log 2>&1

Prometheus Metrics

Install prometheus client:

npm install prom-client

Add metrics endpoint:

import promClient from 'prom-client';

// Create metrics
const register = new promClient.Registry();
const syncCounter = new promClient.Counter({
    name: 'sync_operations_total',
    help: 'Total number of sync operations',
    labelNames: ['direction', 'status'],
    registers: [register]
});

const syncDuration = new promClient.Histogram({
    name: 'sync_duration_seconds',
    help: 'Duration of sync operations',
    labelNames: ['direction'],
    registers: [register]
});

// Expose metrics endpoint
app.get('/metrics', async (req, res) => {
    res.set('Content-Type', register.contentType);
    res.end(await register.metrics());
});

Backup and Recovery

Configuration Backup

#!/bin/bash
# backup.sh

BACKUP_DIR="/backup/sync-server"
DATE=$(date +%Y%m%d-%H%M%S)
APP_DIR="/opt/redmine-gitea-sync"

mkdir -p $BACKUP_DIR

# Backup configuration
tar -czf "$BACKUP_DIR/config-$DATE.tar.gz" \
    "$APP_DIR/.env" \
    "$APP_DIR/package.json" \
    "$APP_DIR/package-lock.json"

# Backup logs
tar -czf "$BACKUP_DIR/logs-$DATE.tar.gz" "$APP_DIR/logs"

# Remove backups older than 30 days
find $BACKUP_DIR -name "*.tar.gz" -mtime +30 -delete

echo "Backup completed: $DATE"

Disaster Recovery

#!/bin/bash
# restore.sh

BACKUP_FILE=$1
RESTORE_DIR="/opt/redmine-gitea-sync"

if [ -z "$BACKUP_FILE" ]; then
    echo "Usage: $0 <backup-file>"
    exit 1
fi

# Stop service
sudo systemctl stop redmine-gitea-sync

# Restore configuration
tar -xzf "$BACKUP_FILE" -C /

# Reinstall dependencies
cd $RESTORE_DIR
npm ci --only=production

# Start service
sudo systemctl start redmine-gitea-sync

echo "Restore completed"

Performance Tuning

Node.js Optimization

# Set Node.js memory limit
NODE_OPTIONS="--max-old-space-size=2048" node server.js

Update systemd service:

[Service]
Environment="NODE_ENV=production"
Environment="NODE_OPTIONS=--max-old-space-size=2048"

Database Connection Pool

If using a database for caching (future):

const pool = {
    min: 2,
    max: 10,
    acquireTimeoutMillis: 30000,
    idleTimeoutMillis: 30000
};

HTTP Keep-Alive

Already configured in axios clients:

const http = require('http');
const https = require('https');

const httpAgent = new http.Agent({ keepAlive: true });
const httpsAgent = new https.Agent({ keepAlive: true });

const client = axios.create({
    httpAgent,
    httpsAgent
});

Nginx Tuning

worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

http {
    # Enable caching
    proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m;
    
    # Compression
    gzip on;
    gzip_vary on;
    gzip_comp_level 6;
    gzip_types text/plain text/css application/json application/javascript;
}

Troubleshooting Production Issues

High Memory Usage

# Check memory usage
ps aux | grep node

# Restart service
sudo systemctl restart redmine-gitea-sync

# Monitor memory
watch -n 5 'ps aux | grep node'

Connection Issues

# Test Redmine connection
curl -H "X-Redmine-API-Key: YOUR_KEY" https://redmine.example.com/issues.json

# Test Gitea connection
curl -H "Authorization: token YOUR_TOKEN" https://gitea.example.com/api/v1/repos

# Check DNS resolution
nslookup redmine.example.com
nslookup gitea.example.com

Webhook Not Triggering

# Check Nginx logs
sudo tail -f /var/log/nginx/sync-server-access.log
sudo tail -f /var/log/nginx/sync-server-error.log

# Check application logs
sudo journalctl -u redmine-gitea-sync -f

# Test webhook manually
curl -X POST http://localhost:3002/webhook/redmine \
  -H "Content-Type: application/json" \
  -d @test-payload.json

Performance Issues

# Check CPU usage
top -p $(pgrep -f "node.*server.js")

# Check network connections
netstat -an | grep :3002

# Analyze slow requests
tail -f /var/log/nginx/sync-server-access.log | awk '{print $4, $7}'

Production Checklist

  • SSL certificates configured and auto-renewal setup
  • Firewall rules configured
  • Environment variables secured (chmod 600 .env)
  • Service configured to start on boot
  • Log rotation configured
  • Monitoring and alerting setup
  • Backup strategy implemented
  • Health checks configured
  • Webhook signatures verified
  • IP whitelisting configured
  • Documentation updated with production URLs
  • Disaster recovery plan documented
  • Performance testing completed
  • Security audit completed

Support and Maintenance

Regular Maintenance Tasks

Weekly:

  • Check logs for errors
  • Verify webhooks are functioning
  • Review sync statistics

Monthly:

  • Update dependencies: npm update
  • Review and rotate logs
  • Test disaster recovery procedures
  • Check SSL certificate expiration

Quarterly:

  • Security audit
  • Performance review
  • Update documentation
  • Backup verification

Getting Help

For production issues:

  1. Check logs: sudo journalctl -u redmine-gitea-sync -n 100
  2. Verify configuration: GET /status
  3. Test connectivity to Redmine and Gitea
  4. Review recent changes
  5. Consult documentation
  6. Open issue in repository with logs and configuration details