somratpro commited on
Commit
320e776
Β·
1 Parent(s): 729c5bd

refactor: optimize startup, improve security, and simplify n8n configuration for v2 compatibility

Browse files
Files changed (6) hide show
  1. .env.example +9 -61
  2. Dockerfile +3 -0
  3. README.md +20 -186
  4. health-server.js +236 -79
  5. n8n-sync.py +7 -8
  6. start.sh +21 -23
.env.example CHANGED
@@ -1,33 +1,10 @@
1
- # =============================================================================
2
- # Hugging8n β€” Environment Variables Reference
3
- # Copy to .env and fill in your values before running locally.
4
- # On HuggingFace Spaces, add Secrets and Variables in Settings β†’ Variables and secrets.
5
- # =============================================================================
6
-
7
- # -----------------------------------------------------------------------------
8
- # REQUIRED β€” Basic Auth (set these as Secrets on HF Spaces)
9
- # -----------------------------------------------------------------------------
10
-
11
- # Username for n8n basic auth login
12
- N8N_BASIC_AUTH_USER=admin
13
-
14
- # Password for n8n basic auth login. REQUIRED to protect your instance.
15
- # Generate a strong password: openssl rand -base64 24
16
- N8N_BASIC_AUTH_PASSWORD=
17
-
18
- # -----------------------------------------------------------------------------
19
- # RECOMMENDED β€” Persistent Backup (set these as Secrets on HF Spaces)
20
- # -----------------------------------------------------------------------------
21
-
22
- # Your Hugging Face write token (to create/update the backup dataset)
23
- # Get one at: https://huggingface.co/settings/tokens
24
  HF_TOKEN=
25
 
26
- # Your Hugging Face username (owner of the backup dataset)
27
- # If not set, inferred from SPACE_AUTHOR_NAME automatically on HF Spaces
28
  HF_USERNAME=
29
 
30
- # Name of the private dataset repo used for backup (auto-created on first run)
31
  BACKUP_DATASET_NAME=hugging8n-backup
32
 
33
  # How often to sync the backup, in seconds
@@ -37,59 +14,30 @@ SYNC_INTERVAL=180
37
  # OPTIONAL β€” n8n Configuration
38
  # -----------------------------------------------------------------------------
39
 
40
- # Timezone for n8n schedule triggers (e.g. Asia/Dhaka, Europe/London, US/Eastern)
41
  GENERIC_TIMEZONE=UTC
42
 
43
  # n8n encryption key β€” protects stored credentials. Persisted in backup.
44
  # Generate with: openssl rand -hex 32
45
- # WARNING: if you lose this key and your backup, all credentials are lost.
46
  N8N_ENCRYPTION_KEY=
47
 
48
- # Override the auto-detected Space hostname (leave blank on HF Spaces)
49
- SPACE_HOST_OVERRIDE=
50
-
51
  # -----------------------------------------------------------------------------
52
- # OPTIONAL β€” Keep-Alive
53
  # -----------------------------------------------------------------------------
54
 
55
- # UptimeRobot Main API key for creating an external keep-alive monitor
56
- # Get one at: https://uptimerobot.com β†’ My Settings β†’ API Settings β†’ Main API Key
57
- # This is NOT needed as a HF Space Secret β€” it's only used by setup-uptimerobot.sh
58
- UPTIMEROBOT_API_KEY=
59
-
60
- # Custom monitor name (defaults to "Hugging8n <space-host>")
61
- UPTIMEROBOT_MONITOR_NAME=
62
-
63
- # UptimeRobot ping interval in minutes (default: 5)
64
- UPTIMEROBOT_INTERVAL=5
65
-
66
- # Comma-separated UptimeRobot alert contact IDs (optional)
67
- UPTIMEROBOT_ALERT_CONTACTS=
68
-
69
- # -----------------------------------------------------------------------------
70
- # ADVANCED β€” n8n Internals (usually leave as defaults)
71
- # -----------------------------------------------------------------------------
72
-
73
- # Disable n8n basic auth (not recommended β€” leaves your instance unprotected)
74
- # N8N_BASIC_AUTH_ACTIVE=false
75
-
76
  # Internal n8n port (default: 5678)
77
  N8N_PORT=5678
78
 
79
  # Public proxy port exposed by HF Spaces (default: 7861)
80
  PUBLIC_PORT=7861
81
 
82
- # Disable n8n telemetry (default: false)
83
- N8N_DIAGNOSTICS_ENABLED=false
84
-
85
- # Disable n8n personalization prompts (default: false)
86
- N8N_PERSONALIZATION_ENABLED=false
87
 
88
  # -----------------------------------------------------------------------------
89
  # BUILD-TIME VARIABLE (HF Spaces: add as Variable, not Secret)
90
  # -----------------------------------------------------------------------------
91
 
92
- # Pin a specific n8n version for reproducibility (default: latest)
93
- # Example: 1.90.0
94
- # On HF Spaces, add this as a Variable (not Secret) so it's passed as a build arg.
95
  N8N_VERSION=latest
 
1
+ # Your Hugging Face write token
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  HF_TOKEN=
3
 
4
+ # Your Hugging Face username (inferred automatically on HF Spaces)
 
5
  HF_USERNAME=
6
 
7
+ # Name of the private dataset repo used for backup
8
  BACKUP_DATASET_NAME=hugging8n-backup
9
 
10
  # How often to sync the backup, in seconds
 
14
  # OPTIONAL β€” n8n Configuration
15
  # -----------------------------------------------------------------------------
16
 
17
+ # Timezone for n8n triggers (e.g. Asia/Dhaka)
18
  GENERIC_TIMEZONE=UTC
19
 
20
  # n8n encryption key β€” protects stored credentials. Persisted in backup.
21
  # Generate with: openssl rand -hex 32
 
22
  N8N_ENCRYPTION_KEY=
23
 
 
 
 
24
  # -----------------------------------------------------------------------------
25
+ # ADVANCED β€” n8n Internals
26
  # -----------------------------------------------------------------------------
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  # Internal n8n port (default: 5678)
29
  N8N_PORT=5678
30
 
31
  # Public proxy port exposed by HF Spaces (default: 7861)
32
  PUBLIC_PORT=7861
33
 
34
+ # Disable unnecessary services
35
+ N8N_RUNNERS_ENABLED=false
36
+ N8N_LICENSE_AUTO_RENEW_ENABLED=false
 
 
37
 
38
  # -----------------------------------------------------------------------------
39
  # BUILD-TIME VARIABLE (HF Spaces: add as Variable, not Secret)
