AutomationFlowsDevOps › Deploy Curated Docker App Stacks to a Hostinger Vps via Forms and Email

Deploy Curated Docker App Stacks to a Hostinger Vps via Forms and Email

ByHostinger @hostinger on n8n.io

This workflow collects a stack choice and notification email via a form, lets you pick a Hostinger VPS, deploys the selected curated app stack as separate Docker Compose projects using the Hostinger API, and emails a per-app deployment summary with ports, container status, and…

Event trigger★★★★☆ complexity27 nodesForm TriggerN8N Nodes Hostinger ApiFormEmail Send
DevOps Trigger: Event Nodes: 27 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #17032 — we link there as the canonical source.

This workflow follows the Emailsend → Form Trigger recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "68d90007-9338-4b91-8231-1995257313ac",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        336,
        7840
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## Deploy a curated Docker app stack to a Hostinger VPS from a form\n\n### How it works\n\nThis workflow deploys a Docker-based stack to a selected Hostinger VPS using a guided form flow. It lists available VPS instances, lets the user choose a target and provide deployment metadata, creates the Docker project, waits for the Hostinger action status, and branches into success or failure handling. On success, it collects container information and emails a stack summary; on failure, it retrieves logs and formats a failure summary.\n\n### Setup steps\n\n- Configure Hostinger API credentials for all Hostinger API nodes.\n- Configure the email sending credentials used by the Send Stack Summary Email node.\n- Review the form fields so they collect the required stack label, notification email, and VPS selection inputs.\n- Update the code nodes that build VPS options, deployment items, action unwrapping, status parsing, and summary email content to match the desired Docker stack and Hostinger API response shape.\n- Set an appropriate wait duration in the Wait for Deployment node so the deployment has enough time before status is checked.\n\n### Customization\n\nCustomize the Docker project payload, polling delay, success email template, and failure summary formatting. If failure notifications are required, connect the failure branch to an email or alerting node."
      },
      "typeVersion": 1
    },
    {
      "id": "52d7c4c7-604e-4eca-af9a-9d0bedc55836",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        896,
        7984
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 320,
        "content": "## Start and list VPS\n\nCaptures the initial stack deployment request and retrieves the available VPS instances from Hostinger, then prepares them as options for the next form."
      },
      "typeVersion": 1
    },
    {
      "id": "682e605a-8b04-4eea-a0c0-1d4263d2b8e2",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1584,
        7984
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Select deployment target\n\nShows the VPS selection form and normalizes the chosen virtual machine, stack label, and notification email into fields used downstream."
      },
      "typeVersion": 1
    },
    {
      "id": "7b5719c8-7ad4-42d5-9daa-c1fb2c052c96",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2240,
        7984
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 320,
        "content": "## Create Docker project\n\nBuilds the deployment payload, creates the Docker project through the Hostinger API, and extracts the returned action details for status tracking."
      },
      "typeVersion": 1
    },
    {
      "id": "ea792104-d1c2-4245-8243-5442720cf7d9",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2896,
        8016
      ],
      "parameters": {
        "color": 7,
        "width": 848,
        "height": 304,
        "content": "## Check deployment status\n\nWaits for the deployment action to progress, fetches its status from Hostinger, unwraps the response, and branches based on whether deployment succeeded."
      },
      "typeVersion": 1
    },
    {
      "id": "755c6e2a-f5cf-4eea-95d6-9efbed1cb640",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3776,
        7840
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Format successful app\n\nOn a successful deployment, lists Docker containers for the project and formats the application details into a success summary block."
      },
      "typeVersion": 1
    },
    {
      "id": "dd05c820-f4e4-4909-ab14-b9abda806cdb",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3776,
        8192
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Format failed deployment\n\nOn a failed deployment, retrieves Docker project logs and formats the stack label, notification email, and failure summary for review."
      },
      "typeVersion": 1
    },
    {
      "id": "76acf594-0c9b-44ab-9f6b-1edcb20c34c6",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4400,
        7872
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 304,
        "content": "## Email deployment summary\n\nAggregates formatted success output, builds a stack summary email, and sends it to the configured notification recipient."
      },
      "typeVersion": 1
    },
    {
      "id": "91064d68-8179-40d4-ae18-b69af84448b1",
      "name": "When Form Submitted",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        944,
        8144
      ],
      "parameters": {
        "options": {},
        "formTitle": "Deploy a Pre-Built App Stack",
        "formFields": {
          "values": [
            {
              "fieldType": "dropdown",
              "fieldLabel": "Stack",
              "fieldOptions": {
                "values": [
                  {
                    "option": "Developer Stack"
                  },
                  {
                    "option": "Hobbyist Stack"
                  },
                  {
                    "option": "Freelancer Stack"
                  },
                  {
                    "option": "AI/LLM Stack"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldLabel": "Notification Email",
              "requiredField": true
            }
          ]
        },
        "formDescription": "Pick a curated stack of vetted apps to deploy to your VPS as a starting point."
      },
      "typeVersion": 2.2
    },
    {
      "id": "d884d7c8-a4c4-41bc-b287-928870a54c4c",
      "name": "Fetch VPS List",
      "type": "n8n-nodes-hostinger-api.hostingerApi",
      "position": [
        1168,
        8144
      ],
      "parameters": {
        "operation": "listVms"
      },
      "typeVersion": 1
    },
    {
      "id": "d20965fc-b0b9-4d7e-b8fe-91c9ca785371",
      "name": "Generate VPS Options",
      "type": "n8n-nodes-base.code",
      "position": [
        1392,
        8144
      ],
      "parameters": {
        "jsCode": "const raw = $input.first().json;\nconst vms = raw.response || raw;\nconst list = Array.isArray(vms) ? vms : (vms.data || []);\nconst options = list.map(vm => `${vm.id} - ${vm.hostname} (${vm.state}, ${vm.plan || 'plan n/a'})`);\nconst cfg = $('When Form Submitted').item.json;\nreturn [{ json: {\n  vpsOptions: options,\n  stackLabel: cfg['Stack'],\n  notifyEmail: cfg['Notification Email'],\n} }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "db8780de-ace3-4c20-b2d2-e6686219bfbf",
      "name": "Display VPS Selection",
      "type": "n8n-nodes-base.form",
      "position": [
        1632,
        8144
      ],
      "parameters": {
        "options": {},
        "defineForm": "json",
        "jsonOutput": "={{ [ { fieldLabel: 'Virtual Machine', fieldType: 'dropdown', fieldOptions: { values: $json.vpsOptions.map(o => ({ option: o })) }, requiredField: true } ] }}"
      },
      "typeVersion": 2.3
    },
    {
      "id": "bcebeb4d-21f3-4112-b81c-3df39984f223",
      "name": "Set VPS Details",
      "type": "n8n-nodes-base.set",
      "position": [
        1856,
        8144
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "virtualMachineId",
              "type": "number",
              "value": "={{ $json['Virtual Machine'].match(/^(\\d+)/)[1] }}"
            },
            {
              "id": "a2",
              "name": "stackLabel",
              "type": "string",
              "value": "={{ $('Generate VPS Options').item.json.stackLabel }}"
            },
            {
              "id": "a3",
              "name": "notifyEmail",
              "type": "string",
              "value": "={{ $('Generate VPS Options').item.json.notifyEmail }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "440a6505-fee1-493f-a517-6135af461ac2",
      "name": "Create Deployment Items",
      "type": "n8n-nodes-base.code",
      "position": [
        2288,
        8144
      ],
      "parameters": {
        "jsCode": "const CATALOG = {\"stacks\": {\"developer\": [\"gitea\", \"code-server\", \"postgresql\", \"pgadmin\", \"dockge\", \"traefik\", \"uptime-kuma\"], \"hobbyist\": [\"homepage\", \"jellyfin\", \"immich\", \"vaultwarden\", \"adguard-home\", \"syncthing\", \"uptime-kuma\"], \"freelancer\": [\"invoiceninja\", \"firefly-iii\", \"calcom\", \"nextcloud\", \"docuseal\", \"planka\", \"vaultwarden\"], \"ai-llm\": [\"ollama\", \"open-webui\", \"anythingllm\", \"flowise\", \"qdrant\", \"litellm\", \"n8n\"]}, \"apps\": {\"gitea\": {\"display_name\": \"Gitea\", \"image\": \"gitea/gitea:latest\", \"secrets\": [], \"host_ports\": [\"3000 (web)\", \"2222 (ssh)\"], \"compose\": \"services:\\n  gitea:\\n    image: gitea/gitea:latest\\n    environment:\\n      - USER_UID=1000\\n      - USER_GID=1000\\n      - GITEA__server__SSH_PORT=2222\\n    volumes:\\n      - gitea-data:/data\\n    ports:\\n      - \\\"3000:3000\\\"\\n      - \\\"2222:22\\\"\\n    restart: unless-stopped\\nvolumes:\\n  gitea-data:\\n\", \"notes\": \"Complete the first-run setup wizard at port 3000 to create the admin account.\", \"confidence\": \"high\"}, \"code-server\": {\"display_name\": \"Code Server\", \"image\": \"codercom/code-server:latest\", \"secrets\": [\"ADMIN_PASSWORD\"], \"host_ports\": [\"8443\"], \"compose\": \"services:\\n  code-server:\\n    image: codercom/code-server:latest\\n    environment:\\n      - PASSWORD=__ADMIN_PASSWORD__\\n    volumes:\\n      - code-server-data:/home/coder/project\\n    ports:\\n      - \\\"8443:8080\\\"\\n    restart: unless-stopped\\nvolumes:\\n  code-server-data:\\n\", \"notes\": \"Login password is included below.\", \"confidence\": \"high\"}, \"postgresql\": {\"display_name\": \"PostgreSQL\", \"image\": \"postgres:16\", \"secrets\": [\"DB_PASSWORD\"], \"host_ports\": [\"5432\"], \"compose\": \"services:\\n  postgresql:\\n    image: postgres:16\\n    environment:\\n      - POSTGRES_USER=admin\\n      - POSTGRES_PASSWORD=__DB_PASSWORD__\\n      - POSTGRES_DB=app\\n    volumes:\\n      - postgresql-data:/var/lib/postgresql/data\\n    ports:\\n      - \\\"5432:5432\\\"\\n    restart: unless-stopped\\nvolumes:\\n  postgresql-data:\\n\", \"notes\": \"Default database 'app', user 'admin' \\u2014 password included below.\", \"confidence\": \"high\"}, \"pgadmin\": {\"display_name\": \"pgAdmin\", \"image\": \"dpage/pgadmin4:latest\", \"secrets\": [\"ADMIN_PASSWORD\"], \"host_ports\": [\"5050\"], \"compose\": \"services:\\n  pgadmin:\\n    image: dpage/pgadmin4:latest\\n    environment:\\n      - PGADMIN_DEFAULT_EMAIL=admin@example.com\\n      - PGADMIN_DEFAULT_PASSWORD=__ADMIN_PASSWORD__\\n    volumes:\\n      - pgadmin-data:/var/lib/pgadmin\\n    ports:\\n      - \\\"5050:80\\\"\\n    restart: unless-stopped\\nvolumes:\\n  pgadmin-data:\\n\", \"notes\": \"Login email admin@example.com \\u2014 password included below. Change the email after first login.\", \"confidence\": \"high\"}, \"dockge\": {\"display_name\": \"Dockge\", \"image\": \"louislam/dockge:1\", \"secrets\": [], \"host_ports\": [\"5001\"], \"compose\": \"services:\\n  dockge:\\n    image: louislam/dockge:1\\n    volumes:\\n      - /var/run/docker.sock:/var/run/docker.sock\\n      - dockge-data:/app/data\\n      - dockge-stacks:/opt/stacks\\n    ports:\\n      - \\\"5001:5001\\\"\\n    restart: unless-stopped\\nvolumes:\\n  dockge-data:\\n  dockge-stacks:\\n\", \"notes\": \"Mounts the Docker socket to manage other compose stacks on this VPS.\", \"confidence\": \"high\"}, \"traefik\": {\"display_name\": \"Traefik\", \"image\": \"traefik:v3.0\", \"secrets\": [], \"host_ports\": [\"80\", \"8082 (dashboard)\"], \"compose\": \"services:\\n  traefik:\\n    image: traefik:v3.0\\n    command:\\n      - \\\"--providers.docker=true\\\"\\n      - \\\"--providers.docker.exposedbydefault=false\\\"\\n      - \\\"--entrypoints.web.address=:80\\\"\\n      - \\\"--api.dashboard=true\\\"\\n      - \\\"--api.insecure=true\\\"\\n    volumes:\\n      - /var/run/docker.sock:/var/run/docker.sock:ro\\n    ports:\\n      - \\\"80:80\\\"\\n      - \\\"8082:8080\\\"\\n    restart: unless-stopped\\n\", \"notes\": \"Dashboard is unauthenticated on 8082 by default \\u2014 lock this down before exposing publicly. Other apps aren't auto-routed through it yet; that needs Traefik labels added per app.\", \"confidence\": \"high\"}, \"uptime-kuma\": {\"display_name\": \"Uptime Kuma\", \"image\": \"louislam/uptime-kuma:1\", \"secrets\": [], \"host_ports\": [\"3001\"], \"compose\": \"services:\\n  uptime-kuma:\\n    image: louislam/uptime-kuma:1\\n    volumes:\\n      - uptime-kuma-data:/app/data\\n    ports:\\n      - \\\"3001:3001\\\"\\n    restart: unless-stopped\\nvolumes:\\n  uptime-kuma-data:\\n\", \"notes\": \"Complete the first-run setup wizard at port 3001 to create the admin account.\", \"confidence\": \"high\"}, \"homepage\": {\"display_name\": \"Homepage\", \"image\": \"ghcr.io/gethomepage/homepage:latest\", \"secrets\": [], \"host_ports\": [\"3003\"], \"compose\": \"services:\\n  homepage:\\n    image: ghcr.io/gethomepage/homepage:latest\\n    volumes:\\n      - homepage-config:/app/config\\n    ports:\\n      - \\\"3003:3000\\\"\\n    restart: unless-stopped\\nvolumes:\\n  homepage-config:\\n\", \"notes\": \"Ships with a default config; edit /app/config inside the container (or via a bind mount) to add service widgets.\", \"confidence\": \"high\"}, \"jellyfin\": {\"display_name\": \"Jellyfin\", \"image\": \"jellyfin/jellyfin:latest\", \"secrets\": [], \"host_ports\": [\"8096\"], \"compose\": \"services:\\n  jellyfin:\\n    image: jellyfin/jellyfin:latest\\n    volumes:\\n      - jellyfin-config:/config\\n      - jellyfin-cache:/cache\\n      - jellyfin-media:/media\\n    ports:\\n      - \\\"8096:8096\\\"\\n    restart: unless-stopped\\nvolumes:\\n  jellyfin-config:\\n  jellyfin-cache:\\n  jellyfin-media:\\n\", \"notes\": \"Media library is empty until you add files to the 'jellyfin-media' volume and complete the setup wizard.\", \"confidence\": \"high\"}, \"immich\": {\"display_name\": \"Immich\", \"image\": \"ghcr.io/immich-app/immich-server:release\", \"secrets\": [\"DB_PASSWORD\"], \"host_ports\": [\"2283\"], \"compose\": \"services:\\n  immich-server:\\n    image: ghcr.io/immich-app/immich-server:release\\n    environment:\\n      - DB_HOSTNAME=immich-postgres\\n      - DB_USERNAME=immich\\n      - DB_PASSWORD=__DB_PASSWORD__\\n      - DB_DATABASE_NAME=immich\\n      - REDIS_HOSTNAME=immich-redis\\n    volumes:\\n      - immich-uploads:/usr/src/app/upload\\n    ports:\\n      - \\\"2283:2283\\\"\\n    depends_on:\\n      - immich-postgres\\n      - immich-redis\\n    restart: unless-stopped\\n  immich-machine-learning:\\n    image: ghcr.io/immich-app/immich-machine-learning:release\\n    volumes:\\n      - immich-model-cache:/cache\\n    restart: unless-stopped\\n  immich-redis:\\n    image: redis:6.2-alpine\\n    restart: unless-stopped\\n  immich-postgres:\\n    image: tensorchord/pgvecto-rs:pg14-v0.2.0\\n    environment:\\n      - POSTGRES_USER=immich\\n      - POSTGRES_PASSWORD=__DB_PASSWORD__\\n      - POSTGRES_DB=immich\\n    volumes:\\n      - immich-postgres-data:/var/lib/postgresql/data\\n    restart: unless-stopped\\nvolumes:\\n  immich-uploads:\\n  immich-model-cache:\\n  immich-postgres-data:\\n\", \"notes\": \"Multi-container app (server, ML, Redis, Postgres w/ pgvecto.rs). Complete the setup wizard at port 2283.\", \"confidence\": \"review\"}, \"vaultwarden\": {\"display_name\": \"Vaultwarden\", \"image\": \"vaultwarden/server:latest\", \"secrets\": [], \"host_ports\": [\"8081\"], \"compose\": \"services:\\n  vaultwarden:\\n    image: vaultwarden/server:latest\\n    volumes:\\n      - vaultwarden-data:/data\\n    ports:\\n      - \\\"8081:80\\\"\\n    restart: unless-stopped\\nvolumes:\\n  vaultwarden-data:\\n\", \"notes\": \"Create your account directly at port 8081 on first visit.\", \"confidence\": \"high\"}, \"adguard-home\": {\"display_name\": \"AdGuard Home\", \"image\": \"adguard/adguardhome:latest\", \"secrets\": [], \"host_ports\": [\"3010 (setup)\", \"53 (dns tcp/udp)\"], \"compose\": \"services:\\n  adguard-home:\\n    image: adguard/adguardhome:latest\\n    volumes:\\n      - adguard-work:/opt/adguardhome/work\\n      - adguard-conf:/opt/adguardhome/conf\\n    ports:\\n      - \\\"3010:3000\\\"\\n      - \\\"53:53/tcp\\\"\\n      - \\\"53:53/udp\\\"\\n    restart: unless-stopped\\nvolumes:\\n  adguard-work:\\n  adguard-conf:\\n\", \"notes\": \"Only run one instance of this per VPS \\u2014 it binds DNS port 53. Complete setup at port 3010.\", \"confidence\": \"high\"}, \"syncthing\": {\"display_name\": \"Syncthing\", \"image\": \"syncthing/syncthing:latest\", \"secrets\": [], \"host_ports\": [\"8384\", \"22000 (tcp/udp)\", \"21027 (udp)\"], \"compose\": \"services:\\n  syncthing:\\n    image: syncthing/syncthing:latest\\n    volumes:\\n      - syncthing-data:/var/syncthing\\n    ports:\\n      - \\\"8384:8384\\\"\\n      - \\\"22000:22000/tcp\\\"\\n      - \\\"22000:22000/udp\\\"\\n      - \\\"21027:21027/udp\\\"\\n    restart: unless-stopped\\nvolumes:\\n  syncthing-data:\\n\", \"notes\": \"Manage devices and folders from the web UI at port 8384.\", \"confidence\": \"high\"}, \"invoiceninja\": {\"display_name\": \"Invoice Ninja\", \"image\": \"invoiceninja/invoiceninja:5\", \"secrets\": [\"DB_PASSWORD\"], \"host_ports\": [\"8085\"], \"compose\": \"services:\\n  invoiceninja:\\n    image: invoiceninja/invoiceninja:5\\n    environment:\\n      - APP_URL=http://localhost:8085\\n      - DB_HOST=invoiceninja-db\\n      - DB_DATABASE=ninja\\n      - DB_USERNAME=ninja\\n      - DB_PASSWORD=__DB_PASSWORD__\\n    volumes:\\n      - invoiceninja-data:/var/www/app/storage\\n    ports:\\n      - \\\"8085:80\\\"\\n    depends_on:\\n      - invoiceninja-db\\n    restart: unless-stopped\\n  invoiceninja-db:\\n    image: mariadb:10.11\\n    environment:\\n      - MYSQL_ROOT_PASSWORD=__DB_PASSWORD__\\n      - MYSQL_DATABASE=ninja\\n      - MYSQL_USER=ninja\\n      - MYSQL_PASSWORD=__DB_PASSWORD__\\n    volumes:\\n      - invoiceninja-db-data:/var/lib/mysql\\n    restart: unless-stopped\\nvolumes:\\n  invoiceninja-data:\\n  invoiceninja-db-data:\\n\", \"notes\": \"Update APP_URL to your real domain/IP once you know it, then complete the setup wizard at port 8085.\", \"confidence\": \"review\"}, \"firefly-iii\": {\"display_name\": \"Firefly III\", \"image\": \"fireflyiii/core:latest\", \"secrets\": [\"DB_PASSWORD\", \"APP_KEY\"], \"host_ports\": [\"8086\"], \"compose\": \"services:\\n  firefly-iii:\\n    image: fireflyiii/core:latest\\n    environment:\\n      - APP_KEY=__APP_KEY__\\n      - DB_HOST=firefly-db\\n      - DB_DATABASE=firefly\\n      - DB_USERNAME=firefly\\n      - DB_PASSWORD=__DB_PASSWORD__\\n    volumes:\\n      - firefly-upload:/var/www/html/storage/upload\\n    ports:\\n      - \\\"8086:8080\\\"\\n    depends_on:\\n      - firefly-db\\n    restart: unless-stopped\\n  firefly-db:\\n    image: mariadb:10.11\\n    environment:\\n      - MYSQL_ROOT_PASSWORD=__DB_PASSWORD__\\n      - MYSQL_DATABASE=firefly\\n      - MYSQL_USER=firefly\\n      - MYSQL_PASSWORD=__DB_PASSWORD__\\n    volumes:\\n      - firefly-db-data:/var/lib/mysql\\n    restart: unless-stopped\\nvolumes:\\n  firefly-upload:\\n  firefly-db-data:\\n\", \"notes\": \"APP_KEY must be a 32-character random string (the workflow generates one) \\u2014 register at port 8086.\", \"confidence\": \"high\"}, \"calcom\": {\"display_name\": \"Cal.com\", \"image\": \"calcom/cal.com:latest\", \"secrets\": [\"DB_PASSWORD\", \"NEXTAUTH_SECRET\"], \"host_ports\": [\"3009\"], \"compose\": \"services:\\n  calcom:\\n    image: calcom/cal.com:latest\\n    environment:\\n      - DATABASE_URL=postgresql://user:password@$3      - NEXTAUTH_SECRET=__NEXTAUTH_SECRET__\\n      - NEXT_PUBLIC_WEBAPP_URL=http://localhost:3009\\n    ports:\\n      - \\\"3009:3000\\\"\\n    depends_on:\\n      - calcom-db\\n    restart: unless-stopped\\n  calcom-db:\\n    image: postgres:15\\n    environment:\\n      - POSTGRES_USER=calcom\\n      - POSTGRES_PASSWORD=__DB_PASSWORD__\\n      - POSTGRES_DB=calcom\\n    volumes:\\n      - calcom-db-data:/var/lib/postgresql/data\\n    restart: unless-stopped\\nvolumes:\\n  calcom-db-data:\\n\", \"notes\": \"Cal.com's self-host image/env requirements change fairly often between versions \\u2014 treat this as a starting point and check calcom/cal.com's current self-hosting docs before relying on it.\", \"confidence\": \"review\"}, \"nextcloud\": {\"display_name\": \"Nextcloud\", \"image\": \"nextcloud:apache\", \"secrets\": [\"ADMIN_PASSWORD\"], \"host_ports\": [\"8087\"], \"compose\": \"services:\\n  nextcloud:\\n    image: nextcloud:apache\\n    environment:\\n      - NEXTCLOUD_ADMIN_USER=admin\\n      - NEXTCLOUD_ADMIN_PASSWORD=__ADMIN_PASSWORD__\\n    volumes:\\n      - nextcloud-data:/var/www/html\\n    ports:\\n      - \\\"8087:80\\\"\\n    restart: unless-stopped\\nvolumes:\\n  nextcloud-data:\\n\", \"notes\": \"Uses SQLite (no separate DB container) \\u2014 fine for light use; migrate to Postgres/MySQL for heavier workloads.\", \"confidence\": \"high\"}, \"docuseal\": {\"display_name\": \"Docuseal\", \"image\": \"docuseal/docuseal:latest\", \"secrets\": [], \"host_ports\": [\"8088\"], \"compose\": \"services:\\n  docuseal:\\n    image: docuseal/docuseal:latest\\n    volumes:\\n      - docuseal-data:/data\\n    ports:\\n      - \\\"8088:3000\\\"\\n    restart: unless-stopped\\nvolumes:\\n  docuseal-data:\\n\", \"notes\": \"Uses its bundled SQLite database. Complete the setup wizard at port 8088.\", \"confidence\": \"high\"}, \"planka\": {\"display_name\": \"Planka\", \"image\": \"ghcr.io/plankanban/planka:latest\", \"secrets\": [\"DB_PASSWORD\", \"SECRET_KEY\"], \"host_ports\": [\"3012\"], \"compose\": \"services:\\n  planka:\\n    image: ghcr.io/plankanban/planka:latest\\n    environment:\\n      - BASE_URL=http://localhost:3012\\n      - DATABASE_URL=postgresql://user:password@$3      - SECRET_KEY=__SECRET_KEY__\\n    ports:\\n      - \\\"3012:1337\\\"\\n    depends_on:\\n      - planka-db\\n    restart: unless-stopped\\n  planka-db:\\n    image: postgres:15\\n    environment:\\n      - POSTGRES_USER=planka\\n      - POSTGRES_PASSWORD=__DB_PASSWORD__\\n      - POSTGRES_DB=planka\\n    volumes:\\n      - planka-db-data:/var/lib/postgresql/data\\n    restart: unless-stopped\\nvolumes:\\n  planka-db-data:\\n\", \"notes\": \"Update BASE_URL to your real domain/IP once known. Create the first admin user via the container's CLI (documented in Planka's README) after it's up.\", \"confidence\": \"high\"}, \"ollama\": {\"display_name\": \"Ollama\", \"image\": \"ollama/ollama:latest\", \"secrets\": [], \"host_ports\": [\"11434\"], \"compose\": \"services:\\n  ollama:\\n    image: ollama/ollama:latest\\n    volumes:\\n      - ollama-data:/root/.ollama\\n    ports:\\n      - \\\"11434:11434\\\"\\n    restart: unless-stopped\\nvolumes:\\n  ollama-data:\\n\", \"notes\": \"No models are pulled by default \\u2014 run 'docker exec -it <container> ollama pull <model>' after deploy, or pull from Open WebUI's UI.\", \"confidence\": \"high\"}, \"open-webui\": {\"display_name\": \"Open WebUI\", \"image\": \"ghcr.io/open-webui/open-webui:main\", \"secrets\": [], \"host_ports\": [\"3005\"], \"compose\": \"services:\\n  open-webui:\\n    image: ghcr.io/open-webui/open-webui:main\\n    environment:\\n      - OLLAMA_BASE_URL=http://host.docker.internal:11434\\n    extra_hosts:\\n      - \\\"host.docker.internal:host-gateway\\\"\\n    volumes:\\n      - open-webui-data:/app/backend/data\\n    ports:\\n      - \\\"3005:8080\\\"\\n    restart: unless-stopped\\nvolumes:\\n  open-webui-data:\\n\", \"notes\": \"Pre-configured to reach the Ollama app on this same VPS via its published port. First account created becomes the admin.\", \"confidence\": \"high\"}, \"anythingllm\": {\"display_name\": \"AnythingLLM\", \"image\": \"mintplexlabs/anythingllm:latest\", \"secrets\": [], \"host_ports\": [\"3006\"], \"compose\": \"services:\\n  anythingllm:\\n    image: mintplexlabs/anythingllm:latest\\n    extra_hosts:\\n      - \\\"host.docker.internal:host-gateway\\\"\\n    volumes:\\n      - anythingllm-storage:/app/server/storage\\n    ports:\\n      - \\\"3006:3001\\\"\\n    restart: unless-stopped\\nvolumes:\\n  anythingllm-storage:\\n\", \"notes\": \"Complete the setup wizard at port 3006 and point it at Ollama (http://host.docker.internal:11434) or Qdrant (http://host.docker.internal:6333) as needed.\", \"confidence\": \"high\"}, \"flowise\": {\"display_name\": \"Flowise\", \"image\": \"flowiseai/flowise:latest\", \"secrets\": [], \"host_ports\": [\"3007\"], \"compose\": \"services:\\n  flowise:\\n    image: flowiseai/flowise:latest\\n    extra_hosts:\\n      - \\\"host.docker.internal:host-gateway\\\"\\n    volumes:\\n      - flowise-data:/root/.flowise\\n    ports:\\n      - \\\"3007:3000\\\"\\n    restart: unless-stopped\\nvolumes:\\n  flowise-data:\\n\", \"notes\": \"Create your account on first visit at port 3007. Reach Ollama in flows via http://host.docker.internal:11434.\", \"confidence\": \"high\"}, \"qdrant\": {\"display_name\": \"Qdrant\", \"image\": \"qdrant/qdrant:latest\", \"secrets\": [], \"host_ports\": [\"6333\", \"6334\"], \"compose\": \"services:\\n  qdrant:\\n    image: qdrant/qdrant:latest\\n    volumes:\\n      - qdrant-data:/qdrant/storage\\n    ports:\\n      - \\\"6333:6333\\\"\\n      - \\\"6334:6334\\\"\\n    restart: unless-stopped\\nvolumes:\\n  qdrant-data:\\n\", \"notes\": \"No auth enabled by default \\u2014 fine on a private network, but add an API key before exposing publicly.\", \"confidence\": \"high\"}, \"litellm\": {\"display_name\": \"LiteLLM\", \"image\": \"ghcr.io/berriai/litellm:main-latest\", \"secrets\": [\"MASTER_KEY\"], \"host_ports\": [\"4000\"], \"compose\": \"services:\\n  litellm:\\n    image: ghcr.io/berriai/litellm:main-latest\\n    environment:\\n      - LITELLM_MASTER_KEY=__MASTER_KEY__\\n    extra_hosts:\\n      - \\\"host.docker.internal:host-gateway\\\"\\n    ports:\\n      - \\\"4000:4000\\\"\\n    restart: unless-stopped\\n\", \"notes\": \"Master key included below \\u2014 use it as the Bearer YOUR_TOKEN_HERE when calling this gateway. Add provider API keys via LiteLLM's config/UI for any hosted models you want to route to.\", \"confidence\": \"review\"}, \"n8n\": {\"display_name\": \"n8n\", \"image\": \"n8nio/n8n:latest\", \"secrets\": [], \"host_ports\": [\"5678\"], \"compose\": \"services:\\n  n8n:\\n    image: n8nio/n8n:latest\\n    extra_hosts:\\n      - \\\"host.docker.internal:host-gateway\\\"\\n    volumes:\\n      - n8n-data:/home/node/.n8n\\n    ports:\\n      - \\\"5678:5678\\\"\\n    restart: unless-stopped\\nvolumes:\\n  n8n-data:\\n\", \"notes\": \"Complete the setup wizard at port 5678. Reach Ollama from workflows via http://host.docker.internal:11434.\", \"confidence\": \"high\"}}};\nconst LABEL_TO_KEY = {\"Developer Stack\": \"developer\", \"Hobbyist Stack\": \"hobbyist\", \"Freelancer Stack\": \"freelancer\", \"AI/LLM Stack\": \"ai-llm\"};\n\nconst cfg = $input.first().json;\nconst stackKey = LABEL_TO_KEY[cfg.stackLabel];\nif (!stackKey) {\n  throw new Error(`Unknown stack selection: ${cfg.stackLabel}`);\n}\nconst appSlugs = CATALOG.stacks[stackKey];\n\nfunction randomToken(len) {\n  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n  let out = '';\n  for (let i = 0; i < len; i++) {\n    out += chars[Math.floor(Math.random() * chars.length)];\n  }\n  return out;\n}\n\nconst items = [];\nfor (const slug of appSlugs) {\n  const app = CATALOG.apps[slug];\n  let content = app.compose;\n  const generatedSecrets = {};\n  for (const secretName of app.secrets) {\n    const token = `__${secretName}__`;\n    const value = randomToken(20);\n    generatedSecrets[secretName] = value;\n    content = content.split(token).join(value);\n  }\n  items.push({\n    json: {\n      virtualMachineId: cfg.virtualMachineId,\n      notifyEmail: cfg.notifyEmail,\n      stackKey: stackKey,\n      stackLabel: cfg.stackLabel,\n      slug: slug,\n      displayName: app.display_name,\n      projectName: `${stackKey}-${slug}`.slice(0, 64),\n      content: content,\n      hostPorts: app.host_ports.join(', '),\n      appNotes: app.notes,\n      generatedSecrets: generatedSecrets,\n    }\n  });\n}\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "a9abbb71-be20-4038-b7c6-be0c421c746f",
      "name": "Initiate Docker Deployment",
      "type": "n8n-nodes-hostinger-api.hostingerApi",
      "position": [
        2496,
        8144
      ],
      "parameters": {
        "resource": "vpsDocker",
        "operation": "createProject",
        "requestBody": "={{ JSON.stringify({ project_name: $json.projectName, content: $json.content, environment: '#' }) }}",
        "virtualMachineId": "={{ $json.virtualMachineId }}"
      },
      "typeVersion": 1
    },
    {
      "id": "acdae42c-dd26-4edd-8f3f-874c88ecb19b",
      "name": "Extract Deployment Action",
      "type": "n8n-nodes-base.code",
      "position": [
        2720,
        8144
      ],
      "parameters": {
        "jsCode": "const raw = $input.item.json;\nconst action = raw.response || raw;\nconst cfg = $('Create Deployment Items').item.json;\nreturn { json: { ...cfg, actionId: action.id, actionState: action.state } };\n"
      },
      "typeVersion": 2
    },
    {
      "id": "83f753a3-15d0-451a-9e6b-976b3e630eb2",
      "name": "Wait 90 Seconds",
      "type": "n8n-nodes-base.wait",
      "position": [
        2944,
        8144
      ],
      "parameters": {
        "amount": 90
      },
      "typeVersion": 1.1
    },
    {
      "id": "5f19a906-fe1c-49ec-83f8-c7663136f849",
      "name": "Fetch Deployment Status",
      "type": "n8n-nodes-hostinger-api.hostingerApi",
      "position": [
        3168,
        8144
      ],
      "parameters": {
        "actionId": "={{ $json.actionId }}",
        "resource": "vpsActions",
        "virtualMachineId": "={{ $json.virtualMachineId }}"
      },
      "typeVersion": 1
    },
    {
      "id": "68f57a01-1ca7-42ce-9d54-3da85d02ae0f",
      "name": "Parse Deployment Status",
      "type": "n8n-nodes-base.code",
      "position": [
        3376,
        8144
      ],
      "parameters": {
        "jsCode": "const raw = $input.item.json;\nconst action = raw.response || raw;\nconst cfg = $('Extract Deployment Action').item.json;\nreturn { json: { ...cfg, finalState: action.state } };\n"
      },
      "typeVersion": 2
    },
    {
      "id": "73e0e09c-d687-403f-832d-6cc14262592b",
      "name": "Check Deployment Success",
      "type": "n8n-nodes-base.if",
      "position": [
        3600,
        8144
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.finalState }}",
              "rightValue": "success"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "59aba707-6f58-4660-945f-a546865a3ec2",
      "name": "Retrieve Container List",
      "type": "n8n-nodes-hostinger-api.hostingerApi",
      "position": [
        3824,
        8000
      ],
      "parameters": {
        "resource": "vpsDocker",
        "operation": "listContainers",
        "virtualMachineId": "={{ $json.virtualMachineId }}",
        "dockerProjectName": "={{ $json.projectName }}"
      },
      "typeVersion": 1
    },
    {
      "id": "6bcb6c95-220d-4b12-8921-499c2f1dfe28",
      "name": "Format Success Output",
      "type": "n8n-nodes-base.code",
      "position": [
        4048,
        8000
      ],
      "parameters": {
        "jsCode": "const raw = $input.item.json;\nconst containers = raw.response || raw;\nconst cfg = $('Parse Deployment Status').item.json;\nconst list = Array.isArray(containers) ? containers : [];\nconst containerLines = list.map(c => `      - ${c.name}: ${c.status}`).join('\\n') || '      (no containers reported yet)';\nconst secretLines = Object.entries(cfg.generatedSecrets || {}).map(([k,v]) => `      - ${k}: ${v}`).join('\\n');\nconst block = [\n  `- ${cfg.displayName} (deployed)`,\n  `    Ports: ${cfg.hostPorts}`,\n  `    Containers:`,\n  containerLines,\n  secretLines ? `    Generated credentials:\\n${secretLines}` : null,\n  cfg.appNotes ? `    Note: ${cfg.appNotes}` : null,\n].filter(Boolean).join('\\n');\nreturn { json: { stackLabel: cfg.stackLabel, notifyEmail: cfg.notifyEmail, summaryBlock: block } };\n"
      },
      "typeVersion": 2
    },
    {
      "id": "5457ca47-1a72-4c4b-a8c6-45b1cb369a0e",
      "name": "Build Failure Report",
      "type": "n8n-nodes-base.set",
      "position": [
        4048,
        8352
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "f1",
              "name": "stackLabel",
              "type": "string",
              "value": "={{ $('Parse Deployment Status').item.json.stackLabel }}"
            },
            {
              "id": "f2",
              "name": "notifyEmail",
              "type": "string",
              "value": "={{ $('Parse Deployment Status').item.json.notifyEmail }}"
            },
            {
              "id": "f3",
              "name": "summaryBlock",
              "type": "string",
              "value": "={{ '- ' + $('Parse Deployment Status').item.json.displayName + ' (NOT deployed - action state: ' + $('Parse Deployment Status').item.json.finalState + ')\\n    Recent logs for project \\'' + $('Parse Deployment Status').item.json.projectName + '\\':\\n' + ((typeof ($json.response || $json) === 'string' ? ($json.response || $json) : JSON.stringify($json.response || $json)).split('\\n').slice(-40).map(l => '      ' + l).join('\\n') || '      (no logs returned)') }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "cb4b65f1-c9c8-4b03-8e84-b09f5df42776",
      "name": "Combine Results",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        4448,
        8000
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "23c30e57-169d-4ac4-b4fd-6ee51914f956",
      "name": "Prepare Summary Email",
      "type": "n8n-nodes-base.code",
      "position": [
        4672,
        8000
      ],
      "parameters": {
        "jsCode": "const rows = $input.first().json.data;\nconst stackLabel = rows[0]?.stackLabel || 'Stack';\nconst notifyEmail = rows[0]?.notifyEmail;\nconst body = `Your ${stackLabel} deployment finished.\\n\\n` + rows.map(r => r.summaryBlock).join('\\n\\n');\nreturn { json: { notifyEmail, stackLabel, body } };\n"
      },
      "typeVersion": 2
    },
    {
      "id": "53f41979-db3e-4217-b8e3-32fed02c49fe",
      "name": "Dispatch Summary Email",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        4896,
        8000
      ],
      "parameters": {
        "text": "={{ $json.body }}",
        "options": {},
        "subject": "=Your {{ $json.stackLabel }} deployment summary",
        "toEmail": "={{ $json.notifyEmail }}",
        "fromEmail": "={{ $json.notifyEmail }}",
        "emailFormat": "text"
      },
      "typeVersion": 2.1
    },
    {
      "id": "1c001dde-75b2-4fe5-8f91-6614dfe7e94b",
      "name": "Fetch Docker Logs",
      "type": "n8n-nodes-hostinger-api.hostingerApi",
      "position": [
        3824,
        8352
      ],
      "parameters": {
        "resource": "vpsDocker",
        "operation": "getLogs",
        "virtualMachineId": "={{ $json.virtualMachineId }}",
        "dockerProjectName": "={{ $json.projectName }}"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Fetch VPS List": {
      "main": [
        [
          {
            "node": "Generate VPS Options",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Results": {
      "main": [
        [
          {
            "node": "Prepare Summary Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set VPS Details": {
      "main": [
        [
          {
            "node": "Create Deployment Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 90 Seconds": {
      "main": [
        [
          {
            "node": "Fetch Deployment Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Docker Logs": {
      "main": [
        [
          {
            "node": "Build Failure Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Form Submitted": {
      "main": [
        [
          {
            "node": "Fetch VPS List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate VPS Options": {
      "main": [
        [
          {
            "node": "Display VPS Selection",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Display VPS Selection": {
      "main": [
        [
          {
            "node": "Set VPS Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Success Output": {
      "main": [
        [
          {
            "node": "Combine Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Summary Email": {
      "main": [
        [
          {
            "node": "Dispatch Summary Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Deployment Items": {
      "main": [
        [
          {
            "node": "Initiate Docker Deployment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Deployment Status": {
      "main": [
        [
          {
            "node": "Parse Deployment Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Deployment Status": {
      "main": [
        [
          {
            "node": "Check Deployment Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retrieve Container List": {
      "main": [
        [
          {
            "node": "Format Success Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Deployment Success": {
      "main": [
        [
          {
            "node": "Retrieve Container List",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fetch Docker Logs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Deployment Action": {
      "main": [
        [
          {
            "node": "Wait 90 Seconds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Initiate Docker Deployment": {
      "main": [
        [
          {
            "node": "Extract Deployment Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow collects a stack choice and notification email via a form, lets you pick a Hostinger VPS, deploys the selected curated app stack as separate Docker Compose projects using the Hostinger API, and emails a per-app deployment summary with ports, container status, and…

Source: https://n8n.io/workflows/17032/ — original creator credit. Request a take-down →

More DevOps workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

DevOps

This template lets you selectively import n8n workflows from a GitHub repository, even when your repository uses deeply nested folder structures.

Form Trigger, GitHub, n8n +1
DevOps

Trigger A Build In Travis Ci When Code Changes Are Push To A Github Repo. Uses githubTrigger, travisCi, noOp. Event-driven trigger; 4 nodes.

Github Trigger, Travis CI
DevOps

Create A Document In Outline For Each New Gitlab Release. Uses gitlabTrigger, httpRequest. Event-driven trigger; 3 nodes.

Gitlab Trigger, HTTP Request
DevOps

Create a release and get all releases. Uses manualTrigger, sentryIo. Event-driven trigger; 3 nodes.

Sentry Io
DevOps

The Docker Immich WHMCS module uses a specially designed workflow for n8n to automate deployment processes. The workflow provides an API interface for the module, receives specific commands, and conne

Ssh