實戰教學 · Infrastructure

用一台 Azure Ubuntu VM,
快速架起 Dify + n8n + HTTPS

從零開始,用 Docker、Caddy 搞定 HTTPS,
建立一套可以從 Internet 存取,適合教學使用的 AI Workflow 環境。

因為最近要上三天的 Dify + n8n 課程,學員大約二十多位,所以我需要先準備一台可以讓大家從外部連線的教學環境。

實際走過一次之後,我最後採用的做法其實比想像中簡單:

一台 Azure Ubuntu VM + Docker + Dify + n8n + Caddy。

這篇把整個過程完整整理一次,目標不是做 Production 級架構,而是讓稍微有 IT 基礎的人,也可以照著完成一套可以上課、可以從 Internet 存取,而且有 HTTPS 的 Dify + n8n 環境。

最後的架構長這樣

整體架構:Caddy Reverse Proxy
Internet
HTTPS :443 ↓
Caddy
↓
https://dify.example.com
→ localhost:8080 → Dify
https://n8n.example.com
→ localhost:5678 → n8n

全部都跑在同一台 Azure Ubuntu VM 裡。對外只需要開 22(SSH)、80(HTTP)、443(HTTPS),其他像 5678、8080、5432、6379 都不需要直接暴露到 Internet。

為什麼我選一台 Ubuntu VM?

一開始我也有想過拆成 VM 1 → Dify、VM 2 → n8n。但這次是三天課程,而且只有二十多位學員。拆成兩台 VM 的確會有比較好的資源隔離,但同時也代表兩組 Docker、兩組 NSG、兩套管理、兩套 HTTPS、兩套問題排除。對教學環境來說,有點太重了。

所以最後我直接用一台:

Azure VM+ Ubuntu Server 24.04 LTS+ 8 vCPU+ 32GB RAM+ 128GB Disk

LLM 本身不會跑在這台 VM 上,Dify / n8n 只是透過 API 呼叫 Azure OpenAI、OpenAI、Gemini、Claude 等模型。VM 真正負責的是 Web UI、Workflow、PostgreSQL、Redis、Vector DB、n8n execution、Dify Worker、文件處理、Webhook,而不是 LLM inference。對十多人的課程環境來說,8 vCPU / 32GB RAM 是一個滿舒服的規格。

為什麼不用 Windows VM?

我原本比較熟 Windows,但這次最後還是選 Ubuntu。原因其實很簡單:如果用 Windows,最後大概還是 Windows → Docker → Linux Container → Dify / n8n。但 Ubuntu 是 Ubuntu → Docker → Dify / n8n,少一層。

Dify、n8n、Docker、Caddy 相關文件和範例,本來就以 Linux 為主。
所以如果這台機器的用途就是「Server」,Ubuntu 反而比較簡單。

Step 1:建立 Azure Ubuntu VM

到 Azure Portal:Virtual machines → Create → Azure virtual machine。我用的設定大致如下:

Ubuntu Server 24.04 LTS 8 vCPU / 32GB RAM OS Disk 128GB Password Auth

Inbound ports 開 22、80、443。第一次用 Linux 的話,用 Password 登入其實比較直覺。當然正式環境會更建議 SSH Key,不過這次的目標是快速把教學環境架起來。

重要:把 Azure Public IP 設成 Static。
如果使用 Dynamic Public IP,VM 做 Stop (deallocated) 之後再 Start,IP 有可能改掉。如果 DNS 是直接指 IP,整個網址就會失效。

Step 2:從 Windows 連進 Ubuntu

Windows 現在本身就有 SSH Client。直接開 PowerShell:

ssh 帳號@3.124.21.25

第一次會看到確認訊息,輸入 yes,再輸入密碼。看到 帳號@dify:~$ 就表示進去了。

Step 3:安裝 Docker

這次我走最簡單的方式:

curl -fsSL https://get.docker.com | sudo sh

安裝完成之後,把目前帳號加入 docker group:

sudo usermod -aG docker $USER

然後 exit 重新 SSH 登入。接著確認 docker --version、docker compose version、docker ps,只要都有正常顯示版本,就代表 Docker 完成。

Step 4:安裝 Dify

如果 Ubuntu 還沒有 Git:

sudo apt update
sudo apt install git -y

接著 clone、設定、啟動:

git clone https://github.com/langgenius/dify.git
cd dify/docker
cp .env.example .env
docker compose up -d

第一次可以先直接用 IP 測試 http://3.124.21.25/install,如果看到 Dify 初始化畫面,就表示 Dify 已經正常啟動。

Dify 的學生帳號怎麼處理?

