Go + Vue 3 全栈实战:从任务 CRUD 到 Docker Compose 部署

  1. 1. 最终会得到什么?
  2. 2. 为什么选择 Go + Vue 3 + Docker?
  3. 3. 一、准备开发环境
  4. 4. 二、一条命令启动开发环境
  5. 5. 三、理解 Go 后端:先做清晰的业务边界
    1. 5.1. 新增自己的业务模块
  6. 6. 四、Vue 3 前端:让类型贯穿请求链路
  7. 7. 五、前后端联调为什么不需要写死地址?
  8. 8. 六、用 Docker Compose 部署生产环境
    1. 8.1. 1. 创建生产配置
    2. 8.2. 2. 构建并启动
    3. 8.3. 3. 为什么生产镜像更小?
    4. 8.4. 4. 配置域名与 HTTPS
  9. 9. 七、数据持久化、升级与备份
  10. 10. 八、几个常见问题
    1. 10.1. Go 在宿主机运行时为什么连不上数据库?
    2. 10.2. 修改了 Vue 环境变量,为什么页面没有变化?
    3. 10.3. 可以切换到 MySQL 吗?
    4. 10.4. Redis 目前做了什么?
  11. 11. 九、这个模板适合谁?
  12. 12. 总结

用 Go + Vue 3 开发网站,真正费时间的通常不是写出第一个接口或页面,而是把数据库迁移、前后端联调、健康检查、容器构建和生产配置连成一套可重复的流程。

这篇教程会完成一条真实的全栈链路:Vue 3 页面通过类型化 API 客户端调用 Go 服务,Go 将数据写入 PostgreSQL,并通过 Docker Compose 同时管理 Web、API、数据库与 Redis。最后,我们会把同一套项目切换到生产形态并部署到服务器。

如果你只想尽快跑起来,可以直接使用我整理的开源模板:glosc-ai/template-go-vue3-docker。它自带任务 CRUD,不是一个只能看到空白首页的“目录模板”。下文所有命令和目录都基于该项目截至 2026 年 8 月 5 日的 main 分支。

最终会得到什么?

运行完成后,整个请求链路如下:

1
2
3
4
5
6
7
8
9
浏览器
└─ Vue 3 页面 / Pinia Store
└─ TypeScript API Client
└─ Vite(开发)或 Nginx(生产)反向代理
└─ Go net/http + Middleware
└─ Task Handler + SQL Store
└─ PostgreSQL / MySQL

Go API ── 就绪检查 ── Redis

项目内置的任务模块支持四个接口:

方法 路径 功能
GET /api/v1/tasks 查询最近 100 条任务
POST /api/v1/tasks 创建任务
PATCH /api/v1/tasks/{id} 修改完成状态
DELETE /api/v1/tasks/{id} 删除任务

这条 CRUD 链路虽然不复杂,却足以验证浏览器、代理、API、参数校验、数据库迁移和持久化是否全部正常。

为什么选择 Go + Vue 3 + Docker?

这套组合适合中小型后台、SaaS 原型、内部工具和 API 服务:

  • Go 编译快、单二进制部署简单,标准库 net/http 已经能胜任常见 API;
  • Vue 3 的组合式 API、TypeScript、Pinia 和 Vite 让页面开发保持轻量;
  • Docker 把 Go、Node、PostgreSQL、Redis 的版本写进配置,减少“在我电脑上能运行”的问题;
  • Docker Compose 很适合一台服务器上的早期生产环境,也方便本地拉起完整依赖。

这里有一个重要取舍:模板后端没有一开始就堆叠 controller/service/repository 三层,而是按业务边界组织代码。tasks 包同时拥有模型、HTTP 行为、SQL 存储和测试。等到业务真的出现复用需求,再抽象公共能力,通常比预先制造很多空接口更容易维护。

一、准备开发环境

本地只需要安装:

  • Git;
  • Docker 24 或更高版本;
  • Docker Compose v2(使用 docker compose 命令)。

克隆项目:

1
2
git clone https://github.com/glosc-ai/template-go-vue3-docker.git
cd template-go-vue3-docker

