Cairn
← 返回博客

从 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、图片加载成功

如果首页能开但没有样式,优先检查:

  1. web/static 是否上传;
  2. systemd 的 WorkingDirectory 是否正确;
  3. 模板里的静态资源路径是否以 /static/ 开头;
  4. 浏览器开发者工具中的 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 负责进程;
  • 环境文件负责秘密;
  • 防火墙负责减少暴露面。

小网站最需要的不是把技术名词堆满架构图,而是出了问题以后,知道该去查哪一层。

评论

暂无评论,来说点什么吧。

请先登录后再发表评论

去登录