尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Web3 全栈开发(八):部署与运维——从 Demo 到能上线的产品
为什么“能跑通”不等于“能上线”前面七篇我把合约、后端、前端全部写了一遍。在本地跑起来功能都对。但如果你现在把这一套直接丢给真实用户大概率会出问题。原因很简单本地环境和线上环境差了十万八千里。本地用的是 Hardhat 节点出块即时线上用的是真实网络可能拥堵、可能回滚。本地的 RPC 是自己的没有限流线上的 RPC 服务商有配额超了就断。本地的合约部署一次就完事线上要考虑升级、要验证源码、要备份部署信息。这一篇我把从 Demo 到产品之间必须跨过的几个坎讲清楚。不是什么高深的技术但少一个环节上线当天就可能出事。部署脚本工程化别再用一个 main 函数第一篇部署脚本我是这么写的asyncfunctionmain(){constpoolawaitStakingPool.deploy(...)awaitpool.waitForDeployment()console.log(部署完成:,awaitpool.getAddress())}这在本地跑跑没问题但线上环境要考虑多网络测试网、主网多环境staging、production部署结果要持久化不能只打印在控制台部署失败要能回滚或者重试用 hardhat-deploy 管理部署我推荐用hardhat-deploy插件。它的核心概念是部署脚本 部署记录npminstall--save-dev hardhat-deploy hardhat-deploy-ethers在hardhat.config.ts里加importhardhat-deploy;importhardhat-deploy-ethers;constconfig:HardhatUserConfig{namedAccounts:{deployer:{default:0,sepolia:0xYourDeployerAddress,mainnet:0xYourDeployerAddress,},},// ...};然后写部署脚本注意目录是deploy/不是scripts/// deploy/01_deploy_staking.tsimport{HardhatRuntimeEnvironment}fromhardhat/types;import{DeployFunction}fromhardhat-deploy/types;constfunc:DeployFunctionasyncfunction(hre:HardhatRuntimeEnvironment){const{deployments,getNamedAccounts}hre;const{deploy,log}deployments;const{deployer}awaitgetNamedAccounts();conststakingTokenawaitdeploy(MockERC20,{from:deployer,args:[Stake Token,STK],log:true,waitConfirmations:5,// 等 5 个确认再算部署完成});constrewardTokenawaitdeploy(MockERC20,{from:deployer,args:[Reward Token,RWD],log:true,waitConfirmations:5,});constpoolawaitdeploy(StakingPool,{from:deployer,args:[stakingToken.address,rewardToken.address],log:true,waitConfirmations:5,});log(StakingPool deployed at:,pool.address);};func.tags[StakingPool];exportdefaultfunc;部署npx hardhat deploy--networksepolia关键差别hardhat-deploy会在deployments/sepolia/目录下生成部署记录包括地址、ABI、构造函数参数、部署区块等。这些信息是所有后续操作的基础——后端要用它拿地址前端要用它拿 ABI运维要用它做对账。初始化操作写成独立脚本部署完合约之后还有初始化操作给合约充值奖励代币、设置 rewardRate、转账 owner。这些不能写在部署脚本里因为部署和初始化是两件事有时候需要重新执行初始化。// deploy/02_init_staking.tsconstfunc:DeployFunctionasyncfunction(hre){const{deployments,getNamedAccounts,ethers}hre;const{get}deployments;const{deployer}awaitgetNamedAccounts();constpoolawaitethers.getContractAt(StakingPool,(awaitget(StakingPool)).address);constrewardTokenawaitethers.getContractAt(MockERC20,(awaitget(MockERC20)).address);// 检查是否已经初始化过constcurrentRateawaitpool.rewardRate();if(currentRate0n){console.log(已初始化过跳过);return;}constfundAmountethers.parseEther(10000);awaitrewardToken.approve(awaitpool.getAddress(),fundAmount);awaitpool.fundRewards(fundAmount);awaitpool.setRewardRate(ethers.parseEther(0.001));console.log(初始化完成);};func.tags[Init];func.dependencies[StakingPool];exportdefaultfunc;dependencies是硬性依赖——跑 Init 之前必须先跑 StakingPool。hardhat-deploy会自动处理顺序。合约验证让用户在 Etherscan 上能读懂合约部署之后如果不验证源码用户在 Etherscan 上看到的只有字节码完全不知道这个合约干了什么。验证过的合约会在 Etherscan 上显示源码、ABI 和可交互界面这对建立用户信任非常重要。用 hardhat-verify 自动验证npminstall--save-dev nomicfoundation/hardhat-verify在hardhat.config.ts里配置 Etherscan API Keyimportnomicfoundation/hardhat-verify;constconfig:HardhatUserConfig{etherscan:{apiKey:{sepolia:process.env.ETHERSCAN_API_KEY!,mainnet:process.env.ETHERSCAN_API_KEY!,},},};部署后跑npx hardhat verify--networksepolia 0x你的合约地址Stake TokenSTK如果合约有构造函数参数验证的时候要一起传进去。验证失败的常见原因原因一构造函数参数不一致。部署时传的是[A, B]验证时传的是[B, A]。参数顺序必须完全一致。原因二编译器版本或者优化设置不一致。hardhat-verify 会读你的hardhat.config.ts如果配置和部署时不一样验证会失败。部署和验证之间不要改配置。原因三Etherscan API Key 有速率限制。免费版的 API Key 每分钟只能调 5 次批量验证多个合约时会触发限流。批量验证时加点延时或者用付费版。原因四代理合约要单独处理。如果用了代理模式要用hardhat-verify的verify:verify手动验证逻辑合约然后在 Etherscan 上关联代理地址。验证成功之后能干什么验证成功之后用户可以在 Etherscan 上看到你的完整源码知道合约在干什么直接调view函数读数据比如查自己的质押量看到所有事件的原始日志用“Write Contract”标签页直接调用合约连钱包就能用这是一个巨大的信任背书。很多用户看到一个没验证的合约直接就关掉了。用 Docker 部署 Go 后端和前端合约部署是“一次性”的事后端和前端是“持续性”的服务。我用 Docker 管理它们的部署。Go 后端的多阶段构建Go 的编译产物是静态二进制特别适合容器化。写一个多阶段 Dockerfile# 构建阶段 FROM golang:1.22-alpine AS builder WORKDIR /build # 先复制依赖文件利用 Docker 缓存 COPY go.mod go.sum ./ RUN go mod download # 复制源码并编译 COPY . . RUN CGO_ENABLED0 GOOSlinux go build -ldflags-s -w -o /build/app ./cmd/server # 运行阶段 FROM alpine:3.19 RUN apk add --no-cache ca-certificates tzdata WORKDIR /app COPY --frombuilder /build/app /app/app EXPOSE 8080 ENTRYPOINT [/app/app]几个优化点值得说CGO_ENABLED0让编译产物是纯静态的可以跑在alpine这种精简镜像上镜像体积能控制在 20MB 以内。-ldflags-s -w去掉符号表和调试信息产物能小 30% 左右。先复制go.mod和go.sum再go mod download这样改了业务代码不会导致依赖重新下载构建速度大幅提升。用 docker-compose 串联服务后端要连 PostgreSQL、Redis再加上前端用 docker-compose 管理最方便version:3.9services:postgres:image:postgres:16-alpineenvironment:POSTGRES_USER:web3POSTGRES_PASSWORD:${POSTGRES_PASSWORD}POSTGRES_DB:stakingvolumes:-pg_data:/var/lib/postgresql/datahealthcheck:test:[CMD-SHELL,pg_isready -U web3]interval:5stimeout:5sretries:5redis:image:redis:7-alpinevolumes:-redis_data:/databackend:build:./backendenv_file:-.env.productiondepends_on:postgres:condition:service_healthyredis:condition:service_startedrestart:unless-stoppedports:-8080:8080frontend:build:./frontendenvironment:NEXT_PUBLIC_API_URL:https://api.yourdomain.comports:-3000:3000restart:unless-stoppedvolumes:pg_data:redis_data:restart: unless-stopped是必须的。Go 后端在运行过程中可能因为 RPC 断连、内存溢出等原因崩溃有了这个配置Docker 会自动重启它。healthcheck配合condition: service_healthy让后端等到数据库真正可用后再启动避免启动时连不上数据库。环境变量管理千万不要把私钥、RPC Key、数据库密码写进代码或者 Dockerfile。全部走环境变量# .env.productionDATABASE_URLpostgres://web3:xxxpostgres:5432/staking?sslmodedisableREDIS_URLredis://redis:6379/0RPC_URLhttps://sepolia.infura.io/v3/xxxPRIVATE_KEY0x...ETHERSCAN_API_KEYxxxGo 里用godotenv或者os.Getenv读funcmustGetEnv(keystring)string{v:os.Getenv(key)ifv{log.Fatalf(环境变量 %s 未设置,key)}returnv}启动时做一次必填校验比运行到一半报错好得多。监控上线之后才是真正的开始服务上线了不代表结束了反而是运维工作的开始。你需要知道后端服务还活着吗索引有没有延迟合约余额够不够RPC 是不是快挂了健康检查接口先给后端加一个健康检查接口func(h*Handler)Health(c*gin.Context){checks:map[string]string{}// 数据库iferr:h.db.Ping(c.Request.Context());err!nil{checks[database]down: err.Error()}else{checks[database]ok}// Redisiferr:h.redis.Ping(c.Request.Context()).Err();err!nil{checks[redis]down: err.Error()}else{checks[redis]ok}// RPC_,err:h.eth.BlockNumber(c.Request.Context())iferr!nil{checks[rpc]down: err.Error()}else{checks[rpc]ok}// 索引延迟latest,_:h.eth.BlockNumber(c.Request.Context())indexed:h.repo.GetLastProcessedBlock(c.Request.Context())lag:latest-indexed checks[indexer_lag]fmt.Sprintf(%d blocks,lag)status:200for_,v:rangechecks{ifstrings.HasPrefix(v,down){status503break}}c.JSON(status,checks)}索引延迟是最关键的指标。如果这个数字持续增长说明索引器挂了需要立刻告警。Prometheus Grafana要更专业的监控用 Prometheus 暴露指标import(github.com/prometheus/client_golang/prometheusgithub.com/prometheus/client_golang/prometheus/promhttp)var(indexedBlockGaugeprometheus.NewGauge(prometheus.GaugeOpts{Name:indexer_last_processed_block,Help:索引器最后处理的区块号,})indexerLagGaugeprometheus.NewGauge(prometheus.GaugeOpts{Name:indexer_lag_blocks,Help:索引延迟的区块数,})eventsProcessedprometheus.NewCounter(prometheus.CounterOpts{Name:indexer_events_processed_total,Help:累计处理的事件数,})txSentprometheus.NewCounterVec(prometheus.CounterOpts{Name:tx_sent_total,Help:发送的交易数,},[]string{status}))funcinit(){prometheus.MustRegister(indexedBlockGauge)prometheus.MustRegister(indexerLagGauge)prometheus.MustRegister(eventsProcessed)prometheus.MustRegister(txSent)}// 暴露 /metrics 接口r.GET(/metrics,gin.WrapH(promhttp.Handler()))然后写几个关键的告警规则# prometheus/alerts.ymlgroups:-name:web3-backendrules:-alert:IndexerLagexpr:indexer_lag_blocks50for:5mannotations:summary:索引器延迟超过 50 个区块-alert:IndexerDownexpr:up{jobweb3-backend} 0for:1mannotations:summary:后端服务已下线-alert:TxFailureRateexpr:rate(tx_sent_total{statusfailed}[5m])0.1for:2mannotations:summary:交易失败率超过 10%合约余额监控对于有资金往来的合约余额监控是必须的。如果合约里的 reward token 被领完了后续用户就领不到奖励了。写一个定时任务每隔几分钟检查一次func(m*Monitor)CheckBalances(ctx context.Context){// 检查 reward token 余额balance,err:m.rewardToken.BalanceOf(bind.CallOpts{Context:ctx},m.poolAddr)iferr!nil{log.Printf(查询余额失败: %v,err)return}rewardBalanceGauge.Set(float64(balance.Uint64()))// 计算还能支撑多少天rate,_:m.pool.RewardRate(bind.CallOpts{Context:ctx})ifrate.Sign()0{secondsLeft:new(big.Int).Div(balance,rate)daysLeft:new(big.Float).Quo(new(big.Float).SetInt(secondsLeft),big.NewFloat(86400),)ifdaysLeft.Cmp(big.NewFloat(7))0{log.Printf(【警告】奖励池还能支撑 %.2f 天,daysLeft)// 发告警m.alert(奖励池余额不足,fmt.Sprintf(剩余 %.2f 天,daysLeft))}}}升级策略代理合约的两难合约不可篡改但业务需求会变。这就是“代理合约”存在的理由——逻辑和数据分离逻辑合约可以替换数据保留。透明代理 vs UUPSOpenZeppelin 提供了两种主流代理模式透明代理Transparent Proxy所有调用都经过代理合约代理合约判断调用者是 admin 还是普通用户。如果是 admin转发到管理合约如果是普通用户转发到逻辑合约。UUPSUniversal Upgradeable Proxy Standard升级逻辑写在逻辑合约里。代理合约只负责转发。更省 Gas也更灵活但要求逻辑合约实现升级函数。我一般用 UUPS// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol; import openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol; import openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol; contract StakingPoolV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable { IERC20 public stakingToken; IERC20 public rewardToken; uint256 public rewardRate; // ... 其他状态变量 /// custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize(address _stakingToken, address _rewardToken) external initializer { __Ownable_init(msg.sender); __UUPSUpgradeable_init(); stakingToken IERC20(_stakingToken); rewardToken IERC20(_rewardToken); } function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} }部署和升级npx hardhat run scripts/deploy_proxy.ts--networksepolia npx hardhat run scripts/upgrade_proxy.ts--networksepolia升级策略的坑坑一状态变量的顺序不能变。升级时如果改了状态变量的声明顺序所有数据都会错位。新增变量只能加在末尾。坑二不能删变量只能标记废弃。删了变量会导致后续变量错位同样的数据混乱。坑三构造函数不能用必须用initialize。代理模式下逻辑合约的构造函数不会被调用因为逻辑合约不是被直接部署的所有初始化逻辑必须放在initialize里。坑四immutable变量在代理模式里不能用。因为immutable是编译时确定的代理合约部署时它就已经固定了而逻辑合约升级后应该重新设置。用immutable会导致升级后数据不对。什么时候不该用代理代理模式不是万能的。它增加了复杂度和攻击面很多漏洞都出在代理合约上。我的判断标准是如果合约只是接收 ETH 和转账没有复杂业务逻辑就不需要代理。因为逻辑简单漏洞风险小就算要改重新部署一个也问题不大。只有业务逻辑复杂、数据量大的合约才值得用代理。运维踩坑记录坑一RPC 服务商限流。免费版的 Infura 和 Alchemy 有严格的速率限制索引器高频请求很容易触发 429。生产环境要用付费版或者多个 RPC 服务商做负载均衡。坑二数据库连接池耗尽。Go 的database/sql默认连接数上限是无限但 PostgreSQL 默认最大连接数只有 100。并发高的时候会报“too many connections”。要显式设置db.SetMaxOpenConns(25)db.SetMaxIdleConns(5)db.SetConnMaxLifetime(5*time.Minute)坑三Docker 容器的时区。Alpine 镜像默认是 UTC容器里time.Now()也是 UTC。如果业务需要本地时区要在 Dockerfile 里装tzdata并设置TZ环境变量。坑四部署脚本的幂等性。部署脚本跑第二遍时如果合约已经部署过不应该重复部署。hardhat-deploy默认会检查但如果用原生 Hardhat 脚本要自己做判断。坑五日志里的敏感信息。打印交易时别把完整的私钥打出来。我在日志里只打账户地址的前六位和后四位私钥永不打印。坑六监控告警的“告警疲劳”。如果告警太频繁运维人员会开始忽略。每个告警都要有意义能触发行动。像“索引延迟超过 50 个块”这种要确认是不是真的需要处理——有时候只是网络波动等几分钟就好了。下一篇预告到这里从合约到前端到部署运维我走完了一整圈。回头看这八篇其实就是在回答一个问题一个完整的 Web3 产品到底包含哪些部分但有一个问题一直没正面回答这些东西怎么展示给面试官或招聘方看下一篇我要写作品集与面试。具体会讲如何把这八篇的代码组织成一个可展示的仓库README 怎么写才能让面试官一眼看懂面试中常见的 Web3 全栈问题附我的回答思路简历上怎么写这个项目才不空洞如何用这个项目引出更深的技术问题这是这个系列的最后一篇也是把前面所有内容“变现”的一篇。我们下一篇见。
RELATED

相关推荐

不用微调也能让大模型更聪明?《医疗大模型基础》第6章:提示词工程与外挂大脑

不用微调也能让大模型更聪明?《医疗大模型基础》第6章:提示词工程与外挂大脑

不用微调也能让大模型更聪明?《医疗大模型基础》第6章:提示词工程与外挂大脑 【免费下载链接】Foundations-of-Medical-LLMs Foundations of Medical Large Language Model Learning 项目地址: https://gitcode.com/gh_mirrors/fo/Foundations-of-Med…

📅 2026/10/8 17:59:17
T3集成指南:如何在Cate中用一个聊天面板并行运行Codex、Claude Code等6大编程Agent

T3集成指南:如何在Cate中用一个聊天面板并行运行Codex、Claude Code等6大编程Agent

T3集成指南:如何在Cate中用一个聊天面板并行运行Codex、Claude Code等6大编程Agent 【免费下载链接】cate An infinite zoomable canvas for coding. Editor, terminal, and browser panels in a spatial workspace. 项目地址: https://gitcode.com/gh_mirrors/ca…

📅 2026/10/8 17:59:17
AD20学习笔记:用TaoToken统一Key打通PCB原理图与Cross Select Modes配置

AD20学习笔记:用TaoToken统一Key打通PCB原理图与Cross Select Modes配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/8 17:59:17
MORE NEWS

更多资讯

📰

2026诺贝尔物理奖:一粒“幽灵粒子”到1立方公里南极望远镜

2026年诺贝尔物理学奖到底牛在哪里? ——从一粒“幽灵粒子”到1立方公里南极望远镜 从宇宙源到南极IceCube——中微子成为一种新的天文学“信使” 一句话先看懂 2026年诺贝尔物理学奖授予Francis Halzen,奖励他对IceCube中微子观测站以及发现天体物理起…

📰

开维引擎五子棋实例:从坐标映射到AI对战完整实现

五子棋这东西,看起来简单,十六根线、三百来个交叉点,规则两句话就能说完。但真要在游戏引擎里把它做成一个能玩的实例,从棋盘坐标映射到落子判胜,再到AI对战甚至网络对战,每一环都有自己的门道。我这次用开…

📰

OpenHarmony下React Native防抖实战:useCallback与自定义Hook解决闭包陷阱

开篇做 OpenHarmony 应用开发这半年,我把 React Native 那套组件化思路搬到了鸿蒙生态里,跑得倒是挺顺畅,但有一个问题一直折磨我——高频事件的防抖处理。尤其是搜索框那种输入一个字符就触发一次请求的场景,在 OpenHarmony 的 R…

📰

2026届计算机毕设选题指南:Web开发与网络安全双赛道详解

每年九、十月份,就会有一大批计算机专业的学生开始为毕业设计焦虑。“老师,2026届毕设到底怎么选?Web开发是不是太没新意了?网络安全是不是特别难?”这类问题我几乎年年都会收到。我自己带过的学生、看过的题目也不少了…

📰

UE材质进阶:Custom HLSL与MPC实战指南

这几个月在项目里啃了不少材质上的硬骨头,从最初只会拖节点连蓝图,到后面被性能和多平台适配逼着去写HLSL,一路踩坑一路填。今天这篇”41”就是想把这些进阶阶段的实操经验梳理一遍。不是给你念引擎文档,而是站在美术和TA的角度&a…

📰

BLE 固件传完了,为什么还不能提示升级成功?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