项目的核心目录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.
├── server/ # Go API
│ ├── auth/ # JWT 基础模块
│ ├── cache/ # Redis 客户端
│ ├── config/ # 环境变量解析与校验
│ ├── database/ # 数据库连接和 SQL 迁移
│ ├── health/ # live / ready 探针
│ ├── tasks/ # 示例业务模块
│ └── Dockerfile
├── web/ # Vue 3 前端
│ ├── src/api/ # 类型化 API 客户端
│ ├── src/features/ # 按业务组织的组件和 Store
│ ├── src/views/ # 页面入口
│ ├── nginx.conf
│ └── Dockerfile
├── docker-compose.dev.yml # 开发环境
├── docker-compose.yml # 生产形态
├── .env.example
└── Makefile

二、一条命令启动开发环境

执行:

1
make dev

Compose 会构建并启动四个服务:

  • Vue 3 + Vite:http://localhost:5173
  • Go API:http://localhost:8080
  • PostgreSQL 17;
  • Redis 7.4。

第一次构建需要下载镜像和依赖。服务启动后,先检查存活和就绪状态:

1
2
curl http://localhost:8080/health/live
curl http://localhost:8080/health/ready

live 只回答“进程是否活着”,ready 还会检查数据库和 Redis 是否可用。容器编排和负载均衡应该根据 ready 判断是否把流量交给 API。

再创建一条任务:

1
2
3
curl -X POST http://localhost:8080/api/v1/tasks \
-H 'Content-Type: application/json' \
-d '{"title":"完成 Go + Vue 3 网站部署"}'

查询任务:

1
curl http://localhost:8080/api/v1/tasks

如果本机的 8080 已被占用,可以改映射到宿主机的其他端口:

1
API_PORT=18080 make dev

此时浏览器仍然访问 5173。Vue 容器通过 Compose 内部网络访问 api:8080,只有你直接调用 API 时才使用 18080

三、理解 Go 后端:先做清晰的业务边界

模板使用 Go 1.25 和标准库 net/http。路由注册非常直接:

1
2
3
4
5
6
func (h *Handler) Register(mux *http.ServeMux) {
mux.HandleFunc("GET /api/v1/tasks", h.list)
mux.HandleFunc("POST /api/v1/tasks", h.create)
mux.HandleFunc("PATCH /api/v1/tasks/{id}", h.update)
mux.HandleFunc("DELETE /api/v1/tasks/{id}", h.delete)
}

Handler 不直接依赖 *sql.DB,而是依赖一个只描述自身需求的 Store 接口:

1
2
3
4
5
6
type Store interface {
List(context.Context) ([]Task, error)
Create(context.Context, string) (Task, error)
SetCompleted(context.Context, int64, bool) (Task, error)
Delete(context.Context, int64) error
}

这样做的好处是:HTTP 测试可以传入内存中的假 Store,生产环境再注入 SQL 实现。接口由使用方定义,边界小,也不会为了“架构整齐”抽象出一套什么业务都能做的通用 Repository。

创建任务时,后端还做了三层保护:

  1. 请求体最大 1 MiB;
  2. 拒绝 JSON 中的未知字段和多个对象;
  3. 标题去除首尾空格后,限制在 1~160 个字符。

这比把所有校验留给前端可靠,因为 API 也可能被脚本、移动端或其他服务调用。

新增自己的业务模块

假设要增加用户模块,可以复制 tasks 的组织思路,而不是把用户代码塞进原目录:

1
2
3
4
5
server/users/
├── users.go # 领域类型
├── handler.go # HTTP 输入、输出和校验
├── store.go # SQL 操作
└── handler_test.go

然后在 server.go 中组装依赖并注册路由。数据库结构变化则新增有序迁移文件:

1
2
server/database/migrations/postgres/002_create_users.sql
server/database/migrations/mysql/002_create_users.sql

已经在线上执行过的迁移不要原地修改。新建下一号迁移,才能让不同环境按相同顺序演进。

四、Vue 3 前端:让类型贯穿请求链路

前端使用 Vue 3、TypeScript、Vite、Vue Router、Pinia、Tailwind CSS v4 和 shadcn-vue。web/src/api/tasks.ts 先声明 API 返回的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export interface Task {
id: number
title: string
completed: boolean
created_at: string
}

export async function createTask(title: string): Promise<Task> {
const response = await request<DataResponse<Task>>('/tasks', {
method: 'POST',
body: JSON.stringify({ title }),
})
return response.data
}

api 目录只处理 HTTP:拼接地址、设置请求头、解析响应和统一错误。features/tasks/store.ts 再负责页面状态:

