diff --git a/.gitignore b/.gitignore index 1801e9a..6f3fa4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Configuration files with sensitive data backup.env config/restic.conf +services/*/.env # Logs *.log diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 0c1e552..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,132 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Critical Convention - -**Use the unified interface scripts that auto-source configuration:** - -```bash -# Installation and setup -./install # Complete setup (config + repository) -./install config # Generate configuration only -./install repo # Initialize repository only - -# All backup operations via manage -./manage list # List backup timers -./manage install # Install service timer -./manage run # Manual backup -./manage restore # Restore (test mode) -./manage restore-prod # Restore (production) -``` - -**NEVER call scripts in backup/ directly** - they are protected and will error. Always use `./manage` or `./install`. - -## Common Commands - -### Initial Setup -```bash -cp backup.env.sample backup.env # Copy configuration template -# Edit backup.env to match environment -./install # Complete installation (config + repository) -``` - -### Service Management -```bash -./manage list # List all backup timers -./manage status # Service backup status -./manage run # Manual backup execution -./manage logs # View backup logs -./manage available # List services with backup.sh -sudo ./manage install # Install systemd timer -``` - -### Backup Operations -```bash -./manage snapshots [service] # List available snapshots -./manage restore # Test restoration -./manage restore-prod # Production restoration -``` - -### Configuration Testing -```bash -# Source configuration manually for utility functions -source backup.env -show_config # Display current configuration -validate_paths # Check directory existence -``` - -## Architecture - -### Configuration System -- **`backup.env`**: Master configuration file at project root containing all environment variables -- **`config/restic.conf`**: Generated Restic-specific configuration (created by `gen-conf.sh`) -- **Variable substitution**: systemd templates use `${VAR}` placeholders replaced during installation - -### Core Components -- **`./manage`**: Primary interface for all backup operations and service management (auto-sources config) -- **`./install`**: Installation script consolidating configuration generation and repository initialization -- **`backup/install-service`**: Systemd timer installer (called via `./manage install`) -- **`backup/restore`**: Advanced restoration tool (called via `./manage restore/restore-prod`) -- **Templates**: `service-backup@.service` and `service-backup@.timer` are systemd unit templates - -### Variable Override Pattern -Configuration uses environment variable defaults with override capability: -```bash -BACKUP_USER="${BACKUP_USER:-citadel}" -PROJECT_ROOT="${PROJECT_ROOT:-/home/nicolas/dev/quantumrick}" -``` - -Users can customize by setting environment variables before sourcing `backup.env`. - -### Systemd Integration -- Templates in `backup/` directory contain `${VARIABLE}` placeholders -- `install-service` script performs `sed` substitution to generate final systemd units -- Generated units placed in `/etc/systemd/system/` with proper permissions - -### Security Model -- Restic passwords auto-generated with OpenSSL -- Configuration files have restricted permissions (600) -- Scripts validate directory existence before operations -- Restoration includes test mode for safety - -## Key Files and Their Roles - -### Configuration Layer -- `backup.env`: Master configuration with all variables and utility functions -- `config/restic.conf`: Generated Restic authentication and repository settings - -### Operational Scripts -- `./manage`: Main interface (list, status, run, logs commands) - auto-sources configuration -- `./install`: Consolidated installation script (config + repository) -- `backup/install-service`: Systemd timer installation with template substitution -- `backup/list-snapshots`: Snapshot browsing utility -- `backup/restore`: Production-grade restoration tool - -### Templates and Restoration -- `backup/service-backup@.{service,timer}`: Systemd unit templates with variable placeholders - -## Development Guidelines - -### Script Protection -All scripts in `backup/` are protected against direct execution: -- Use `CALLED_FROM_MANAGE` environment variable check -- Scripts error with helpful message if called directly -- Always route through `./manage` interface - -### Adding New Scripts -1. If adding scripts to `backup/`, include protection check: - ```bash - if [ "${CALLED_FROM_MANAGE:-}" != "true" ]; then - echo "ERROR: Use ./manage instead" - exit 1 - fi - ``` -2. Add corresponding command to `./manage` with `CALLED_FROM_MANAGE=true` -3. Follow existing error handling and logging patterns - -### Configuration Changes -- All path variables should have environment override capability -- Maintain backward compatibility with default values -- Update both `backup.env` and this documentation if adding new variables -- Test with both `./manage` and `./install` interfaces \ No newline at end of file diff --git a/claude.md b/claude.md new file mode 100644 index 0000000..e20e3e6 --- /dev/null +++ b/claude.md @@ -0,0 +1,12 @@ +#My name is Nicolas + +This file provides guidance to Claude Code when working with this repository. + +This is a pack of script for manage a self hosted server. + +I interact with you in french, you can anwser in french +IMPORTANT all code comment and documentation must be write in english +IMPORTANT do not analyse file mentioned in .gitignore + +Keep things simple +Use shell script diff --git a/services/nextcloud/docker-compose.yml b/services/nextcloud/docker-compose.yml new file mode 100644 index 0000000..60fd80c --- /dev/null +++ b/services/nextcloud/docker-compose.yml @@ -0,0 +1,66 @@ +services: + nextcloud: + image: lscr.io/linuxserver/nextcloud:latest + container_name: nextcloud + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - /home/citadel/data/nextcloud/config:/config + - /home/citadel/data/nextcloud/data:/data + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + networks: + - services + - nextcloud_internal + restart: unless-stopped + depends_on: + - nextcloud_db + - nextcloud_redis + labels: + - "com.docker.compose.project=nextcloud" + - "backup.enable=true" + - "backup.path=/config,/data" + + nextcloud_db: + image: postgres:16-alpine + container_name: nextcloud_db + environment: + - POSTGRES_DB=${DB_NAME} + - POSTGRES_USER=${DB_USER} + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C + volumes: + - nextcloud_db_data:/var/lib/postgresql/data + networks: + - nextcloud_internal + restart: unless-stopped + labels: + - "com.docker.compose.project=nextcloud" + - "backup.enable=true" + + nextcloud_redis: + image: redis:7-alpine + container_name: nextcloud_redis + command: redis-server --requirepass ${REDIS_PASSWORD} + volumes: + - nextcloud_redis_data:/data + networks: + - nextcloud_internal + restart: unless-stopped + labels: + - "com.docker.compose.project=nextcloud" + +volumes: + nextcloud_db_data: + name: nextcloud_db_data + nextcloud_redis_data: + name: nextcloud_redis_data + +networks: + services: + external: true + nextcloud_internal: + driver: bridge + name: nextcloud_internal diff --git a/services/nextcloud/generate-secret.sh b/services/nextcloud/generate-secret.sh new file mode 100644 index 0000000..6e9aeb4 --- /dev/null +++ b/services/nextcloud/generate-secret.sh @@ -0,0 +1,67 @@ +#!/bin/bash +set -e + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_header() { + echo -e "${BLUE}$1${NC}" +} + +# Function to generate secure password +generate_password() { + openssl rand -base64 32 | tr -d "=+/" | cut -c1-25 +} + +log_header "Generating Nextcloud secrets" +echo "=============================" + +ENV_FILE=".env" + +# Check if .env file exists +if [ ! -f "$ENV_FILE" ]; then + log_warn ".env file not found. Please ensure it exists first." + exit 1 +fi + +# Generate passwords +log_info "Generating secure passwords..." +DB_PASSWORD=$(generate_password) +REDIS_PASSWORD=$(generate_password) + +# Backup existing .env file +log_info "Creating backup of existing .env file..." +cp "$ENV_FILE" "${ENV_FILE}.backup.$(date +%Y%m%d_%H%M%S)" + +# Update .env file +log_info "Updating .env file with generated passwords..." +sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=${DB_PASSWORD}/" "$ENV_FILE" +sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=${REDIS_PASSWORD}/" "$ENV_FILE" + +# Display generated passwords (for reference) +echo "" +log_header "Generated credentials:" +echo "======================" +echo "Database Password: $DB_PASSWORD" +echo "Redis Password: $REDIS_PASSWORD" +echo "" +log_info "Passwords have been saved to $ENV_FILE" +log_info "A backup of the previous .env file has been created" + +# Security reminder +echo "" +log_warn "SECURITY REMINDER:" +echo "- Keep these passwords secure" +echo "- Do not share them in version control" +echo "- The backup file also contains these passwords" diff --git a/services/paperless/.env.sample b/services/paperless/.env.sample new file mode 100644 index 0000000..a3e20ca --- /dev/null +++ b/services/paperless/.env.sample @@ -0,0 +1,80 @@ +# Paperless-NGX Environment Configuration Sample +# Copy this file to .env and adapt the values according to your environment + +# === Database Configuration === +# PostgreSQL database name +POSTGRES_DB=paperless + +# PostgreSQL username +POSTGRES_USER=paperless + +# PostgreSQL password (change this to a secure password) +POSTGRES_PASSWORD=your_secure_database_password_here + +# === Docker Container Configuration === +# User ID mapping for container (use your user ID) +USERMAP_UID=1000 + +# Group ID mapping for container (use your group ID) +USERMAP_GID=1000 + +# === Data Directory Configuration === +# Base directory for paperless data storage +# Adjust the path according to your system +PAPERLESS_DATA_DIR=/home/your_user/data/paperless + +# === Paperless-NGX Configuration === +# Public URL for accessing the paperless service +# Replace with your actual domain or IP address +PAPERLESS_URL=https://paperless.yourdomain.com + +# CSRF trusted origins (add your domain if different from PAPERLESS_URL) +# Example: https://paperless.yourdomain.com,https://paperless.internal.com +PAPERLESS_CSRF_TRUSTED_ORIGINS= + +# Time zone for the application +# See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones +PAPERLESS_TIME_ZONE=Europe/Brussels + +# Primary OCR language (3-letter code) +# Common values: eng (English), fra/fre (French), deu (German), spa (Spanish) +PAPERLESS_OCR_LANGUAGE=eng + +# Available OCR languages (space-separated list) +# Include all languages you want to support for document recognition +PAPERLESS_OCR_LANGUAGES="eng fra" + +# === Optional Configuration === +# Uncomment and configure as needed: + +# Secret key for Django (generate a secure random string) +# PAPERLESS_SECRET_KEY=your_32_character_secret_key_here + +# Admin user configuration (for initial setup) +# PAPERLESS_ADMIN_USER=admin +# PAPERLESS_ADMIN_PASSWORD=your_admin_password +# PAPERLESS_ADMIN_MAIL=admin@yourdomain.com + +# Additional security settings +# PAPERLESS_ALLOWED_HOSTS=paperless.yourdomain.com,localhost,127.0.0.1 +# PAPERLESS_CORS_ALLOWED_HOSTS=https://paperless.yourdomain.com + +# Email configuration (for notifications) +# PAPERLESS_EMAIL_HOST=smtp.yourdomain.com +# PAPERLESS_EMAIL_PORT=587 +# PAPERLESS_EMAIL_HOST_USER=paperless@yourdomain.com +# PAPERLESS_EMAIL_HOST_PASSWORD=your_email_password +# PAPERLESS_EMAIL_USE_TLS=true +# PAPERLESS_DEFAULT_FROM_EMAIL=paperless@yourdomain.com + +# Consumer configuration +# PAPERLESS_CONSUMER_POLLING=0 +# PAPERLESS_CONSUMER_DELETE_DUPLICATES=true +# PAPERLESS_CONSUMER_RECURSIVE=true + +# === Notes === +# 1. Ensure the PAPERLESS_DATA_DIR exists and has proper permissions +# 2. Change all default passwords to secure values +# 3. Adjust USERMAP_UID and USERMAP_GID to match your system user +# 4. Update PAPERLESS_URL to your actual domain or IP address +# 5. Configure OCR languages based on your document languages \ No newline at end of file diff --git a/services/paperless/backup.sh b/services/paperless/backup.sh new file mode 100755 index 0000000..085e4f6 --- /dev/null +++ b/services/paperless/backup.sh @@ -0,0 +1,149 @@ +#!/bin/bash + +# Generic Service Backup Script for Docker Compose services +# Location: /home/citadel/services/paperless/backup.sh +# Runs daily at 3 AM via systemd timer + +set -e + +# Service Configuration +SERVICE_NAME="paperless" +SERVICE_DIR="/home/citadel/services/$SERVICE_NAME" +DATA_DIR="/home/citadel/data/$SERVICE_NAME" +TEMP_BACKUP_DIR="/tmp/$SERVICE_NAME" +CONFIG_FILE="/home/citadel/backup/restic.conf" +COMPOSE_FILE="$SERVICE_DIR/docker-compose.yml" + +# Logging +LOG_FILE="/var/log/$SERVICE_NAME-backup.log" +exec 1> >(tee -a "$LOG_FILE") +exec 2> >(tee -a "$LOG_FILE" >&2) + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +cleanup() { + log "Cleaning up temporary files..." + rm -rf "$TEMP_BACKUP_DIR" + + # Ensure containers are running in case of error + if docker compose -f "$COMPOSE_FILE" ps --services --filter "status=exited" | grep -q .; then + log "Some containers are stopped, restarting..." + cd "$SERVICE_DIR" + docker compose up -d + fi +} + +# Set up cleanup on exit +trap cleanup EXIT + +log "=== Starting $SERVICE_NAME Backup ===" + +# Check if configuration exists +if [ ! -f "$CONFIG_FILE" ]; then + log "ERROR: Configuration file $CONFIG_FILE not found!" + exit 1 +fi + +# Source Restic configuration +source "$CONFIG_FILE" + +# Create temporary backup directory +log "Creating temporary backup directory: $TEMP_BACKUP_DIR" +mkdir -p "$TEMP_BACKUP_DIR" + +# Navigate to service directory +cd "$SERVICE_DIR" + +# Check if compose file exists +if [ ! -f "$COMPOSE_FILE" ]; then + log "ERROR: Docker compose file $COMPOSE_FILE not found!" + exit 1 +fi + +log "Stopping $SERVICE_NAME containers..." +docker compose down + +# Wait a moment for containers to fully stop +sleep 5 + +log "Creating PostgreSQL database dump..." +# Start only the database container for backup +docker compose up -d db + +# Wait for database to be ready +sleep 10 + +# Get database credentials from .env +if [ -f "$SERVICE_DIR/.env" ]; then + source "$SERVICE_DIR/.env" +else + log "ERROR: .env file not found!" + exit 1 +fi + +# Create database dump +DUMP_FILE="$TEMP_BACKUP_DIR/${SERVICE_NAME}_db_$(date +%Y%m%d_%H%M%S).sql" +log "Creating database dump: $DUMP_FILE" + +docker compose exec -T db pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" > "$DUMP_FILE" + +if [ $? -eq 0 ]; then + log "✅ Database dump created successfully" +else + log "❌ Database dump failed!" + exit 1 +fi + +# Stop database container +docker compose down + +log "Copying application data to temporary directory..." +# Copy data directories +cp -r "$DATA_DIR"/* "$TEMP_BACKUP_DIR/" 2>/dev/null || true + +# Copy service configuration +cp -r "$SERVICE_DIR" "$TEMP_BACKUP_DIR/service_config" + +log "Creating Restic backup..." + +restic backup "$TEMP_BACKUP_DIR" \ + --tag "$SERVICE_NAME" \ + --tag "daily" + +if [ $? -eq 0 ]; then + log "✅ Restic backup completed successfully with tag: $SERVICE_NAME" +else + log "❌ Restic backup failed!" + exit 1 +fi + +log "Restarting $SERVICE_NAME containers..." +docker compose up -d + +# Wait for services to be ready +sleep 15 + +# Check if services are running +if docker compose ps --services --filter "status=running" | grep -q "webserver"; then + log "✅ $SERVICE_NAME containers restarted successfully" +else + log "⚠️ Warning: Some containers may not be running properly" +fi + +log "Running Restic maintenance (forget old snapshots)..." +# Keep: 7 daily, 4 weekly, 12 monthly, 2 yearly +restic forget \ + --tag "$SERVICE_NAME" \ + --keep-daily 7 \ + --keep-weekly 4 \ + --keep-monthly 12 \ + --keep-yearly 2 \ + --prune + +log "=== Backup completed successfully ===" + +# Show backup statistics +log "Current repository stats:" +restic stats --mode raw-data diff --git a/services/paperless/docker-compose.yml b/services/paperless/docker-compose.yml new file mode 100644 index 0000000..9b25a84 --- /dev/null +++ b/services/paperless/docker-compose.yml @@ -0,0 +1,79 @@ +services: + broker: + image: docker.io/library/redis:8 + restart: unless-stopped + volumes: + - redisdata:/data + networks: + - paperless-internal + + db: + image: postgres:13 + restart: unless-stopped + volumes: + - paperless-ngx_pgdata:/var/lib/postgresql/data + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + networks: + - paperless-internal + + webserver: + image: ghcr.io/paperless-ngx/paperless-ngx:2.14.1 + restart: unless-stopped + depends_on: + - db + - broker + - gotenberg + - tika + volumes: + - ${PAPERLESS_DATA_DIR}/data:/usr/src/paperless/data + - ${PAPERLESS_DATA_DIR}/media:/usr/src/paperless/media + - ${PAPERLESS_DATA_DIR}/export:/usr/src/paperless/export + - ${PAPERLESS_DATA_DIR}/consume:/usr/src/paperless/consume + env_file: .env + environment: + PAPERLESS_REDIS: redis://broker:6379 + PAPERLESS_DBHOST: db + PAPERLESS_DBNAME: ${POSTGRES_DB} + PAPERLESS_DBUSER: ${POSTGRES_USER} + PAPERLESS_DBPASS: ${POSTGRES_PASSWORD} + PAPERLESS_TIKA_ENABLED: 1 + PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000 + PAPERLESS_TIKA_ENDPOINT: http://tika:9998 + networks: + - paperless-internal + - services + - mail + + gotenberg: + image: docker.io/gotenberg/gotenberg:8.20 + restart: unless-stopped + command: + - "gotenberg" + - "--chromium-disable-javascript=true" + - "--chromium-allow-list=file:///tmp/.*" + networks: + - paperless-internal + + tika: + image: docker.io/apache/tika:latest + restart: unless-stopped + networks: + - paperless-internal + +volumes: + paperless-ngx_pgdata: + external: true + redisdata: + +networks: + paperless-internal: + name: paperless-internal + services: + external: true + name: services + mail: + external: true + name: mail diff --git a/services/paperless/restore b/services/paperless/restore new file mode 100755 index 0000000..deb60ff --- /dev/null +++ b/services/paperless/restore @@ -0,0 +1,594 @@ +#!/bin/bash + +# Paperless Restore Script +# Location: /home/citadel/services/paperless/restore +# Usage: ./restore --test|--production|--extract +# ./restore --clean +# ./restore --clean-all + +set -euo pipefail + +# Configuration +readonly SERVICE_NAME="paperless" +readonly SERVICE_DIR="/home/citadel/services/$SERVICE_NAME" +readonly TEST_DIR="$SERVICE_DIR/test-restore" +readonly CONFIG_FILE="/home/citadel/backup/restic.conf" +readonly SCRIPT_NAME=$(basename "$0") + +# Colors +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly BLUE='\033[0;34m' +readonly NC='\033[0m' + +# Logging functions +log() { echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; } +success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } + +# Global variables +SNAPSHOT_ID="" +MODE="" +TEMP_DIR="" + +# Show help +show_help() { + cat << EOF +Usage: $SCRIPT_NAME --test|--production|--extract + $SCRIPT_NAME --clean|--clean-all + +Arguments: + snapshot_id The restic snapshot ID to restore from + +Options: + --test Restore to test instance (isolated environment) + --production Restore to production instance (NOT YET IMPLEMENTED) + --extract Extract snapshot to /tmp directory only + --clean Clean test environment (stop containers and remove volumes) + --clean-all Clean test environment AND remove all temp directories + -h, --help Show this help message + +Examples: + $SCRIPT_NAME abc123 --test # Restore snapshot abc123 to test instance + $SCRIPT_NAME abc123 --extract # Extract snapshot abc123 to /tmp only + $SCRIPT_NAME latest --production # Restore latest snapshot to production + $SCRIPT_NAME --clean # Clean test environment + $SCRIPT_NAME --clean-all # Clean test environment + temp directories + +The test instance will be created in: $TEST_DIR +EOF +} + +# Cleanup function +cleanup() { + if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ] && [[ "$TEMP_DIR" == /tmp/* ]]; then + log "Cleaning up temporary directory: $TEMP_DIR" + rm -rf "$TEMP_DIR" + fi +} + +# Set up cleanup on exit (only for extract mode) +setup_cleanup() { + if [ "$MODE" = "extract" ]; then + trap cleanup EXIT + fi +} + +# Parse arguments +parse_arguments() { + if [ $# -eq 0 ]; then + show_help + exit 0 + fi + + while [ $# -gt 0 ]; do + case "$1" in + --test) + MODE="test" + shift + ;; + --production) + MODE="production" + shift + ;; + --extract) + MODE="extract" + shift + ;; + --clean) + MODE="clean" + shift + ;; + --clean-all) + MODE="clean-all" + shift + ;; + -h|--help) + show_help + exit 0 + ;; + -*) + error "Unknown option: $1" + ;; + *) + if [ -z "$SNAPSHOT_ID" ]; then + SNAPSHOT_ID="$1" + else + error "Too many arguments" + fi + shift + ;; + esac + done + + # Validate required arguments + if [ "$MODE" != "clean" ] && [ "$MODE" != "clean-all" ]; then + if [ -z "$SNAPSHOT_ID" ]; then + error "Snapshot ID is required (except for --clean or --clean-all mode)" + fi + fi + + if [ -z "$MODE" ]; then + error "Mode (--test, --production, --extract, --clean, or --clean-all) is required" + fi + + # Clean modes don't need snapshot ID + if [ "$MODE" = "clean" ] || [ "$MODE" = "clean-all" ]; then + if [ -n "$SNAPSHOT_ID" ]; then + error "Snapshot ID should not be provided with --clean or --clean-all mode" + fi + fi +} + +# Check prerequisites +check_prerequisites() { + # Skip restic checks for clean modes + if [ "$MODE" = "clean" ] || [ "$MODE" = "clean-all" ]; then + # Only check Docker for clean modes + if ! command -v docker &>/dev/null; then + error "Docker not found" + fi + + if ! docker compose version &>/dev/null; then + error "Docker Compose not found" + fi + + log "Prerequisites OK (clean mode)" + return + fi + + # Check configuration file + if [ ! -f "$CONFIG_FILE" ]; then + error "Config file not found: $CONFIG_FILE" + fi + + # Source config + source "$CONFIG_FILE" + + # Check required variables + if [ -z "${RESTIC_REPOSITORY:-}" ]; then + error "RESTIC_REPOSITORY not defined in config" + fi + + # Test restic access + if ! restic snapshots &>/dev/null; then + error "Cannot access Restic repository" + fi + + # Check Docker (except for extract mode) + if [ "$MODE" != "extract" ]; then + if ! command -v docker &>/dev/null; then + error "Docker not found" + fi + + # Check docker compose + if ! docker compose version &>/dev/null; then + error "Docker Compose not found" + fi + fi + + log "Prerequisites OK" +} + +# Create secure temporary directory +create_temp_dir() { + if [ "$MODE" = "extract" ]; then + # For extract mode, create in /tmp with readable name + TEMP_DIR="/tmp/paperless-extract-$(date +%Y%m%d-%H%M%S)" + mkdir -p "$TEMP_DIR" + chmod 755 "$TEMP_DIR" # More permissive for extract mode + else + # For other modes, use secure temp + TEMP_DIR=$(mktemp -d "/tmp/paperless-restore-$(date +%Y%m%d-%H%M%S)-XXXXXX") + chmod 700 "$TEMP_DIR" + fi + + if [ ! -d "$TEMP_DIR" ]; then + error "Failed to create temporary directory" + fi + + log "Created temporary directory: $TEMP_DIR" +} + +# Extract snapshot +extract_snapshot() { + log "Extracting snapshot $SNAPSHOT_ID to temporary directory" + + if ! restic restore "$SNAPSHOT_ID" --target "$TEMP_DIR"; then + error "Failed to extract snapshot $SNAPSHOT_ID" + fi + + success "Snapshot extracted successfully" + + # Show what was extracted + log "Extracted contents:" + ls -la "$TEMP_DIR" | head -10 + + # Show paperless data structure if exists + if [ -d "$TEMP_DIR/tmp/paperless" ]; then + echo "" + log "Paperless data structure:" + ls -la "$TEMP_DIR/tmp/paperless/" | head -10 + fi +} + +# Extract only mode +extract_only() { + log "Starting extract-only mode" + + # Create temp directory in /tmp + create_temp_dir + + # Extract snapshot + extract_snapshot + + # Display information about extracted content + echo "" + success "Snapshot $SNAPSHOT_ID extracted to: $TEMP_DIR" + echo "" + echo "📁 Extracted structure:" + if [ -d "$TEMP_DIR/tmp/paperless" ]; then + echo " Main data location: $TEMP_DIR/tmp/paperless/" + ls -la "$TEMP_DIR/tmp/paperless/" | head -10 + else + find "$TEMP_DIR" -maxdepth 2 -type d | head -20 + fi + echo "" + echo "📊 Content summary:" + echo " Database dumps: $(find "$TEMP_DIR" -name "*.sql" | wc -l) files" + echo " Data directories: $(find "$TEMP_DIR/tmp/paperless" -maxdepth 1 -type d 2>/dev/null | grep -v "^$TEMP_DIR/tmp/paperless$" | wc -l) directories" + echo " Total size: $(du -sh "$TEMP_DIR" | cut -f1)" + echo "" + echo "💡 Manual inspection commands:" + echo " cd $TEMP_DIR/tmp/paperless" + echo " ls -la" + echo " find . -name '*.sql' -exec ls -lh {} +" + echo "" + echo "⚠️ The directory will remain until you delete it manually:" + echo " rm -rf $TEMP_DIR" + echo "" + + # Don't run cleanup for extract mode - leave directory for user + trap - EXIT +} + +# Clean test environment +clean_test_environment() { + log "Starting cleanup of test environment" + + # Navigate to service directory to ensure we have access to .env + cd "$SERVICE_DIR" + + # Stop test containers + log "Stopping test containers..." + if [ -d "$TEST_DIR" ]; then + cd "$TEST_DIR" + docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test down --remove-orphans 2>/dev/null || true + cd "$SERVICE_DIR" + else + log "Test directory doesn't exist, checking for running containers anyway..." + fi + + # Try to stop containers by name pattern (in case they're running from different location) + log "Stopping any remaining paperless test containers..." + docker stop paperless-webserver-test paperless-db-test paperless-broker-test paperless-gotenberg-test paperless-tika-test 2>/dev/null || true + docker rm paperless-webserver-test paperless-db-test paperless-broker-test paperless-gotenberg-test paperless-tika-test 2>/dev/null || true + + # Remove test volumes + log "Removing test volumes..." + local volumes_to_remove=( + "paperless-ngx_pgdata-test" + "redisdata-test" + "paperless-data-test" + "paperless-media-test" + "paperless-export-test" + "paperless-consume-test" + ) + + for volume in "${volumes_to_remove[@]}"; do + if docker volume ls -q | grep -q "^${volume}$"; then + log "Removing volume: $volume" + docker volume rm "$volume" 2>/dev/null || warning "Failed to remove volume: $volume" + else + log "Volume not found (already removed): $volume" + fi + done + + # Clean up any dangling images related to paperless + log "Cleaning up dangling images..." + docker image prune -f &>/dev/null || true + + success "Test environment cleaned successfully" + echo "" + echo "🧹 Cleanup completed!" + echo " ✅ Test containers stopped and removed" + echo " ✅ Test volumes removed" + echo " ✅ Dangling images cleaned" + echo "" + echo "💡 The production environment remains untouched." + echo "💡 The test directory ($TEST_DIR) is preserved." + echo "💡 You can now run a fresh test restore if needed." +} + +# Clean all environment (test + temp directories) +clean_all_environment() { + log "Starting cleanup of test environment and temp directories" + + # First, run the standard test environment cleanup + clean_test_environment + + # Then clean temporary directories + echo "" + log "Cleaning temporary directories..." + + # Find and remove paperless restore temp directories + local temp_dirs_restore=() + local temp_dirs_extract=() + + # Use find to safely locate temp directories + while IFS= read -r -d '' dir; do + temp_dirs_restore+=("$dir") + done < <(find /tmp -maxdepth 1 -type d -name "paperless-restore-*" -print0 2>/dev/null) + + while IFS= read -r -d '' dir; do + temp_dirs_extract+=("$dir") + done < <(find /tmp -maxdepth 1 -type d -name "paperless-extract-*" -print0 2>/dev/null) + + # Remove restore temp directories + if [ ${#temp_dirs_restore[@]} -gt 0 ]; then + log "Found ${#temp_dirs_restore[@]} restore temp directories to remove" + for dir in "${temp_dirs_restore[@]}"; do + if [ -d "$dir" ]; then + log "Removing: $(basename "$dir")" + rm -rf "$dir" || warning "Failed to remove: $dir" + fi + done + else + log "No restore temp directories found" + fi + + # Remove extract temp directories + if [ ${#temp_dirs_extract[@]} -gt 0 ]; then + log "Found ${#temp_dirs_extract[@]} extract temp directories to remove" + for dir in "${temp_dirs_extract[@]}"; do + if [ -d "$dir" ]; then + log "Removing: $(basename "$dir")" + rm -rf "$dir" || warning "Failed to remove: $dir" + fi + done + else + log "No extract temp directories found" + fi + + # Show final summary + echo "" + success "Complete cleanup finished!" + echo "" + echo "🧹 Full cleanup completed!" + echo " ✅ Test containers stopped and removed" + echo " ✅ Test volumes removed" + echo " ✅ Dangling images cleaned" + echo " ✅ Restore temp directories removed: ${#temp_dirs_restore[@]}" + echo " ✅ Extract temp directories removed: ${#temp_dirs_extract[@]}" + echo "" + echo "💡 The production environment remains untouched." + echo "💡 The test directory ($TEST_DIR) is preserved." + echo "💡 System is now clean and ready for fresh operations." +} + +# Restore to test instance +restore_to_test() { + log "Starting restore to test instance" + + # Create test directory if it doesn't exist + mkdir -p "$TEST_DIR" + + # Navigate to test directory + cd "$TEST_DIR" + + log "Stopping test containers (if running)..." + docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test down --remove-orphans 2>/dev/null || true + + # Remove existing test volumes to ensure clean restore + log "Removing existing test volumes..." + docker volume rm paperless-ngx_pgdata-test redisdata-test \ + paperless-data-test paperless-media-test \ + paperless-export-test paperless-consume-test 2>/dev/null || true + + # Find database dump + local db_dump + db_dump=$(find "$TEMP_DIR" -name "*_db_*.sql" | head -1) + + if [ -z "$db_dump" ]; then + error "No database dump found in snapshot" + fi + + log "Found database dump: $(basename "$db_dump")" + + # Create PostgreSQL volume before starting database container + log "Creating PostgreSQL volume..." + docker volume create paperless-ngx_pgdata-test + + # Start only the database container for restore + log "Starting test database container..." + docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test up -d db-test + + # Wait for database to be ready + log "Waiting for database to be ready..." + sleep 15 + + # Source environment variables + if [ -f "$SERVICE_DIR/.env" ]; then + source "$SERVICE_DIR/.env" + else + error "Environment file not found: $SERVICE_DIR/.env" + fi + + # Restore database + log "Restoring database from dump..." + if docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test exec -T db-test psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" < "$db_dump"; then + success "Database restored successfully" + else + error "Database restore failed" + fi + + # Stop database container + docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test down + + # Restore data volumes using temporary containers + log "Restoring data volumes..." + + # Create ALL volumes (Docker Compose will not create them since they're external) + log "Creating all required volumes..." + docker volume create paperless-ngx_pgdata-test + docker volume create redisdata-test + docker volume create paperless-data-test + docker volume create paperless-media-test + docker volume create paperless-export-test + docker volume create paperless-consume-test + + # Function to restore volume data + restore_volume_data() { + local source_path="$1" + local volume_name="$2" + local container_path="$3" + + if [ -d "$source_path" ]; then + log "Restoring $volume_name from $source_path" + docker run --rm \ + -v "$volume_name:$container_path" \ + -v "$source_path:$container_path-source:ro" \ + alpine:latest \ + sh -c "cp -rf $container_path-source/* $container_path/ 2>/dev/null || true" + else + warning "Source path not found: $source_path" + fi + } + + # Restore each data directory + restore_volume_data "$TEMP_DIR/tmp/paperless/data" "paperless-data-test" "/data" + restore_volume_data "$TEMP_DIR/tmp/paperless/media" "paperless-media-test" "/media" + restore_volume_data "$TEMP_DIR/tmp/paperless/export" "paperless-export-test" "/export" + restore_volume_data "$TEMP_DIR/tmp/paperless/consume" "paperless-consume-test" "/consume" + + # Start all test containers + log "Starting test instance..." + docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test up -d + + # Wait for services to be ready + log "Waiting for services to start..." + sleep 30 + + # Check if webserver is running + if docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test ps --filter "status=running" | grep -q "webserver-test"; then + success "Test instance started successfully" + echo "" + echo "🎉 Test instance is ready!" + echo "📍 Container name: paperless-webserver-test" + echo "🌐 Access: Configure SWAG to redirect paperless.alouettes.jombi.fr temporarily" + echo "🔧 Docker compose location: $TEST_DIR" + echo "" + echo "💡 To stop the test instance:" + echo " cd $TEST_DIR && docker compose --env-file $SERVICE_DIR/.env -p paperless-test down" + echo "" + echo "💡 To clean test environment completely:" + echo " $SERVICE_DIR/restore --clean" + else + error "Test instance failed to start properly" + fi +} + +# Restore to production (not implemented) +restore_to_production() { + echo "" + echo "🚧 PRODUCTION RESTORE NOT YET IMPLEMENTED 🚧" + echo "" + echo "Production restore functionality is not yet available." + echo "This feature requires additional safety measures and validation." + echo "" + echo "For now, you can:" + echo "1. Use --test mode to restore to the test instance" + echo "2. Verify the restored data in the test environment" + echo "3. Manually copy data from test to production if needed" + echo "" + echo "Production restore will be implemented in a future version." + echo "" + exit 1 +} + +# Main function +main() { + echo "=== Paperless Restore Script ===" + + # Parse arguments + parse_arguments "$@" + + # Display operation info + if [ "$MODE" = "clean" ]; then + log "Mode: $MODE" + else + log "Snapshot ID: $SNAPSHOT_ID" + log "Mode: $MODE" + fi + + # Check prerequisites + check_prerequisites + + # Setup cleanup if needed + setup_cleanup + + # Execute based on mode + case "$MODE" in + clean) + clean_test_environment + ;; + clean-all) + clean_all_environment + ;; + extract) + extract_only + ;; + test) + # Create temporary directory and extract + create_temp_dir + extract_snapshot + restore_to_test + ;; + production) + # Create temporary directory and extract + create_temp_dir + extract_snapshot + restore_to_production + ;; + *) + error "Invalid mode: $MODE" + ;; + esac + + success "Operation completed!" +} + +# Entry point +main "$@" diff --git a/services/paperless/test-restore/docker-compose.yml b/services/paperless/test-restore/docker-compose.yml new file mode 100644 index 0000000..fbc1754 --- /dev/null +++ b/services/paperless/test-restore/docker-compose.yml @@ -0,0 +1,101 @@ +services: + broker-test: + image: docker.io/library/redis:8 + restart: unless-stopped + container_name: paperless-broker-test + volumes: + - redisdata-test:/data + networks: + - paperless-test-internal + + db-test: + image: postgres:13 + restart: unless-stopped + container_name: paperless-db-test + volumes: + - paperless-ngx_pgdata-test:/var/lib/postgresql/data + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + networks: + - paperless-test-internal + + webserver-test: + image: ghcr.io/paperless-ngx/paperless-ngx:2.14.1 + restart: unless-stopped + container_name: paperless-webserver-test + depends_on: + - db-test + - broker-test + - gotenberg-test + - tika-test + volumes: + - paperless-data-test:/usr/src/paperless/data + - paperless-media-test:/usr/src/paperless/media + - paperless-export-test:/usr/src/paperless/export + - paperless-consume-test:/usr/src/paperless/consume + env_file: ../.env + environment: + PAPERLESS_REDIS: redis://broker-test:6379 + PAPERLESS_DBHOST: db-test + PAPERLESS_DBNAME: ${POSTGRES_DB} + PAPERLESS_DBUSER: ${POSTGRES_USER} + PAPERLESS_DBPASS: ${POSTGRES_PASSWORD} + PAPERLESS_TIKA_ENABLED: 1 + PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg-test:3000 + PAPERLESS_TIKA_ENDPOINT: http://tika-test:9998 + # Override URL for test instance + PAPERLESS_URL: https://paperless.alouettes.jombi.fr + networks: + - paperless-test-internal + - services + - mail + + gotenberg-test: + image: docker.io/gotenberg/gotenberg:8.20 + restart: unless-stopped + container_name: paperless-gotenberg-test + command: + - "gotenberg" + - "--chromium-disable-javascript=true" + - "--chromium-allow-list=file:///tmp/.*" + networks: + - paperless-test-internal + + tika-test: + image: docker.io/apache/tika:latest + restart: unless-stopped + container_name: paperless-tika-test + networks: + - paperless-test-internal + +volumes: + paperless-ngx_pgdata-test: + external: true + name: paperless-ngx_pgdata-test + redisdata-test: + external: true + name: redisdata-test + paperless-data-test: + external: true + name: paperless-data-test + paperless-media-test: + external: true + name: paperless-media-test + paperless-export-test: + external: true + name: paperless-export-test + paperless-consume-test: + external: true + name: paperless-consume-test + +networks: + paperless-test-internal: + name: paperless-test-internal + services: + external: true + name: services + mail: + external: true + name: mail diff --git a/services/proton-bridge/docker-compose.yml b/services/proton-bridge/docker-compose.yml new file mode 100644 index 0000000..8e5b1ae --- /dev/null +++ b/services/proton-bridge/docker-compose.yml @@ -0,0 +1,16 @@ +services: + protonmail-bridge: + image: shenxn/protonmail-bridge + restart: unless-stopped + volumes: + - protonmail:/root + networks: + - mail + +volumes: + protonmail: + name: protonmail + +networks: + mail: + external: true diff --git a/services/swag/docker-compose.yml b/services/swag/docker-compose.yml new file mode 100644 index 0000000..e479114 --- /dev/null +++ b/services/swag/docker-compose.yml @@ -0,0 +1,35 @@ +services: + swag: + image: ghcr.io/linuxserver/swag:latest + container_name: swag + cap_add: + - NET_ADMIN + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + - URL=${DOMAIN} + - SUBDOMAINS=${SUBDOMAINS} + - VALIDATION=dns + - DNSPLUGIN=ovh + - EMAIL=${EMAIL} + - ONLY_SUBDOMAINS=false + - STAGING=false + volumes: + - /home/citadel/data/swag:/config + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + ports: + - "80:80" + - "443:443" + networks: + - services + restart: unless-stopped + labels: + - "com.docker.compose.project=swag" + - "backup.enable=true" + - "backup.path=/config" + +networks: + services: + external: true diff --git a/services/swag/start.sh b/services/swag/start.sh new file mode 100644 index 0000000..3b1c103 --- /dev/null +++ b/services/swag/start.sh @@ -0,0 +1,76 @@ +#!/bin/bash +set -e + +# Configuration +SERVICE_NAME="swag" +SERVICE_DIR="/home/citadel/services/$SERVICE_NAME" +DATA_DIR="/home/citadel/data/$SERVICE_NAME" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Create necessary directories +log_info "Creating directories..." +mkdir -p "$SERVICE_DIR" +mkdir -p "$DATA_DIR" + +# Set correct ownership +log_info "Setting directory ownership..." +sudo chown -R $(id -u):$(id -g) "$SERVICE_DIR" "$DATA_DIR" + +# Check if Docker network exists +if ! docker network ls | grep -q "services"; then + log_warn "Docker network 'services' not found. Creating..." + docker network create services +fi + +# Navigate to service directory +cd "$SERVICE_DIR" + +# Check if .env file exists +if [ ! -f ".env" ]; then + log_error ".env file not found in $SERVICE_DIR" + log_info "Please create the .env file with required variables" + exit 1 +fi + +# Update .env with current user IDs +log_info "Updating .env with current user IDs..." +sed -i "s/^PUID=.*/PUID=$(id -u)/" .env +sed -i "s/^PGID=.*/PGID=$(id -g)/" .env + +# Check if OVH credentials are configured +if [ ! -f "$DATA_DIR/dns-conf/ovh.ini" ]; then + log_warn "OVH DNS credentials not found at $DATA_DIR/dns-conf/ovh.ini" + log_info "Remember to configure OVH API credentials after first start" +fi + +# Start the service +log_info "Starting SWAG service..." +docker-compose up -d + +# Check if service is running +sleep 5 +if docker-compose ps | grep -q "Up"; then + log_info "SWAG service started successfully" + log_info "Check logs with: docker-compose logs -f" +else + log_error "SWAG service failed to start" + log_info "Check logs with: docker-compose logs" + exit 1 +fi diff --git a/services/taiga/.env.backup.20250604_161336 b/services/taiga/.env.backup.20250604_161336 new file mode 100644 index 0000000..937d54d --- /dev/null +++ b/services/taiga/.env.backup.20250604_161336 @@ -0,0 +1,38 @@ +# Taiga Configuration for production with SWAG +# ============================================== + +# Taiga's URLs - Variables to define where Taiga should be served +TAIGA_SCHEME=https +TAIGA_DOMAIN=taiga.alouettes.jombi.fr +SUBPATH="" +WEBSOCKETS_SCHEME=wss + +# Taiga's Secret Key - Variable to provide cryptographic signing +# IMPORTANT: Change this to a secure random value! +SECRET_KEY="CHANGE_ME_TO_SECURE_SECRET_KEY" + +# Taiga's Database settings - Variables to create the Taiga database and connect to it +POSTGRES_USER=taiga +POSTGRES_PASSWORD=CHANGE_ME_TO_SECURE_DB_PASSWORD + +# Taiga's SMTP settings - Variables to send Taiga's emails to the users +EMAIL_BACKEND=console # change to "smtp" when configuring email +EMAIL_HOST=smtp.host.example.com +EMAIL_PORT=587 +EMAIL_HOST_USER=user +EMAIL_HOST_PASSWORD=password +EMAIL_DEFAULT_FROM=noreply@alouettes.jombi.fr +EMAIL_USE_TLS=True +EMAIL_USE_SSL=False + +# Taiga's RabbitMQ settings - Variables to leave messages for the realtime and asynchronous events +RABBITMQ_USER=taiga +RABBITMQ_PASS=CHANGE_ME_TO_SECURE_RABBITMQ_PASSWORD +RABBITMQ_VHOST=taiga +RABBITMQ_ERLANG_COOKIE=CHANGE_ME_TO_SECURE_ERLANG_COOKIE + +# Taiga's Attachments settings +ATTACHMENTS_MAX_AGE=360 + +# Taiga's Telemetry +ENABLE_TELEMETRY=False diff --git a/services/taiga/.env.backup.20250604_161517 b/services/taiga/.env.backup.20250604_161517 new file mode 100644 index 0000000..937d54d --- /dev/null +++ b/services/taiga/.env.backup.20250604_161517 @@ -0,0 +1,38 @@ +# Taiga Configuration for production with SWAG +# ============================================== + +# Taiga's URLs - Variables to define where Taiga should be served +TAIGA_SCHEME=https +TAIGA_DOMAIN=taiga.alouettes.jombi.fr +SUBPATH="" +WEBSOCKETS_SCHEME=wss + +# Taiga's Secret Key - Variable to provide cryptographic signing +# IMPORTANT: Change this to a secure random value! +SECRET_KEY="CHANGE_ME_TO_SECURE_SECRET_KEY" + +# Taiga's Database settings - Variables to create the Taiga database and connect to it +POSTGRES_USER=taiga +POSTGRES_PASSWORD=CHANGE_ME_TO_SECURE_DB_PASSWORD + +# Taiga's SMTP settings - Variables to send Taiga's emails to the users +EMAIL_BACKEND=console # change to "smtp" when configuring email +EMAIL_HOST=smtp.host.example.com +EMAIL_PORT=587 +EMAIL_HOST_USER=user +EMAIL_HOST_PASSWORD=password +EMAIL_DEFAULT_FROM=noreply@alouettes.jombi.fr +EMAIL_USE_TLS=True +EMAIL_USE_SSL=False + +# Taiga's RabbitMQ settings - Variables to leave messages for the realtime and asynchronous events +RABBITMQ_USER=taiga +RABBITMQ_PASS=CHANGE_ME_TO_SECURE_RABBITMQ_PASSWORD +RABBITMQ_VHOST=taiga +RABBITMQ_ERLANG_COOKIE=CHANGE_ME_TO_SECURE_ERLANG_COOKIE + +# Taiga's Attachments settings +ATTACHMENTS_MAX_AGE=360 + +# Taiga's Telemetry +ENABLE_TELEMETRY=False diff --git a/services/taiga/docker-compose-inits.yml b/services/taiga/docker-compose-inits.yml new file mode 100644 index 0000000..48ffd12 --- /dev/null +++ b/services/taiga/docker-compose-inits.yml @@ -0,0 +1,50 @@ +x-environment: &default-back-environment + # Database settings + POSTGRES_DB: taiga + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_HOST: taiga-db + POSTGRES_PORT: 5432 + + # Taiga settings + TAIGA_SECRET_KEY: ${SECRET_KEY} + TAIGA_SITES_SCHEME: ${TAIGA_SCHEME} + TAIGA_SITES_DOMAIN: ${TAIGA_DOMAIN} + TAIGA_SUBPATH: ${SUBPATH} + + # Email settings + EMAIL_BACKEND: ${EMAIL_BACKEND} + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_PORT: ${EMAIL_PORT} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + DEFAULT_FROM_EMAIL: ${EMAIL_DEFAULT_FROM} + EMAIL_USE_TLS: ${EMAIL_USE_TLS} + EMAIL_USE_SSL: ${EMAIL_USE_SSL} + + # RabbitMQ settings for events + EVENTS_PUSH_BACKEND: "rabbitmq" + EVENTS_PUSH_BACKEND_URL: "amqp://${RABBITMQ_USER}:${RABBITMQ_PASS}@taiga-events-rabbitmq:5672/${RABBITMQ_VHOST}" + + # RabbitMQ settings for async + CELERY_BROKER_URL: "amqp://${RABBITMQ_USER}:${RABBITMQ_PASS}@taiga-async-rabbitmq:5672/${RABBITMQ_VHOST}" + + # Telemetry + ENABLE_TELEMETRY: ${ENABLE_TELEMETRY} + +x-volumes: &default-back-volumes + - /home/citadel/data/taiga/static:/taiga-back/static + - /home/citadel/data/taiga/media:/taiga-back/media + +services: + taiga-manage: + image: taigaio/taiga-back:latest + environment: *default-back-environment + depends_on: + - taiga-db + entrypoint: "python manage.py" + volumes: *default-back-volumes + networks: + - taiga_internal + - services + - mail diff --git a/services/taiga/docker-compose.yml b/services/taiga/docker-compose.yml new file mode 100644 index 0000000..f991fc3 --- /dev/null +++ b/services/taiga/docker-compose.yml @@ -0,0 +1,193 @@ +x-environment: &default-back-environment + # Database settings + POSTGRES_DB: taiga + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_HOST: taiga-db + POSTGRES_PORT: 5432 + + # Taiga settings + TAIGA_SECRET_KEY: ${SECRET_KEY} + TAIGA_SITES_SCHEME: ${TAIGA_SCHEME} + TAIGA_SITES_DOMAIN: ${TAIGA_DOMAIN} + TAIGA_SUBPATH: ${SUBPATH} + + # Email settings + EMAIL_BACKEND: ${EMAIL_BACKEND} + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_PORT: ${EMAIL_PORT} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + DEFAULT_FROM_EMAIL: ${EMAIL_DEFAULT_FROM} + EMAIL_USE_TLS: ${EMAIL_USE_TLS} + EMAIL_USE_SSL: ${EMAIL_USE_SSL} + + # RabbitMQ settings for events + EVENTS_PUSH_BACKEND: "rabbitmq" + EVENTS_PUSH_BACKEND_URL: "amqp://${RABBITMQ_USER}:${RABBITMQ_PASS}@taiga-events-rabbitmq:5672/${RABBITMQ_VHOST}" + + # RabbitMQ settings for async + CELERY_BROKER_URL: "amqp://${RABBITMQ_USER}:${RABBITMQ_PASS}@taiga-async-rabbitmq:5672/${RABBITMQ_VHOST}" + + # Telemetry + ENABLE_TELEMETRY: ${ENABLE_TELEMETRY} + +x-volumes: &default-back-volumes + - /home/citadel/data/taiga/static:/taiga-back/static + - /home/citadel/data/taiga/media:/taiga-back/media + +services: + taiga-db: + image: postgres:12.3 + environment: + POSTGRES_DB: taiga + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 2s + timeout: 15s + retries: 5 + start_period: 3s + volumes: + - taiga-db-data:/var/lib/postgresql/data + networks: + - taiga_internal + restart: unless-stopped + labels: + - "com.docker.compose.project=taiga" + + taiga-back: + image: taigaio/taiga-back:latest + environment: *default-back-environment + volumes: *default-back-volumes + networks: + - taiga_internal + - mail + depends_on: + taiga-db: + condition: service_healthy + taiga-events-rabbitmq: + condition: service_started + taiga-async-rabbitmq: + condition: service_started + restart: unless-stopped + labels: + - "com.docker.compose.project=taiga" + - "backup.enable=true" + + taiga-async: + image: taigaio/taiga-back:latest + entrypoint: ["/taiga-back/docker/async_entrypoint.sh"] + environment: *default-back-environment + volumes: *default-back-volumes + networks: + - taiga_internal + depends_on: + taiga-db: + condition: service_healthy + taiga-async-rabbitmq: + condition: service_started + restart: unless-stopped + labels: + - "com.docker.compose.project=taiga" + + taiga-async-rabbitmq: + image: rabbitmq:3.8-management-alpine + environment: + RABBITMQ_ERLANG_COOKIE: ${RABBITMQ_ERLANG_COOKIE} + RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS} + RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST} + hostname: taiga-async-rabbitmq + volumes: + - taiga-async-rabbitmq-data:/var/lib/rabbitmq + networks: + - taiga_internal + restart: unless-stopped + labels: + - "com.docker.compose.project=taiga" + + taiga-front: + image: taigaio/taiga-front:latest + environment: + TAIGA_URL: "${TAIGA_SCHEME}://${TAIGA_DOMAIN}" + TAIGA_WEBSOCKETS_URL: "${WEBSOCKETS_SCHEME}://${TAIGA_DOMAIN}" + TAIGA_SUBPATH: "${SUBPATH}" + networks: + - taiga_internal + restart: unless-stopped + labels: + - "com.docker.compose.project=taiga" + + taiga-events: + image: taigaio/taiga-events:latest + environment: + RABBITMQ_USER: ${RABBITMQ_USER} + RABBITMQ_PASS: ${RABBITMQ_PASS} + TAIGA_SECRET_KEY: ${SECRET_KEY} + networks: + - taiga_internal + depends_on: + - taiga-events-rabbitmq + restart: unless-stopped + labels: + - "com.docker.compose.project=taiga" + + taiga-events-rabbitmq: + image: rabbitmq:3.8-management-alpine + environment: + RABBITMQ_ERLANG_COOKIE: ${RABBITMQ_ERLANG_COOKIE} + RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS} + RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST} + hostname: taiga-events-rabbitmq + volumes: + - taiga-events-rabbitmq-data:/var/lib/rabbitmq + networks: + - taiga_internal + restart: unless-stopped + labels: + - "com.docker.compose.project=taiga" + + taiga-protected: + image: taigaio/taiga-protected:latest + environment: + MAX_AGE: ${ATTACHMENTS_MAX_AGE} + SECRET_KEY: ${SECRET_KEY} + networks: + - taiga_internal + restart: unless-stopped + labels: + - "com.docker.compose.project=taiga" + + taiga-gateway: + image: nginx:1.19-alpine + volumes: + - /home/citadel/data/taiga/static:/taiga/static:ro + - /home/citadel/data/taiga/media:/taiga/media:ro + - ./taiga-gateway.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - taiga_internal + - services + depends_on: + - taiga-front + - taiga-back + - taiga-events + +volumes: + taiga-db-data: + name: taiga-db-data + taiga-async-rabbitmq-data: + name: taiga-async-rabbitmq-data + taiga-events-rabbitmq-data: + name: taiga-events-rabbitmq-data + +networks: + services: + external: true + mail: + external: true + taiga_internal: + driver: bridge + name: taiga_internal diff --git a/services/taiga/generate-secrets.sh b/services/taiga/generate-secrets.sh new file mode 100644 index 0000000..6806b1e --- /dev/null +++ b/services/taiga/generate-secrets.sh @@ -0,0 +1,87 @@ +#!/bin/bash +set -e + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_header() { + echo -e "${BLUE}$1${NC}" +} + +# Function to generate secure random string (alphanumeric only) +generate_secret() { + local length=${1:-32} + cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w "$length" | head -n 1 +} + +ENV_FILE=".env" + +log_header "Taiga Secrets Generator (Alternative Method)" +echo "==============================================" + +# Check if .env file exists +if [[ ! -f "$ENV_FILE" ]]; then + log_error ".env file not found! Please create it first." + exit 1 +fi + +# Create backup +log_info "Creating backup of .env file..." +cp "$ENV_FILE" "${ENV_FILE}.backup.$(date +%Y%m%d_%H%M%S)" + +# Generate secrets +log_info "Generating secure secrets..." + +SECRET_KEY=$(generate_secret 50) +DB_PASSWORD=$(generate_secret 32) +RABBITMQ_PASSWORD=$(generate_secret 32) +ERLANG_COOKIE=$(generate_secret 20) + +# Create new .env file using awk (more robust than sed) +log_info "Updating .env file with new secrets..." + +awk -v secret_key="$SECRET_KEY" \ + -v db_password="$DB_PASSWORD" \ + -v rabbitmq_password="$RABBITMQ_PASSWORD" \ + -v erlang_cookie="$ERLANG_COOKIE" ' +{ + if ($0 ~ /^SECRET_KEY="CHANGE_ME_TO_SECURE_SECRET_KEY"/) { + print "SECRET_KEY=\"" secret_key "\"" + } else if ($0 ~ /^POSTGRES_PASSWORD=CHANGE_ME_TO_SECURE_DB_PASSWORD/) { + print "POSTGRES_PASSWORD=" db_password + } else if ($0 ~ /^RABBITMQ_PASS=CHANGE_ME_TO_SECURE_RABBITMQ_PASSWORD/) { + print "RABBITMQ_PASS=" rabbitmq_password + } else if ($0 ~ /^RABBITMQ_ERLANG_COOKIE=CHANGE_ME_TO_SECURE_ERLANG_COOKIE/) { + print "RABBITMQ_ERLANG_COOKIE=" erlang_cookie + } else { + print $0 + } +}' "$ENV_FILE" > "${ENV_FILE}.tmp" && mv "${ENV_FILE}.tmp" "$ENV_FILE" + +log_info "Secrets generated and updated successfully!" +echo "" +log_warn "IMPORTANT: Keep these credentials secure!" +echo "- SECRET_KEY: $SECRET_KEY (50 chars)" +echo "- POSTGRES_PASSWORD: $DB_PASSWORD (32 chars)" +echo "- RABBITMQ_PASS: $RABBITMQ_PASSWORD (32 chars)" +echo "- RABBITMQ_ERLANG_COOKIE: $ERLANG_COOKIE (20 chars)" +echo "" +log_info "Original .env file backed up." +echo "" +log_warn "Next step: Review EMAIL settings in .env if you want to configure SMTP" diff --git a/services/taiga/taiga-gateway.conf b/services/taiga/taiga-gateway.conf new file mode 100644 index 0000000..ebaeb68 --- /dev/null +++ b/services/taiga/taiga-gateway.conf @@ -0,0 +1,75 @@ +server { + listen 80 default_server; + + client_max_body_size 100M; + charset utf-8; + + # Frontend + location / { + proxy_pass http://taiga-front/; + proxy_pass_header Server; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Scheme $scheme; + } + + # API + location /api/ { + proxy_pass http://taiga-back:8000/api/; + proxy_pass_header Server; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Scheme $scheme; + } + + # Admin + location /admin/ { + proxy_pass http://taiga-back:8000/admin/; + proxy_pass_header Server; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Scheme $scheme; + } + + # Static + location /static/ { + alias /taiga/static/; + } + + # Media + location /_protected/ { + internal; + alias /taiga/media/; + add_header Content-disposition "attachment"; + } + + # Unprotected section + location /media/exports/ { + alias /taiga/media/exports/; + add_header Content-disposition "attachment"; + } + + location /media/ { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://taiga-protected:8003/; + proxy_redirect off; + } + + # Events + location /events { + proxy_pass http://taiga-events:8888/events; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_connect_timeout 7d; + proxy_send_timeout 7d; + proxy_read_timeout 7d; + } +} diff --git a/services/taiga/taiga-manage.sh b/services/taiga/taiga-manage.sh new file mode 100644 index 0000000..61843aa --- /dev/null +++ b/services/taiga/taiga-manage.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# Copyright (c) 2021-present Kaleidos INC + +set -x +exec docker compose -f docker-compose.yml -f docker-compose-inits.yml run --rm taiga-manage $@