這次的想法不是 15 個人共用同一組帳號,而是:一套 Dify + 一個 Course Workspace + 每個學生一個帳號。老師是 Owner / Admin,學生可以給 Editor 權限,這樣每個人有自己的登入身份,但大家還是使用同一套 Dify。

每個人一個帳號,不代表每個人的 Workspace 資源完全隔離。
因為大家還是在同一個 Workspace,所以課程中最好要求大家統一命名(例如 S01-Lab01、S02-Lab01),這樣比較不容易互相搞混。

Step 5:安裝 n8n

n8n 比 Dify 簡單很多:

docker volume create n8n_data

docker run -d \
  --name n8n \
  --restart unless-stopped \
  -p 5678:5678 \
  -v n8n_data:/home/node/.n8n \
  docker.n8n.io/n8nio/n8n

docker ps 應該可以看到 n8n 正在執行。

n8n 出現 Code Sandbox 畫面怎麼辦?

n8n 新版第一次進去時,可能會看到 AI Assistant 的設定畫面,裡面會要求設定 Code Sandbox。如果只是要一般課程用途,其實不一定需要。因為不用這個 AI Assistant,n8n 本身還是可以正常使用 Workflow、Webhook、HTTP Request、AI Agent、Code Node 等功能。我最後選 Turn off for this instance,這只是把 AI Assistant 關掉,不是把 n8n 關掉。

n8n 要不要每個學生一套?

這其實取決於課程設計。如果所有人共用同一套 n8n,最大的問題是「誰改了我的 Workflow?」「誰刪了我的東西?」。所以如果要做比較完整的教學環境,我會比較偏向 Dify 一套多帳號,n8n 每個學生一套 instance(n8n01.example.com、n8n02.example.com …)。

第一個目標不是先做 15 套,而是 Dify 能跑、n8n 能跑、HTTPS 能跑。先把整條路打通,再複製會比較容易。

接下來就是 HTTPS

到這裡其實 Dify、n8n、Docker、Azure VM 都已經完成。真正稍微容易卡住的地方,是 HTTPS。我最後用的是 Caddy,而不是 Nginx + Certbot。原因只有一個:比較簡單。

為什麼 Caddy 不用自己申請 SSL 憑證?

其實不是「沒有 SSL 憑證」,而是 Caddy 自己幫你申請。傳統做法可能是申請 Certificate → Domain Validation → 下載憑證 → 安裝 → 設定 Private Key → 到期前續期。Caddy 直接幫你做完,它會透過 ACME,向 Let's Encrypt 之類的 CA 申請憑證。

Caddy 自動化管理 SSL
你的網域
↓
DNS
↓
Azure VM
↓
Caddy
↓
自動完成 Domain Validation
→ 申請 Certificate
→ 自動續期

所以看起來像是「沒申請憑證」,其實只是你沒有手動碰到它。

Step 6:先準備 DNS

假設自己的網域是 example.com,可以建立 dify.example.com 和 n8n.example.com。最直覺的做法是 A Record 指向 VM 的 Public IP。

A Record 也可以改成 CNAME

Azure VM 也可以設定一個 DNS Name(例如 yourserver.southeastasia.cloudapp.azure.com)。可以改用 CNAME 指向這個 Azure hostname,對 Caddy 來說沒有差,只要最後網域可以解析到正確的 VM,Caddy 就可以完成 HTTPS 驗證。

A Record 和 CNAME 該選哪個?
如果 Azure Public IP 已經是 Static,A Record 很直接。如果比較想跟 Azure VM 的 DNS Name 綁定,CNAME 也很漂亮。像 dify.example.com、n8n.example.com 這種 subdomain 很適合 CNAME。

Step 7:把 Dify 的 80 / 443 讓給 Caddy

一開始 Dify 自己的 nginx 會占用 80 和 443,但 Caddy 也需要這兩個 port,所以一定會衝突。這是整個過程中我實際碰到的問題之一。

進入 ~/dify/docker,編輯 .env:

# 找到並修改
EXPOSE_NGINX_PORT=80      → EXPOSE_NGINX_PORT=8080
EXPOSE_NGINX_SSL_PORT=443 → EXPOSE_NGINX_SSL_PORT=8443

最後變成 Dify HTTP → 8080、Dify HTTPS → 8443。實際上之後我們主要只會讓 Caddy reverse proxy 到 localhost:8080。

Step 8:重建 Dify

docker compose down
docker compose up -d

測試 curl http://localhost:8080,只要有 HTML 回來,就表示 Dify 已經移到 8080。

Step 9 – 12:安裝與設定 Caddy

Step 9:安裝 Caddy