1
2
3
4
5
6
7
8
9
10
export const useTaskStore = defineStore('tasks', () => {
const items = ref<Task[]>([])
const loading = ref(false)
const completedCount = computed(
() => items.value.filter(task => task.completed).length,
)

// load、add、toggle、remove 省略
return { items, loading, completedCount }
})

推荐保持以下边界:

  • src/api:传输类型和请求函数;
  • src/features/业务名:Pinia Store 与领域组件;
  • src/views:组合页面,不直接散落 fetch
  • src/components/ui:由 shadcn-vue 管理的通用 UI 源码。

这样,当后端错误格式、API 前缀或鉴权方式改变时,只需要集中修改 API 层。

五、前后端联调为什么不需要写死地址?

浏览器始终请求相对路径 /api/v1

开发环境中,Vite 把 /api/health 转发给 Go:

1
2
3
4
5
6
7
8
server: {
proxy: {
'/api': {
target: 'http://api:8080',
changeOrigin: true,
},
},
}

生产环境中,Nginx 做相同的事:

1
2
3
4
5
6
7
8
9
10
11
location /api/ {
proxy_pass http://api:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location / {
try_files $uri $uri/ /index.html;
}

try_files 最后的 /index.html 也很关键:没有它,用户刷新 Vue Router 的前端路由时会得到 Nginx 404。

这套代理策略还有一个实际收益:浏览器看到的 Web 和 API 同源,生产环境一般不需要把内部服务名或 API 主机名写进前端镜像。

六、用 Docker Compose 部署生产环境

下面假设服务器已经安装 Docker 与 Compose,并且代码位于服务器本地。

1. 创建生产配置

1
cp .env.example .env

至少修改以下几项:

1
2
3
4
5
6
7
APP_ENV=production
POSTGRES_PASSWORD=请替换为高强度随机密码
DATABASE_URL=postgres://app:同一个数据库密码@postgres:5432/app?sslmode=disable
JWT_SECRET=请替换为至少32字符的高强度随机字符串
CORS_ORIGINS=https://your-domain.com
WEB_PORT=3000
API_PORT=8080

不要提交 .env。更不要继续使用示例中的数据库密码和 JWT Secret。若密码包含 @:/ 等 URL 特殊字符,需要先进行 URL 编码,或者使用不需要转义的高强度随机字符串。

启动前先让 Compose 展开并检查配置:

1
docker compose config

2. 构建并启动

1
make up

它等价于:

1
docker compose up --build -d

查看容器和日志:

1
2
docker compose ps
docker compose logs -f api web

验证 Web、Nginx 与后端依赖:

1
2
3
4
curl http://127.0.0.1:3000/nginx-health
curl http://127.0.0.1:3000/health/live
curl http://127.0.0.1:3000/health/ready
curl http://127.0.0.1:3000/api/v1/tasks

浏览器只需要访问 http://服务器IP:3000。生产 Web 容器由 Nginx 提供静态文件,并将 /api/health 转发到 Compose 网络中的 API 容器。

3. 为什么生产镜像更小?

Go Dockerfile 使用多阶段构建:

  1. golang:1.25-alpine 中下载依赖并编译;
  2. CGO_ENABLED=0 生成静态二进制;
  3. 最终只把二进制复制到非 root 的 Distroless 镜像。

Vue Dockerfile 也分为三步:

  1. npm ci 按 lockfile 安装依赖;
  2. 执行类型检查和 Vite 构建;
  3. 只把 dist 复制到 Nginx 镜像。

Node、npm、Go 编译器和源代码都不会进入最终运行镜像,既减少体积,也缩小了攻击面。

4. 配置域名与 HTTPS

模板中的 Nginx 负责站内静态资源和 API 代理,但没有替你申请 TLS 证书。公网部署时,可以在宿主机或同一 Docker 网络前再放一层 Caddy、Traefik、Nginx Proxy Manager,或者使用云厂商负载均衡。

外层反向代理只需要把域名转发到:

1
http://127.0.0.1:3000

确认域名可以访问后,再把 .env 中的 CORS_ORIGINS 改为实际的 HTTPS 域名并重新创建 API 容器:

1
docker compose up -d --force-recreate api

