一键上云:Vultr × Caddy 可直接部署模板(Terraform + Docker Compose + Caddyfile)
目标:一条命令把 Caddy 网关和你的应用部署到 Vultr,自动 HTTPS、反向代理、静态托管、日志可观测开箱即用。本文包含完整模板与操作步骤,直接复制即可落地。

🧭 方案概览
基础设施即代码:用 Terraform 创建 Vultr 实例、防火墙组、SSH 公钥注入等。
云端初始化:cloud-init 在首启时安装 Docker & Compose。
应用编排:Docker Compose 起 Caddy(入口)+ 示例 app(Flask/Gunicorn)。
自动 HTTPS:Caddy 通过 Let’s Encrypt 自动申请/续签证书(需域名 A 记录指向实例 IP)。
可观测性:结构化访问日志(JSON)落盘,便于后续接入 Loki/ELK。
你可以把示例 app 替换成你的服务,Caddy 仍旧自动代理与签证书。
🗂 目录结构
vultr-caddy-starter/
├─ terraform/
│ ├─ main.tf
│ ├─ variables.tf
│ ├─ outputs.tf
│ └─ cloud-init.yaml
├─ compose/
│ ├─ docker-compose.yml
│ ├─ Caddyfile
│ ├─ app/ # 示例后端(可删换为你的)
│ │ ├─ Dockerfile
│ │ └─ wsgi.py
├─ .env.example
└─ Makefile
🔑 准备工作
Vultr API Token:在 Vultr 控制台创建(只读不可用,需读写)。
SSH 公钥:
ssh-keygen -t ed25519生成,公钥内容填到variables.tf或.tfvars。域名解析:将你的域名(如
example.com)的 A 记录 指向后文输出的实例公网 IP(Terraform 输出中会显示)。本地安装:
terraform、docker、docker compose(仅用于本地构建镜像时需要)。
🧱 Terraform:创建 Vultr 资源
terraform/main.tf
terraform {required_version = ">= 1.6.0"required_providers {vultr = {source = "vultr/vultr"version = "~> 2.18"}}
}provider "vultr" {api_key = var.vultr_api_key
}# 可选:上传你的 SSH 公钥到 Vultr 账户,供实例注入
resource "vultr_ssh_key" "main" {name = "caddy-starter-key"ssh_key = var.ssh_public_key
}# 防火墙组 & 规则(仅放行 22/80/443)
resource "vultr_firewall_group" "web" {description = "caddy web ingress"
}resource "vultr_firewall_rule" "allow_ssh" {firewall_group_id = vultr_firewall_group.web.idprotocol = "tcp"port = "22"subnet = "0.0.0.0"subnet_size = 0
}resource "vultr_firewall_rule" "allow_http" {firewall_group_id = vultr_firewall_group.web.idprotocol = "tcp"port = "80"subnet = "0.0.0.0"subnet_size = 0
}resource "vultr_firewall_rule" "allow_https" {firewall_group_id = vultr_firewall_group.web.idprotocol = "tcp"port = "443"subnet = "0.0.0.0"subnet_size = 0
}# 创建实例(Ubuntu 22.04 LTS 示例)
resource "vultr_instance" "caddy_host" {region = var.region # 例: "sea"、"sjo"、"fra"plan = var.plan # 例: "vc2-1c-1gb"os_id = 1743 # Ubuntu 22.04 x64(如变更可在 Vultr 文档查最新 ID)label = "caddy-starter"hostname = var.hostnamefirewall_group_id = vultr_firewall_group.web.idenable_ipv6 = truebackups = "disabled"ssh_key_ids = [vultr_ssh_key.main.id]user_data = file("${path.module}/cloud-init.yaml")tags = ["caddy", "starter"]# 更换为你的私有网络等(可选)# vpc_ids = [ vultr_vpc.main.id ]
}
terraform/variables.tf
variable "vultr_api_key" {type = stringdescription = "Vultr API token"sensitive = true
}variable "ssh_public_key" {type = stringdescription = "Your SSH public key content"
}variable "region" {type = stringdefault = "sea" # 例:西雅图description = "Vultr region code"
}variable "plan" {type = stringdefault = "vc2-1c-1gb"description = "Vultr compute plan"
}variable "hostname" {type = stringdefault = "caddy-gw"description = "Instance hostname"
}
terraform/outputs.tf
output "instance_ip" {value = vultr_instance.caddy_host.main_ipdescription = "Public IPv4 address of the instance"
}output "ssh_connect" {value = "ssh root@${vultr_instance.caddy_host.main_ip}"description = "SSH command (root for first login; cloud-init will create a non-root user if you modify it)"
}
☁️ cloud-init:首启自动安装 Docker & Compose
terraform/cloud-init.yaml
#cloud-config
package_update: true
package_upgrade: trueusers:- name: devopsgroups: [sudo, docker]shell: /bin/bashsudo: ['ALL=(ALL) NOPASSWD:ALL']ssh_authorized_keys:- ${SSH_PUBLIC_KEY} # 可用模板工具替换;或直接省略用 root 登录write_files:- path: /etc/motdpermissions: '0644'content: |Welcome to Vultr Caddy Host 🚀- path: /opt/compose/.envpermissions: '0644'content: |DOMAIN=example.comEMAIL=admin@example.com- path: /opt/compose/docker-compose.ymlpermissions: '0644'content: |services:caddy:image: caddy:2.8restart: unless-stoppedports:- "80:80"- "443:443"volumes:- caddy_data:/data- caddy_config:/config- /opt/compose/Caddyfile:/etc/caddy/Caddyfile:ro- /var/log/caddy:/var/log/caddy- /opt/site:/srv/www:rodepends_on:- appapp:build: ./apprestart: unless-stoppedexpose:- "5000"environment:- APP_ENV=prodvolumes:caddy_data:caddy_config:- path: /opt/compose/Caddyfilepermissions: '0644'content: |{# 全局设置:出现问题时便于定位email {env.EMAIL}}{env.DOMAIN} {encode gzip zstd@api path /api/*handle @api {reverse_proxy app:5000 {header_up X-Request-ID {http.request.id}}}handle {root * /srv/wwwfile_server}# 健康检查与日志respond /health 200log {output file /var/log/caddy/access.logformat json}}- path: /opt/compose/app/Dockerfilepermissions: '0644'content: |FROM python:3.11-slimWORKDIR /appRUN pip install --no-cache-dir flask gunicornCOPY wsgi.py .CMD ["gunicorn", "-b", "0.0.0.0:5000", "wsgi:app", "--workers=2", "--threads=4"]- path: /opt/compose/app/wsgi.pypermissions: '0644'content: |from flask import Flask, jsonifyapp = Flask(__name__)@app.route("/api/hello")def hello():return jsonify(ok=True, msg="Hello from Vultr + Caddy + Docker!")if __name__ == "__main__":app.run(host="0.0.0.0", port=5000)- path: /opt/site/index.htmlpermissions: '0644'content: |<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Vultr × Caddy Starter</title></head><body><h1>Vultr × Caddy Starter</h1><p>Static site served by Caddy.</p><p>Try API: <code>/api/hello</code></p></body></html>runcmd:# 安装 Docker & Compose(Ubuntu/Debian)- curl -fsSL https://get.docker.com | sh- usermod -aG docker devops || true- mkdir -p /var/log/caddy- cd /opt/compose && docker compose up -d
提示
若你要用 通配符证书 或 DNS-01 验证,可改为带 DNS 插件的 Caddy 镜像并在全局块配置
acme_dns。默认
第一次启动后,确保你的域名 A 记录已指向实例 IP,Caddy 才能自动签证书。
🧩 Docker Compose(本地调试可用)
如果你想先在本地跑通(非必须),compose/docker-compose.yml 与云端一致。只需:
cd compose
cp ../.env.example ./.env # 设置 DOMAIN/EMAIL
docker compose up -d --build
浏览器访问:
静态站点:
http://localhost(本地无证书,线上自动 https)API:
http://localhost/api/hello
🧪 一键部署步骤
填变量
复制
.env.example为.env,改DOMAIN与EMAIL(线上 cloud-init 也会写入)。准备 Terraform 变量:
创建terraform/terraform.tfvars(或以环境变量方式传入):vultr_api_key = "YOUR_VULTR_API_TOKEN" ssh_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI.... yourname" region = "sea" plan = "vc2-1c-1gb" hostname = "caddy-gw"
创建资源
cd terraform terraform init terraform apply -auto-approve输出会显示
instance_ip与ssh_connect。设置域名解析
在你的域名服务商处,将A记录(如example.com)指向instance_ip。等解析生效(通常几分钟)。验证服务
http://example.com/health返回200https://example.com/能看到静态页https://example.com/api/hello返回 JSON
更新应用
SSH 到实例:ssh root@INSTANCE_IP cd /opt/compose # 替换为你的 app 镜像,或修改 app 目录并重建 docker compose up -d --build
🔐 安全与生产化建议
只暴露 80/443:后端服务仅
expose,不ports;对外流量统一走 Caddy。非 root 运行:业务镜像使用非特权用户(
USER指令);Caddy 官方镜像默认非 root。证书持久化:
/data持久化到卷(模板已提供),避免重建丢证书导致频繁签发。安全响应头:
header {X-Frame-Options DENYX-Content-Type-Options nosniffReferrer-Policy no-referrerContent-Security-Policy "default-src 'self'" }健康检查/熔断(多副本时):
reverse_proxy {to app:5000lb_policy least_connhealth_uri /healthtry_duration 30s }HTTP/3:开放 UDP/443 以启用 QUIC(Vultr 网络端口需允许)。
日志采集:
/var/log/caddy/access.log用 Fluent Bit/Vector 发往 Loki/ES。
🧰 Makefile(可选,简化操作)
Makefile
TF=cd terraform
apply:$(TF) && terraform apply -auto-approvedestroy:$(TF) && terraform destroy -auto-approvessh:@$(TF) && terraform output -raw ssh_connectip:@$(TF) && terraform output -raw instance_ip
🧷 常见问题速查
证书失败 / 仍是 http:确认域名 A 记录已生效、80/443 未被其他进程占用、防火墙放行。首次签发需几秒到数十秒。
502 / 回源失败:SSH 进实例
curl localhost:5000/health测试后端是否存活;确认 Compose 服务名、端口与reverse_proxy一致。日志无输出:检查
/var/log/caddy权限;确认log { output file ... }已配置。地域/系统/套餐:
os_id、plan、region可按需调整(保持与 Vultr 可用清单一致)。
🧩 换成你的应用
后端替换:把
compose/app/换成你的代码或直接拉取你的镜像,更新docker-compose.yml中app服务。多服务路由:新增一个服务
app2,在Caddyfile添加:@admin path /admin/* handle @admin {reverse_proxy app2:7000 }子域拆分:复制 server 块:
api.example.com {reverse_proxy app:5000 } static.example.com {root * /srv/wwwfile_server }
✅ 总结
用 Terraform 把 Vultr 资源声明化,可重复、可版本化。
用 cloud-init 在首启时完成 Docker/Compose & 模板落盘与拉起。
用 Caddy 做入口层,自动 HTTPS、反向代理、静态托管与日志一把梭。
全文模板可直接复制并按需小改域名与镜像,即可投入实战。
