注册 Render
Render | The cloud for builders
- 不建议使用临时邮箱注册,优先使用自建域名邮箱或 Microsoft 邮箱(Outlook、Hotmail)。临时邮箱更容易触发
Add Card验证,最终是否要求添加付款方式以 Render 实际风控结果为准。 - 点击邮箱中的激活链接完成注册。
- 激活后使用账号密码登录 Render 控制台。
免费计划限制
Render 的 Hobby workspace 可以创建免费 Web Service,但免费额度由同一 workspace 内的所有服务共享。
| 限额 | 规则 |
|---|---|
| 出站带宽 | 每月包含 5 GB;超出后按 $0.15/GB 计费。未添加付款方式时,额度耗尽后免费服务会暂停到下个月。 |
| 免费实例时长 | 每个 workspace 每个自然月共 750 小时;服务休眠时不消耗时长,额度每月重置且不结转。 |
启用本文的 Cloudflare Worker 保活后,Web Service 会持续消耗免费实例时长。因此,750 小时基本只够一个服务整月持续运行;部署多个服务时,应允许部分服务空闲休眠或改用付费实例。

官方说明:
通用创建 Docker Web Service
后续 CPA 和 Nacos 都使用 Render 的 Web Service + Existing Image 方式部署 Docker 镜像。
- 进入 Render 控制台。
- 新建
Web Service。 - 在来源类型中选择
Existing Image。

以 CPA 为例部署 Docker 服务
CPA 这里指 cli-proxy-api 镜像服务。
填写 Docker 镜像
镜像地址:
eceasy/cli-proxy-api:latest

选择区域和免费实例
区域选择 Singapore (Southeast Asia),距离国内较近。

实例类型选择 Free。

导入环境变量
点击 Add from .env,批量导入 CPA 运行参数。

导入内容:
DEPLOY=cloud
MANAGEMENT_PASSWORD=<CPA_MANAGEMENT_PASSWORD>
MANAGEMENT_STATIC_PATH=/tmp/cpa-static

环境变量导入后,展开 Advanced 面板。

添加 Secret File
在 Advanced 面板中点击 Add Secret File。

填写:
Filename:config.yamlFile Contents:
port: 8317
auth-dir: "/tmp/auths"
debug: false
logging-to-file: false
usage-statistics-enabled: false
request-retry: 3
quota-exceeded:
switch-project: true
switch-preview-model: true
api-keys:
- "<API_KEY>"

保存 Secret File。

填写 Docker Command
config.yaml 会挂载到 /etc/secrets/config.yaml,启动命令填写:
./CLIProxyAPI --config /etc/secrets/config.yaml

点击 Deploy Web Service 开始部署。
验证 CPA 部署
部署日志出现启动完成后,访问 Render 分配的服务地址,即可打开 CPA 面板。

以 Nacos 为例部署 Docker 服务
填写 Docker 镜像
镜像地址:
docker.io/nacos/nacos-server:v1.4.7

选择区域和免费实例
区域选择 Singapore (Southeast Asia)。

实例类型选择 Free。

配置 MySQL 环境变量
先在 MySQL 中创建 nacos 库,并按需导入同目录的 nacos.sql 初始化表结构。
在 Render 环境变量区域点击 Add Environment Variable 或 Add from .env。


环境变量内容:
SPRING_DATASOURCE_PLATFORM=mysql
MYSQL_SERVICE_HOST=<AIVEN_MYSQL_HOST>
MYSQL_SERVICE_PORT=<AIVEN_MYSQL_PORT>
MYSQL_SERVICE_DB_NAME=nacos
MYSQL_SERVICE_USER=<AIVEN_MYSQL_USER>
MYSQL_SERVICE_PASSWORD=<AIVEN_MYSQL_PASSWORD>
MYSQL_DATABASE_NUM=1
MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=10000&socketTimeout=30000&autoReconnect=true&useSSL=true
JVM_XMS=128m
JVM_XMX=256m
JVM_XMN=64m
JVM_MS=64m
JVM_MMS=128m
MODE=standalone

确认变量后点击 Deploy Web Service。

验证 Nacos 部署
查看部署日志,等待状态从 In Progress 变为 Live。

日志出现 Nacos started successfully 和 Your service is live 后,访问 Render 分配的服务地址验证。

