Workflow: PUQ Docker NextCloud deploy
Integrations: code, httpRequest, if, respondToWebhook, set, ssh, stickyNote, switch, webhook

{"id": "d3xtaER6gl4aqLZR", "meta": {"instanceId": "workflow-a9196e6d", "versionId": "1.0.0", "createdAt": "2025-09-29T07:07:43.752828", "updatedAt": "2025-09-29T07:07:43.752836", "owner": "n8n-user", "license": "MIT", "category": "automation", "status": "active", "priority": "high", "environment": "production"}, "name": "PUQ Docker NextCloud deploy", "tags": ["automation", "n8n", "production-ready", "excellent", "optimized"], "nodes": [{"id": "dc9d4284-0ff7-4068-af3d-2b7f38451118", "name": "If Node", "type": "n8n-nodes-base.if", "position": [540, 920], "parameters": {"options": {}, "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "or", "conditions": [{"id": "b702e607-888a-42c9-b9a7-f9d2a64dfccd", "operator": {"type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.server_domain }}", "rightValue": "=d01-test.uuq.pl"}, {"id": "8a6662a4-4539-4ab1-bd5b-46b0a0d6e023", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.server_domain }}", "rightValue": "d02-test.uuq.pl"}]}}, "typeVersion": 2.2, "notes": "This if node performs automated tasks as part of the workflow."}, {"id": "b015bca6-fe71-4eb4-8e99-2904911c03b3", "name": "Parametrs", "type": "n8n-nodes-base.set", "position": [320, 920], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "370ddc4e-0fc0-48f6-9b30-ebdfba72c62f", "name": "clients_dir", "type": "string", "value": "/opt/docker/clients"}, {"id": "92202bb8-6113-4bc5-9a29-79d238456df2", "name": "mount_dir", "type": "string", "value": "/mnt"}, {"id": "baa52df2-9c10-42b2-939f-f05ea85ea2be", "name": "screen_left", "type": "string", "value": "{{"}, {"id": "2b19ed99-2630-412a-98b6-4be44d35d2e7", "name": "screen_right", "type": "string", "value": "}}"}]}}, "typeVersion": 3.4, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "b0c5ccb8-0692-4bb0-99e1-769fde372e0f", "name": "API", "type": "n8n-nodes-base.webhook", "position": [0, 920], "webhookId": "4e8168b3-2cad-462a-9750-152986331ce2", "parameters": {"path": "docker-nextcloud", "options": {}, "httpMethod": ["POST"], "responseMode": "responseNode", "authentication": "{{ $credentials.basicAuth }}", "multipleMethods": true}, "credentials": {"httpBasicAuth": {"id": "0gzq1np6ZmIrtK5o", "name": "nextcloud"}}, "typeVersion": 2, "notes": "This webhook node performs automated tasks as part of the workflow."}, {"id": "bcaf7ce1-464a-492e-b7f5-50ba8e465171", "name": "422-Invalid server domain", "type": "n8n-nodes-base.respondToWebhook", "position": [500, 1240], "parameters": {"options": {"responseCode": 422}, "respondWith": "json", "responseBody": "[{\n  \"status\": \"error\",\n  \"error\": \"Invalid server domain\"\n}]"}, "typeVersion": 1.1, "alwaysOutputData": false, "notes": "This respondToWebhook node performs automated tasks as part of the workflow."}, {"id": "3c642087-bd6b-4996-890b-4d50fbca8c55", "name": "Container Actions", "type": "n8n-nodes-base.switch", "position": [940, 1740], "parameters": {"rules": {"values": [{"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "66ad264d-5393-410c-bfa3-011ab8eb234a", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "container_start"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "b48957a0-22c0-4ac0-82ef-abd9e7ab0207", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "container_stop"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "727971bf-4218-41c1-9b07-22df4b947852", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "container_mount_disk"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "0c80b1d9-e7ca-4cf3-b3ac-b40fdf4dd8f8", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "container_unmount_disk"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "72a60c6b-5dc5-48db-8d3a-e083ffad6ae2", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "container_get_acl"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "74eb2334-6176-46ef-b444-d99b439fea17", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "container_set_acl"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "817ef082-a2d8-4b13-a8df-6e946878653b", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "container_get_net"}]}, "renameOutput": true}]}, "options": {}}, "typeVersion": 3.2, "notes": "This switch node performs automated tasks as part of the workflow."}, {"id": "396e6074-98ec-47df-956c-ce5c3b75e57e", "name": "Container Stats", "type": "n8n-nodes-base.switch", "position": [940, 1080], "parameters": {"rules": {"values": [{"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "66ad264d-5393-410c-bfa3-011ab8eb234a", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "container_information_inspect"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "b48957a0-22c0-4ac0-82ef-abd9e7ab0207", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "container_information_stats"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "50ede522-af22-4b7a-b1fd-34b27fd3fadd", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "container_log"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "d3070310-d3c2-4200-9765-495cf69fa835", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "dependent_containers_information_stats"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "dc17d6ad-4fa1-4006-8718-8188efa5f458", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "container_update_dns_record"}]}, "renameOutput": true}]}, "options": {}}, "typeVersion": 3.2, "notes": "This switch node performs automated tasks as part of the workflow."}, {"id": "d0084a58-b157-4635-955a-8638f348bf72", "name": "Inspect", "type": "n8n-nodes-base.set", "position": [1260, 760], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/{{ $('API').item.json.body.domain }}\"\nCONTAINER_NAME=\"{{ $('API').item.json.body.domain }}\"\n\nINSPECT_JSON=\"{}\"\nif sudo docker ps -a --filter \"name=$CONTAINER_NAME\" | grep -q \"$CONTAINER_NAME\"; then\n  INSPECT_JSON=$(sudo docker inspect \"$CONTAINER_NAME\")\nfi\n\necho \"{\\\"inspect\\\": $INSPECT_JSON}\"\n\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "cec87c49-d7ea-4407-bc4c-21ea75b25baa", "name": "Stat", "type": "n8n-nodes-base.set", "position": [1260, 920], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/{{ $('API').item.json.body.domain }}\"\nSTATUS_FILE=\"$COMPOSE_DIR/status.json\"\nIMG_FILE=\"$COMPOSE_DIR/data.img\"\nMOUNT_DIR=\"{{ $('Parametrs').item.json.mount_dir }}/{{ $('API').item.json.body.domain }}\"\nCONTAINER_NAME=\"{{ $('API').item.json.body.domain }}_nextcloud\"\n\n# Initialize empty container data\nINSPECT_JSON=\"{}\"\nSTATS_JSON=\"{}\"\n\n# Check if container is running\nif sudo docker ps -a --filter \"name=$CONTAINER_NAME\" | grep -q \"$CONTAINER_NAME\"; then\n  # Get Docker inspect info in JSON (as raw string)\n  INSPECT_JSON=$(sudo docker inspect \"$CONTAINER_NAME\")\n\n  # Get Docker stats info in JSON (as raw string)\n  STATS_JSON=$(sudo docker stats --no-stream --format \"{{ $('Parametrs').item.json.screen_left }}json .{{ $('Parametrs').item.json.screen_right }}\" \"$CONTAINER_NAME\")\n  STATS_JSON=${STATS_JSON:-'{}'}\nfi\n\n# Initialize disk info variables\nMOUNT_USED=\"N/A\"\nMOUNT_FREE=\"N/A\"\nMOUNT_TOTAL=\"N/A\"\nMOUNT_PERCENT=\"N/A\"\nIMG_SIZE=\"N/A\"\nIMG_PERCENT=\"N/A\"\nDISK_STATS_IMG=\"N/A\"\n\n# Check if mount directory exists and is accessible\nif [ -d \"$MOUNT_DIR\" ]; then\n  if mount | grep -q \"$MOUNT_DIR\"; then\n    # Get disk usage for mounted directory\n    DISK_STATS_MOUNT=$(df -h \"$MOUNT_DIR\" | tail -n 1)\n    MOUNT_USED=$(echo \"$DISK_STATS_MOUNT\" | awk '{print $3}')\n    MOUNT_FREE=$(echo \"$DISK_STATS_MOUNT\" | awk '{print $4}')\n    MOUNT_TOTAL=$(echo \"$DISK_STATS_MOUNT\" | awk '{print $2}')\n    MOUNT_PERCENT=$(echo \"$DISK_STATS_MOUNT\" | awk '{print $5}')\n  fi\nfi\n\n# Check if image file exists\nif [ -f \"$IMG_FILE\" ]; then\n  # Get disk usage for image file\n  IMG_SIZE=$(du -sh \"$IMG_FILE\" | awk '{print $1}')\nfi\n\n# Manually create a combined JSON object\nFINAL_JSON=\"{\\\"inspect\\\": $INSPECT_JSON, \\\"stats\\\": $STATS_JSON, \\\"disk\\\": {\\\"mounted\\\": {\\\"used\\\": \\\"$MOUNT_USED\\\", \\\"free\\\": \\\"$MOUNT_FREE\\\", \\\"total\\\": \\\"$MOUNT_TOTAL\\\", \\\"percent\\\": \\\"$MOUNT_PERCENT\\\"}, \\\"img_file\\\": {\\\"size\\\": \\\"$IMG_SIZE\\\"}}}\"\n\n# Output the result\necho \"$FINAL_JSON\"\n\nexit 0"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "80dcd9b2-f1f5-44c3-98e8-38dae5ad4edb", "name": "Start", "type": "n8n-nodes-base.set", "position": [1400, 1500], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/{{ $('API').item.json.body.domain }}\"\nSTATUS_FILE=\"$COMPOSE_DIR/status.json\"\nIMG_FILE=\"$COMPOSE_DIR/data.img\"\nMOUNT_DIR=\"{{ $('Parametrs').item.json.mount_dir }}/{{ $('API').item.json.body.domain }}\"\n\n# Function to log an error, write to status file, and print to console\nhandle_error() {\n    echo \"error: $1\"\n    exit 1\n}\n\nif ! df -h | grep -q \"$MOUNT_DIR\"; then\n    handle_error \"The file $IMG_FILE is not mounted to $MOUNT_DIR\"\nfi\n\nif sudo docker ps --filter \"name={{ $('API').item.json.body.domain }}\" --filter \"status=running\" -q | grep -q .; then\n    handle_error \"{{ $('API').item.json.body.domain }} container is running\"\nfi\n\n# Change to the compose directory\ncd \"$COMPOSE_DIR\" > /dev/null 2>&1 || handle_error \"Failed to change directory to $COMPOSE_DIR\"\n\n# Start the Docker containers\nif ! sudo docker-compose up -d > /dev/null 2>error.log; then\n    ERROR_MSG=$(tail -n 10 error.log)\n    handle_error \"Docker-compose failed: $ERROR_MSG\"\nfi\n\n# Success\necho \"success\"\n\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "9cde27ca-4749-4660-9d46-d3161946b627", "name": "Stop", "type": "n8n-nodes-base.set", "position": [1400, 1660], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/{{ $('API').item.json.body.domain }}\"\nSTATUS_FILE=\"$COMPOSE_DIR/status.json\"\nIMG_FILE=\"$COMPOSE_DIR/data.img\"\nMOUNT_DIR=\"{{ $('Parametrs').item.json.mount_dir }}/{{ $('API').item.json.body.domain }}\"\n\n# Function to log an error, write to status file, and print to console\nhandle_error() {\n    echo \"error: $1\"\n    exit 1\n}\n\n# Check if Docker container is running\nif ! sudo docker ps --filter \"name={{ $('API').item.json.body.domain }}\" --filter \"status=running\" -q | grep -q .; then\n    handle_error \"{{ $('API').item.json.body.domain }} container is not running\"\nfi\n\n# Stop and remove the Docker containers (also remove associated volumes)\nif ! sudo docker-compose -f \"$COMPOSE_DIR/docker-compose.yml\" down > /dev/null 2>&1; then\n    handle_error \"Failed to stop and remove docker-compose containers\"\nfi\n\necho \"success\"\n\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "f957ffb7-ccb5-41b2-b89e-ef1a92942251", "name": "Mount Disk", "type": "n8n-nodes-base.set", "position": [1400, 1820], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/{{ $('API').item.json.body.domain }}\"\nSTATUS_FILE=\"$COMPOSE_DIR/status.json\"\nIMG_FILE=\"$COMPOSE_DIR/data.img\"\nMOUNT_DIR=\"{{ $('Parametrs').item.json.mount_dir }}/{{ $('API').item.json.body.domain }}\"\n\n# Function to log an error, write to status file, and print to console\nhandle_error() {\n    echo \"error: $1\"\n    exit 1\n}\n\n# Create necessary directories with permissions\nsudo mkdir -p \"$MOUNT_DIR\" > /dev/null 2>&1 || handle_error \"Failed to create $MOUNT_DIR\"\nsudo chmod 777 \"$MOUNT_DIR\" > /dev/null 2>&1 || handle_error \"Failed to set permissions on $MOUNT_DIR\"\n\nif df -h | grep -q \"$MOUNT_DIR\"; then\n    handle_error \"The file $IMG_FILE is mounted to $MOUNT_DIR\"\nfi\n\nif ! grep -q \"$IMG_FILE\" /etc/fstab; then\n    echo \"$IMG_FILE $MOUNT_DIR ext4 loop 0 0\" | sudo tee -a /etc/fstab > /dev/null || handle_error \"Failed to add entry to /etc/fstab\"\nfi\n\nsudo mount -a || handle_error \"Failed to mount entries from /etc/fstab\"\n\necho \"success\"\n\nexit 0\n "}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "00cb7b5b-429e-494f-b2a9-1c0c45ac8d66", "name": "Unmount Disk", "type": "n8n-nodes-base.set", "position": [1400, 1980], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/{{ $('API').item.json.body.domain }}\"\nSTATUS_FILE=\"$COMPOSE_DIR/status.json\"\nIMG_FILE=\"$COMPOSE_DIR/data.img\"\nMOUNT_DIR=\"{{ $('Parametrs').item.json.mount_dir }}/{{ $('API').item.json.body.domain }}\"\n\n# Function to log an error, write to status file, and print to console\nhandle_error() {\n    echo \"error: $1\"\n    exit 1\n}\n\nif ! df -h | grep -q \"$MOUNT_DIR\"; then\n    handle_error \"The file $IMG_FILE is not mounted to $MOUNT_DIR\"\nfi\n\n# Remove the mount entry from /etc/fstab if it exists\nif grep -q \"$IMG_FILE\" /etc/fstab; then\n    sudo sed -i \"\\|$(printf '%s\\n' \"$IMG_FILE\" | sed 's/[.[\\*^$]/\\\\&/g')|d\" /etc/fstab\nfi\n\n# Unmount the image if it is mounted (using fstab)\nif mount | grep -q \"$MOUNT_DIR\"; then\n    sudo umount \"$MOUNT_DIR\" > /dev/null 2>&1 || handle_error \"Failed to unmount $MOUNT_DIR\"\nfi\n\n# Remove the mount directory (if needed)\nif ! sudo rm -rf \"$MOUNT_DIR\" > /dev/null 2>&1; then\n    handle_error \"Failed to remove $MOUNT_DIR\"\nfi\n\necho \"success\"\n\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "49487b07-8b7f-48c4-b7d0-819336ce6691", "name": "Log", "type": "n8n-nodes-base.set", "position": [1420, 1040], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\nCONTAINER_NAME=\"{{ $('API').item.json.body.domain }}_nextcloud\"\nLOGS_JSON=\"{}\"\n\n# Function to return error in JSON format\nhandle_error() {\n    echo \"{\\\"status\\\": \\\"error\\\", \\\"message\\\": \\\"$1\\\"}\"\n    exit 1\n}\n\n# Check if the container exists\nif ! sudo docker ps -a | grep -q \"$CONTAINER_NAME\" > /dev/null 2>&1; then\n    handle_error \"Container $CONTAINER_NAME not found\"\nfi\n\n# Get logs of the container\nLOGS=$(sudo docker logs --tail 1000 \"$CONTAINER_NAME\" 2>&1)\nif [ $? -ne 0 ]; then\n    handle_error \"Failed to retrieve logs for $CONTAINER_NAME\"\nfi\n\n# Escape double quotes in logs for valid JSON\nLOGS_ESCAPED=$(echo \"$LOGS\" | sed 's/\"/\\\\\"/g' | sed ':a;N;$!ba;s/\\n/\\\\n/g')\n\n# Format logs as JSON\nLOGS_JSON=\"{\\\"logs\\\": \\\"$LOGS_ESCAPED\\\"}\"\n\necho \"$LOGS_JSON\"\nexit 0"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "f8dfb4a8-5887-4796-9d1e-f882947fe9e8", "name": "Sticky Note", "type": "n8n-nodes-base.stickyNote", "position": [0, 0], "parameters": {"color": 6, "width": 639, "height": 909, "content": "## \ud83d\udc4b Welcome to PUQ Docker NextCloud deploy!\n# Template for Docker NextCloud: API Backend for WHMCS/WISECP by PUQcloud\n\nThis is an Docker NextCloud template that creates an API backend for the WHMCS/WISECP module developed by PUQcloud.\n\n## Setup Instructions\n\n### 1. Configure API Webhook and SSH Access\n- Create a Credential (Basic Auth) for the **Webhook API Block** in n8n.\n- Create a Credential for **SSH access** to a server with Docker installed (**SSH Block**).\n\n### 2. Install Required Packages on the Docker Server\nRun the following command on your server:\n```\napt-get install sqlite3 apache2-utils -y\n```\n### 3. Modify Template Parameters\nIn the **Parameters** block of the template, update the following settings:\n\n- `server_domain` \u2013 must match the domain of the WHMCS/WISECP Docker server.\n- `clients_dir` \u2013 directory where user data related to Docker and disks will be stored.\n- `mount_dir` \u2013 default mount point for the container disk (recommended not to change).\n\n**Do not modify** the following technical parameters:\n\n- `screen_left`\n- `screen_right`\n\n## Additional Resources\n- Full documentation: [{{ $env.WEBHOOK_URL }}]({{ $env.WEBHOOK_URL }}\n- WHMCS module: [{{ $env.WEBHOOK_URL }}]({{ $env.WEBHOOK_URL }}\n\n"}, "typeVersion": 1, "notes": "This stickyNote node performs automated tasks as part of the workflow."}, {"id": "29bd957b-a5be-4a6e-81e3-ba7d88462d93", "name": "Deploy-docker-compose", "type": "n8n-nodes-base.set", "position": [1340, 20], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "docker-compose", "type": "string", "value": "=version: \"3.8\"\n\nservices:\n  {{ $('API').item.json.body.domain }}_nextcloud:\n    image: nextcloud:latest\n    container_name: {{ $('API').item.json.body.domain }}_nextcloud\n    environment:\n      NEXTCLOUD_ADMIN_USER: {{ $('API').item.json.body.nc_admin_user }}\n      NEXTCLOUD_ADMIN_PASSWORD: {{ $('API').item.json.body.nc_admin_password }}\n      NEXTCLOUD_TRUSTED_DOMAINS: {{ $('API').item.json.body.domain }}\n      MYSQL_PASSWORD: {{ $('API').item.json.body.mysql_password }}\n      MYSQL_DATABASE: {{ $('API').item.json.body.mysql_database }}\n      MYSQL_USER: {{ $('API').item.json.body.mysql_user }}\n      MYSQL_HOST: {{ $('API').item.json.body.domain }}_db\n      REDIS_HOST: {{ $('API').item.json.body.domain }}_redis\n      VIRTUAL_HOST: {{ $('API').item.json.body.domain }}\n      LETSENCRYPT_HOST: {{ $('API').item.json.body.domain }}\n    volumes:\n      - \"{{ $('Parametrs').item.json.mount_dir }}/{{ $('API').item.json.body.domain }}/config:/var/www/html/config\"\n      - \"{{ $('Parametrs').item.json.mount_dir }}/{{ $('API').item.json.body.domain }}/data:/var/www/html/data\"\n      - \"{{ $('Parametrs').item.json.mount_dir }}/{{ $('API').item.json.body.domain }}/html:/var/www/html\"\n    networks:\n      - nginx-proxy_web\n    depends_on:\n      - {{ $('API').item.json.body.domain }}_db\n      - {{ $('API').item.json.body.domain }}_redis\n      - {{ $('API').item.json.body.domain }}_collabora\n    mem_limit: \"{{ $('API').item.json.body.ram }}G\"\n    cpus: \"{{ $('API').item.json.body.cpu }}\"\n\n  {{ $('API').item.json.body.domain }}_db:\n    image: mariadb:11.4\n    container_name: {{ $('API').item.json.body.domain }}_db\n    environment:\n      MYSQL_ROOT_PASSWORD: {{ $('API').item.json.body.mysql_root_password }}\n      MYSQL_PASSWORD: {{ $('API').item.json.body.mysql_password }}\n      MYSQL_DATABASE: {{ $('API').item.json.body.mysql_database }}\n      MYSQL_USER: {{ $('API').item.json.body.mysql_user }}\n    volumes:\n      - \"{{ $('Parametrs').item.json.mount_dir }}/{{ $('API').item.json.body.domain }}/db:/var/lib/mysql\"\n    networks:\n      - nginx-proxy_web\n    mem_limit: \"{{ Number($('API').item.json.body.ram) / 2 }}G\"\n    cpus: \"{{ Number($('API').item.json.body.cpu) / 2 }}\"\n\n  {{ $('API').item.json.body.domain }}_redis:\n    image: redis:alpine\n    container_name: {{ $('API').item.json.body.domain }}_redis\n    networks:\n      - nginx-proxy_web\n    mem_limit: \"{{ Number($('API').item.json.body.ram) / 4 }}G\"\n    cpus: \"{{ Number($('API').item.json.body.cpu) / 4 }}\"\n\n  {{ $('API').item.json.body.domain }}_collabora:\n    image: collabora/code\n    container_name: {{ $('API').item.json.body.domain }}_collabora\n    environment:\n      - domain={{ $('API').item.json.body.office_domain_escaped }}:443\n      - server_name=office.{{ $('API').item.json.body.domain }}\n      - username={{ $('API').item.json.body.mysql_user }}\n      - password={{ $('API').item.json.body.mysql_password }}\n      - \"dictionaries=ru_RU uk_UA pl_PL en\"\n      - \"extra_params=--o:ssl.enable=true --o:ssl.termination=true --o:net.proto=https --o:ssl.le=true --o:storage.wopi.host=https://{{ $('API').item.json.body.domain }}\"\n      - VIRTUAL_HOST=office.{{ $('API').item.json.body.domain }}\n      - LETSENCRYPT_HOST=office.{{ $('API').item.json.body.domain }}\n      - VIRTUAL_PROTO=https\n      - VIRTUAL_PORT=9980\n    cap_add:\n      - MKNOD\n      - SYS_ADMIN\n    extra_hosts:\n      - \"{{ $('API').item.json.body.domain }}:77.87.125.201\"\n    dns:\n      - 8.8.8.8\n      - 8.8.4.4\n    networks:\n      - nginx-proxy_web\n    mem_limit: \"{{ Number($('API').item.json.body.ram) }}G\"\n    cpus: \"{{ Number($('API').item.json.body.cpu) / 2 }}\"\n\nnetworks:\n  nginx-proxy_web:\n    external: true\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "4243c90b-de8a-4931-972b-5f700edb09d4", "name": "Version", "type": "n8n-nodes-base.set", "position": [1380, 2640], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\n# Define the container name dynamically using an API call\nCONTAINER_NAME=\"{{ $('API').item.json.body.domain }}_nextcloud\"\nVERSION_JSON=\"{}\"\n\n# Function to handle errors and return a JSON-formatted message\nhandle_error() {\n    echo \"{\\\"status\\\": \\\"error\\\", \\\"message\\\": \\\"$1\\\"}\"\n    exit 1\n}\n\n# Check if the container exists by searching for its name in the list of all Docker containers\nif ! sudo docker ps -a | grep -q \"$CONTAINER_NAME\" > /dev/null 2>&1; then\n    handle_error \"Container $CONTAINER_NAME not found\"\nfi\n\n# Retrieve the Nextcloud status as a JSON response from the container\n# The '-u 33' option ensures that the command is executed as the Nextcloud user (www-data)\nNEXTCLOUD_STATUS=$(sudo docker exec -u 33 \"$CONTAINER_NAME\" php occ status --output=json 2>/dev/null)\n\n# Validate if the command was executed successfully and if the output is not empty\nif [ $? -ne 0 ] || [ -z \"$NEXTCLOUD_STATUS\" ]; then\n    handle_error \"Failed to retrieve Nextcloud status for $CONTAINER_NAME\"\nfi\n\n# Extract the Nextcloud version string from the JSON response\nVERSION=$(echo \"$NEXTCLOUD_STATUS\" | jq -r '.versionstring')\n\n# Ensure that a valid version string was extracted\nif [ -z \"$VERSION\" ]; then\n    handle_error \"Failed to parse Nextcloud version from response\"\nfi\n\n# Construct a JSON-formatted output containing the Nextcloud version\nVERSION_JSON=\"{\\\"version\\\": \\\"$VERSION\\\"}\"\n\n# Print the JSON result\necho \"$VERSION_JSON\"\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "4f13c4f2-82dd-478f-915b-247a071db107", "name": "Users", "type": "n8n-nodes-base.set", "position": [1380, 2780], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\n# Define the container name dynamically using an API call\nCONTAINER_NAME=\"{{ $('API').item.json.body.domain }}_nextcloud\"\nUSERS_JSON=\"{}\"\n\n# Function to handle errors and return a JSON-formatted message\nhandle_error() {\n    echo \"{\\\"status\\\": \\\"error\\\", \\\"message\\\": \\\"$1\\\"}\"\n    exit 1\n}\n\n# Check if the container exists by searching for its name in the list of all Docker containers\nif ! sudo docker ps -a | grep -q \"$CONTAINER_NAME\" > /dev/null 2>&1; then\n    handle_error \"Container $CONTAINER_NAME not found\"\nfi\n\n# Retrieve the list of Nextcloud users and reformat it into a proper JSON array\nUSERS=$(sudo docker exec -u 33 \"$CONTAINER_NAME\" php occ user:list --output=json 2>/dev/null | jq -c 'to_entries | map({username: .key, displayname: .value})')\n\n# Validate if the command executed successfully and output is not empty\nif [ $? -ne 0 ] || [ -z \"$USERS\" ]; then\n    handle_error \"Failed to retrieve users from Nextcloud\"\nfi\n\n# Construct a JSON-formatted output containing all retrieved users\nUSERS_JSON=\"{\\\"users\\\": $USERS}\"\n\n# Print the JSON result\necho \"$USERS_JSON\"\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "6d385bc7-01f1-4d42-b16e-a2e45927ef7f", "name": "Change Password", "type": "n8n-nodes-base.set", "position": [1380, 2960], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\nCONTAINER_NAME=\"{{ $('API').item.json.body.domain }}_nextcloud\"\nNC_USER=\"{{ $('API').item.json.body.user_email }}\"\nNEW_PASSWORD=\"{{ $('API').item.json.body.password }}\"\n\n# Function to output error in JSON format and exit with code 1\nhandle_error() {\n    echo \"{\\\"status\\\": \\\"error\\\", \\\"message\\\": \\\"$1\\\"}\"\n    exit 1\n}\n\n# Check if container name is provided\nif [ -z \"$CONTAINER_NAME\" ]; then\n    handle_error \"No container name provided\"\nfi\n\n# Check if Nextcloud username is provided\nif [ -z \"$NC_USER\" ]; then\n    handle_error \"No Nextcloud user provided\"\nfi\n\n# Check if password is provided\nif [ -z \"$NEW_PASSWORD\" ]; then\n    handle_error \"No password provided\"\nfi\n\n# Run command in container\n#   -u 33                  => as UID 33 (often www-data in Nextcloud)\n#   -e OC_PASS=\"$NEW_PASSWORD\" => pass password through environment to container\n#   php occ user:resetpassword --password-from-env \"$NC_USER\"\n# returns 0 if successful\n\nOUTPUT=$( sudo docker exec -u 33 \\\n                    -e OC_PASS=\"$NEW_PASSWORD\" \\\n                    \"$CONTAINER_NAME\" \\\n                    php occ user:resetpassword --password-from-env \"$NC_USER\" 2>&1 )\n\n# Check return code\nif [ $? -ne 0 ]; then\n    handle_error \"Failed to reset password. Output: $OUTPUT\"\nfi\n\necho \"{\\\"status\\\": \\\"success\\\"}\"\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "dd283191-a5cd-4d29-8c2d-0ef42b63f69c", "name": "NextCloud", "type": "n8n-nodes-base.switch", "position": [920, 2620], "parameters": {"rules": {"values": [{"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "66ad264d-5393-410c-bfa3-011ab8eb234a", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "app_version"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "b48957a0-22c0-4ac0-82ef-abd9e7ab0207", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "app_users"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "7c862a6f-5df1-499c-b9c6-9b266e2bebec", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "change_password"}]}, "renameOutput": true}]}, "options": {}}, "typeVersion": 3.2, "notes": "This switch node performs automated tasks as part of the workflow."}, {"id": "9f5e3d3e-4f6d-4967-aefe-b953c5c3418b", "name": "nginx", "type": "n8n-nodes-base.set", "position": [1080, 140], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "main", "type": "string", "value": "=# Increase max body size for large file uploads\nclient_max_body_size 50000M;\n\n# Proxy headers\nproxy_set_header Host              $http_host;\nproxy_set_header X-Real-IP         $remote_addr;\nproxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;\nproxy_set_header X-Forwarded-Proto $scheme;\n\n# WebSocket support\nproxy_http_version 1.1;\nproxy_set_header   Upgrade    $http_upgrade;\nproxy_set_header   Connection \"upgrade\";\n\n# Timeouts\nproxy_read_timeout 600s;\nproxy_send_timeout 600s;\nsend_timeout       600s;\n\n# Additional optimizations\nproxy_buffering off;\nproxy_buffer_size 128k;\nproxy_buffers 4 256k;\nproxy_busy_buffers_size 256k;\nproxy_temp_file_write_size 256k;\nproxy_connect_timeout 600s;\n"}, {"id": "6507763a-21d4-4ff0-84d2-5dc9d21b7430", "name": "main_location", "type": "string", "value": "="}, {"id": "d00aa07a-0641-43ef-8fd2-5fb9ef62e313", "name": "office", "type": "string", "value": "=server_name  office.{{ $('API').item.json.body.domain }};\n\n# static files\n location ^~ /browser {\n   proxy_pass {{ $env.WEBHOOK_URL }}{{ $('API').item.json.body.domain }};\n   proxy_set_header Host $host;\n }\n\n\n # WOPI discovery URL\n location ^~ /hosting/discovery {\n   proxy_pass {{ $env.WEBHOOK_URL }}{{ $('API').item.json.body.domain }};\n   proxy_set_header Host $host;\n }\n\n\n # Capabilities\n location ^~ /hosting/capabilities {\n   proxy_pass {{ $env.WEBHOOK_URL }}{{ $('API').item.json.body.domain }};\n   proxy_set_header Host $host;\n }\n\n\n # main websocket\n location ~ ^/cool/(.*)/ws$ {\n   proxy_pass {{ $env.WEBHOOK_URL }}{{ $('API').item.json.body.domain }};\n   proxy_set_header Upgrade $http_upgrade;\n   proxy_set_header Connection \"Upgrade\";\n   proxy_set_header Host $host;\n   proxy_read_timeout 36000s;\n }\n\n\n # download, presentation and image upload\n location ~ ^/(c|l)ool {\n   proxy_pass {{ $env.WEBHOOK_URL }}{{ $('API').item.json.body.domain }};\n   proxy_set_header Host $host;\n }\n\n # Admin Console websocket\n location ^~ /cool/adminws {\n   proxy_pass {{ $env.WEBHOOK_URL }}{{ $('API').item.json.body.domain }};\n   proxy_set_header Upgrade $http_upgrade;\n   proxy_set_header Connection \"Upgrade\";\n   proxy_set_header Host $host;\n   proxy_read_timeout 36000s;\n }\n"}, {"id": "c00fb803-8b9f-4aca-a1b1-2e3da42fc8d1", "name": "office_location", "type": "string", "value": "="}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "fa40012b-0e58-4d6c-af19-b9dd6c72386d", "name": "Test Connection", "type": "n8n-nodes-base.set", "onError": "continueRegularOutput", "position": [1920, -40], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\n# Function to log an error, print to console\nhandle_error() {\n    echo \"error: $1\"\n    exit 1\n}\n\n# Check if Docker is installed\nif ! command -v docker &> /dev/null; then\n    handle_error \"Docker is not installed\"\nfi\n\n# Check if Docker service is running\nif ! systemctl is-active --quiet docker; then\n    handle_error \"Docker service is not running\"\nfi\n\n# Check if nginx-proxy container is running\nif ! sudo docker ps --filter \"name=nginx-proxy\" --filter \"status=running\" -q > /dev/null; then\n    handle_error \"nginx-proxy container is not running\"\nfi\n\n# Check if letsencrypt-nginx-proxy-companion container is running\nif ! sudo docker ps --filter \"name=letsencrypt-nginx-proxy-companion\" --filter \"status=running\" -q > /dev/null; then\n    handle_error \"letsencrypt-nginx-proxy-companion container is not running\"\nfi\n\n# If everything is successful\necho \"success\"\n\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "12240691-bcbe-407c-b53c-89cf84bc190f", "name": "ChangePackage", "type": "n8n-nodes-base.set", "onError": "continueRegularOutput", "position": [1920, 840], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\n# Get values for variables from templates\nDOMAIN=\"{{ $('API').item.json.body.domain }}\"\nCONTAINER_NAME=\"{{ $('API').item.json.body.domain }}_nextcloud\"\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/$DOMAIN\"\nCOMPOSE_FILE=\"$COMPOSE_DIR/docker-compose.yml\"\nSTATUS_FILE=\"$COMPOSE_DIR/status\"\nIMG_FILE=\"$COMPOSE_DIR/data.img\"\nNGINX_DIR=\"$COMPOSE_DIR/nginx\"\nVHOST_DIR=\"/opt/docker/nginx-proxy/nginx/vhost.d\"\nMOUNT_DIR=\"{{ $('Parametrs').item.json.mount_dir }}/$DOMAIN\"\nDOCKER_COMPOSE_TEXT='{{ JSON.stringify($('Deploy-docker-compose').item.json['docker-compose']).base64Encode() }}'\n\nNGINX_MAIN_TEXT='{{ JSON.stringify($('nginx').item.json['main']).base64Encode() }}'\nNGINX_MAIN_FILE=\"$NGINX_DIR/$DOMAIN\"\nVHOST_MAIN_FILE=\"$VHOST_DIR/$DOMAIN\"\n\nNGINX_MAIN_LOCATION_TEXT='{{ JSON.stringify($('nginx').item.json['main_location']).base64Encode() }}'\nNGINX_MAIN_LOCATION_FILE=\"$NGINX_DIR/$DOMAIN\"_location\nVHOST_MAIN_LOCATION_FILE=\"$VHOST_DIR/$DOMAIN\"_location\n\nNGINX_OFFICE_TEXT='{{ JSON.stringify($('nginx').item.json['office']).base64Encode() }}'\nNGINX_OFFICE_FILE=\"$NGINX_DIR/office.$DOMAIN\"\nVHOST_OFFICE_FILE=\"$VHOST_DIR/office.$DOMAIN\"\n\nNGINX_OFFICE_LOCATION_TEXT='{{ JSON.stringify($('nginx').item.json['office_location']).base64Encode() }}'\nNGINX_OFFICE_LOCATION_FILE=\"$NGINX_DIR/office.$DOMAIN\"_location\nVHOST_OFFICE_LOCATION_FILE=\"$VHOST_DIR/office.$DOMAIN\"_location\n\nDISK_SIZE=\"{{ $('API').item.json.body.disk }}\"\n\n# Function to log an error, write to status file, and print to office\nhandle_error() {\n    STATUS_JSON=\"{\\\"status\\\": \\\"error\\\", \\\"message\\\": \\\"$1\\\"}\"\n    echo \"$STATUS_JSON\" | sudo tee \"$STATUS_FILE\" > /dev/null\n    echo \"error: $1\"\n    exit 1\n}\n\n# Get nginx-proxy IP address before installing Nextcloud Office\nget_proxy_ip() {\n    local ip=\"\"\n    local retries=10  # Try a few times\n    local count=0\n    while [[ -z \"$ip\" && $count -lt $retries ]]; do\n        ip=$(sudo docker inspect -f '{{ $('Parametrs').item.json.screen_left }}range .NetworkSettings.Networks{{ $('Parametrs').item.json.screen_right }}{{ $('Parametrs').item.json.screen_left }}.IPAddress{{ $('Parametrs').item.json.screen_right }}{{ $('Parametrs').item.json.screen_left }}end{{ $('Parametrs').item.json.screen_right }}' nginx-proxy)\n        if [[ -z \"$ip\" ]]; then\n            echo \"[DEBUG] nginx-proxy IP not found, retrying ($count/$retries)...\" >> \"$STATUS_FILE\"\n            sleep 2  # Wait a bit before retrying\n        fi\n        ((count++))\n    done\n\n    if [[ -z \"$ip\" ]]; then\n        echo \"[ERROR] Failed to retrieve nginx-proxy IP after $retries attempts!\" >> \"$STATUS_FILE\"\n        handle_error \"Failed to retrieve nginx-proxy IP\"\n    fi\n\n    echo \"[DEBUG] Detected nginx-proxy IP: $ip\" >> \"$STATUS_FILE\"\n    echo \"$ip\"\n}\n\n# Get the IP address of Nextcloud Office\nget_office_ip() {\n    local ip=\"\"\n    local retries=10  # Try a few times\n    local count=0\n    while [[ -z \"$ip\" && $count -lt $retries ]]; do\n        ip=$(sudo docker inspect -f '{{ $('Parametrs').item.json.screen_left }}range .NetworkSettings.Networks{{ $('Parametrs').item.json.screen_right }}{{ $('Parametrs').item.json.screen_left }}.IPAddress{{ $('Parametrs').item.json.screen_right }}{{ $('Parametrs').item.json.screen_left }}end{{ $('Parametrs').item.json.screen_right }}' \"$DOMAIN\"_collabora)\n        if [[ -z \"$ip\" ]]; then\n            echo \"[DEBUG] office IP not found, retrying ($count/$retries)...\" >> \"$STATUS_FILE\"\n            sleep 2  # Wait a bit before retrying\n        fi\n        ((count++))\n    done\n\n    if [[ -z \"$ip\" ]]; then\n        echo \"[ERROR] Failed to retrieve office IP after $retries attempts!\" >> \"$STATUS_FILE\"\n        handle_error \"Failed to retrieve office IP\"\n    fi\n\n    # Convert IP to subnet by replacing the last octet with 0 and adding /24\n    local subnet=$(echo \"$ip\" | sed 's/\\.[0-9]*$/.0\\/24/')\n    echo \"[DEBUG] Detected office subnet: $subnet\" >> \"$STATUS_FILE\"\n    echo \"$subnet\"\n}\n\n# Check if the compose file exists before stopping the container\nif [ -f \"$COMPOSE_FILE\" ]; then\n    sudo docker-compose -f \"$COMPOSE_FILE\" down > /dev/null 2>&1 || handle_error \"Failed to stop containers\"\nelse\n    handle_error \"docker-compose.yml not found\"\nfi\n\n# Unmount the image if it is currently mounted\nif mount | grep -q \"$MOUNT_DIR\"; then\n    sudo umount \"$MOUNT_DIR\" > /dev/null 2>&1 || handle_error \"Failed to unmount $MOUNT_DIR\"\nfi\n\n# Create docker-compose.yml file\necho -e \"$DOCKER_COMPOSE_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$COMPOSE_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $COMPOSE_FILE\"\n\n# Create NGINX configuration files\necho -e \"$NGINX_MAIN_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$NGINX_MAIN_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $NGINX_MAIN_FILE\"\necho -e \"$NGINX_MAIN_LOCATION_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$NGINX_MAIN_LOCATION_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $NGINX_MAIN_LOCATION_FILE\"\n\necho -e \"$NGINX_OFFICE_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$NGINX_OFFICE_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $NGINX_OFFICE_FILE\"\necho -e \"$NGINX_OFFICE_LOCATION_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$NGINX_OFFICE_LOCATION_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $NGINX_OFFICE_LOCATION_FILE\"\n\n# Resize or extend the disk image to match DISK_SIZE\nif [ -f \"$IMG_FILE\" ]; then\n    DESIRED_SIZE_BYTES=$((DISK_SIZE * 1024 * 1024 * 1024))\n    CURRENT_SIZE_BYTES=$(stat -c %s \"$IMG_FILE\")\n\n    # Expand or shrink as needed\n    if [ \"$CURRENT_SIZE_BYTES\" -lt \"$DESIRED_SIZE_BYTES\" ]; then\n        # echo \"[INFO] Expanding image to $DISK_SIZE GB...\"\n        sudo truncate -s \"$DESIRED_SIZE_BYTES\" \"$IMG_FILE\" || handle_error \"Failed to expand $IMG_FILE\" 2>/dev/null\n\n        LOOP_DEV=$(sudo losetup --find --show \"$IMG_FILE\" 2>/dev/null) || handle_error \"Failed to setup loop device\" \n        sudo e2fsck -fy \"$LOOP_DEV\" || { sudo losetup -d \"$LOOP_DEV\"; handle_error \"Filesystem check failed\" ; } 2>/dev/null\n        sudo resize2fs \"$LOOP_DEV\" || { sudo losetup -d \"$LOOP_DEV\"; handle_error \"resize2fs after expand failed\" ; } 2>/dev/null\n        sudo losetup -d \"$LOOP_DEV\" 2>/dev/null\n\n    elif [ \"$CURRENT_SIZE_BYTES\" -gt \"$DESIRED_SIZE_BYTES\" ]; then\n        # echo \"[INFO] Shrinking image to $DISK_SIZE GB...\"\n        LOOP_DEV=$(sudo losetup --find --show \"$IMG_FILE\" 2>/dev/null) || handle_error \"Failed to setup loop device\" \n        sudo e2fsck -fy \"$LOOP_DEV\" || { sudo losetup -d \"$LOOP_DEV\"; handle_error \"Filesystem check failed\" ; } 2>/dev/null\n        sudo resize2fs -M \"$LOOP_DEV\" || { sudo losetup -d \"$LOOP_DEV\"; handle_error \"resize2fs -M failed\" ; } 2>/dev/null\n\n        BLOCKS=$(sudo tune2fs -l \"$LOOP_DEV\" | grep '^Block count:' | awk '{print $3}')\n        BLOCK_SIZE=$(sudo tune2fs -l \"$LOOP_DEV\" | grep '^Block size:' | awk '{print $3}')\n        MIN_BYTES=$((BLOCKS * BLOCK_SIZE))\n        sudo losetup -d \"$LOOP_DEV\" 2>/dev/null\n\n        if [ \"$DESIRED_SIZE_BYTES\" -lt \"$MIN_BYTES\" ]; then\n            handle_error \"DISK_SIZE too small. Minimum size is $((MIN_BYTES / 1024 / 1024 / 1024)) GB\"\n        fi\n\n        sudo truncate -s \"$DESIRED_SIZE_BYTES\" \"$IMG_FILE\" || handle_error \"Failed to truncate to desired size\"\n\n        LOOP_DEV=$(sudo losetup --find --show \"$IMG_FILE\" 2>/dev/null) || handle_error \"Failed to setup loop device (after shrink)\"\n        sudo resize2fs \"$LOOP_DEV\" || { sudo losetup -d \"$LOOP_DEV\"; handle_error \"resize2fs after shrink failed\" ; } 2>/dev/null\n        sudo losetup -d \"$LOOP_DEV\" 2>/dev/null\n    fi\n\n    # Remove the old line from /etc/fstab (if it exists) and add it again\n    sudo sed -i \"\\|$IMG_FILE|d\" /etc/fstab\n    echo \"$IMG_FILE $MOUNT_DIR ext4 loop 0 0\" | sudo tee -a /etc/fstab > /dev/null || handle_error \"Failed to update /etc/fstab\"\n\n    # Create the folder if it doesn't exist\n    sudo mkdir -p \"$MOUNT_DIR\"\n    sudo chmod 777 \"$MOUNT_DIR\"\n\n    # Try to mount manually\n    if ! sudo mount \"$MOUNT_DIR\"; then\n        echo \"[WARN] mount -a failed, trying manual mount with loop\"\n        LOOP_DEV=$(sudo losetup --find --show \"$IMG_FILE\") || handle_error \"Failed to setup loop device (manual)\"\n        sudo mount -t ext4 \"$LOOP_DEV\" \"$MOUNT_DIR\" || {\n            sudo losetup -d \"$LOOP_DEV\"\n            handle_error \"Manual mount failed\"\n        }\n    fi\nelse\n    handle_error \"Disk image $IMG_FILE does not exist\"\nfi\n\n# Mount the disk only if it is not already mounted\nif ! mount | grep -q \"$MOUNT_DIR\"; then\n    sudo mount -a || handle_error \"Failed to mount entries from /etc/fstab\"\nfi\n\n# Change to the compose directory\ncd \"$COMPOSE_DIR\" > /dev/null 2>&1 || handle_error \"Failed to change directory to $COMPOSE_DIR\"\n\n# Copy NGINX configuration files instead of creating symbolic links\nsudo cp -f \"$NGINX_MAIN_FILE\" \"$VHOST_MAIN_FILE\" || handle_error \"Failed to copy $NGINX_MAIN_FILE to $VHOST_MAIN_FILE\"\nsudo chmod 777 \"$VHOST_MAIN_FILE\" || handle_error \"Failed to set permissions on $VHOST_MAIN_FILE\"\n\nsudo cp -f \"$NGINX_MAIN_LOCATION_FILE\" \"$VHOST_MAIN_LOCATION_FILE\" || handle_error \"Failed to copy $NGINX_MAIN_LOCATION_FILE to $VHOST_MAIN_LOCATION_FILE\"\nsudo chmod 777 \"$VHOST_MAIN_LOCATION_FILE\" || handle_error \"Failed to set permissions on $VHOST_MAIN_LOCATION_FILE\"\n\nsudo cp -f \"$NGINX_OFFICE_FILE\" \"$VHOST_OFFICE_FILE\" || handle_error \"Failed to copy $NGINX_OFFICE_FILE to $VHOST_OFFICE_FILE\"\nsudo chmod 777 \"$VHOST_OFFICE_FILE\" || handle_error \"Failed to set permissions on $VHOST_OFFICE_FILE\"\n\nsudo cp -f \"$NGINX_OFFICE_LOCATION_FILE\" \"$VHOST_OFFICE_LOCATION_FILE\" || handle_error \"Failed to copy $NGINX_OFFICE_LOCATION_FILE to $VHOST_OFFICE_LOCATION_FILE\"\nsudo chmod 777 \"$VHOST_OFFICE_LOCATION_FILE\" || handle_error \"Failed to set permissions on $VHOST_OFFICE_LOCATION_FILE\"\n\n# Start Docker containers using docker-compose\nif ! sudo docker compose up -d > /dev/null 2>error.log; then\n    ERROR_MSG=$(tail -n 10 error.log)  # Read the last 10 lines from error.log\n    handle_error \"Docker-compose failed: $ERROR_MSG\"\nfi\n\n# --- Function that installs Nextcloud Office (Collabora) in the background ---\ninstall_nextcloud_office() {\n    MAX_RETRIES=60\n    COUNTER=0\n\n\n    # 1) Wait until \"installed: true\" in occ status\n    while true; do\n        STATUS_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ status 2>&1)\"\n        if echo \"$STATUS_OUTPUT\" | grep -q \"installed: true\"; then\n            echo \"[OfficeSetup] Nextcloud reports installed: true. Proceeding...\" >> \"$STATUS_FILE\"\n            break\n        else\n            echo \"[OfficeSetup] [$COUNTER/$MAX_RETRIES] Nextcloud not fully installed yet, waiting...\" >> \"$STATUS_FILE\"\n            sleep 2\n            ((COUNTER++))\n            if [ $COUNTER -ge $MAX_RETRIES ]; then\n                echo \"[OfficeSetup] Nextcloud did not report 'installed: true' within time limit. Skipping Office install.\" >> \"$STATUS_FILE\"\n                return\n            fi\n        fi\n    done\n\n    # Get the nginx-proxy IP\n    PROXY_IP=$(get_proxy_ip)\n\n    echo \"[OfficeSetup] Detected nginx-proxy IP: $PROXY_IP\" >> \"$STATUS_FILE\"\n    \n\n    # Write the needed parameters to the Nextcloud config\n    echo \"[OfficeSetup] Setting overwrite protocol/host/cli.url in Nextcloud config...\" >> \"$STATUS_FILE\"\n    sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set overwriteprotocol --value=https 2>&1\n    sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set overwritehost --value=\"$DOMAIN\" 2>&1\n    sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set overwrite.cli.url --value=\"{{ $env.WEBHOOK_URL }}\" 2>&1\n\n    # Add the nginx-proxy IP to the trusted_proxies list\n    echo \"[OfficeSetup] Adding nginx-proxy IP to trusted_proxies...\" >> \"$STATUS_FILE\"\n    # *** NEW BLOCK *** - Get the IP address of the reverse proxy\n    sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set trusted_proxies 0 --value=\"$PROXY_IP\" 2>&1\n\n    echo \"[OfficeSetup] Installing Nextcloud Office (richdocuments)...\" >> \"$STATUS_FILE\"\n\n    # 2) Install the richdocuments app\n    INSTALL_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ app:install richdocuments 2>&1 || echo \"[OfficeSetup] App already installed\")\"\n    echo \"[OfficeSetup] app:install richdocuments => $INSTALL_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 3) Set the Collabora Online URL in Nextcloud\n    WOPI_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:app:set richdocuments wopi_url --value=\"{{ $env.WEBHOOK_URL }}\" 2>&1)\"\n    echo \"[OfficeSetup] wopi_url => $WOPI_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 4) Enable the app\n    ENABLE_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ app:enable richdocuments 2>&1)\"\n    echo \"[OfficeSetup] app:enable richdocuments => $ENABLE_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 5) Allow local remote servers (Fix for Collabora access issues)\n    ALLOW_LOCAL_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set allow_local_remote_servers --value=true --type=bool 2>&1)\"\n    echo \"[OfficeSetup] allow_local_remote_servers => $ALLOW_LOCAL_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 6) Apply changes by running maintenance repair\n    REPAIR_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ maintenance:repair 2>&1)\"\n    echo \"[OfficeSetup] maintenance:repair => $REPAIR_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 7) Activate Collabora Online configuration\n    ACTIVATE_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ richdocuments:activate-config 2>&1)\"\n    echo \"[OfficeSetup] richdocuments:activate-config => $ACTIVATE_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 8) Refresh cache by scanning all files\n    SCAN_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ files:scan --all 2>&1)\"\n    echo \"[OfficeSetup] files:scan --all => $SCAN_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 9) Double-check if the app is enabled\n    APP_LIST=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ app:list 2>&1)\"\n    echo \"[OfficeSetup] occ app:list => $APP_LIST\" >> \"$STATUS_FILE\"\n\n    # 10) Perform the migrations\n    MIGRATION_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ maintenance:repair --include-expensive 2>&1)\"\n    echo \"[OfficeSetup] maintenance:repair --include-expensive => $MIGRATION_OUTPUT\" >> \"$STATUS_FILE\"\n\n    if echo \"$APP_LIST\" | grep -q \"richdocuments: enabled\"; then\n        echo \"[OfficeSetup] Nextcloud Office successfully installed and configured!\" >> \"$STATUS_FILE\"\n    else\n        echo \"[OfficeSetup] Nextcloud Office installation failed or not enabled.\" >> \"$STATUS_FILE\"\n    fi\n\n    OFFICE_IP_SUBNET=$(get_office_ip)\n    echo \"[OfficeSetup] Detected office IP: $OFFICE_IP_SUBNET\" >> \"$STATUS_FILE\"\n\n    # Write the needed parameters to the Collabora config\n    # 1) Collabora \n    ACTIVATE_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:app:set richdocuments wopi_allowlist --value=\"$OFFICE_IP_SUBNET\" 2>&1)\"\n    echo \"[OfficeSetup] richdocuments:wopi_allowlist => $ACTIVATE_OUTPUT\" >> \"$STATUS_FILE\"\n\n}\n\n# Export DOMAIN so it's visible to the function in background\nexport DOMAIN\nexport CONTAINER_NAME\n\n# Export the get_proxy_ip function for visibility in nohup\nexport -f get_proxy_ip\n# Export the get_office_ip function for visibility in nohup\nexport -f get_office_ip\n\n\n# Run the installation in the background, no blocking\n nohup bash -c \"$(\n  declare -f install_nextcloud_office\n  echo 'install_nextcloud_office'\n )\" > /tmp/office_install.log 2>&1 &\n\n\n# Update status file\necho \"active\" | sudo tee \"$STATUS_FILE\" > /dev/null\n\necho \"success\"\n\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "5c01e300-9eb2-42db-b609-1ddb1d0140e7", "name": "Terminated", "type": "n8n-nodes-base.set", "onError": "continueRegularOutput", "position": [1920, 660], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\nDOMAIN=\"{{ $('API').item.json.body.domain }}\"\nCONTAINER_NAME=\"{{ $('API').item.json.body.domain }}_nextcloud\"\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/$DOMAIN\"\nCOMPOSE_FILE=\"$COMPOSE_DIR/docker-compose.yml\"\nSTATUS_FILE=\"$COMPOSE_DIR/status\"\nIMG_FILE=\"$COMPOSE_DIR/data.img\"\nNGINX_DIR=\"$COMPOSE_DIR/nginx\"\nVHOST_DIR=\"/opt/docker/nginx-proxy/nginx/vhost.d\"\nCRON_SCRIPT=\"{{ $('Parametrs').item.json.clients_dir }}/$DOMAIN/cron.sh\"\n\nVHOST_MAIN_FILE=\"$VHOST_DIR/$DOMAIN\"\nVHOST_MAIN_LOCATION_FILE=\"$VHOST_DIR/$DOMAIN\"_location\nVHOST_OFFICE_FILE=\"$VHOST_DIR/office.$DOMAIN\"\nVHOST_OFFICE_LOCATION_FILE=\"$VHOST_DIR/office.$DOMAIN\"_location\nMOUNT_DIR=\"{{ $('Parametrs').item.json.mount_dir }}/$DOMAIN\"\n\n# Function to log an error, write to status file, and print to office\nhandle_error() {\n    STATUS_JSON=\"{\\\"status\\\": \\\"error\\\", \\\"message\\\": \\\"$1\\\"}\"\n    echo \"error: $1\"\n    exit 1\n}\n\n# Function to remove Nextcloud cron job\nremove_nextcloud_cron() {\n    echo \"[CRON] Removing Nextcloud cron job...\" >> /dev/null\n    \n    # Remove from crontab\n    crontab -l 2>/dev/null | grep -v \"$CONTAINER_NAME\" | crontab -\n    \n    echo \"[CRON] Nextcloud cron job removed successfully!\" >> /dev/null\n}\n\n# Stop and remove the Docker containers\nif [ -f \"$COMPOSE_FILE\" ]; then\n    sudo docker-compose -f \"$COMPOSE_FILE\" down > /dev/null 2>&1\nfi\n\n# Remove the mount entry from /etc/fstab if it exists\nif grep -q \"$IMG_FILE\" /etc/fstab; then\n    sudo sed -i \"\\|$(printf '%s\\n' \"$IMG_FILE\" | sed 's/[.[\\*^$]/\\\\&/g')|d\" /etc/fstab\nfi\n\n# Unmount the image if it is still mounted\nif mount | grep -q \"$MOUNT_DIR\"; then\n    sudo umount \"$MOUNT_DIR\" > /dev/null 2>&1 || handle_error \"Failed to unmount $MOUNT_DIR\"\nfi\n\n# Remove all related directories and files\nfor item in \"$COMPOSE_DIR\" \"$VHOST_MAIN_FILE\" \"$VHOST_MAIN_LOCATION_FILE\" \"$VHOST_OFFICE_FILE\" \"$VHOST_OFFICE_LOCATION_FILE\"; do\n    if [ -e \"$item\" ]; then\n        sudo rm -rf \"$item\" || handle_error \"Failed to remove $item\"\n    fi\ndone\n\nexport CONTAINER_NAME\n\n# Remove the cron after execution\nremove_nextcloud_cron\n\necho \"success\"\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "264dac81-0eda-4a49-b209-c7dda4dd649d", "name": "Unsuspend", "type": "n8n-nodes-base.set", "onError": "continueRegularOutput", "position": [1920, 480], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\nDOMAIN=\"{{ $('API').item.json.body.domain }}\"\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/$DOMAIN\"\nCOMPOSE_FILE=\"$COMPOSE_DIR/docker-compose.yml\"\nSTATUS_FILE=\"$COMPOSE_DIR/status\"\nIMG_FILE=\"$COMPOSE_DIR/data.img\"\nNGINX_DIR=\"$COMPOSE_DIR/nginx\"\nVHOST_DIR=\"/opt/docker/nginx-proxy/nginx/vhost.d\"\nMOUNT_DIR=\"{{ $('Parametrs').item.json.mount_dir }}/$DOMAIN\"\nDOCKER_COMPOSE_TEXT='{{ JSON.stringify($('Deploy-docker-compose').item.json[\"docker-compose\"]).base64Encode() }}'\n\nNGINX_MAIN_TEXT='{{ JSON.stringify($('nginx').item.json['main']).base64Encode() }}'\nNGINX_MAIN_FILE=\"$NGINX_DIR/$DOMAIN\"\nVHOST_MAIN_FILE=\"$VHOST_DIR/$DOMAIN\"\n\nNGINX_MAIN_LOCATION_TEXT='{{ JSON.stringify($('nginx').item.json['main_location']).base64Encode() }}'\nNGINX_MAIN_LOCATION_FILE=\"$NGINX_DIR/$DOMAIN\"_location\nVHOST_MAIN_LOCATION_FILE=\"$VHOST_DIR/$DOMAIN\"_location\n\nNGINX_OFFICE_TEXT='{{ JSON.stringify($('nginx').item.json['office']).base64Encode() }}'\nNGINX_OFFICE_FILE=\"$NGINX_DIR/office.$DOMAIN\"\nVHOST_OFFICE_FILE=\"$VHOST_DIR/office.$DOMAIN\"\n\nNGINX_OFFICE_LOCATION_TEXT='{{ JSON.stringify($('nginx').item.json['office_location']).base64Encode() }}'\nNGINX_OFFICE_LOCATION_FILE=\"$NGINX_DIR/office.$DOMAIN\"_location\nVHOST_OFFICE_LOCATION_FILE=\"$VHOST_DIR/office.$DOMAIN\"_location\n\nDISK_SIZE=\"{{ $('API').item.json.body.disk }}\"\n\n# Function to log an error, write to status file, and print to office\nhandle_error() {\n    STATUS_JSON=\"{\\\"status\\\": \\\"error\\\", \\\"message\\\": \\\"$1\\\"}\"\n    echo \"$STATUS_JSON\" | sudo tee \"$STATUS_FILE\" > /dev/null\n    echo \"error: $1\"\n    exit 1\n}\n\n\n# Get nginx-proxy IP address before installing Nextcloud Office\nget_proxy_ip() {\n    local ip=\"\"\n    local retries=10  # Try a few times\n    local count=0\n    while [[ -z \"$ip\" && $count -lt $retries ]]; do\n        ip=$(sudo docker inspect -f '{{ $('Parametrs').item.json.screen_left }}range .NetworkSettings.Networks{{ $('Parametrs').item.json.screen_right }}{{ $('Parametrs').item.json.screen_left }}.IPAddress{{ $('Parametrs').item.json.screen_right }}{{ $('Parametrs').item.json.screen_left }}end{{ $('Parametrs').item.json.screen_right }}' nginx-proxy)\n        if [[ -z \"$ip\" ]]; then\n            echo \"[DEBUG] nginx-proxy IP not found, retrying ($count/$retries)...\" >> \"$STATUS_FILE\"\n            sleep 2  # Wait a bit before retrying\n        fi\n        ((count++))\n    done\n\n    if [[ -z \"$ip\" ]]; then\n        echo \"[ERROR] Failed to retrieve nginx-proxy IP after $retries attempts!\" >> \"$STATUS_FILE\"\n        handle_error \"Failed to retrieve nginx-proxy IP\"\n    fi\n\n    echo \"[DEBUG] Detected nginx-proxy IP: $ip\" >> \"$STATUS_FILE\"\n    echo \"$ip\"\n}\n\n# Get the IP address of Nextcloud Office\nget_office_ip() {\n    local ip=\"\"\n    local retries=10  # Try a few times\n    local count=0\n    while [[ -z \"$ip\" && $count -lt $retries ]]; do\n        ip=$(sudo docker inspect -f '{{ $('Parametrs').item.json.screen_left }}range .NetworkSettings.Networks{{ $('Parametrs').item.json.screen_right }}{{ $('Parametrs').item.json.screen_left }}.IPAddress{{ $('Parametrs').item.json.screen_right }}{{ $('Parametrs').item.json.screen_left }}end{{ $('Parametrs').item.json.screen_right }}' \"$DOMAIN\"_collabora)\n        if [[ -z \"$ip\" ]]; then\n            echo \"[DEBUG] office IP not found, retrying ($count/$retries)...\" >> \"$STATUS_FILE\"\n            sleep 2  # Wait a bit before retrying\n        fi\n        ((count++))\n    done\n\n    if [[ -z \"$ip\" ]]; then\n        echo \"[ERROR] Failed to retrieve office IP after $retries attempts!\" >> \"$STATUS_FILE\"\n        handle_error \"Failed to retrieve office IP\"\n    fi\n\n    # Convert IP to subnet by replacing the last octet with 0 and adding /24\n    local subnet=$(echo \"$ip\" | sed 's/\\.[0-9]*$/.0\\/24/')\n    echo \"[DEBUG] Detected office subnet: $subnet\" >> \"$STATUS_FILE\"\n    echo \"$subnet\"\n}\n\n# Create necessary directories with permissions\nfor dir in \"$COMPOSE_DIR\" \"$NGINX_DIR\" \"$MOUNT_DIR\"; do\n    sudo mkdir -p \"$dir\" || handle_error \"Failed to create $dir\"\n    sudo chmod -R 777 \"$dir\" || handle_error \"Failed to set permissions on $dir\"\ndone\n\n# Check if the image is already mounted using fstab\nif ! grep -q \"$IMG_FILE\" /etc/fstab; then\n    echo \"$IMG_FILE $MOUNT_DIR ext4 loop 0 0\" | sudo tee -a /etc/fstab > /dev/null || handle_error \"Failed to add fstab entry for $IMG_FILE\"\nfi\n\n# Apply the fstab changes and mount the image\nif ! mount | grep -q \"$MOUNT_DIR\"; then\n    sudo mount -a || handle_error \"Failed to mount image using fstab\"\nfi\n\n# Create docker-compose.yml file\necho -e \"$DOCKER_COMPOSE_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$COMPOSE_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $COMPOSE_FILE\"\n\n# Create NGINX configuration files\necho -e \"$NGINX_MAIN_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$NGINX_MAIN_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $NGINX_MAIN_FILE\"\necho -e \"$NGINX_MAIN_LOCATION_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$NGINX_MAIN_LOCATION_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $NGINX_MAIN_LOCATION_FILE\"\n\necho -e \"$NGINX_OFFICE_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$NGINX_OFFICE_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $NGINX_OFFICE_FILE\"\necho -e \"$NGINX_OFFICE_LOCATION_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$NGINX_OFFICE_LOCATION_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $NGINX_OFFICE_LOCATION_FILE\"\n\n# Copy NGINX configuration files instead of creating symbolic links\nsudo cp -f \"$NGINX_MAIN_FILE\" \"$VHOST_MAIN_FILE\" || handle_error \"Failed to copy $NGINX_MAIN_FILE to $VHOST_MAIN_FILE\"\nsudo chmod 777 \"$VHOST_MAIN_FILE\" || handle_error \"Failed to set permissions on $VHOST_MAIN_FILE\"\n\nsudo cp -f \"$NGINX_MAIN_LOCATION_FILE\" \"$VHOST_MAIN_LOCATION_FILE\" || handle_error \"Failed to copy $NGINX_MAIN_LOCATION_FILE to $VHOST_MAIN_LOCATION_FILE\"\nsudo chmod 777 \"$VHOST_MAIN_LOCATION_FILE\" || handle_error \"Failed to set permissions on $VHOST_MAIN_LOCATION_FILE\"\n\nsudo cp -f \"$NGINX_OFFICE_FILE\" \"$VHOST_OFFICE_FILE\" || handle_error \"Failed to copy $NGINX_OFFICE_FILE to $VHOST_OFFICE_FILE\"\nsudo chmod 777 \"$VHOST_OFFICE_FILE\" || handle_error \"Failed to set permissions on $VHOST_OFFICE_FILE\"\n\nsudo cp -f \"$NGINX_OFFICE_LOCATION_FILE\" \"$VHOST_OFFICE_LOCATION_FILE\" || handle_error \"Failed to copy $NGINX_OFFICE_LOCATION_FILE to $VHOST_OFFICE_LOCATION_FILE\"\nsudo chmod 777 \"$VHOST_OFFICE_LOCATION_FILE\" || handle_error \"Failed to set permissions on $VHOST_OFFICE_LOCATION_FILE\"\n\n# Change to the compose directory\ncd \"$COMPOSE_DIR\" || handle_error \"Failed to change directory to $COMPOSE_DIR\"\n\n# Start Docker containers using docker-compose\n> error.log\nif ! sudo docker compose up -d > error.log 2>&1; then\n    ERROR_MSG=$(tail -n 10 error.log)  # Read the last 10 lines from error.log\n    handle_error \"Docker-compose failed: $ERROR_MSG\"\nfi\n\n\n\n# Function to add Nextcloud cron job\nadd_nextcloud_cron() {\n    echo \"[CRON] Adding Nextcloud cron job...\" >> /dev/null\n    \n    # Create cron command\n    CRON_CMD=\"*/5 * * * * sudo docker exec -u www-data $CONTAINER_NAME php cron.php --force\"\n    \n    # Add to crontab (remove old if exists)\n    (crontab -l 2>/dev/null | grep -v \"$CONTAINER_NAME\"; echo \"$CRON_CMD\") | crontab -\n    \n    echo \"[CRON] Nextcloud cron job added successfully!\" >> /dev/null\n}\n\n# Function to remove Nextcloud cron job\nremove_nextcloud_cron() {\n    echo \"[CRON] Removing Nextcloud cron job...\" >> /dev/null\n    \n    # Remove from crontab\n    crontab -l 2>/dev/null | grep -v \"$CONTAINER_NAME\" | crontab -\n    \n    echo \"[CRON] Nextcloud cron job removed successfully!\" >> /dev/null\n}\n\n\n\n# --- Function that installs Nextcloud Office (Collabora) in the background ---\ninstall_nextcloud_office() {\n    MAX_RETRIES=60\n    COUNTER=0\n\n\n    # 1) Wait until \"installed: true\" in occ status\n    while true; do\n        STATUS_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ status 2>&1)\"\n        if echo \"$STATUS_OUTPUT\" | grep -q \"installed: true\"; then\n            echo \"[OfficeSetup] Nextcloud reports installed: true. Proceeding...\" >> \"$STATUS_FILE\"\n            break\n        else\n            echo \"[OfficeSetup] [$COUNTER/$MAX_RETRIES] Nextcloud not fully installed yet, waiting...\" >> \"$STATUS_FILE\"\n            sleep 2\n            ((COUNTER++))\n            if [ $COUNTER -ge $MAX_RETRIES ]; then\n                echo \"[OfficeSetup] Nextcloud did not report 'installed: true' within time limit. Skipping Office install.\" >> \"$STATUS_FILE\"\n                return\n            fi\n        fi\n    done\n\n    # Get the nginx-proxy IP\n    PROXY_IP=$(get_proxy_ip)\n\n    echo \"[OfficeSetup] Detected nginx-proxy IP: $PROXY_IP\" >> \"$STATUS_FILE\"\n    \n\n    # Write the needed parameters to the Nextcloud config\n    echo \"[OfficeSetup] Setting overwrite protocol/host/cli.url in Nextcloud config...\" >> \"$STATUS_FILE\"\n    sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set overwriteprotocol --value=https 2>&1\n    sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set overwritehost --value=\"$DOMAIN\" 2>&1\n    sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set overwrite.cli.url --value=\"{{ $env.WEBHOOK_URL }}\" 2>&1\n\n    # Add the nginx-proxy IP to the trusted_proxies list\n    echo \"[OfficeSetup] Adding nginx-proxy IP to trusted_proxies...\" >> \"$STATUS_FILE\"\n    # *** NEW BLOCK *** - Get the IP address of the reverse proxy\n    sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set trusted_proxies 0 --value=\"$PROXY_IP\" 2>&1\n\n    echo \"[OfficeSetup] Installing Nextcloud Office (richdocuments)...\" >> \"$STATUS_FILE\"\n\n    # 2) Install the richdocuments app\n    INSTALL_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ app:install richdocuments 2>&1 || echo \"[OfficeSetup] App already installed\")\"\n    echo \"[OfficeSetup] app:install richdocuments => $INSTALL_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 3) Set the Collabora Online URL in Nextcloud\n    WOPI_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:app:set richdocuments wopi_url --value=\"{{ $env.WEBHOOK_URL }}\" 2>&1)\"\n    echo \"[OfficeSetup] wopi_url => $WOPI_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 4) Enable the app\n    ENABLE_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ app:enable richdocuments 2>&1)\"\n    echo \"[OfficeSetup] app:enable richdocuments => $ENABLE_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 5) Allow local remote servers (Fix for Collabora access issues)\n    ALLOW_LOCAL_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set allow_local_remote_servers --value=true --type=bool 2>&1)\"\n    echo \"[OfficeSetup] allow_local_remote_servers => $ALLOW_LOCAL_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 6) Apply changes by running maintenance repair\n    REPAIR_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ maintenance:repair 2>&1)\"\n    echo \"[OfficeSetup] maintenance:repair => $REPAIR_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 7) Activate Collabora Online configuration\n    ACTIVATE_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ richdocuments:activate-config 2>&1)\"\n    echo \"[OfficeSetup] richdocuments:activate-config => $ACTIVATE_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 8) Refresh cache by scanning all files\n    SCAN_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ files:scan --all 2>&1)\"\n    echo \"[OfficeSetup] files:scan --all => $SCAN_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 9) Double-check if the app is enabled\n    APP_LIST=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ app:list 2>&1)\"\n    echo \"[OfficeSetup] occ app:list => $APP_LIST\" >> \"$STATUS_FILE\"\n\n    # 10) Perform the migrations\n    MIGRATION_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ maintenance:repair --include-expensive 2>&1)\"\n    echo \"[OfficeSetup] maintenance:repair --include-expensive => $MIGRATION_OUTPUT\" >> \"$STATUS_FILE\"\n\n    if echo \"$APP_LIST\" | grep -q \"richdocuments: enabled\"; then\n        echo \"[OfficeSetup] Nextcloud Office successfully installed and configured!\" >> \"$STATUS_FILE\"\n    else\n        echo \"[OfficeSetup] Nextcloud Office installation failed or not enabled.\" >> \"$STATUS_FILE\"\n    fi\n\n    OFFICE_IP_SUBNET=$(get_office_ip)\n    echo \"[OfficeSetup] Detected office IP: $OFFICE_IP_SUBNET\" >> \"$STATUS_FILE\"\n\n    # Write the needed parameters to the Collabora config\n    # 1) Collabora \n    ACTIVATE_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:app:set richdocuments wopi_allowlist --value=\"$OFFICE_IP_SUBNET\" 2>&1)\"\n    echo \"[OfficeSetup] richdocuments:wopi_allowlist => $ACTIVATE_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 2) Add Nextcloud cron job\n    add_nextcloud_cron\n}\n\n# Export DOMAIN so it's visible to the function in background\nexport DOMAIN\nexport CONTAINER_NAME\n\n# Export the get_proxy_ip function for visibility in nohup\nexport -f get_proxy_ip\n# Export the get_office_ip function for visibility in nohup\nexport -f get_office_ip\n# Export the add_nextcloud_cron function for visibility in nohup\nexport -f add_nextcloud_cron\n\n\n# Run the installation in the background\nnohup bash -c \"$(\n  declare -f install_nextcloud_office\n  echo 'install_nextcloud_office'\n )\" > /tmp/office_install.log 2>&1 &\n\n# If everything is successful, update the status file and print success message\necho \"active\" | sudo tee \"$STATUS_FILE\" > /dev/null\necho \"success\"\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "616b6f8b-707f-473f-8065-f3e1623ece2c", "name": "Suspend", "type": "n8n-nodes-base.set", "onError": "continueRegularOutput", "position": [1920, 320], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\nDOMAIN=\"{{ $('API').item.json.body.domain }}\"\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/$DOMAIN\"\nCOMPOSE_FILE=\"$COMPOSE_DIR/docker-compose.yml\"\nSTATUS_FILE=\"$COMPOSE_DIR/status\"\nIMG_FILE=\"$COMPOSE_DIR/data.img\"\nNGINX_DIR=\"$COMPOSE_DIR/nginx\"\nVHOST_DIR=\"/opt/docker/nginx-proxy/nginx/vhost.d\"\nMOUNT_DIR=\"{{ $('Parametrs').item.json.mount_dir }}/$DOMAIN\"\n\nVHOST_MAIN_FILE=\"$VHOST_DIR/$DOMAIN\"\nVHOST_MAIN_LOCATION_FILE=\"$VHOST_DIR/$DOMAIN\"_location\nVHOST_OFFICE_FILE=\"$VHOST_DIR/office.$DOMAIN\"\nVHOST_OFFICE_LOCATION_FILE=\"$VHOST_DIR/office.$DOMAIN\"_location\n\n# Function to log an error, write to status file, and print to office\nhandle_error() {\n    STATUS_JSON=\"{\\\"status\\\": \\\"error\\\", \\\"message\\\": \\\"$1\\\"}\"\n    echo \"$STATUS_JSON\" | sudo tee \"$STATUS_FILE\" > /dev/null\n    echo \"error: $1\"\n    exit 1\n}\n\n# Stop and remove Docker containers (also remove associated volumes)\nif [ -f \"$COMPOSE_FILE\" ]; then\n    if ! sudo docker-compose -f \"$COMPOSE_FILE\" down > /dev/null 2>&1; then\n        handle_error \"Failed to stop and remove docker-compose containers\"\n    fi\nelse\n    echo \"Warning: docker-compose.yml not found, skipping container stop.\"\nfi\n\n# Remove mount entry from /etc/fstab if it exists\nif grep -q \"$IMG_FILE\" /etc/fstab; then\n    sudo sed -i \"\\|$(printf '%s\\n' \"$IMG_FILE\" | sed 's/[.[\\*^$]/\\\\&/g')|d\" /etc/fstab\nfi\n\n# Unmount the image if it is mounted\nif mount | grep -q \"$MOUNT_DIR\"; then\n    sudo umount \"$MOUNT_DIR\" > /dev/null 2>&1 || handle_error \"Failed to unmount $MOUNT_DIR\"\nfi\n\n# Remove the mount directory\nif [ -d \"$MOUNT_DIR\" ]; then\n    sudo rm -rf \"$MOUNT_DIR\" > /dev/null 2>&1 || handle_error \"Failed to remove $MOUNT_DIR\"\nfi\n\n# Remove NGINX configuration files\n[ -f \"$VHOST_MAIN_FILE\" ] && sudo rm -f \"$VHOST_MAIN_FILE\" || handle_error \"Warning: $VHOST_MAIN_FILE not found.\"\n[ -f \"$VHOST_MAIN_LOCATION_FILE\" ] && sudo rm -f \"$VHOST_MAIN_LOCATION_FILE\" || handle_error \"Warning: $VHOST_MAIN_LOCATION_FILE not found.\"\n[ -f \"$VHOST_OFFICE_FILE\" ] && sudo rm -f \"$VHOST_OFFICE_FILE\" || handle_error \"Warning: $VHOST_OFFICE_FILE not found.\"\n[ -f \"$VHOST_OFFICE_LOCATION_FILE\" ] && sudo rm -f \"$VHOST_OFFICE_LOCATION_FILE\" || handle_error \"Warning: $VHOST_OFFICE_LOCATION_FILE not found.\"\n\n# Update status\necho \"suspended\" | sudo tee \"$STATUS_FILE\" > /dev/null\n\n# Success\necho \"success\"\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "e09ef109-d4bd-4d2f-acad-a442854bc299", "name": "Deploy", "type": "n8n-nodes-base.set", "onError": "continueRegularOutput", "position": [1920, 160], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\n# Get values for variables from templates\nDOMAIN=\"{{ $('API').item.json.body.domain }}\"\nCONTAINER_NAME=\"{{ $('API').item.json.body.domain }}_nextcloud\"\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/$DOMAIN\"\nCOMPOSE_FILE=\"$COMPOSE_DIR/docker-compose.yml\"\nSTATUS_FILE=\"$COMPOSE_DIR/status\"\nIMG_FILE=\"$COMPOSE_DIR/data.img\"\nNGINX_DIR=\"$COMPOSE_DIR/nginx\"\nVHOST_DIR=\"/opt/docker/nginx-proxy/nginx/vhost.d\"\nMOUNT_DIR=\"{{ $('Parametrs').item.json.mount_dir }}/$DOMAIN\"\nDOCKER_COMPOSE_TEXT='{{ JSON.stringify($('Deploy-docker-compose').item.json['docker-compose']).base64Encode() }}'\n\nNGINX_MAIN_TEXT='{{ JSON.stringify($('nginx').item.json['main']).base64Encode() }}'\nNGINX_MAIN_FILE=\"$NGINX_DIR/$DOMAIN\"\nVHOST_MAIN_FILE=\"$VHOST_DIR/$DOMAIN\"\n\nNGINX_MAIN_LOCATION_TEXT='{{ JSON.stringify($('nginx').item.json['main_location']).base64Encode() }}'\nNGINX_MAIN_LOCATION_FILE=\"$NGINX_DIR/$DOMAIN\"_location\nVHOST_MAIN_LOCATION_FILE=\"$VHOST_DIR/$DOMAIN\"_location\n\nNGINX_OFFICE_TEXT='{{ JSON.stringify($('nginx').item.json['office']).base64Encode() }}'\nNGINX_OFFICE_FILE=\"$NGINX_DIR/office.$DOMAIN\"\nVHOST_OFFICE_FILE=\"$VHOST_DIR/office.$DOMAIN\"\n\nNGINX_OFFICE_LOCATION_TEXT='{{ JSON.stringify($('nginx').item.json['office_location']).base64Encode() }}'\nNGINX_OFFICE_LOCATION_FILE=\"$NGINX_DIR/office.$DOMAIN\"_location\nVHOST_OFFICE_LOCATION_FILE=\"$VHOST_DIR/office.$DOMAIN\"_location\n\nDISK_SIZE=\"{{ $('API').item.json.body.disk }}\"\n\n# Get nginx-proxy IP address before installing Nextcloud Office\nget_proxy_ip() {\n    local ip=\"\"\n    local retries=10  # Try a few times\n    local count=0\n    while [[ -z \"$ip\" && $count -lt $retries ]]; do\n        ip=$(sudo docker inspect -f '{{ $('Parametrs').item.json.screen_left }}range .NetworkSettings.Networks{{ $('Parametrs').item.json.screen_right }}{{ $('Parametrs').item.json.screen_left }}.IPAddress{{ $('Parametrs').item.json.screen_right }}{{ $('Parametrs').item.json.screen_left }}end{{ $('Parametrs').item.json.screen_right }}' nginx-proxy)\n        if [[ -z \"$ip\" ]]; then\n            echo \"[DEBUG] nginx-proxy IP not found, retrying ($count/$retries)...\" >> \"$STATUS_FILE\"\n            sleep 2  # Wait a bit before retrying\n        fi\n        ((count++))\n    done\n\n    if [[ -z \"$ip\" ]]; then\n        echo \"[ERROR] Failed to retrieve nginx-proxy IP after $retries attempts!\" >> \"$STATUS_FILE\"\n        handle_error \"Failed to retrieve nginx-proxy IP\"\n    fi\n\n    echo \"[DEBUG] Detected nginx-proxy IP: $ip\" >> \"$STATUS_FILE\"\n    echo \"$ip\"\n}\n\n# Get the IP address of Nextcloud Office\nget_office_ip() {\n    local ip=\"\"\n    local retries=10  # Try a few times\n    local count=0\n    while [[ -z \"$ip\" && $count -lt $retries ]]; do\n        ip=$(sudo docker inspect -f '{{ $('Parametrs').item.json.screen_left }}range .NetworkSettings.Networks{{ $('Parametrs').item.json.screen_right }}{{ $('Parametrs').item.json.screen_left }}.IPAddress{{ $('Parametrs').item.json.screen_right }}{{ $('Parametrs').item.json.screen_left }}end{{ $('Parametrs').item.json.screen_right }}' \"$DOMAIN\"_collabora)\n        if [[ -z \"$ip\" ]]; then\n            echo \"[DEBUG] office IP not found, retrying ($count/$retries)...\" >> \"$STATUS_FILE\"\n            sleep 2  # Wait a bit before retrying\n        fi\n        ((count++))\n    done\n\n    if [[ -z \"$ip\" ]]; then\n        echo \"[ERROR] Failed to retrieve office IP after $retries attempts!\" >> \"$STATUS_FILE\"\n        handle_error \"Failed to retrieve office IP\"\n    fi\n\n    # Convert IP to subnet by replacing the last octet with 0 and adding /24\n    local subnet=$(echo \"$ip\" | sed 's/\\.[0-9]*$/.0\\/24/')\n    echo \"[DEBUG] Detected office subnet: $subnet\" >> \"$STATUS_FILE\"\n    echo \"$subnet\"\n}\n\n# Function to handle errors: write to the status file and print the message to office\nhandle_error() {\n    STATUS_JSON=\"{\\\"status\\\": \\\"error\\\", \\\"message\\\": \\\"$1\\\"}\"\n    echo \"$STATUS_JSON\" | sudo tee \"$STATUS_FILE\" > /dev/null  # Write error to the status file\n    echo \"error: $1\"  # Print the error message to the office\n    exit 1  # Exit the script with an error code\n}\n\n# Check if the directory already exists. If yes, exit with an error.\nif [ -d \"$COMPOSE_DIR\" ]; then\n    echo \"error: Directory $COMPOSE_DIR already exists\"\n    exit 1\nfi\n\n# Create necessary directories with permissions\nsudo mkdir -p \"$COMPOSE_DIR\" > /dev/null 2>&1 || handle_error \"Failed to create $COMPOSE_DIR\"\nsudo mkdir -p \"$NGINX_DIR\" > /dev/null 2>&1 || handle_error \"Failed to create $NGINX_DIR\"\nsudo mkdir -p \"$MOUNT_DIR\" > /dev/null 2>&1 || handle_error \"Failed to create $MOUNT_DIR\"\n\n# Set permissions on the created directories\nsudo chmod -R 777 \"$COMPOSE_DIR\" > /dev/null 2>&1 || handle_error \"Failed to set permissions on $COMPOSE_DIR\"\nsudo chmod -R 777 \"$NGINX_DIR\" > /dev/null 2>&1 || handle_error \"Failed to set permissions on $NGINX_DIR\"\nsudo chmod -R 777 \"$MOUNT_DIR\" > /dev/null 2>&1 || handle_error \"Failed to set permissions on $MOUNT_DIR\"\n\n# Create docker-compose.yml file\necho -e \"$DOCKER_COMPOSE_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$COMPOSE_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $COMPOSE_FILE\"\n\n# Create NGINX configuration files\necho -e \"$NGINX_MAIN_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$NGINX_MAIN_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $NGINX_MAIN_FILE\"\necho -e \"$NGINX_MAIN_LOCATION_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$NGINX_MAIN_LOCATION_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $NGINX_MAIN_LOCATION_FILE\"\n\necho -e \"$NGINX_OFFICE_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$NGINX_OFFICE_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $NGINX_OFFICE_FILE\"\necho -e \"$NGINX_OFFICE_LOCATION_TEXT\" | base64 --decode | sed 's/\\\\n/\\n/g' | sed 's/\\\\\"/\"/g' | sed '1s/^\"//' | sed '$s/\"$//' | sudo tee \"$NGINX_OFFICE_LOCATION_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $NGINX_OFFICE_LOCATION_FILE\"\n\n# Change to the compose directory\ncd \"$COMPOSE_DIR\" > /dev/null 2>&1 || handle_error \"Failed to change directory to $COMPOSE_DIR\"\n\n# Create data.img file if it doesn't exist\nif [ ! -f \"$IMG_FILE\" ]; then\n    sudo fallocate -l \"$DISK_SIZE\"G \"$IMG_FILE\" > /dev/null 2>&1 || sudo truncate -s \"$DISK_SIZE\"G \"$IMG_FILE\" > /dev/null 2>&1 || handle_error \"Failed to create $IMG_FILE\"\n    sudo mkfs.ext4 \"$IMG_FILE\" > /dev/null 2>&1 || handle_error \"Failed to format $IMG_FILE\"  # Format the image as ext4\n    sync  # Synchronize the data to disk\nfi\n\n# Add an entry to /etc/fstab for mounting if not already present\nif ! grep -q \"$IMG_FILE\" /etc/fstab; then\n    echo \"$IMG_FILE $MOUNT_DIR ext4 loop 0 0\" | sudo tee -a /etc/fstab > /dev/null || handle_error \"Failed to add entry to /etc/fstab\"\nfi\n\n# Mount all entries in /etc/fstab\nsudo mount -a || handle_error \"Failed to mount entries from /etc/fstab\"\n\n# Set permissions on the mount directory\nsudo chmod -R 777 \"$MOUNT_DIR\" > /dev/null 2>&1 || handle_error \"Failed to set permissions on $MOUNT_DIR\"\n\n# Copy NGINX configuration files instead of creating symbolic links\nsudo cp -f \"$NGINX_MAIN_FILE\" \"$VHOST_MAIN_FILE\" || handle_error \"Failed to copy $NGINX_MAIN_FILE to $VHOST_MAIN_FILE\"\nsudo chmod 777 \"$VHOST_MAIN_FILE\" || handle_error \"Failed to set permissions on $VHOST_MAIN_FILE\"\n\nsudo cp -f \"$NGINX_MAIN_LOCATION_FILE\" \"$VHOST_MAIN_LOCATION_FILE\" || handle_error \"Failed to copy $NGINX_MAIN_LOCATION_FILE to $VHOST_MAIN_LOCATION_FILE\"\nsudo chmod 777 \"$VHOST_MAIN_LOCATION_FILE\" || handle_error \"Failed to set permissions on $VHOST_MAIN_LOCATION_FILE\"\n\nsudo cp -f \"$NGINX_OFFICE_FILE\" \"$VHOST_OFFICE_FILE\" || handle_error \"Failed to copy $NGINX_OFFICE_FILE to $VHOST_OFFICE_FILE\"\nsudo chmod 777 \"$VHOST_OFFICE_FILE\" || handle_error \"Failed to set permissions on $VHOST_OFFICE_FILE\"\n\nsudo cp -f \"$NGINX_OFFICE_LOCATION_FILE\" \"$VHOST_OFFICE_LOCATION_FILE\" || handle_error \"Failed to copy $NGINX_OFFICE_LOCATION_FILE to $VHOST_OFFICE_LOCATION_FILE\"\nsudo chmod 777 \"$VHOST_OFFICE_LOCATION_FILE\" || handle_error \"Failed to set permissions on $VHOST_OFFICE_LOCATION_FILE\"\n\n# Start Docker containers using docker-compose\nif ! sudo docker compose up -d > /dev/null 2>error.log; then\n    ERROR_MSG=$(tail -n 10 error.log)  # Read the last 10 lines from error.log\n    handle_error \"Docker-compose failed: $ERROR_MSG\"\nfi\n\n# Function to add Nextcloud cron job\nadd_nextcloud_cron() {\n    echo \"[CRON] Adding Nextcloud cron job...\" >> /dev/null\n    \n    # Create cron command\n    CRON_CMD=\"*/5 * * * * sudo docker exec -u www-data $CONTAINER_NAME php cron.php --force\"\n    \n    # Add to crontab (remove old if exists)\n    (crontab -l 2>/dev/null | grep -v \"$CONTAINER_NAME\"; echo \"$CRON_CMD\") | crontab -\n    \n    echo \"[CRON] Nextcloud cron job added successfully!\" >> /dev/null\n}\n\n# Function to remove Nextcloud cron job\nremove_nextcloud_cron() {\n    echo \"[CRON] Removing Nextcloud cron job...\" >> /dev/null\n    \n    # Remove from crontab\n    crontab -l 2>/dev/null | grep -v \"$CONTAINER_NAME\" | crontab -\n    \n    echo \"[CRON] Nextcloud cron job removed successfully!\" >> /dev/null\n}\n\n\n\n# --- Function that installs Nextcloud Office (Collabora) in the background ---\ninstall_nextcloud_office() {\n    MAX_RETRIES=60\n    COUNTER=0\n\n\n    # 1) Wait until \"installed: true\" in occ status\n    while true; do\n        STATUS_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ status 2>&1)\"\n        if echo \"$STATUS_OUTPUT\" | grep -q \"installed: true\"; then\n            echo \"[OfficeSetup] Nextcloud reports installed: true. Proceeding...\" >> \"$STATUS_FILE\"\n            break\n        else\n            echo \"[OfficeSetup] [$COUNTER/$MAX_RETRIES] Nextcloud not fully installed yet, waiting...\" >> \"$STATUS_FILE\"\n            sleep 2\n            ((COUNTER++))\n            if [ $COUNTER -ge $MAX_RETRIES ]; then\n                echo \"[OfficeSetup] Nextcloud did not report 'installed: true' within time limit. Skipping Office install.\" >> \"$STATUS_FILE\"\n                return\n            fi\n        fi\n    done\n\n    # Get the nginx-proxy IP\n    PROXY_IP=$(get_proxy_ip)\n\n    echo \"[OfficeSetup] Detected nginx-proxy IP: $PROXY_IP\" >> \"$STATUS_FILE\"\n    \n\n    # Write the needed parameters to the Nextcloud config\n    echo \"[OfficeSetup] Setting overwrite protocol/host/cli.url in Nextcloud config...\" >> \"$STATUS_FILE\"\n    sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set overwriteprotocol --value=https 2>&1\n    sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set overwritehost --value=\"$DOMAIN\" 2>&1\n    sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set overwrite.cli.url --value=\"{{ $env.WEBHOOK_URL }}\" 2>&1\n\n    # Add the nginx-proxy IP to the trusted_proxies list\n    echo \"[OfficeSetup] Adding nginx-proxy IP to trusted_proxies...\" >> \"$STATUS_FILE\"\n    # *** NEW BLOCK *** - Get the IP address of the reverse proxy\n    sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set trusted_proxies 0 --value=\"$PROXY_IP\" 2>&1\n\n    echo \"[OfficeSetup] Installing Nextcloud Office (richdocuments)...\" >> \"$STATUS_FILE\"\n\n    # 2) Install the richdocuments app\n    INSTALL_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ app:install richdocuments 2>&1 || echo \"[OfficeSetup] App already installed\")\"\n    echo \"[OfficeSetup] app:install richdocuments => $INSTALL_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 3) Set the Collabora Online URL in Nextcloud\n    WOPI_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:app:set richdocuments wopi_url --value=\"{{ $env.WEBHOOK_URL }}\" 2>&1)\"\n    echo \"[OfficeSetup] wopi_url => $WOPI_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 4) Enable the app\n    ENABLE_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ app:enable richdocuments 2>&1)\"\n    echo \"[OfficeSetup] app:enable richdocuments => $ENABLE_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 5) Allow local remote servers (Fix for Collabora access issues)\n    ALLOW_LOCAL_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:system:set allow_local_remote_servers --value=true --type=bool 2>&1)\"\n    echo \"[OfficeSetup] allow_local_remote_servers => $ALLOW_LOCAL_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 6) Apply changes by running maintenance repair\n    REPAIR_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ maintenance:repair 2>&1)\"\n    echo \"[OfficeSetup] maintenance:repair => $REPAIR_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 7) Activate Collabora Online configuration\n    ACTIVATE_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ richdocuments:activate-config 2>&1)\"\n    echo \"[OfficeSetup] richdocuments:activate-config => $ACTIVATE_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 8) Refresh cache by scanning all files\n    SCAN_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ files:scan --all 2>&1)\"\n    echo \"[OfficeSetup] files:scan --all => $SCAN_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 9) Double-check if the app is enabled\n    APP_LIST=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ app:list 2>&1)\"\n    echo \"[OfficeSetup] occ app:list => $APP_LIST\" >> \"$STATUS_FILE\"\n\n    # 10) Perform the migrations\n    MIGRATION_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ maintenance:repair --include-expensive 2>&1)\"\n    echo \"[OfficeSetup] maintenance:repair --include-expensive => $MIGRATION_OUTPUT\" >> \"$STATUS_FILE\"\n\n    if echo \"$APP_LIST\" | grep -q \"richdocuments: enabled\"; then\n        echo \"[OfficeSetup] Nextcloud Office successfully installed and configured!\" >> \"$STATUS_FILE\"\n    else\n        echo \"[OfficeSetup] Nextcloud Office installation failed or not enabled.\" >> \"$STATUS_FILE\"\n    fi\n\n    OFFICE_IP_SUBNET=$(get_office_ip)\n    echo \"[OfficeSetup] Detected office IP: $OFFICE_IP_SUBNET\" >> \"$STATUS_FILE\"\n\n    # Write the needed parameters to the Collabora config\n    # 1) Collabora \n    ACTIVATE_OUTPUT=\"$(sudo docker exec -u www-data \"$CONTAINER_NAME\" php occ config:app:set richdocuments wopi_allowlist --value=\"$OFFICE_IP_SUBNET\" 2>&1)\"\n    echo \"[OfficeSetup] richdocuments:wopi_allowlist => $ACTIVATE_OUTPUT\" >> \"$STATUS_FILE\"\n\n    # 2) Add Nextcloud cron job\n    add_nextcloud_cron\n}\n\n# Export DOMAIN so it's visible to the function in background\nexport DOMAIN\nexport CONTAINER_NAME\n\n# Export the get_proxy_ip function for visibility in nohup\nexport -f get_proxy_ip\n# Export the get_office_ip function for visibility in nohup\nexport -f get_office_ip\n# Export the add_nextcloud_cron function for visibility in nohup\nexport -f add_nextcloud_cron\n\n\n# Run the installation in the background\nnohup bash -c \"$(\n  declare -f install_nextcloud_office\n  echo 'install_nextcloud_office'\n )\" > /tmp/office_install.log 2>&1 &\n\n# If everything is successful, update the status file and print success message\necho \"active\" | sudo tee \"$STATUS_FILE\" > /dev/null\necho \"success\"\n\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "8b9f1482-cc21-4f7b-aa82-fdb47643d807", "name": "Service Actions", "type": "n8n-nodes-base.switch", "position": [1640, 140], "parameters": {"rules": {"values": [{"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "3afdd2f1-fe93-47c2-95cd-bac9b1d94eeb", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "test_connection"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "102f10e9-ec6c-4e63-ba95-0fe6c7dc0bd1", "operator": {"type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "create"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "f62dfa34-6751-4b34-adcc-3d6ba1b21a8c", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "suspend"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "384d2026-b753-4c27-94c2-8f4fc189eb5f", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "unsuspend"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "0e190a97-827a-4e87-8222-093ff7048b21", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "terminate"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "6f7832f3-b61d-4517-ab6b-6007998136dd", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "change_package"}]}, "renameOutput": true}]}, "options": {}}, "typeVersion": 3.2, "notes": "This switch node performs automated tasks as part of the workflow."}, {"id": "b27da6f4-859b-4b8a-9542-f0cad5f2cbfc", "name": "If1", "type": "n8n-nodes-base.if", "position": [920, 320], "parameters": {"options": {}, "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "or", "conditions": [{"id": "8602bd4c-9693-4d5f-9e7d-5ee62210baca", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "create"}, {"id": "1c630b59-0e5a-441d-8aa5-70b31338d897", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "change_package"}, {"id": "b3eb7052-a70f-438e-befd-8c5240df32c7", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "unsuspend"}]}}, "typeVersion": 2.2, "notes": "This if node performs automated tasks as part of the workflow."}, {"id": "0af4d346-d369-412b-b8f1-9847c5deb645", "name": "Dependent containers Stat", "type": "n8n-nodes-base.set", "onError": "continueRegularOutput", "position": [1300, 1180], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/{{ $('API').item.json.body.domain }}\"\nIMG_FILE=\"$COMPOSE_DIR/data.img\"\nMOUNT_DIR=\"{{ $('Parametrs').item.json.mount_dir }}/{{ $('API').item.json.body.domain }}\"\n\nCONTAINER_NAME_ML=\"{{ $('API').item.json.body.domain }}_collabora\"\nCONTAINER_NAME_DB=\"{{ $('API').item.json.body.domain }}_db\"\nCONTAINER_NAME_REDIS=\"{{ $('API').item.json.body.domain }}_redis\"\n\n# Initialize empty container data\nINSPECT_JSON_ML=\"{}\"\nSTATS_JSON_ML=\"{}\"\n\nINSPECT_JSON_DB=\"{}\"\nSTATS_JSON_DB=\"{}\"\n\nINSPECT_JSON_REDIS=\"{}\"\nSTATS_JSON_REDIS=\"{}\"\n\n# Check if container is running\nif sudo docker ps -a --filter \"name=$CONTAINER_NAME_ML\" | grep -q \"$CONTAINER_NAME_ML\"; then\n  # Get Docker inspect info in JSON (as raw string)\n  INSPECT_JSON_ML=$(sudo docker inspect \"$CONTAINER_NAME_ML\")\n  # Get Docker stats info in JSON (as raw string)\n  STATS_JSON_ML=$(sudo docker stats --no-stream --format \"{{ $('Parametrs').item.json.screen_left }}json .{{ $('Parametrs').item.json.screen_right }}\" \"$CONTAINER_NAME_ML\")\n  STATS_JSON_ML=${STATS_JSON_ML:-'{}'}\nfi\n\n# Check if container is running\nif sudo docker ps -a --filter \"name=$CONTAINER_NAME_DB\" | grep -q \"$CONTAINER_NAME_DB\"; then\n  # Get Docker inspect info in JSON (as raw string)\n  INSPECT_JSON_DB=$(sudo docker inspect \"$CONTAINER_NAME_DB\")\n  # Get Docker stats info in JSON (as raw string)\n  STATS_JSON_DB=$(sudo docker stats --no-stream --format \"{{ $('Parametrs').item.json.screen_left }}json .{{ $('Parametrs').item.json.screen_right }}\" \"$CONTAINER_NAME_DB\")\n  STATS_JSON_DB=${STATS_JSON_DB:-'{}'}\nfi\n\n# Check if container is running\nif sudo docker ps -a --filter \"name=$CONTAINER_NAME_REDIS\" | grep -q \"$CONTAINER_NAME_REDIS\"; then\n  # Get Docker inspect info in JSON (as raw string)\n  INSPECT_JSON_REDIS=$(sudo docker inspect \"$CONTAINER_NAME_REDIS\")\n  # Get Docker stats info in JSON (as raw string)\n  STATS_JSON_REDIS=$(sudo docker stats --no-stream --format \"{{ $('Parametrs').item.json.screen_left }}json .{{ $('Parametrs').item.json.screen_right }}\" \"$CONTAINER_NAME_REDIS\")\n  STATS_JSON_REDIS=${STATS_JSON_REDIS:-'{}'}\nfi\n\n# Manually create a combined JSON object\nFINAL_JSON=\"{\\\"inspect_ml\\\": $INSPECT_JSON_ML, \\\"stats_ml\\\": $STATS_JSON_ML,\\\"inspect_db\\\": $INSPECT_JSON_DB, \\\"stats_db\\\": $STATS_JSON_DB,\\\"inspect_redis\\\": $INSPECT_JSON_REDIS, \\\"stats_redis\\\": $STATS_JSON_REDIS}\"\n\n# Output the result\necho \"$FINAL_JSON\"\n\nexit 0"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "2e53a360-deb3-41e7-8ddd-c06a3733e4bd", "name": "GET ACL", "type": "n8n-nodes-base.set", "onError": "continueRegularOutput", "position": [1400, 2140], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\n# Get values for variables from templates\nDOMAIN=\"{{ $('API').item.json.body.domain }}\"\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/$DOMAIN\"\nNGINX_DIR=\"$COMPOSE_DIR/nginx\"\n\nNGINX_MAIN_ACL_FILE=\"$NGINX_DIR/$DOMAIN\"_acl\n\n# Function to log an error and exit\nhandle_error() {\n    echo \"error: $1\"\n    exit 1\n}\n\n# Read files if they exist, else assign empty array\nif [[ -f \"$NGINX_MAIN_ACL_FILE\" ]]; then\n    MAIN_IPS=$(cat \"$NGINX_MAIN_ACL_FILE\" | jq -R -s 'split(\"\\n\") | map(select(length > 0))')\nelse\n    MAIN_IPS=\"[]\"\nfi\n\n# Output JSON\necho \"{ \\\"main_ips\\\": $MAIN_IPS}\"\n\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "ec319d39-328f-4af6-a6d1-23ba6efb11d2", "name": "SET ACL", "type": "n8n-nodes-base.set", "onError": "continueRegularOutput", "position": [1400, 2320], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\n# Get values for variables from templates\nDOMAIN=\"{{ $('API').item.json.body.domain }}\"\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/$DOMAIN\"\nNGINX_DIR=\"$COMPOSE_DIR/nginx\"\nVHOST_DIR=\"/opt/docker/nginx-proxy/nginx/vhost.d\"\n\nNGINX_MAIN_ACL_FILE=\"$NGINX_DIR/$DOMAIN\"_acl\nNGINX_MAIN_ACL_TEXT=\"{{ $('API').item.json.body.main_ips }}\"\nVHOST_MAIN_LOCATION_FILE=\"$VHOST_DIR/$DOMAIN\"_location\nNGINX_MAIN_LOCATION_FILE=\"$NGINX_DIR/$DOMAIN\"_location\n\n# Function to log an error and exit\nhandle_error() {\n    echo \"error: $1\"\n    exit 1\n}\n\nupdate_nginx_acl() {\n    ACL_FILE=$1\n    LOCATION_FILE=$2\n    \n    if [ -s \"$ACL_FILE\" ]; then\n        VALID_LINES=$(grep -vE '^\\s*$' \"$ACL_FILE\")\n        if [ -n \"$VALID_LINES\" ]; then\n            while IFS= read -r line; do\n                echo \"allow $line;\" | sudo tee -a \"$LOCATION_FILE\" > /dev/null || handle_error \"Failed to update $LOCATION_FILE\"\n            done <<< \"$VALID_LINES\"\n            echo \"deny all;\" | sudo tee -a \"$LOCATION_FILE\" > /dev/null || handle_error \"Failed to update $LOCATION_FILE\"\n        fi\n    fi\n}\n\n# Create or overwrite the file with the content from variables\necho \"$NGINX_MAIN_ACL_TEXT\" | sudo tee \"$NGINX_MAIN_ACL_FILE\" > /dev/null\n\nsudo cp -f \"$NGINX_MAIN_LOCATION_FILE\" \"$VHOST_MAIN_LOCATION_FILE\" || handle_error \"Failed to copy $NGINX_MAIN_LOCATION_FILE to $VHOST_MAIN_LOCATION_FILE\"\nsudo chmod 777 \"$VHOST_MAIN_LOCATION_FILE\" || handle_error \"Failed to set permissions on $VHOST_MAIN_LOCATION_FILE\"\n\nupdate_nginx_acl \"$NGINX_MAIN_ACL_FILE\" \"$VHOST_MAIN_LOCATION_FILE\"\n\n# Reload Nginx with sudo\nif sudo docker exec nginx-proxy nginx -s reload; then\n    echo \"success\"\nelse\n    handle_error \"Failed to reload Nginx.\"\nfi\n\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "a80322f9-1d95-4d09-b659-e98cfd31ed4b", "name": "GET NET", "type": "n8n-nodes-base.set", "onError": "continueRegularOutput", "position": [1400, 2460], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "21f4453e-c136-4388-be90-1411ae78e8a5", "name": "sh", "type": "string", "value": "=#!/bin/bash\n\n# Get values for variables from templates\nDOMAIN=\"{{ $('API').item.json.body.domain }}\"\nCONTAINER_NAME=\"{{ $('API').item.json.body.domain }}_nextcloud\"\nCOMPOSE_DIR=\"{{ $('Parametrs').item.json.clients_dir }}/$DOMAIN\"\nNGINX_DIR=\"$COMPOSE_DIR/nginx\"\nNET_IN_FILE=\"$COMPOSE_DIR/net_in\"\nNET_OUT_FILE=\"$COMPOSE_DIR/net_out\"\n\n# Function to log an error and exit\nhandle_error() {\n    echo \"error: $1\"\n    exit 1\n}\n\n# Get current network statistics from container\nSTATS=$(sudo docker exec \"$CONTAINER_NAME\" cat /proc/net/dev | grep eth0) || handle_error \"Failed to get network stats\"\nNET_IN_NEW=$(echo \"$STATS\" | awk '{print $2}')  # RX bytes (received)\nNET_OUT_NEW=$(echo \"$STATS\" | awk '{print $10}') # TX bytes (transmitted)\n\n# Ensure directory exists\nmkdir -p \"$COMPOSE_DIR\"\n\n# Read old values, create files if they don't exist\nif [[ -f \"$NET_IN_FILE\" ]]; then\n    NET_IN_OLD=$(sudo cat \"$NET_IN_FILE\")\nelse\n    NET_IN_OLD=0\nfi\n\nif [[ -f \"$NET_OUT_FILE\" ]]; then\n    NET_OUT_OLD=$(sudo cat \"$NET_OUT_FILE\")\nelse\n    NET_OUT_OLD=0\nfi\n\n# Save new values\necho \"$NET_IN_NEW\" | sudo tee \"$NET_IN_FILE\" > /dev/null\necho \"$NET_OUT_NEW\" | sudo tee \"$NET_OUT_FILE\" > /dev/null\n\n# Output JSON\necho \"{ \\\"net_in_new\\\": $NET_IN_NEW, \\\"net_out_new\\\": $NET_OUT_NEW, \\\"net_in_old\\\": $NET_IN_OLD, \\\"net_out_old\\\": $NET_OUT_OLD }\"\n\nexit 0\n"}]}}, "typeVersion": 3.4, "alwaysOutputData": true, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "3158fb78-50c5-4ee2-b9ff-34947867457d", "name": "If2", "type": "n8n-nodes-base.if", "position": [3240, -320], "parameters": {"options": {}, "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "ac3730e4-8776-486b-b393-60ef103d35ea", "operator": {"type": "string", "operation": "notEquals"}, "leftValue": "={{ $('Split domain').item.json.mainDomain }}", "rightValue": "d01-test.uuq.pl"}, {"id": "5baca1f0-fa26-4b78-ae94-44b876ac4fee", "operator": {"type": "string", "operation": "notEquals"}, "leftValue": "={{ $('Split domain').item.json.mainDomain }}", "rightValue": "d02-test.uuq.pl"}]}}, "typeVersion": 2.2, "notes": "This if node performs automated tasks as part of the workflow."}, {"id": "8076c40a-793b-4831-af1d-6afe0bb46f35", "name": "Split domain", "type": "n8n-nodes-base.code", "position": [2740, -320], "parameters": {"jsCode": "const domain = $('API').item.json.body.domain;\n\nconst parts = domain.split('.');\n\nlet subDomain = '';\nlet mainDomain = domain;\n\nif (parts.length > 2) {\n    subDomain = parts[0]; \n    mainDomain = parts.slice(1).join('.');\n}\n\nreturn {\n  json: {\n    subDomain: subDomain,\n    mainDomain: mainDomain\n  }\n};\n"}, "typeVersion": 2, "notes": "This code node performs automated tasks as part of the workflow."}, {"id": "042d1043-a64e-4aa9-84b5-fa4f47591542", "name": "DNS Service Actions", "type": "n8n-nodes-base.switch", "position": [3640, -400], "parameters": {"rules": {"values": [{"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "8ac3b338-9407-4c8b-8e88-935cb017fbbe", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "container_update_dns_record"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "102f10e9-ec6c-4e63-ba95-0fe6c7dc0bd1", "operator": {"type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "create"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "f62dfa34-6751-4b34-adcc-3d6ba1b21a8c", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "suspend"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "384d2026-b753-4c27-94c2-8f4fc189eb5f", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "unsuspend"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "0e190a97-827a-4e87-8222-093ff7048b21", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "terminate"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "6f7832f3-b61d-4517-ab6b-6007998136dd", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.command }}", "rightValue": "change_package"}]}, "renameOutput": true}]}, "options": {}}, "typeVersion": 3.2, "notes": "This switch node performs automated tasks as part of the workflow."}, {"id": "38117189-c233-40c3-8dd0-67d94f4e868a", "name": "DNS Parametrs", "type": "n8n-nodes-base.set", "position": [3000, -320], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "a6328600-7ee0-4031-9bdb-fcee99b79658", "name": "api_url", "type": "string", "value": "{{ $env.WEBHOOK_URL }}"}, {"id": "370ddc4e-0fc0-48f6-9b30-ebdfba72c62f", "name": "api_key", "type": "string", "value": "your_api_key"}]}}, "typeVersion": 3.4, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "f9cf3c3e-83d0-46ea-983b-7f536ae8356d", "name": "Add record", "type": "n8n-nodes-base.httpRequest", "onError": "continueRegularOutput", "position": [4000, -440], "parameters": {"url": "{{ $env.BASE_URL }}", "body": "={\n  \"rrsets\": [\n    {\n      \"name\": \"{{ $('API').item.json.body.domain }}.\",\n      \"type\": \"CNAME\",\n      \"changetype\": \"REPLACE\",\n      \"ttl\": 300,\n      \"records\": [\n        {\n          \"content\": \"{{ $('API').item.json.body.server_domain }}.\",\n          \"disabled\": false\n        }\n      ]\n    }\n  ]\n}\n", "method": "PATCH", "options": {}, "sendBody": true, "contentType": "raw", "sendHeaders": true, "rawContentType": "application/json", "headerParameters": {"parameters": [{"name": "X-API-Key", "value": "={{ $('DNS Parametrs').item.json.api_key }}"}, {"name": "Content-Type", "value": "application/json"}]}}, "typeVersion": 4.2, "alwaysOutputData": true, "notes": "This httpRequest node performs automated tasks as part of the workflow."}, {"id": "f50c9fd0-efaa-42b3-aa03-ff66ca400299", "name": "Delete record", "type": "n8n-nodes-base.httpRequest", "onError": "continueRegularOutput", "position": [4000, -280], "parameters": {"url": "{{ $env.BASE_URL }}", "body": "={\n  \"rrsets\": [\n    {\n      \"name\": \"{{ $('API').item.json.body.domain }}.\",\n      \"type\": \"CNAME\",\n      \"changetype\": \"REPLACE\",\n      \"ttl\": 300,\n      \"records\": []\n    }\n  ]\n}\n", "method": "PATCH", "options": {}, "sendBody": true, "contentType": "raw", "sendHeaders": true, "rawContentType": "application/json", "headerParameters": {"parameters": [{"name": "X-API-Key", "value": "={{ $('DNS Parametrs').item.json.api_key }}"}, {"name": "Content-Type", "value": "application/json"}]}}, "typeVersion": 4.2, "alwaysOutputData": true, "notes": "This httpRequest node performs automated tasks as part of the workflow."}, {"id": "d1823b67-662c-4bab-815b-0dda8da8284e", "name": "API answer1", "type": "n8n-nodes-base.respondToWebhook", "onError": "continueRegularOutput", "position": [4000, -580], "parameters": {"options": {"responseCode": 200}, "respondWith": "json", "responseBody": "{\n  \"status\": \"success\",\n  \"message\": \"\",\n  \"data\": \"\"\n}\n"}, "typeVersion": 1.1, "alwaysOutputData": true, "notes": "This respondToWebhook node performs automated tasks as part of the workflow."}, {"id": "446fdfae-adf1-45c1-b237-705a235e735a", "name": "d01-test.uuq.pl", "type": "n8n-nodes-base.ssh", "onError": "continueErrorOutput", "position": [2900, 1560], "parameters": {"cwd": "=/", "command": "={{ $json.sh }}"}, "credentials": {"sshPassword": {"id": "AxPODSmAvTNzqrJb", "name": "SSH puq on d01-test.uuq.pl"}}, "executeOnce": true, "typeVersion": 1, "notes": "This ssh node performs automated tasks as part of the workflow."}, {"id": "0a52c410-c82a-4cb6-872d-3dd328224db0", "name": "d02-test.uuq.pl", "type": "n8n-nodes-base.ssh", "onError": "continueErrorOutput", "position": [2900, 1840], "parameters": {"cwd": "=/", "command": "={{ $json.sh }}"}, "credentials": {"sshPassword": {"id": "JseVEj5f5icL4csj", "name": "d02-test.uuq.pl"}}, "executeOnce": true, "typeVersion": 1, "notes": "This ssh node performs automated tasks as part of the workflow."}, {"id": "9721b93a-4b20-4ea6-965e-44277613edee", "name": "Servers Switch", "type": "n8n-nodes-base.switch", "position": [2560, 1700], "parameters": {"rules": {"values": [{"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"operator": {"type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.server_domain }}", "rightValue": "d01-test.uuq.pl"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "a032f373-4856-4b2d-b722-9a3ad36d12e7", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $('API').item.json.body.server_domain }}", "rightValue": "d02-test.uuq.pl"}]}, "renameOutput": true}]}, "options": {}}, "typeVersion": 3.2, "notes": "This switch node performs automated tasks as part of the workflow."}, {"id": "84d45b2c-e8bc-4a68-9c6a-051e12451c48", "name": "Code", "type": "n8n-nodes-base.code", "position": [3400, 1740], "parameters": {"mode": "runOnceForEachItem", "jsCode": "try {\n  if ($json.stdout === 'success') {\n    return {\n      json: {\n        status: 'success',\n        message: '',\n        data: '',\n      }\n    };\n  }\n\n  const parsedData = JSON.parse($json.stdout);\n\n  return {\n    json: {\n      status: parsedData.status === 'error' ? 'error' : 'success',\n      message: parsedData.message || (parsedData.status === 'error' ? 'An error occurred' : ''),\n      data: parsedData || '',\n    }\n  };\n\n} catch (error) {\n  return {\n    json: {\n      status: 'error',\n      message: $json.stdout??$json.error,\n      data: '',\n    }\n  };\n}"}, "executeOnce": false, "retryOnFail": false, "typeVersion": 2, "alwaysOutputData": false, "notes": "This code node performs automated tasks as part of the workflow."}, {"id": "d7e8d93c-0292-44ef-ac21-8c934d60a750", "name": "API answer2", "type": "n8n-nodes-base.respondToWebhook", "position": [3800, 1740], "parameters": {"options": {"responseCode": 200}, "respondWith": "allIncomingItems"}, "typeVersion": 1.1, "alwaysOutputData": true, "notes": "This respondToWebhook node performs automated tasks as part of the workflow."}], "active": true, "pinData": {}, "settings": {"callerPolicy": "workflowsFromSameOwner", "executionOrder": "v1", "saveManualExecutions": true, "saveExecutionProgress": true, "saveDataErrorExecution": "all", "saveDataSuccessExecution": "all", "errorWorkflow": null, "timezone": "UTC", "executionTimeout": 3600, "maxExecutions": 1000, "retryOnFail": true, "retryCount": 3, "retryDelay": 1000}, "versionId": "db430021-ac5c-4b7d-8512-8a6f04dc4952", "connections": {"b0c5ccb8-0692-4bb0-99e1-769fde372e0f": {"main": [[{"node": "error-handler-b0c5ccb8-0692-4bb0-99e1-769fde372e0f", "type": "main", "index": 0}], [{"node": "error-handler-b0c5ccb8-0692-4bb0-99e1-769fde372e0f-753a9baf", "type": "main", "index": 0}], [{"node": "error-handler-b0c5ccb8-0692-4bb0-99e1-769fde372e0f-18a16c0f", "type": "main", "index": 0}], [{"node": "error-handler-b0c5ccb8-0692-4bb0-99e1-769fde372e0f-2b2fb3c2", "type": "main", "index": 0}], [{"node": "error-handler-b0c5ccb8-0692-4bb0-99e1-769fde372e0f-888de4e0", "type": "main", "index": 0}], [{"node": "error-handler-b0c5ccb8-0692-4bb0-99e1-769fde372e0f-84f2a439", "type": "main", "index": 0}], [{"node": "error-handler-b0c5ccb8-0692-4bb0-99e1-769fde372e0f-c41efa07", "type": "main", "index": 0}], [{"node": "error-handler-b0c5ccb8-0692-4bb0-99e1-769fde372e0f-20cd09c0", "type": "main", "index": 0}], [{"node": "error-handler-b0c5ccb8-0692-4bb0-99e1-769fde372e0f-77a11647", "type": "main", "index": 0}]]}, "bcaf7ce1-464a-492e-b7f5-50ba8e465171": {"main": [[{"node": "error-handler-bcaf7ce1-464a-492e-b7f5-50ba8e465171", "type": "main", "index": 0}], [{"node": "error-handler-bcaf7ce1-464a-492e-b7f5-50ba8e465171-388a3d9f", "type": "main", "index": 0}], [{"node": "error-handler-bcaf7ce1-464a-492e-b7f5-50ba8e465171-e53a0258", "type": "main", "index": 0}], [{"node": "error-handler-bcaf7ce1-464a-492e-b7f5-50ba8e465171-166e3079", "type": "main", "index": 0}], [{"node": "error-handler-bcaf7ce1-464a-492e-b7f5-50ba8e465171-25222818", "type": "main", "index": 0}], [{"node": "error-handler-bcaf7ce1-464a-492e-b7f5-50ba8e465171-5008bcae", "type": "main", "index": 0}], [{"node": "error-handler-bcaf7ce1-464a-492e-b7f5-50ba8e465171-5a380c76", "type": "main", "index": 0}], [{"node": "error-handler-bcaf7ce1-464a-492e-b7f5-50ba8e465171-8473b81a", "type": "main", "index": 0}], [{"node": "error-handler-bcaf7ce1-464a-492e-b7f5-50ba8e465171-023f6a4f", "type": "main", "index": 0}]]}, "f9cf3c3e-83d0-46ea-983b-7f536ae8356d": {"main": [[{"node": "error-handler-f9cf3c3e-83d0-46ea-983b-7f536ae8356d", "type": "main", "index": 0}], [{"node": "error-handler-f9cf3c3e-83d0-46ea-983b-7f536ae8356d-61353823", "type": "main", "index": 0}], [{"node": "error-handler-f9cf3c3e-83d0-46ea-983b-7f536ae8356d-c5e11de0", "type": "main", "index": 0}], [{"node": "error-handler-f9cf3c3e-83d0-46ea-983b-7f536ae8356d-3207a039", "type": "main", "index": 0}], [{"node": "error-handler-f9cf3c3e-83d0-46ea-983b-7f536ae8356d-8ad14260", "type": "main", "index": 0}], [{"node": "error-handler-f9cf3c3e-83d0-46ea-983b-7f536ae8356d-93246e0d", "type": "main", "index": 0}], [{"node": "error-handler-f9cf3c3e-83d0-46ea-983b-7f536ae8356d-1f1ee366", "type": "main", "index": 0}], [{"node": "error-handler-f9cf3c3e-83d0-46ea-983b-7f536ae8356d-7741de15", "type": "main", "index": 0}], [{"node": "error-handler-f9cf3c3e-83d0-46ea-983b-7f536ae8356d-e0f15b92", "type": "main", "index": 0}]]}, "f50c9fd0-efaa-42b3-aa03-ff66ca400299": {"main": [[{"node": "error-handler-f50c9fd0-efaa-42b3-aa03-ff66ca400299", "type": "main", "index": 0}], [{"node": "error-handler-f50c9fd0-efaa-42b3-aa03-ff66ca400299-81ce7535", "type": "main", "index": 0}], [{"node": "error-handler-f50c9fd0-efaa-42b3-aa03-ff66ca400299-6d4b368d", "type": "main", "index": 0}], [{"node": "error-handler-f50c9fd0-efaa-42b3-aa03-ff66ca400299-29448e0e", "type": "main", "index": 0}], [{"node": "error-handler-f50c9fd0-efaa-42b3-aa03-ff66ca400299-e0fcf923", "type": "main", "index": 0}], [{"node": "error-handler-f50c9fd0-efaa-42b3-aa03-ff66ca400299-589fd25a", "type": "main", "index": 0}], [{"node": "error-handler-f50c9fd0-efaa-42b3-aa03-ff66ca400299-361debb3", "type": "main", "index": 0}], [{"node": "error-handler-f50c9fd0-efaa-42b3-aa03-ff66ca400299-9979f19d", "type": "main", "index": 0}], [{"node": "error-handler-f50c9fd0-efaa-42b3-aa03-ff66ca400299-b4c65e79", "type": "main", "index": 0}]]}, "d1823b67-662c-4bab-815b-0dda8da8284e": {"main": [[{"node": "error-handler-d1823b67-662c-4bab-815b-0dda8da8284e", "type": "main", "index": 0}], [{"node": "error-handler-d1823b67-662c-4bab-815b-0dda8da8284e-42359bdc", "type": "main", "index": 0}], [{"node": "error-handler-d1823b67-662c-4bab-815b-0dda8da8284e-0ba2fdd6", "type": "main", "index": 0}], [{"node": "error-handler-d1823b67-662c-4bab-815b-0dda8da8284e-78e8e42e", "type": "main", "index": 0}], [{"node": "error-handler-d1823b67-662c-4bab-815b-0dda8da8284e-036db2be", "type": "main", "index": 0}], [{"node": "error-handler-d1823b67-662c-4bab-815b-0dda8da8284e-bdbf38f6", "type": "main", "index": 0}], [{"node": "error-handler-d1823b67-662c-4bab-815b-0dda8da8284e-d9613a64", "type": "main", "index": 0}], [{"node": "error-handler-d1823b67-662c-4bab-815b-0dda8da8284e-0a207fba", "type": "main", "index": 0}], [{"node": "error-handler-d1823b67-662c-4bab-815b-0dda8da8284e-0368ba63", "type": "main", "index": 0}]]}, "d7e8d93c-0292-44ef-ac21-8c934d60a750": {"main": [[{"node": "error-handler-d7e8d93c-0292-44ef-ac21-8c934d60a750", "type": "main", "index": 0}], [{"node": "error-handler-d7e8d93c-0292-44ef-ac21-8c934d60a750-bd9a9859", "type": "main", "index": 0}], [{"node": "error-handler-d7e8d93c-0292-44ef-ac21-8c934d60a750-043ca280", "type": "main", "index": 0}], [{"node": "error-handler-d7e8d93c-0292-44ef-ac21-8c934d60a750-92a37af4", "type": "main", "index": 0}], [{"node": "error-handler-d7e8d93c-0292-44ef-ac21-8c934d60a750-18ec2eab", "type": "main", "index": 0}], [{"node": "error-handler-d7e8d93c-0292-44ef-ac21-8c934d60a750-a459612c", "type": "main", "index": 0}], [{"node": "error-handler-d7e8d93c-0292-44ef-ac21-8c934d60a750-326eb863", "type": "main", "index": 0}], [{"node": "error-handler-d7e8d93c-0292-44ef-ac21-8c934d60a750-05bd9b4d", "type": "main", "index": 0}], [{"node": "error-handler-d7e8d93c-0292-44ef-ac21-8c934d60a750-f468c582", "type": "main", "index": 0}]]}}, "description": "Automated workflow: PUQ Docker NextCloud deploy. This workflow integrates 10 different services: webhook, stickyNote, httpRequest, code, switch. It contains 56 nodes and follows best practices for error handling and security.", "notes": "Excellent quality workflow: PUQ Docker NextCloud deploy. This workflow has been optimized for production use with comprehensive error handling, security, and documentation."}