sudo apt update
sudo apt install -y caddy

Step 10:設定 Caddyfile

sudo nano /etc/caddy/Caddyfile

內容其實只有幾行:

dify.example.com {
    reverse_proxy localhost:8080
}

n8n.example.com {
    reverse_proxy localhost:5678
}

意思就是 dify.example.com → localhost:8080、n8n.example.com → localhost:5678。而 HTTPS、Certificate、Renewal 都讓 Caddy 自己處理。

Step 11:檢查設定

sudo caddy validate --config /etc/caddy/Caddyfile

正常會看到 Valid configuration。

Step 12:啟動 Caddy

sudo systemctl restart caddy
sudo systemctl status caddy --no-pager -l

正常應該看到 Active: active (running)。

我實際遇到的錯誤:443 已經被占用

第一次啟動 Caddy 時,遇到 Job for caddy.service failed because the control process exited with error code.。先 sudo caddy validate 結果是 Valid configuration,表示不是 Caddyfile 語法錯。

真正的錯誤是:listen tcp :443: bind: address already in use。

執行 sudo ss -ltnp | grep -E ':80|:443',看到 0.0.0.0:443 docker-proxy。這表示 443 還被 Dify 的 nginx 占著。解法就是前面把 EXPOSE_NGINX_SSL_PORT 改成 8443,然後 docker compose down && docker compose up -d。

再確認 port 狀態,理想上會看到 8080 → docker-proxy、8443 → docker-proxy,而 443 不應該再被 Docker 占用。這時再 sudo systemctl restart caddy 就正常了。

Step 13:測試 HTTPS

瀏覽器直接開 https://dify.example.com 以及 https://n8n.example.com。如果 DNS 正常、80 / 443 有開,而且 Caddy 啟動成功,第一次進去時 Caddy 會自動處理 Certificate,最後瀏覽器就會看到正常的 HTTPS 🔒。

Dify HTTPS 後的關鍵修正:WebSocket 與 Trigger URL

Dify 網頁用 HTTPS 能正常開啟,不代表所有功能都會正確。

我實際遇到的第二個問題是:Workflow 編輯器執行時,一直停在「同步資料,只需幾秒鐘…」,無法繼續。

圖片

打開瀏覽器 DevTools → Network,如果看到 WebSocket Request URL 類似:

ws://localhost/socket.io/?EIO=4&transport=websocket

問題就很明確了 — Dify 前端還在嘗試連 ws://localhost,但瀏覽器的 localhost 指的是使用者自己的電腦,不是 Azure VM,WebSocket 當然連不上。

修正 NEXT_PUBLIC_SOCKET_URL

進入 ~/dify/docker,編輯 .env:

cd ~/dify/docker
      nano .env

找到(如果沒有這一行也可以自行加入):

NEXT_PUBLIC_SOCKET_URL=ws://localhost

改成:

NEXT_PUBLIC_SOCKET_URL=wss://dify.example.com
注意:https:// 對應 wss://,不是 ws://。
既然 Dify 對外已經是 https://dify.example.com,WebSocket 就應該是 wss://dify.example.com。

同時修正 TRIGGER_URL

另外一個容易忽略的設定是 TRIGGER_URL。如果原本是:

TRIGGER_URL=http://localhost

改成:

TRIGGER_URL=https://dify.example.com

TRIGGER_URL 是 Dify 用來組合 Trigger / Webhook 相關公開網址的基底。如果保留 http://localhost,之後建立 Trigger 或 Webhook 時,就可能產生指向 localhost 的網址,外部服務當然無法呼叫。

修改後重建 Containers

docker compose down
      docker compose up -d

重建後,用無痕視窗或 Ctrl + F5 重新開啟 Dify,再進 DevTools → Network 確認 WebSocket URL 已經變成:

wss://dify.example.com/socket.io/?EIO=4&transport=websocket

如果已經指向正確網域且用 wss://,Workflow 執行應該就不再卡住。

建議一次檢查所有 localhost 設定

既然從 HTTP / localhost 改成正式 HTTPS Domain,最好順便檢查 .env 裡是否還有其他 localhost 設定:

grep -n "localhost" .env
      grep -E 'URL=|SOCKET|ENDPOINT' .env

特別注意這幾個變數:

CONSOLE_API_URL
      CONSOLE_WEB_URL
      SERVICE_API_URL
      APP_API_URL
      APP_WEB_URL
      FILES_URL
      TRIGGER_URL
      ENDPOINT_URL_TEMPLATE
      NEXT_PUBLIC_SOCKET_URL