Cloudflare Worker 保活
Render 免费实例长时间无请求会休眠,可以用 Cloudflare Worker 的 Cron Trigger 定时请求服务地址。
创建 Worker
进入 Cloudflare 控制台,打开 Compute 下的 Workers & Pages。

点击 Create application。

选择 Start with Hello World!。

点击 Deploy 创建 Worker。

编辑并部署保活脚本
进入 Worker 概览页,点击 Edit code。

复制下方保活脚本到编辑器后,点击 Deploy。

部署完成后退出编辑面板,回到 Worker 概览页。

配置定时触发器
进入 Settings。

在 Trigger events 中点击 Add。

选择 Cron triggers。

可以直接选择每 10 分钟执行一次。

也可以切换到 Cron expression,填写:
*/10 * * * *

添加完成后,在 Trigger events 中能看到 scheduled() 每 10 分钟执行一次。

验证保活
- 访问 Worker 根路径会手动执行一次完整保活。
- 访问
/status可以查看 Worker 当前状态。 - 访问
/health可以快速检查前 5 个域名。
保活脚本
// Cloudflare Worker 脚本 - 优化版
// 需要保活的域名列表
const DOMAINS = [
'<RENDER_SERVICE_URL>'
];
// 配置参数
const CONFIG = {
BATCH_SIZE: 20, // 每批处理的域名数量,避免超过Cloudflare限制
BATCH_DELAY: 2000, // 批次间延时(毫秒)
REQUEST_TIMEOUT: 30000, // 单个请求超时时间
RETRY_COUNT: 5, // 重试次数
RETRY_DELAY: 1000 // 重试延时
};
// 工具函数:延时
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// 工具函数:分批处理数组
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
// 单个域名保活检查
async function checkSingleDomain(domain, retryCount = 0) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), CONFIG.REQUEST_TIMEOUT);
const response = await fetch(domain, {
method: 'GET',
headers: {
'User-Agent': 'Cloudflare-Worker-Bot/2.0',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Accept': '*/*'
},
signal: controller.signal,
cf: {
cacheTtl: 0,
cacheEverything: false
}
});
clearTimeout(timeoutId);
const result = {
domain,
status: response.ok ? 'success' : 'failed',
statusCode: response.status,
responseTime: Date.now(),
timestamp: new Date().toISOString(),
retryCount
};
// 记录详细状态
if (!response.ok) {
result.statusText = response.statusText;
}
return result;
} catch (error) {
// 处理超时和其他错误
if (error.name === 'AbortError') {
if (retryCount < CONFIG.RETRY_COUNT) {
console.log(`[${new Date().toISOString()}] ${domain}: 超时,重试 ${retryCount + 1}/${CONFIG.RETRY_COUNT}`);
await sleep(CONFIG.RETRY_DELAY * (retryCount + 1));
return checkSingleDomain(domain, retryCount + 1);
}
return {
domain,
status: 'timeout',
error: '请求超时',
timestamp: new Date().toISOString(),
retryCount
};
}
// 其他错误重试
if (retryCount < CONFIG.RETRY_COUNT) {
console.log(`[${new Date().toISOString()}] ${domain}: 错误,重试 ${retryCount + 1}/${CONFIG.RETRY_COUNT} - ${error.message}`);
await sleep(CONFIG.RETRY_DELAY * (retryCount + 1));
return checkSingleDomain(domain, retryCount + 1);
}
return {
domain,
status: 'error',
error: error.message,
timestamp: new Date().toISOString(),
retryCount
};
}
}
// 批次处理域名
async function processBatch(domainBatch, batchIndex) {
console.log(`[${new Date().toISOString()}] 开始处理第 ${batchIndex + 1} 批,共 ${domainBatch.length} 个域名`);
const batchResults = await Promise.allSettled(
domainBatch.map(domain => checkSingleDomain(domain))
);
const results = batchResults.map(result => result.value || result.reason);
// 统计本批次结果
const stats = {
success: results.filter(r => r.status === 'success').length,
failed: results.filter(r => r.status === 'failed').length,
error: results.filter(r => r.status === 'error').length,
timeout: results.filter(r => r.status === 'timeout').length
};
console.log(`[${new Date().toISOString()}] 第 ${batchIndex + 1} 批完成: 成功 ${stats.success}, 失败 ${stats.failed}, 错误 ${stats.error}, 超时 ${stats.timeout}`);
return results;
}
// 主保活处理函数
async function processKeepAlive() {
const startTime = Date.now();
console.log(`[${new Date().toISOString()}] 开始执行保活任务,共 ${DOMAINS.length} 个域名`);
// 将域名分批
const domainBatches = chunkArray(DOMAINS, CONFIG.BATCH_SIZE);
console.log(`[${new Date().toISOString()}] 分为 ${domainBatches.length} 批处理,每批最多 ${CONFIG.BATCH_SIZE} 个`);
const allResults = [];
// 逐批处理
for (let i = 0; i < domainBatches.length; i++) {
const batchResults = await processBatch(domainBatches[i], i);
allResults.push(...batchResults);
// 批次间延时(除了最后一批)
if (i < domainBatches.length - 1) {
console.log(`[${new Date().toISOString()}] 等待 ${CONFIG.BATCH_DELAY}ms 后处理下一批`);
await sleep(CONFIG.BATCH_DELAY);
}
}
// 最终统计
const finalStats = {
total: allResults.length,
success: allResults.filter(r => r.status === 'success').length,
failed: allResults.filter(r => r.status === 'failed').length,
error: allResults.filter(r => r.status === 'error').length,
timeout: allResults.filter(r => r.status === 'timeout').length,
duration: Date.now() - startTime
};
console.log(`[${new Date().toISOString()}] 保活任务完成: 总计 ${finalStats.total}, 成功 ${finalStats.success}, 失败 ${finalStats.failed}, 错误 ${finalStats.error}, 超时 ${finalStats.timeout}, 耗时 ${finalStats.duration}ms`);
return {
stats: finalStats,
results: allResults,
timestamp: new Date().toISOString()
};
}
export default {
// 定时触发器
async scheduled(event, env, ctx) {
try {
const result = await processKeepAlive();
console.log(`[${new Date().toISOString()}] 定时任务完成`);
return result;
} catch (error) {
console.error(`[${new Date().toISOString()}] 定时任务失败:`, error);
throw error;
}
},
// HTTP请求处理
async fetch(request, env, ctx) {
const url = new URL(request.url);
try {
// 检查路径
if (url.pathname === '/status') {
// 返回简单状态
return new Response(JSON.stringify({
status: 'active',
domains: DOMAINS.length,
config: CONFIG,
timestamp: new Date().toISOString()
}), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
if (url.pathname === '/health') {
// 快速健康检查(仅检查前5个域名)
const testDomains = DOMAINS.slice(0, 5).map(d => d.split(',')[0].trim());
const quickResults = await Promise.allSettled(
testDomains.map(domain => checkSingleDomain(domain))
);
return new Response(JSON.stringify({
status: 'health_check',
results: quickResults.map(r => r.value || r.reason),
timestamp: new Date().toISOString()
}), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
// 默认执行完整保活检查
const result = await processKeepAlive();
return new Response(JSON.stringify({
status: 'completed',
...result
}), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
} catch (error) {
console.error(`[${new Date().toISOString()}] 请求处理失败:`, error);
return new Response(JSON.stringify({
status: 'error',
message: error.message,
timestamp: new Date().toISOString()
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
}
};
常见问题
Render 免费实例为什么需要保活
Render 免费实例长时间无请求会休眠,后续第一次访问可能变慢。Cloudflare Worker 定时请求服务地址,可以降低服务休眠带来的冷启动影响。
Secret File 路径怎么写
Render 的 Secret File 会挂载到 /etc/secrets/<Filename>。本文 CPA 示例中 Filename 是 config.yaml,所以启动命令使用 /etc/secrets/config.yaml。
遇到 Add Card 弹窗怎么办
如果创建免费服务时出现 Add Card 弹窗,说明当前账号触发了付款方式验证。先确认 workspace 为 Hobby 计划、实例类型选择了 Free,并按“注册 Render”中的邮箱要求检查账号。是否能够免绑卡以 Render 当前风控结果为准。

Nacos 连接 MySQL 失败怎么排查
- 确认 Aiven MySQL 的 Host、Port、用户名、密码和数据库名正确。
- 确认已创建
nacos库,并导入nacos.sql表结构。 - 保留
MYSQL_SERVICE_DB_PARAM中的useSSL=true,避免云数据库 SSL 连接问题。