模板默认还将 API 的 8080 映射到宿主机,方便排查问题。公网服务器应使用防火墙阻止外部直连该端口,或者按实际需求移除 api.ports,让外部流量统一经过 Web 容器或网关。

七、数据持久化、升级与备份

Compose 使用命名卷保存 PostgreSQL 和 Redis 数据。普通的 docker compose down 不会删除数据,但下面这个命令会删除卷:

1
docker compose down -v

生产环境不要随手加 -v

更新项目前,先备份 PostgreSQL:

1
2
3
docker compose exec -T postgres sh -c \
'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' \
> backup.sql

先在 CI 或开发机运行 make test,确认 Go race tests、Vue 类型检查和生产构建全部通过。服务器只负责拉取已经验证的代码并重建容器:

1
2
3
git pull --ff-only
docker compose up --build -d
docker compose ps

模板在 AUTO_MIGRATE=true 时会在 API 启动阶段执行尚未应用的 SQL 迁移。正式项目仍应在升级前备份,并为破坏性 schema 变更设计可回滚方案。

八、几个常见问题

Go 在宿主机运行时为什么连不上数据库?

容器内可以用 postgres:5432redis:6379,因为它们是 Compose 服务名;宿主机运行 make api 时应改用 localhost:5432localhost:6379

另外,Go 程序不会自动读取 .env。用宿主机启动时,需要由 shell、IDE 或进程管理器注入环境变量。

修改了 Vue 环境变量,为什么页面没有变化?

VITE_ 开头的变量会在前端构建时写入静态文件。修改后需要重新构建 Web 镜像:

1
docker compose up --build -d web

密码、JWT Secret 等敏感信息绝对不能使用 VITE_ 前缀,因为进入浏览器的变量对用户可见。

可以切换到 MySQL 吗?

可以。后端已经包含 PostgreSQL 与 MySQL 两套驱动和迁移目录:

1
2
DB_DRIVER=mysql
DATABASE_URL=app:password@tcp(mysql-host:3306)/app?parseTime=true&charset=utf8mb4

不过默认 Compose 文件启动的是 PostgreSQL。切换 MySQL 时,还需要提供可访问的 MySQL 实例,或自行在 Compose 中增加 MySQL 服务。

Redis 目前做了什么?

模板已经完成 Redis 连接、关闭和就绪检查,为缓存、限流或会话留出了可靠入口;任务 CRUD 本身没有为了“展示技术栈”而强行缓存。真正出现读热点后,再为具体查询设计缓存键、TTL 和失效策略。

九、这个模板适合谁?

template-go-vue3-docker 比较适合这些场景:

  • 想快速启动 Go + Vue 3 前后端分离项目;
  • 需要 PostgreSQL、Redis、迁移、健康检查和 Docker 的基础设施;
  • 希望从一个真实 CRUD 学习完整请求链路;
  • 想保留简洁结构,再按业务添加 usersbillingjobs 等模块;
  • 需要通过 GitHub Actions 测试,并在 Release 后发布多架构 GHCR 镜像。

它不是“下载后直接交付”的万能后台:完整注册登录、权限模型、对象存储、监控告警、数据库备份、域名和 TLS 仍然要按项目需求完成。模板的价值,是把大量重复但容易出错的工程起点准备好,让你把时间放在真正的业务上。

开始自己的项目时,建议按以下顺序修改:

  1. 在 GitHub 使用模板或直接 Fork;
  2. 全局替换 Go module github.com/gloscai/template-go-vue3-docker/server
  3. 修改 web/package.json、页面品牌、镜像名和环境变量;
  4. 参考 tasks 创建第一个真实业务模块;
  5. 上线前替换全部密码与 JWT Secret,补齐 TLS、备份和监控。

总结

一个能长期维护的 Go + Vue 3 网站,不只由“Go 写接口、Vue 写页面”组成。更重要的是让配置有统一入口、让前后端类型和错误可追踪、让数据库变更可迁移、让容器有健康检查、让开发与生产使用相同的请求路径。

如果你准备做一个新的全栈项目,可以从 glosc-ai/template-go-vue3-docker 开始:先用 make dev 跑通完整链路,再沿着 tasks 的边界替换成自己的业务。欢迎 Star、Fork,也欢迎提交 Issue 或 Pull Request,一起把这个模板打磨得更实用。

投喂小莫
给快要饿死的小莫投喂点零食吧~
投喂小莫
分享
分享提示信息