40
  # -----------------------------------------------------------------------------
41
 
42
+ # Pin a specific n8n version for reproducibility
 
 
43
  N8N_VERSION=latest
Dockerfile CHANGED
@@ -37,5 +37,8 @@ USER node
37
 
38
  EXPOSE 7861
39
 
 
 
 
40
  ENTRYPOINT ["/usr/bin/tini", "--"]
41
  CMD ["/home/node/app/start.sh"]
 
37
 
38
  EXPOSE 7861
39
 
40
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=60s \
41
+ CMD curl -f http://localhost:7861/health || exit 1
42
+
43
  ENTRYPOINT ["/usr/bin/tini", "--"]
44
  CMD ["/home/node/app/start.sh"]
README.md CHANGED
@@ -8,43 +8,22 @@ app_port: 7861
8
  pinned: true
9
  license: mit
10
  secrets:
11
- - name: N8N_BASIC_AUTH_PASSWORD
12
- description: Password to log in to your n8n instance. Required to protect your workflows.
13
  - name: HF_TOKEN
14
- description: HuggingFace token with write access. Used for automatic backup of your workflows and credentials to a private dataset.
15
  ---
16
 
17
- <!-- Badges -->
18
- [![GitHub Stars](https://img.shields.io/github/stars/somratpro/hugging8n?style=flat-square)](https://github.com/somratpro/Hugging8N)
19
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
20
- [![HF Space](https://img.shields.io/badge/πŸ€—%20HuggingFace-Space-blue?style=flat-square)](https://huggingface.co/spaces/somratpro/Hugging8n)
21
- [![n8n](https://img.shields.io/badge/n8n-workflow%20automation-orange?style=flat-square)](https://n8n.io)
22
 
23
- **Self-hosted n8n workflow automation β€” free, no server needed.** Hugging8n runs [n8n](https://n8n.io) on HuggingFace Spaces Docker, giving you a full-featured workflow automation platform with 400+ integrations. Your workflows, credentials, and settings are automatically backed up to a private HuggingFace Dataset so nothing is lost on restart.
24
-
25
- ## Table of Contents
26
-
27
- - [✨ Features](#-features)
28
- - [πŸš€ Quick Start](#-quick-start)
29
- - [πŸ” Authentication](#-authentication)
30
- - [πŸ’Ύ Persistent Backup](#-persistent-backup)
31
- - [πŸ’“ Staying Alive](#-staying-alive)
32
- - [πŸ—οΈ Architecture](#-architecture)
33
- - [πŸ’» Local Development](#-local-development)
34
- - [πŸ› Troubleshooting](#-troubleshooting)
35
- - [🀝 Contributing](#-contributing)
36
- - [πŸ“„ License](#-license)
37
 
38
  ## ✨ Features
39
 
40
- - ⚑ **Zero Config:** Duplicate this Space, set your credentials, and n8n is running in minutes.
41
- - πŸ”Œ **400+ Integrations:** Connect any service β€” HTTP, webhooks, databases, Slack, Gmail, GitHub, and more.
42
- - πŸ’Ύ **Persistent Backup:** Workflows, credentials, and the SQLite database back up automatically to a private HF Dataset. Restored on every startup so nothing is lost.
43
- - πŸ” **Basic Auth:** n8n is protected by basic auth out of the box β€” just set your username and password.
44
- - 🐳 **Docker Native:** Runs on the free HF Spaces tier (2 vCPU, 16GB RAM) with SQLite β€” no external database needed.
45
- - ⏰ **External Keep-Alive:** Optional UptimeRobot integration to prevent free Space sleep.
46
- - 🌐 **Health Endpoint:** `/health` returns service and sync status for monitoring.
47
- - 🏠 **100% HF-Native:** Runs entirely on HuggingFace's free infrastructure.
48
 
49
  ## πŸš€ Quick Start
50
 
@@ -52,173 +31,28 @@ secrets:
52
 
53
  [![Duplicate this Space](https://huggingface.co/datasets/huggingface/badges/resolve/main/duplicate-this-space-xl.svg)](https://huggingface.co/spaces/somratpro/Hugging8n?duplicate=true)
54
 
55
- Click the button above to duplicate the template into your own account.
56
-
57
- ### Step 2: Add Your Secrets
58
-
59
- Navigate to your new Space's **Settings**, scroll to the **Variables and secrets** section, and add the following under **Secrets**:
60
-
61
- #### Required β€” Authentication
62
-
63
- | Secret | Description |
64
- | :--- | :--- |
65
- | `N8N_BASIC_AUTH_PASSWORD` | Password to log in to your n8n instance. Set this to protect your workflows. |
66
-
67
- #### Required β€” Persistent Backup *(Highly Recommended)*
68
-
69
- | Secret | Description |
70
- | :--- | :--- |
71
- | `HF_TOKEN` | HuggingFace token with write access. Get one at [hf.co/settings/tokens](https://huggingface.co/settings/tokens). |
72
-
73
- > [!TIP]
74
- > Without `HF_TOKEN`, n8n will still run, but workflows and credentials will be **lost every time the Space restarts**. It is strongly recommended to set this.
75
-
76
- #### Optional β€” Configuration
77
 
78
- | Variable / Secret | Default | Description |
79
- | :--- | :--- | :--- |
80
- | `N8N_BASIC_AUTH_USER` | `admin` | Username for n8n login |
81
- | `HF_USERNAME` | *(auto-detected)* | Your HF username, for naming the backup dataset |
82
- | `BACKUP_DATASET_NAME` | `hugging8n-backup` | Name of the private dataset repo for backup |
83
- | `SYNC_INTERVAL` | `180` | How often to back up, in seconds |
84
- | `GENERIC_TIMEZONE` | `UTC` | Timezone for schedule triggers (e.g. `Asia/Dhaka`) |
85
- | `N8N_ENCRYPTION_KEY` | *(auto-generated)* | Encryption key for stored credentials. Set explicitly so it survives Space rebuilds. |
86
 
87
- #### Build-Time Variable (add as Variable, not Secret)
88
 
89
- | Variable | Default | Description |
90
- | :--- | :--- | :--- |
91
- | `N8N_VERSION` | `latest` | Pin a specific n8n version (e.g. `1.90.0`) for reproducibility |
92
-
93
- > [!IMPORTANT]
94
- > On HuggingFace Spaces, `N8N_VERSION` must be added as a **Variable** (not a Secret) so it is passed as a Docker build arg during image build.
95
-
96
- ### Step 3: Deploy & Run
97
-
98
- That's it! The Space will build and start automatically. Watch progress in the **Logs** tab. First build takes a few minutes as n8n installs.
99
-
100
- ### Step 4: Log In
101
-
102
- Once the Space is running, open it and log in with:
103
- - **Username:** the value of `N8N_BASIC_AUTH_USER` (default: `admin`)
104
- - **Password:** the value of `N8N_BASIC_AUTH_PASSWORD`
105
-
106
- > [!WARNING]
107
- > If you did not set `N8N_BASIC_AUTH_PASSWORD`, your n8n instance is **unprotected**. Anyone with the Space URL can access your workflows and credentials. Set this secret immediately.
108
 
109
  ## πŸ” Authentication
110
 
111
- Hugging8n uses n8n's built-in basic auth to protect your instance. It is enabled by default.
112
-
113
- | Variable | Default | Description |
114
- | :--- | :--- | :--- |
115
- | `N8N_BASIC_AUTH_ACTIVE` | `true` | Set to `false` to disable basic auth (not recommended) |
116
- | `N8N_BASIC_AUTH_USER` | `admin` | Login username |
117
- | `N8N_BASIC_AUTH_PASSWORD` | *(none)* | Login password β€” **must be set** |
118
 
119
  ## πŸ’Ύ Persistent Backup
120
 
121
- Hugging8n automatically backs up your entire n8n data directory (`/home/node/.n8n`) to a **private** HuggingFace Dataset.
122
-
123
- **What is backed up:**
124
- - All workflows
125
- - All credentials (encrypted)
126
- - SQLite database (hot-copy via `sqlite3 .backup`)
127
- - n8n encryption key
128
- - User data
129
-
130
- **How it works:**
131
- 1. On startup: restore from dataset (if it exists)
132
- 2. Every `SYNC_INTERVAL` seconds: detect changes and upload
133
- 3. On shutdown (`SIGTERM`): run a final backup before exiting
134
-
135
- | Variable | Default | Description |
136
- | :--- | :--- | :--- |
137
- | `HF_TOKEN` | β€” | HF write token |
138
- | `HF_USERNAME` | *(auto)* | Dataset owner username |
139
- | `BACKUP_DATASET_NAME` | `hugging8n-backup` | Dataset repo name |
140
- | `SYNC_INTERVAL` | `180` | Backup interval in seconds |
141
-
142
- > [!TIP]
143
- > Set `N8N_ENCRYPTION_KEY` explicitly. If n8n auto-generates it and the Space is rebuilt (not just restarted), the key will be different and your backed-up credentials will be unreadable.
144
-
145
- ## πŸ’“ Staying Alive *(Recommended on Free HF Spaces)*
146
-
147
- Free HF Spaces sleep after periods of inactivity. Set up an external UptimeRobot monitor to keep yours awake.
148
-
149
- 1. Create a free account at [uptimerobot.com](https://uptimerobot.com)
150
- 2. Get your **Main API Key** from My Settings β†’ API Settings
151
- 3. Run the helper script:
152
- ```bash
153
- UPTIMEROBOT_API_KEY=your-key ./setup-uptimerobot.sh your-space.hf.space
154
- ```
155
- 4. UptimeRobot will ping `/health` every 5 minutes from outside HF, keeping the Space awake.
156
-
157
- > [!NOTE]
158
- > This works for **public** Spaces only. Private Spaces cannot be pinged by external monitors.
159
 
160
  ## πŸ—οΈ Architecture
161
 
162
- ```
163
- Hugging8n/
164
- β”œβ”€β”€ Dockerfile # Builds on node:22-slim, installs n8n + Python sync
165
- β”œβ”€β”€ start.sh # Startup orchestrator: restore β†’ sync loop β†’ proxy β†’ n8n
166
- β”œβ”€β”€ n8n-sync.py # Backup/restore via huggingface_hub
167
- β”œβ”€β”€ health-server.js # HTTP + WebSocket reverse proxy (port 7861 β†’ 5678)
168
- β”œβ”€β”€ setup-uptimerobot.sh # One-shot UptimeRobot monitor creation
169
- β”œβ”€β”€ .env.example # All environment variable documentation
170
- └── README.md # This file
171
- ```
172
-
173
- **Startup sequence:**
174
- 1. Read environment variables and set n8n config
175
- 2. Warn if `N8N_BASIC_AUTH_PASSWORD` is not set
176
- 3. Restore backup from HF Dataset (if `HF_TOKEN` is set)
177
- 4. Start backup sync loop in background
178
- 5. Start health/proxy server on port 7861
179
- 6. Start n8n on port 5678
180
- 7. On `SIGTERM` / `SIGINT`: final backup + clean exit
181
-
182
- ## πŸ’» Local Development
183
-
184
- ```bash
185
- git clone https://github.com/somratpro/Hugging8N.git
186
- cd Hugging8N
187
- cp .env.example .env
188
- # Fill in N8N_BASIC_AUTH_PASSWORD and optionally HF_TOKEN
189
- ```
190
-
191
- **With Docker:**
192
-
193
- ```bash
194
- docker build -t hugging8n .
195
- docker run -p 7861:7861 --env-file .env hugging8n
196
- ```
197
-
198
- Then open `http://localhost:7861`.
199
-
200
- **Pin an n8n version:**
201
-
202
- ```bash
203
- docker build --build-arg N8N_VERSION=1.90.0 -t hugging8n .
204
- ```
205
-
206
- ## πŸ› Troubleshooting
207
-
208
- - **Can't log in:** Make sure `N8N_BASIC_AUTH_PASSWORD` is set in Secrets. Default username is `admin`.
209
- - **Workflows lost after restart:** Set `HF_TOKEN` and `HF_USERNAME` so the backup dataset is created and restored.
210
- - **n8n editor shows "disconnected":** The WebSocket proxy may not have connected yet β€” wait a few seconds and refresh. Check Space logs for errors.
211
- - **Space keeps sleeping:** Use `setup-uptimerobot.sh` to set up an external keep-alive monitor.
212
- - **Credentials unreadable after rebuild:** Make sure `N8N_ENCRYPTION_KEY` is set explicitly as a Secret. If it was auto-generated, it changes on rebuild.
213
- - **Build fails:** If you pinned `N8N_VERSION`, verify the version exists at [npmjs.com/package/n8n](https://www.npmjs.com/package/n8n?activeTab=versions).
214
- - **Backup failing:** Check Space logs for `Sync failed`. Verify `HF_TOKEN` has write access and `HF_USERNAME` is correct.
215
-
216
- ## 🀝 Contributing
217
-
218
- Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
219
-
220
- ## πŸ“„ License
221
-
222
- MIT β€” see [LICENSE](LICENSE) for details.
223
 
224
  *Made with ❀️ by [@somratpro](https://github.com/somratpro)*
 
8
  pinned: true
9
  license: mit
10
  secrets:
 
 
11
  - name: HF_TOKEN
12
+ description: HuggingFace token with write access. Used for automatic backup.
13
  ---
14
 
15
+ # ♾️ Hugging8n
 
 
 
 
16
 
17
+ **Self-hosted n8n workflow automation β€” free, no server needed.** Hugging8n runs [n8n](https://n8n.io) on HuggingFace Spaces Docker, serving a premium dashboard at `/` and the n8n editor at `/app/`.
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  ## ✨ Features
20
 
21
+ - ⚑ **Zero Config:** Duplicate this Space, set `HF_TOKEN`, and you're ready.
22
+ - πŸ’Ύ **Persistent Backup:** Workflows and credentials back up automatically to a private HF Dataset.
23
+ - πŸ” **Secure by Default:** Uses n8n v2's built-in user management. No more insecure environment variables.
24
+ - 🐳 **Docker Native:** Optimized for the free HF Spaces tier.
25
+ - 🌐 **Dashboard UI:** Beautiful management interface at the root URL.
26
+ - ⏰ **Built-in Keep-Alive:** Easily setup UptimeRobot from the dashboard.
 
 
27
 
28
  ## πŸš€ Quick Start
29
 
 
31
 
32
  [![Duplicate this Space](https://huggingface.co/datasets/huggingface/badges/resolve/main/duplicate-this-space-xl.svg)](https://huggingface.co/spaces/somratpro/Hugging8n?duplicate=true)
33
 
34
+ ### Step 2: Add Your HF_TOKEN
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ Add your HuggingFace token with **write** access to the Space Secrets. This enables automatic backup so your workflows aren't lost on restart.
 
 
 
 
 
 
 
37
 
38
+ ### Step 3: Set Up Auth
39
 
40
+ When the Space starts, visit the URL and click **Open n8n Editor**. On the first run, n8n will ask you to create an owner account. **This is your primary login.**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  ## πŸ” Authentication
43
 
44
+ Hugging8n uses n8n's native user management.
45
+ 1. The first person to visit `/app/` on a fresh install becomes the owner.
46
+ 2. If you are restoring from a backup, your existing user accounts will be active.
 
 
 
 
47
 
48
  ## πŸ’Ύ Persistent Backup
49
 
50
+ Your data is synced to a private dataset named `hugging8n-backup` in your HF account.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  ## πŸ—οΈ Architecture
53
 
54
+ - `/` : Premium Dashboard (Status, Uptime, Keep-Alive setup)
55
+ - `/app/` : n8n Workflow Editor
56
+ - `/health` : Health check endpoint
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  *Made with ❀️ by [@somratpro](https://github.com/somratpro)*
health-server.js CHANGED
@@ -1,109 +1,266 @@
1
  const http = require("http");
2
- const net = require("net");
3
  const fs = require("fs");
 
4
 
5
- const PUBLIC_PORT = Number(process.env.PUBLIC_PORT || 7861);
6
  const TARGET_PORT = Number(process.env.N8N_PORT || 5678);
7
  const TARGET_HOST = "127.0.0.1";
8
- const STATUS_FILE = "/tmp/hugging8n-sync-status.json";
 
 
9
 
10
- function getStatus() {
11
  try {
12
- return JSON.parse(fs.readFileSync(STATUS_FILE, "utf8"));
13
  } catch {
14
- return {
15
- status: "unknown",
16
- message: "No sync status yet",
17
- timestamp: new Date().toISOString(),
18
- };
19
  }
20
  }
21
 
22
- function writeJson(res, statusCode, payload) {
23
- res.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
24
- res.end(JSON.stringify(payload));
 
 
 
 
25
  }
26
 
27
- function buildHeaders(req) {
28
- return {
29
- ...req.headers,
30
- host: req.headers.host || "",
31
- "x-forwarded-for": req.socket.remoteAddress || "",
32
- "x-forwarded-host": req.headers.host || "",
33
- "x-forwarded-proto": req.headers["x-forwarded-proto"] || "https",
34
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
- const server = http.createServer((req, res) => {
38
- if (req.url === "/health") {
39
- return writeJson(res, 200, {
40
- ok: true,
41
- service: "hugging8n",
42
- n8nPort: TARGET_PORT,
43
- ...getStatus(),
 
 
 
 
 
 
44
  });
 
 
 
 
 
 
 
 
 
 
45
  }
46
 
47
- if (req.url === "/status") {
48
- return writeJson(res, 200, getStatus());
 
 
 
 
49
  }
50
 
51
- const upstream = http.request(
52
- {
53
- hostname: TARGET_HOST,
54
- port: TARGET_PORT,
55
- path: req.url,
56
- method: req.method,
57
- headers: buildHeaders(req),
58
- },
59
- (upstreamRes) => {
60
- res.writeHead(upstreamRes.statusCode || 502, upstreamRes.headers);
61
- upstreamRes.pipe(res);
62
- },
63
- );
64
-
65
- upstream.on("error", (error) => {
66
- writeJson(res, 502, {
67
- ok: false,
68
- error: "upstream_unavailable",
69
- detail: error.message,
 
 
 
 
 
70
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  });
72
 
73
- req.pipe(upstream);
74
  });
75
 
76
  server.on("upgrade", (req, socket, head) => {
77
- const upstream = net.connect(TARGET_PORT, TARGET_HOST, () => {
78
- const headers = {
79
- ...req.headers,
80
- host: `${TARGET_HOST}:${TARGET_PORT}`,
81
- "x-forwarded-for": req.socket.remoteAddress || "",
82
- "x-forwarded-host": req.headers.host || "",
83
- "x-forwarded-proto": req.headers["x-forwarded-proto"] || "https",
84
- upgrade: req.headers.upgrade || "websocket",
85
- connection: "Upgrade",
86
- };
87
-
88
- const headerLines = Object.entries(headers)
89
- .map(([key, value]) => `${key}: ${value}`)
90
- .join("\r\n");
91
-
92
- upstream.write(
93
- `${req.method} ${req.url} HTTP/${req.httpVersion}\r\n` +
94
- headerLines +
95
- "\r\n\r\n",
96
- );
97
-
98
- if (head && head.length) upstream.write(head);
99
- upstream.pipe(socket);
100
- socket.pipe(upstream);
101
  });
102
-
103
- upstream.on("error", () => socket.destroy());
104
- socket.on("error", () => upstream.destroy());
105
  });
106
 
107
- server.listen(PUBLIC_PORT, "0.0.0.0", () => {
108
- console.log(`Hugging8n proxy listening on ${PUBLIC_PORT}, forwarding to ${TARGET_PORT}`);
109
- });
 
1
  const http = require("http");
2
+ const https = require("https");
3
  const fs = require("fs");
4
+ const net = require("net");
5
 
6
+ const PORT = Number(process.env.PUBLIC_PORT || 7861);
7
  const TARGET_PORT = Number(process.env.N8N_PORT || 5678);
8
  const TARGET_HOST = "127.0.0.1";
9
+ const SYNC_STATUS_FILE = "/tmp/hugging8n-sync-status.json";
10
+ const APP_BASE = "/app";
11
+ const startTime = Date.now();
12
 
13
+ function parseRequestUrl(url) {
14
  try {
15
+ return new URL(url, "http://localhost");
16
  } catch {
17
+ return new URL("http://localhost/");
 
 
 
 
18
  }
19
  }
20
 
21
+ function getStatus() {
22
+ try {
23
+ if (fs.existsSync(SYNC_STATUS_FILE)) {
24
+ return JSON.parse(fs.readFileSync(SYNC_STATUS_FILE, "utf8"));
25
+ }
26
+ } catch {}
27
+ return { status: "unknown", message: "Initial startup...", timestamp: new Date().toISOString() };
28
  }
29
 
30
+ function renderDashboard(data) {
31
+ const syncBadge = (status) => {
32
+ let cls = "status-offline";
33
+ if (status === "success" || status === "configured" || status === "restored") cls = "status-online";
34
+ if (status === "syncing" || status === "restoring") cls = "status-syncing";
35
+ return `<div class="status-badge ${cls}">${cls === 'status-online' ? '<div class="pulse"></div>' : ''}${String(status).toUpperCase()}</div>`;
 
36
  };
37
+
38
+ const keepAwakeHtml = data.isPrivate
39
+ ? `<div class="helper-summary"><strong>Private Space detected.</strong> External monitors cannot access private health URLs.</div>`
40
+ : `
41
+ <div id="uptimerobot-flow">
42
+ <div class="helper-summary" id="uptimerobot-summary">Setup a free monitor to prevent this Space from sleeping.</div>
43
+ <button id="uptimerobot-toggle" class="helper-toggle">Set Up Monitor</button>
44
+ <div id="uptimerobot-shell" class="hidden" style="margin-top:12px">
45
+ <input id="uptimerobot-key" class="helper-input" type="password" placeholder="UptimeRobot Main API Key">
46
+ <button id="uptimerobot-btn" class="helper-button">Create Monitor</button>
47
+ </div>
48
+ </div>
49
+ `;
50
+
51
+ return `
52
+ <!DOCTYPE html>
53
+ <html lang="en">
54
+ <head>
55
+ <meta charset="UTF-8">
56
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
57
+ <title>Hugging8n Dashboard</title>
58
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
59
+ <style>
60
+ :root { --bg: #0f172a; --card: rgba(30, 41, 59, 0.7); --accent: linear-gradient(135deg, #3b82f6, #8b5cf6); --text: #f8fafc; --text-dim: #94a3b8; --success: #10b981; --error: #ef4444; }
61
+ * { box-sizing: border-box; margin: 0; padding: 0; }
62
+ body { font-family: 'Outfit', sans-serif; background: var(--bg); color: var(--text); display: flex; justify-content: center; min-height: 100vh; padding: 40px 20px; background-image: radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%), radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.15) 0px, transparent 50%); }
63
+ .dashboard { width: 100%; max-width: 600px; background: var(--card); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 24px; padding: 40px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); }
64
+ h1 { font-size: 2.2rem; margin-bottom: 8px; background: var(--accent); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-align: center; }
65
+ .subtitle { color: var(--text-dim); text-align: center; font-size: 0.9rem; margin-bottom: 40px; text-transform: uppercase; letter-spacing: 1px; }
66
+ .stats { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px; }
67
+ .card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05); padding: 20px; border-radius: 16px; }
68
+ .label { color: var(--text-dim); font-size: 0.75rem; text-transform: uppercase; margin-bottom: 8px; display: block; }
69
+ .value { font-size: 1.1rem; font-weight: 600; }
70
+ .btn { display: block; background: var(--accent); color: #fff; padding: 16px; border-radius: 16px; text-align: center; text-decoration: none; font-weight: 600; margin-top: 8px; transition: transform 0.2s; box-shadow: 0 10px 20px -5px rgba(59, 130, 246, 0.4); }
71
+ .btn:hover { transform: scale(1.02); }
72
+ .status-badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 600; }
73
+ .status-online { background: rgba(16, 185, 129, 0.1); color: var(--success); }
74
+ .status-syncing { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
75
+ .status-offline { background: rgba(239, 68, 68, 0.1); color: var(--error); }
76
+ .pulse { width: 8px; height: 8px; border-radius: 50%; background: currentColor; animation: pulse 2s infinite; }
77
+ @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); } 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } }
78
+ .helper-input { width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: #fff; padding: 12px; border-radius: 12px; margin-bottom: 12px; }
79
+ .helper-button { background: var(--accent); color: #fff; border: 0; padding: 12px; border-radius: 12px; cursor: pointer; width: 100%; font-weight: 600; }
80
+ .helper-toggle { background: rgba(255,255,255,0.05); color: #fff; border: 1px solid rgba(255,255,255,0.1); padding: 8px 16px; border-radius: 12px; cursor: pointer; font-size: 0.85rem; }
81
+ .hidden { display: none; }
82
+ .helper-summary { background: rgba(255,255,255,0.03); padding: 12px; border-radius: 12px; font-size: 0.85rem; color: var(--text-dim); margin-bottom: 12px; }
83
+ .helper-result { margin-top: 12px; font-size: 0.85rem; padding: 10px; border-radius: 8px; display: none; }
84
+ .helper-result.ok { display: block; background: rgba(16, 185, 129, 0.1); color: var(--success); }
85
+ .helper-result.error { display: block; background: rgba(239, 68, 68, 0.1); color: var(--error); }
86
+ </style>
87
+ </head>
88
+ <body>
89
+ <div class="dashboard">
90
+ <h1>♾️ Hugging8n</h1>
91
+ <p class="subtitle">Workflow Automation Space</p>
92
+
93
+ <div class="stats">
94
+ <div class="card"><span class="label">Uptime</span><span class="value" id="uptime">${data.uptimeHuman}</span></div>
95
+ <div class="card"><span class="label">n8n Port</span><span class="value">${TARGET_PORT}</span></div>
96
+ </div>
97
+
98
+ <div class="card" style="margin-bottom:16px">
99
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px">
100
+ <span class="label" style="margin-bottom:0">Sync Status</span>
101
+ <div id="sync-badge">${syncBadge(data.sync.status)}</div>
102
+ </div>
103
+ <div style="font-size:0.85rem; color:var(--text-dim)">
104
+ Last Activity: <span id="sync-time" style="color:var(--text)">${data.sync.timestamp}</span>
105
+ <div id="sync-msg" style="margin-top:4px">${data.sync.message}</div>
106
+ </div>
107
+ </div>
108
+
109
+ <a href="/app/" class="btn">Open n8n Editor</a>
110
+
111
+ <div class="card" style="margin-top:24px">
112
+ <span class="label">Keep Alive</span>
113
+ ${keepAwakeHtml}
114
+ <div id="uptimerobot-result" class="helper-result"></div>
115
+ </div>
116
+ </div>
117
+
118
+ <script>
119
+ async function refresh() {
120
+ try {
121
+ const res = await fetch('/status' + window.location.search);
122
+ const d = await res.json();
123
+ document.getElementById('uptime').textContent = d.uptime;
124
+ document.getElementById('sync-time').textContent = d.sync.timestamp;
125
+ document.getElementById('sync-msg').textContent = d.sync.message;
126
+
127
+ const s = d.sync.status;
128
+ let cls = "status-offline";
129
+ if (s === "success" || s === "configured" || s === "restored") cls = "status-online";
130
+ if (s === "syncing" || s === "restoring") cls = "status-syncing";
131
+ document.getElementById('sync-badge').innerHTML = '<div class="status-badge ' + cls + '">' + (cls === 'status-online' ? '<div class="pulse"></div>' : '') + s.toUpperCase() + '</div>';
132
+ } catch (e) {}
133
+ }
134
+ setInterval(refresh, 5000);
135
+
136
+ const toggle = document.getElementById('uptimerobot-toggle');
137
+ if (toggle) {
138
+ toggle.onclick = () => document.getElementById('uptimerobot-shell').classList.toggle('hidden');
139
+ document.getElementById('uptimerobot-btn').onclick = async () => {
140
+ const key = document.getElementById('uptimerobot-key').value;
141
+ const res = document.getElementById('uptimerobot-result');
142
+ if (!key) return;
143
+ res.className = 'helper-result'; res.textContent = 'Creating...'; res.style.display = 'block';
144
+ try {
145
+ const r = await fetch('/uptimerobot/setup' + window.location.search, {
146
+ method: 'POST',
147
+ headers: { 'Content-Type': 'application/json' },
148
+ body: JSON.stringify({ apiKey: key })
149
+ });
150
+ const data = await r.json();
151
+ res.className = 'helper-result ' + (r.ok ? 'ok' : 'error');
152
+ res.textContent = data.message;
153
+ } catch (e) { res.className = 'helper-result error'; res.textContent = 'Connection failed'; }
154
+ };
155
+ }
156
+ </script>
157
+ </body>
158
+ </html>`;
159
  }
160
 
161
+ async function resolveSpaceIsPrivate(req) {
162
+ const params = new URLSearchParams(parseRequestUrl(req.url).search);
163
+ const token = params.get("__sign");
164
+ if (!token) return false;
165
+ try {
166
+ const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
167
+ const sub = payload.sub || "";
168
+ const match = sub.match(/^\/spaces\/([^/]+)\/([^/]+)$/);
169
+ if (!match) return false;
170
+ return new Promise((resolve) => {
171
+ https.get(`https://huggingface.co/api/spaces/${match[1]}/${match[2]}`, { headers: { "User-Agent": "Hugging8n" } }, (res) => {
172
+ resolve(res.statusCode === 401 || res.statusCode === 403 || res.statusCode === 404);
173
+ }).on("error", () => resolve(false));
174
  });
175
+ } catch { return false; }
176
+ }
177
+
178
+ const server = http.createServer(async (req, res) => {
179
+ const url = parseRequestUrl(req.url);
180
+ const pathname = url.pathname;
181
+
182
+ if (pathname === "/health") {
183
+ res.writeHead(200, { "Content-Type": "application/json" });
184
+ return res.end(JSON.stringify({ status: "ok", ...getStatus() }));
185
  }
186
 
187
+ if (pathname === "/status") {
188
+ const uptime = Math.floor((Date.now() - startTime) / 1000);
189
+ return res.end(JSON.stringify({
190
+ uptime: `${Math.floor(uptime/3600)}h ${Math.floor((uptime%3600)/60)}m`,
191
+ sync: getStatus()
192
+ }));
193
  }
194
 
195
+ if (pathname === "/uptimerobot/setup" && req.method === "POST") {
196
+ let body = "";
197
+ req.on("data", c => body += c);
198
+ req.on("end", async () => {
199
+ try {
200
+ const { apiKey } = JSON.parse(body);
201
+ const host = req.headers.host;
202
+ const monitorUrl = `https://${host}/health`;
203
+ const post = (p, d) => new Promise((res, rej) => {
204
+ const r = https.request({ hostname: "api.uptimerobot.com", port: 443, path: p, method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" } }, r => {
205
+ let b = ""; r.on("data", c => b += c); r.on("end", () => res(JSON.parse(b)));
206
+ }); r.on("error", rej); r.write(new URLSearchParams(d).toString()); r.end();
207
+ });
208
+
209
+ const existing = await post("/v2/getMonitors", { api_key: apiKey, format: "json" });
210
+ if (existing.monitors?.some(m => m.url === monitorUrl)) {
211
+ res.writeHead(200); return res.end(JSON.stringify({ message: "Monitor already exists." }));
212
+ }
213
+ const created = await post("/v2/newMonitor", { api_key: apiKey, format: "json", type: "1", friendly_name: `Hugging8n ${host}`, url: monitorUrl, interval: "300" });
214
+ if (created.stat === "ok") {
215
+ res.writeHead(200); return res.end(JSON.stringify({ message: "Monitor created successfully!" }));
216
+ }
217
+ res.writeHead(400); res.end(JSON.stringify({ message: created.error?.message || "Failed to create monitor." }));
218
+ } catch (e) { res.writeHead(400); res.end(JSON.stringify({ message: "Invalid request." })); }
219
  });
220
+ return;
221
+ }
222
+
223
+ if (pathname === "/" || pathname === "/dashboard") {
224
+ const uptime = Math.floor((Date.now() - startTime) / 1000);
225
+ const isPrivate = await resolveSpaceIsPrivate(req);
226
+ res.writeHead(200, { "Content-Type": "text/html" });
227
+ return res.end(renderDashboard({
228
+ uptimeHuman: `${Math.floor(uptime/3600)}h ${Math.floor((uptime%3600)/60)}m`,
229
+ sync: getStatus(),
230
+ isPrivate
231
+ }));
232
+ }
233
+
234
+ // Proxy to n8n
235
+ const proxyPath = pathname.startsWith(APP_BASE) ? pathname.slice(APP_BASE.length) || "/" : pathname;
236
+ const proxyHeaders = { ...req.headers, host: `127.0.0.1:${TARGET_PORT}`, "x-forwarded-for": req.socket.remoteAddress, "x-forwarded-proto": "https" };
237
+
238
+ const proxyReq = http.request({ hostname: TARGET_HOST, port: TARGET_PORT, path: proxyPath + url.search, method: req.method, headers: proxyHeaders }, (proxyRes) => {
239
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
240
+ proxyRes.pipe(res);
241
+ });
242
+
243
+ proxyReq.on("error", () => {
244
+ res.writeHead(503, { "Content-Type": "application/json" });
245
+ res.end(JSON.stringify({ status: "starting", message: "n8n is initializing, please wait..." }));
246
  });
247
 
248
+ req.pipe(proxyReq);
249
  });
250
 
251
  server.on("upgrade", (req, socket, head) => {
252
+ const url = parseRequestUrl(req.url);
253
+ const proxyPath = url.pathname.startsWith(APP_BASE) ? url.pathname.slice(APP_BASE.length) || "/" : url.pathname;
254
+ const proxySocket = net.connect(TARGET_PORT, TARGET_HOST, () => {
255
+ proxySocket.write(`${req.method} ${proxyPath}${url.search} HTTP/${req.httpVersion}\r\n`);
256
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
257
+ proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`);
258
+ }
259
+ proxySocket.write("\r\n");
260
+ if (head && head.length) proxySocket.write(head);
261
+ proxySocket.pipe(socket).pipe(proxySocket);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  });
263
+ proxySocket.on("error", () => socket.destroy());
 
 
264
  });
265
 
266
+ server.listen(PORT, "0.0.0.0", () => console.log(`Dashboard/Proxy on ${PORT} -> n8n on ${TARGET_PORT}`));
 
 
n8n-sync.py CHANGED
@@ -9,6 +9,7 @@ import subprocess
9
  import sys
10
  import tempfile
11
  import time
 
12
  from pathlib import Path
13
 
14
  os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
@@ -26,7 +27,7 @@ HF_USERNAME = (
26
  )
27
  BACKUP_DATASET_NAME = os.environ.get("BACKUP_DATASET_NAME", "hugging8n-backup").strip()
28
  HF_API = HfApi(token=HF_TOKEN) if HF_TOKEN else None
29
- RUNNING = True
30
 
31
 
32
  def write_status(status: str, message: str) -> None:
@@ -187,8 +188,7 @@ def sync_once(last_fingerprint: str | None = None) -> str:
187
 
188
 
189
  def handle_signal(_sig, _frame) -> None:
190
- global RUNNING
191
- RUNNING = False
192
 
193
 
194
  def loop() -> int:
@@ -198,16 +198,15 @@ def loop() -> int:
198
  last_fingerprint = fingerprint_dir(N8N_HOME)
199
  write_status("configured", f"Backup loop active with {INTERVAL}s interval.")
200
 
201
- while RUNNING:
202
  try:
203
  last_fingerprint = sync_once(last_fingerprint)
204
  except Exception as exc:
205
  write_status("error", f"Sync failed: {exc}")
206
  print(f"Sync failed: {exc}", file=sys.stderr)
207
- for _ in range(INTERVAL):
208
- if not RUNNING:
209
- break
210
- time.sleep(1)
211
 
212
  try:
213
  sync_once(None)
 
9
  import sys
10
  import tempfile
11
  import time
12
+ import threading
13
  from pathlib import Path
14
 
15
  os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
 
27
  )
28
  BACKUP_DATASET_NAME = os.environ.get("BACKUP_DATASET_NAME", "hugging8n-backup").strip()
29
  HF_API = HfApi(token=HF_TOKEN) if HF_TOKEN else None
30
+ STOP_EVENT = threading.Event()
31
 
32
 
33
  def write_status(status: str, message: str) -> None:
 
188
 
189
 
190
  def handle_signal(_sig, _frame) -> None:
191
+ STOP_EVENT.set()
 
192
 
193
 
194
  def loop() -> int:
 
198
  last_fingerprint = fingerprint_dir(N8N_HOME)
199
  write_status("configured", f"Backup loop active with {INTERVAL}s interval.")
200
 
201
+ while not STOP_EVENT.is_set():
202
  try:
203
  last_fingerprint = sync_once(last_fingerprint)
204
  except Exception as exc:
205
  write_status("error", f"Sync failed: {exc}")
206
  print(f"Sync failed: {exc}", file=sys.stderr)
207
+
208
+ if STOP_EVENT.wait(INTERVAL):
209
+ break
 
210
 
211
  try:
212
  sync_once(None)
start.sh CHANGED
@@ -1,6 +1,9 @@
1
  #!/bin/bash
2
  set -euo pipefail
3
 
 
 
 
4
  APP_DIR="/home/node/app"
5
  N8N_HOME="/home/node/.n8n"
6
  N8N_PORT="${N8N_PORT:-5678}"
@@ -12,16 +15,15 @@ mkdir -p "$N8N_HOME"
12
  SPACE_HOST_DETECTED="${SPACE_HOST_OVERRIDE:-${SPACE_HOST:-}}"
13
  if [ -n "$SPACE_HOST_DETECTED" ]; then
14
  export N8N_HOST="${N8N_HOST:-$SPACE_HOST_DETECTED}"
15
- export WEBHOOK_URL="${WEBHOOK_URL:-https://${SPACE_HOST_DETECTED}/}"
16
- export N8N_EDITOR_BASE_URL="${N8N_EDITOR_BASE_URL:-https://${SPACE_HOST_DETECTED}/}"
 
17
  fi
18
 
19
  export N8N_PORT
20
  export N8N_PROTOCOL="${N8N_PROTOCOL:-https}"
21
  export N8N_PROXY_HOPS="${N8N_PROXY_HOPS:-1}"
22
  export N8N_LISTEN_ADDRESS="${N8N_LISTEN_ADDRESS:-0.0.0.0}"
23
- # Must be false: HF Spaces terminates TLS at its edge; n8n sees plain HTTP internally.
24
- # Secure cookies require HTTPS end-to-end and will break login on HF Spaces.
25
  export N8N_SECURE_COOKIE="${N8N_SECURE_COOKIE:-false}"
26
  export N8N_DIAGNOSTICS_ENABLED="${N8N_DIAGNOSTICS_ENABLED:-false}"
27
  export N8N_PERSONALIZATION_ENABLED="${N8N_PERSONALIZATION_ENABLED:-false}"
@@ -30,16 +32,11 @@ export N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS="${N8N_ENFORCE_SETTINGS_FILE_PERMIS
30
  export GENERIC_TIMEZONE="${GENERIC_TIMEZONE:-${TZ:-UTC}}"
31
  export TZ="${TZ:-$GENERIC_TIMEZONE}"
32
 
33
- # Basic auth β€” enabled by default to protect your n8n instance
34
- export N8N_BASIC_AUTH_ACTIVE="${N8N_BASIC_AUTH_ACTIVE:-true}"
35
- if [ "${N8N_BASIC_AUTH_ACTIVE}" = "true" ]; then
36
- export N8N_BASIC_AUTH_USER="${N8N_BASIC_AUTH_USER:-admin}"
37
- export N8N_BASIC_AUTH_PASSWORD="${N8N_BASIC_AUTH_PASSWORD:-}"
38
- if [ -z "${N8N_BASIC_AUTH_PASSWORD:-}" ]; then
39
- echo "⚠️ WARNING: N8N_BASIC_AUTH_ACTIVE=true but N8N_BASIC_AUTH_PASSWORD is not set."
40
- echo " Your n8n instance is NOT protected. Set N8N_BASIC_AUTH_PASSWORD in Secrets."
41
- fi
42
- fi
43
 
44
  echo ""
45
  echo " ╔════════════════════════════════════╗"
@@ -61,15 +58,9 @@ fi
61
 
62
  cleanup() {
63
  echo "Stopping Hugging8n..."
64
- if [ -n "${SYNC_PID:-}" ] && kill -0 "$SYNC_PID" 2>/dev/null; then
65
- kill "$SYNC_PID" 2>/dev/null || true
66
- fi
67
- if [ -n "${N8N_PID:-}" ] && kill -0 "$N8N_PID" 2>/dev/null; then
68
- kill "$N8N_PID" 2>/dev/null || true
69
- fi
70
- if [ -n "${PROXY_PID:-}" ] && kill -0 "$PROXY_PID" 2>/dev/null; then
71
- kill "$PROXY_PID" 2>/dev/null || true
72
- fi
73
  if [ -n "${HF_TOKEN:-}" ]; then
74
  echo "Running final backup pass..."
75
  python3 "$APP_DIR/n8n-sync.py" sync-once || true
@@ -89,4 +80,11 @@ PROXY_PID=$!
89
  n8n start &
90
  N8N_PID=$!
91
 
 
 
 
 
 
 
 
92
  wait "$N8N_PID"
 
1
  #!/bin/bash
2
  set -euo pipefail
3
 
4
+ # Tighten default file permissions for any files created by this process
5
+ umask 0077
6
+
7
  APP_DIR="/home/node/app"
8
  N8N_HOME="/home/node/.n8n"
9
  N8N_PORT="${N8N_PORT:-5678}"
 
15
  SPACE_HOST_DETECTED="${SPACE_HOST_OVERRIDE:-${SPACE_HOST:-}}"
16
  if [ -n "$SPACE_HOST_DETECTED" ]; then
17
  export N8N_HOST="${N8N_HOST:-$SPACE_HOST_DETECTED}"
18
+ # Updated for /app base path
19
+ export WEBHOOK_URL="${WEBHOOK_URL:-https://${SPACE_HOST_DETECTED}/app/}"
20
+ export N8N_EDITOR_BASE_URL="${N8N_EDITOR_BASE_URL:-https://${SPACE_HOST_DETECTED}/app/}"
21
  fi
22
 
23
  export N8N_PORT
24
  export N8N_PROTOCOL="${N8N_PROTOCOL:-https}"
25
  export N8N_PROXY_HOPS="${N8N_PROXY_HOPS:-1}"
26
  export N8N_LISTEN_ADDRESS="${N8N_LISTEN_ADDRESS:-0.0.0.0}"
 
 
27
  export N8N_SECURE_COOKIE="${N8N_SECURE_COOKIE:-false}"
28
  export N8N_DIAGNOSTICS_ENABLED="${N8N_DIAGNOSTICS_ENABLED:-false}"
29
  export N8N_PERSONALIZATION_ENABLED="${N8N_PERSONALIZATION_ENABLED:-false}"
 
32
  export GENERIC_TIMEZONE="${GENERIC_TIMEZONE:-${TZ:-UTC}}"
33
  export TZ="${TZ:-$GENERIC_TIMEZONE}"
34
 
35
+ # Disable noisy or unnecessary services
36
+ export N8N_RUNNERS_ENABLED="${N8N_RUNNERS_ENABLED:-false}"
37
+ export N8N_LICENSE_AUTO_RENEW_ENABLED="${N8N_LICENSE_AUTO_RENEW_ENABLED:-false}"
38
+
39
+ # n8n v2 uses built-in user management.
 
 
 
 
 
40
 
41
  echo ""
42
  echo " ╔════════════════════════════════════╗"
 
58
 
59
  cleanup() {
60
  echo "Stopping Hugging8n..."
61
+ [ -n "${SYNC_PID:-}" ] && kill "$SYNC_PID" 2>/dev/null || true
62
+ [ -n "${N8N_PID:-}" ] && kill "$N8N_PID" 2>/dev/null || true
63
+ [ -n "${PROXY_PID:-}" ] && kill "$PROXY_PID" 2>/dev/null || true
 
 
 
 
 
 
64
  if [ -n "${HF_TOKEN:-}" ]; then
65
  echo "Running final backup pass..."
66
  python3 "$APP_DIR/n8n-sync.py" sync-once || true
 
80
  n8n start &
81
  N8N_PID=$!
82
 
83
+ # Readiness probe
84
+ echo "Waiting for n8n to be ready on port ${N8N_PORT}..."
85
+ until curl -sf "http://127.0.0.1:${N8N_PORT}/healthz" > /dev/null 2>&1; do
86
+ sleep 1
87
+ done
88
+ echo "n8n is ready!"
89
+
90
  wait "$N8N_PID"