从 localhost 到 HTTPS:Go 网站上线全记录
从 localhost 到 HTTPS:Go 网站上线全记录
本地运行成功,只能说明程序在熟悉的环境里愿意配合。
真正上线时,域名、端口、权限、数据库、证书、服务重启和文件路径会一起登场。任何一个环节少配一行,浏览器都可能只回你一句冷静的“连接失败”。
这篇文章记录一套适合小型 Go 网站的上线流程:
Internet
|
Caddy :80 / :443
|
Go :8080
|
PostgreSQL :5432
Go 和 PostgreSQL 只监听本机地址,公网只开放 SSH、HTTP 和 HTTPS。下面使用 example.com、示例用户名和示例密码,实际部署时请替换为自己的配置。
一、上线前先确定发布内容
生产服务器不需要接收整个开发目录。
通常需要上传:
cmd/
internal/
web/
migrations/
deploy/
go.mod
go.sum
config.yaml
docker-compose.yml
通常不需要上传:
.git/
.idea/
.vscode/
node_modules/
docs/
临时测试数据
本地编译产物
.env
尤其不要上传包含真实密码的 .env。配置文件可以进仓库,秘密不能。
可以用白名单式 rsync:
rsync -av --delete --delete-excluded --prune-empty-dirs \
--include '/cmd/***' \
--include '/internal/***' \
--include '/web/***' \
--include '/migrations/***' \
--include '/deploy/***' \
--include '/go.mod' \
--include '/go.sum' \
--include '/config.yaml' \
--include '/docker-compose.yml' \
--exclude '*' \
./ ubuntu@example.com:/opt/cairn/
白名单比“先全部上传,再排除几个目录”更可靠。新出现的本地文件默认不会被带到服务器。
第一次运行前建议加 --dry-run:
rsync -avn --delete --delete-excluded --prune-empty-dirs ...
确认列表无误后,再去掉 n。--delete 会删除服务器目标目录中本地已不存在的文件,路径写错时非常有执行力,必须先预览。
二、安装运行环境
先确认服务器架构:
uname -m
常见输出:
x86_64
这对应 Go 下载包里的 amd64。安装时应从 Go 官方下载与项目版本匹配的发行包:
cd /tmp
curl -LO https://go.dev/dl/go1.26.4.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.26.4.linux-amd64.tar.gz
把 Go 加入当前用户的 PATH:
echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.profile
source ~/.profile
go version
还需要 Docker、Docker Compose 和 Caddy。安装方式会随 Linux 发行版变化,应优先使用它们的官方仓库,而不是复制多年前的安装脚本。
安装后至少确认:
docker --version
docker compose version
caddy version
三、生产配置与秘密分离
建立只允许管理员读取的环境变量文件:
sudo install -d -m 0750 /etc/cairn
sudo nano /etc/cairn/cairn.env
内容示例:
DB_PASSWORD=请替换为强密码
CAIRN_ENVIRONMENT=production
CAIRN_SERVER_HOST=127.0.0.1
CAIRN_SERVER_PORT=8080
CAIRN_DATABASE_HOST=127.0.0.1
CAIRN_DATABASE_PORT=5432
CAIRN_DATABASE_USER=cairn
CAIRN_DATABASE_NAME=cairn
CAIRN_DATABASE_SSLMODE=disable
设置权限:
sudo chmod 640 /etc/cairn/cairn.env
这里的数据库 sslmode=disable 不是让数据库裸奔在公网。前提是 PostgreSQL 只绑定到 127.0.0.1,应用和数据库在同一台机器通信。
docker-compose.yml 中也应限制端口:
services:
db:
image: postgres:16
environment:
POSTGRES_USER: cairn
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
POSTGRES_DB: cairn
ports:
- "127.0.0.1:5432:5432"
启动时显式指定环境文件:
cd /opt/cairn
sudo docker compose --env-file /etc/cairn/cairn.env up -d
sudo docker compose --env-file /etc/cairn/cairn.env ps
如果忘记 --env-file,Compose 会说 DB_PASSWORD 没有值。这不是数据库坏了,只是 Compose 不会自动猜测秘密藏在哪个文件里。
四、执行数据库迁移
安装 golang-migrate 时需要包含 PostgreSQL 驱动:
go install -tags postgres github.com/golang-migrate/migrate/v4/cmd/migrate@latest
如果漏掉 -tags postgres,命令本身仍可能成功安装,但执行时会报:
database driver: unknown driver postgres
这类报错很有迷惑性:工具存在、版本命令也能运行,只是编译时没有把 PostgreSQL 驱动装进去。
执行迁移:
sudo bash -c 'source /etc/cairn/cairn.env && /home/ubuntu/go/bin/migrate \
-path /opt/cairn/migrations \
-database "postgres://cairn:${DB_PASSWORD}@127.0.0.1:5432/cairn?sslmode=disable" up'
检查当前版本:
sudo bash -c 'source /etc/cairn/cairn.env && /home/ubuntu/go/bin/migrate \
-path /opt/cairn/migrations \
-database "postgres://cairn:${DB_PASSWORD}@127.0.0.1:5432/cairn?sslmode=disable" version'
不要把迁移失败简单处理成 force。数据库出现 dirty 状态时,应先查看失败脚本已经执行了哪些语句,修复或手动回滚后再调整版本。force 只改版本记录,不会替你恢复数据结构。
五、编译并交给 systemd
在服务器编译:
cd /opt/cairn
mkdir -p bin
go build -trimpath -ldflags="-s -w" -o bin/cairn ./cmd/server
创建不能登录系统的专用用户:
sudo useradd --system --home /opt/cairn --shell /usr/sbin/nologin cairn
sudo chown -R cairn:cairn /opt/cairn
服务文件 /etc/systemd/system/cairn.service:
[Unit]
Description=Cairn web service
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
User=cairn
Group=cairn
WorkingDirectory=/opt/cairn
EnvironmentFile=/etc/cairn/cairn.env
ExecStart=/opt/cairn/bin/cairn
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/cairn
[Install]
WantedBy=multi-user.target
启用并启动:
sudo systemctl daemon-reload
sudo systemctl enable --now cairn
sudo systemctl status cairn --no-pager
先不要急着配域名。直接从服务器本机检查应用:
curl http://127.0.0.1:8080/healthz
预期得到 200 和健康状态 JSON。只有这一关通过,才轮到反向代理上场。
六、让 Caddy 接管域名和 HTTPS
先把域名的 A 记录指向服务器公网 IP,并确认解析:
getent ahostsv4 example.com
getent ahostsv4 www.example.com
Caddyfile 可以保持很短:
example.com, www.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:8080
}
验证配置并重载:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
sudo systemctl status caddy --no-pager
Caddy 会自动申请证书,并把 HTTP 请求跳转到 HTTPS。证书申请需要满足:
- 域名已经解析到这台服务器;
- 云防火墙和系统防火墙允许 80、443 端口;
- 没有其他服务占用这些端口;
- 公网能访问服务器。
查看申请过程:
sudo journalctl -u caddy -n 100 --no-pager
七、正确验证 HTTP 响应
检查首页时,下面这条命令可能返回 405:
curl -I https://example.com
因为 -I 发送的是 HEAD 请求,而应用路由可能只注册了 GET。这并不代表网站打不开。
应继续测试真正的 GET:
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/
curl -sS https://example.com/healthz
也可以显式发送 GET 并查看响应头:
curl -i https://example.com/healthz
上线验收至少包括:
GET / 200
GET /posts 200
GET /tools/json 200
GET /tools/diff 200
GET /healthz 200
HTTP 自动跳转 HTTPS
静态 CSS、JS、图片加载成功
如果首页能开但没有样式,优先检查:
web/static是否上传;- systemd 的
WorkingDirectory是否正确; - 模板里的静态资源路径是否以
/static/开头; - 浏览器开发者工具中的 404 请求。
八、最后再开防火墙
启用 UFW 前,先允许 SSH。顺序反过来,可能会亲手把自己关在服务器外面。
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
生产环境不需要向公网开放:
- 8080:Go 只监听
127.0.0.1; - 5432:PostgreSQL 只映射到
127.0.0.1。
云平台的安全组也要采用同样的原则。UFW 放行了端口,不代表云防火墙一定放行;反过来也一样。
九、上线不是“浏览器能打开”就结束
一次完整上线还应确认:
sudo systemctl is-active cairn
sudo systemctl is-active caddy
sudo docker compose --env-file /etc/cairn/cairn.env ps
sudo journalctl -u cairn -n 50 --no-pager
然后从另一台设备访问域名,测试登录、评论、文章详情和工具页。服务器本机访问成功,只能证明本机链路正常,不能代替公网验证。
这套架构没有容器编排平台,也没有复杂发布系统,但边界很清楚:
- Caddy 负责公网入口和证书;
- Go 负责应用;
- PostgreSQL 负责数据;
- systemd 负责进程;
- 环境文件负责秘密;
- 防火墙负责减少暴露面。
小网站最需要的不是把技术名词堆满架构图,而是出了问题以后,知道该去查哪一层。
评论
暂无评论,来说点什么吧。
请先登录后再发表评论
去登录