返回服务器部署
服务器部署 / 发布 2026-05-28 13:17 / 更新 2026-07-18 14:07

Render 免费部署 Docker 服务

整理 Render 免费部署 Docker 服务、CPA 与 Nacos 示例、Cloudflare Worker 定时保活流程。

应用部署RenderDockerCloudflareNacosCPA免费
Render 免费部署 Docker 服务

注册 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 小时基本只够一个服务整月持续运行;部署多个服务时,应允许部分服务空闲休眠或改用付费实例。

Render Hobby 计划免费额度

官方说明:

通用创建 Docker Web Service

后续 CPA 和 Nacos 都使用 Render 的 Web Service + Existing Image 方式部署 Docker 镜像。

  1. 进入 Render 控制台。
  2. 新建 Web Service
  3. 在来源类型中选择 Existing Image

Render 新建 Web Service 入口

以 CPA 为例部署 Docker 服务

CPA 这里指 cli-proxy-api 镜像服务。

填写 Docker 镜像

镜像地址:

eceasy/cli-proxy-api:latest

Render 选择 Existing Image 并填写 CPA 镜像

选择区域和免费实例

区域选择 Singapore (Southeast Asia),距离国内较近。

Render 为 CPA 选择 Singapore 区域

实例类型选择 Free

Render 为 CPA 选择 Free 实例

导入环境变量

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

Render CPA 环境变量区域点击 Add from env

导入内容:

DEPLOY=cloud
MANAGEMENT_PASSWORD=<CPA_MANAGEMENT_PASSWORD>
MANAGEMENT_STATIC_PATH=/tmp/cpa-static

Render CPA 粘贴 env 环境变量

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

Render CPA 环境变量导入后展开 Advanced

添加 Secret File

Advanced 面板中点击 Add Secret File

Render CPA 点击 Add Secret File

填写:

  • Filenameconfig.yaml
  • File 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>"

Render CPA 编辑 config.yaml Secret File

保存 Secret File。

Render CPA 保存 config.yaml Secret File

填写 Docker Command

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

./CLIProxyAPI --config /etc/secrets/config.yaml

Render CPA 填写 Docker Command

点击 Deploy Web Service 开始部署。

验证 CPA 部署

部署日志出现启动完成后,访问 Render 分配的服务地址,即可打开 CPA 面板。

Render CPA 部署完成并访问面板

以 Nacos 为例部署 Docker 服务

填写 Docker 镜像

镜像地址:

docker.io/nacos/nacos-server:v1.4.7

Render 选择 Existing Image 并填写 Nacos 镜像

选择区域和免费实例

区域选择 Singapore (Southeast Asia)

Render 为 Nacos 选择 Singapore 区域

实例类型选择 Free

Render 为 Nacos 选择 Free 实例

配置 MySQL 环境变量

先在 MySQL 中创建 nacos 库,并按需导入同目录的 nacos.sql 初始化表结构。

在 Render 环境变量区域点击 Add Environment VariableAdd from .env

Render Nacos 环境变量区域

Render Nacos 点击 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

Render Nacos 粘贴 MySQL env 环境变量

确认变量后点击 Deploy Web Service

Render Nacos 点击 Deploy Web Service

验证 Nacos 部署

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

Render Nacos 部署进行中

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

Render Nacos 部署完成并显示 Live

Cloudflare Worker 保活

Render 免费实例长时间无请求会休眠,可以用 Cloudflare Worker 的 Cron Trigger 定时请求服务地址。

创建 Worker

进入 Cloudflare 控制台,打开 Compute 下的 Workers & Pages

Cloudflare 进入 Workers and Pages

点击 Create application

Cloudflare Workers and Pages 创建应用

选择 Start with Hello World!

Cloudflare Worker 选择 Hello World 模板

点击 Deploy 创建 Worker。

Cloudflare Worker 部署 Hello World 模板

编辑并部署保活脚本

进入 Worker 概览页,点击 Edit code

Cloudflare Worker 点击 Edit code

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

Cloudflare Worker 粘贴保活脚本并部署

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

Cloudflare Worker 退出代码编辑器

配置定时触发器

进入 Settings

Cloudflare Worker 打开 Settings

Trigger events 中点击 Add

Cloudflare Worker 添加触发事件

选择 Cron triggers

Cloudflare Worker 选择 Cron triggers

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

Cloudflare Worker 配置每 10 分钟执行

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

*/10 * * * *

Cloudflare Worker 填写 Cron expression

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

Cloudflare Worker Cron Trigger 创建完成

验证保活

  • 访问 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 示例中 Filenameconfig.yaml,所以启动命令使用 /etc/secrets/config.yaml

遇到 Add Card 弹窗怎么办

如果创建免费服务时出现 Add Card 弹窗,说明当前账号触发了付款方式验证。先确认 workspace 为 Hobby 计划、实例类型选择了 Free,并按“注册 Render”中的邮箱要求检查账号。是否能够免绑卡以 Render 当前风控结果为准。

Render 可能出现的 Add Card 弹窗

Nacos 连接 MySQL 失败怎么排查

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