其中這次已經確定要修改的重點是 NEXT_PUBLIC_SOCKET_URL 和 TRIGGER_URL。如果課程會使用 Dify 的外部 Trigger / Endpoint,也建議確認 ENDPOINT_URL_TEMPLATE 是否仍指向 localhost。

Caddy 通常不用額外設定 WebSocket

Caddy 的 reverse_proxy 本身就會處理 WebSocket Upgrade,不需要像傳統 Nginx 一樣額外設定 Upgrade、Connection 等 Header。Dify 自己的 nginx 也會處理 /socket.io/ 的轉發。所以整個流向會是:

WebSocket 流向
Browser
wss://dify.example.com/socket.io/
↓
Caddy :443
↓
Dify nginx :8080
↓ /socket.io/
Dify WebSocket Service

n8n 還有一個細節:Webhook URL

n8n 頁面本身透過 Caddy 能正常 HTTPS 開啟,不代表 n8n 產生的 Webhook URL 一定正確。如果它還顯示 http://localhost:5678/...,那就要讓 n8n 知道自己對外的網址。

可以把原本 container 刪掉(資料放在 Volume 裡不會遺失),重新啟動並加上環境變數:

docker stop n8n
docker rm n8n

docker run -d \
  --name n8n \
  --restart unless-stopped \
  -p 127.0.0.1:5678:5678 \
  -e N8N_HOST=n8n.example.com \
  -e N8N_PROTOCOL=https \
  -e WEBHOOK_URL=https://n8n.example.com/ \
  -e N8N_EDITOR_BASE_URL=https://n8n.example.com/ \
  -v n8n_data:/home/node/.n8n \
  docker.n8n.io/n8nio/n8n

這裡還順便把 -p 5678:5678 改成 -p 127.0.0.1:5678:5678,意思是 n8n 的 5678 只讓本機存取,外面的人只能透過 443 → Caddy → 5678 進來,這樣比較安全。

Azure NSG 最後應該長怎樣?

最後 Internet 對 VM 只需要 22、80、443。其中 22 最好只允許自己的 IP。其他像 5678、8080、8443、5432、6379 都不用直接開。

晚上可以把 Azure VM 關掉嗎?

可以。這是教學環境,所以晚上根本沒必要讓它一直燒錢。直接到 Azure Portal:Virtual Machine → Stop,最好確認狀態變成 Stopped (deallocated),這樣 VM 的 Compute 費用才會停止(Disk、Public IP 等資源還是可能有少量費用)。

隔天開機服務會自己回來嗎?

大部分會。n8n 啟動時有 --restart unless-stopped,所以 Docker 起來之後 n8n 會自動回來。Caddy 是 systemd service,也會隨系統啟動。Dify 的 Docker Compose containers 正常也會依照 restart policy 回來。

隔天 Azure VM Start 之後,如果網站沒有馬上出現,可以 SSH 進去看 docker ps 以及 sudo systemctl status caddy,通常就能知道是哪一層沒起來。

最後,我這次的實際配置

最終環境配置
Azure VM — Ubuntu 24.04
8 vCPU / 32GB RAM
↓
Docker
↓
Dify
Web / API / Worker
PostgreSQL / Redis
Sandbox ...
n8n
:5678 (localhost only)
↑
Caddy
:80 :443 → Reverse Proxy

對外:https://dify.example.com、https://n8n.example.com。DNS 可以直接 A Record → Azure Static Public IP,也可以 CNAME → Azure VM DNS Name,最後都回到同一台 Azure VM。

我覺得這套架構最適合教學的原因

這次最大的心得其實不是 Dify 或 n8n,而是:

教學環境最重要的不是架構有多漂亮,而是要簡單、穩定、壞了容易重建。

如果為了一堂三天課,硬是做多台 VM、Load Balancer、Managed PostgreSQL、Redis Service、Kubernetes、Container Apps、Key Vault、Private Network……當然可以。但那已經不是在準備 Dify / n8n 課程了,那是在準備一堂 Cloud Infrastructure 課程。😆

對我這次的需求來說,Ubuntu + Docker + Dify + n8n + Caddy 已經足夠。而且整套架起來之後,要停、要開、要重建,都很直覺。

如果只是要準備一個十多人的

AI Workflow / Agent 教學環境,
我目前會優先選這種方式。

一台 VM,兩套開源工具,一個自動化 HTTPS 方案。
簡單、穩定、可複製。

相關資源

  1. Dify — GitHub (langgenius/dify)
  2. Dify 官方文件
  3. n8n — GitHub (n8n-io/n8n)
  4. n8n 官方文件
  5. Caddy 官方文件
  6. Docker — Ubuntu 安裝指南