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 →
{
"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
}
]
]
}
}
}
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This template lets you selectively import n8n workflows from a GitHub repository, even when your repository uses deeply nested folder structures.
Trigger A Build In Travis Ci When Code Changes Are Push To A Github Repo. Uses githubTrigger, travisCi, noOp. Event-driven trigger; 4 nodes.
Create A Document In Outline For Each New Gitlab Release. Uses gitlabTrigger, httpRequest. Event-driven trigger; 3 nodes.
Create a release and get all releases. Uses manualTrigger, sentryIo. Event-driven trigger; 3 nodes.
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