news 2026/9/8 15:50:03

一个 Dockerfile 打包前端 + Go 后端:用 ShiyuAdmin 讲透真实项目容器化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一个 Dockerfile 打包前端 + Go 后端:用 ShiyuAdmin 讲透真实项目容器化

很多 Docker 教程都会从下面这几条命令开始:

dockerbuilddockerrundockerps

这些命令当然要会。

但真正到了项目里,问题通常会变成:

  • 前端和后端怎么一起打包?
  • PostgreSQL、Redis 怎么启动?
  • 数据库还没启动,后端就启动了怎么办?
  • 前端怎么访问后端 API?
  • 数据库容器删除以后,数据会不会丢?
  • 开发环境和生产环境怎么共用一套 Docker 配置?

这些问题,单独背 Docker 命令解决不了。

最近我维护的一个开源后台项目ShiyuAdmin,正好包含一套比较完整的 Docker 实践。

项目地址:

https://github.com/Rodert/ShiyuAdmin

今天就不背概念了,直接拿这个真实项目拆 Docker。


一、整个 Docker 架构是什么样的?

ShiyuAdmin 最终运行时主要有三个容器:

┌──────────────────┐ │ Browser │ └────────┬─────────┘ │ localhost:18000 │ ┌────────▼─────────┐ │ shiyu-app │ │ │ │ Nginx │ │ ┌────────┐ │ │ │ React │ │ │ └────────┘ │ │ │ │ │ /api ▼ │ │ Go Backend │ └──────┬────┬──────┘ │ │ ┌────────┘ └────────┐ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ PostgreSQL │ │ Redis │ │ 5432 │ │ 6379 │ └──────────────┘ └──────────────┘

对应 Docker Compose 中:

services:shiyu-postgres:image:postgres:15shiyu-redis:image:redis:7shiyu-app:image:ghcr.io/rodert/shiyuadmin:latest

这里已经体现了 Docker 一个非常重要的思想:

一个完整系统,不等于一个容器。

数据库是一个服务,Redis 是一个服务,业务系统也是一个服务。

Docker Compose 就负责把它们组织起来。


二、Dockerfile 最值得学的:多阶段构建

ShiyuAdmin 的 Dockerfile 不是简单地:

FROM ubuntu COPY . . RUN xxx

而是使用了Multi-stage Build,多阶段构建

整个过程分成三个阶段:

Node ↓ 构建 React 前端 Go ↓ 编译 Go 后端 Nginx ↓ 最终运行镜像

先看第一阶段。

FROM node:20-alpine AS frontend-builder WORKDIR /app COPY frontend/shiyu-admin-web/package.json ./ RUN npm install \ --legacy-peer-deps \ --no-audit \ --loglevel=error COPY frontend/shiyu-admin-web/ ./ RUN npm run build

它的任务非常单纯:

把前端项目编译成静态文件。

最终会得到:

dist/

这里有一个很容易被忽略的小细节:

COPY package.json ./ RUN npm install COPY frontend/ ./

为什么不直接:

COPY frontend/ ./ RUN npm install

因为 Docker 有Layer Cache

package.json 没变化的时候:

npm install

这一层可以直接使用缓存。

你只是改了:

src/App.tsx

Docker 没必要重新下载所有 npm 依赖。

这就是 Dockerfile 优化中非常经典的一种方式:

先复制依赖描述文件 ↓ 安装依赖 ↓ 再复制业务代码

三、第二阶段:编译 Go 后端

接下来是 Go:

FROM golang:1.23-alpine AS backend-builder WORKDIR /app ENV GOPROXY=https://proxy.golang.org,direct RUN apk add --no-cache gcc musl-dev sqlite-dev COPY backend/shiyu-admin-backend/go.mod \ backend/shiyu-admin-backend/go.sum ./ RUN go mod download COPY backend/shiyu-admin-backend/ ./ RUN CGO_ENABLED=1 GOOS=linux \ go build \ -trimpath \ -ldflags='-s -w' \ -o /server \ ./cmd/server

这里其实和前端是同样的思路。

先:

COPY go.mod go.sum ./ RUN go mod download

再:

COPY backend/ ./

原因还是 Docker 缓存。

只要:

go.mod go.sum

没发生变化:

go mod download

这一层就有机会直接复用。

对于大型 Go 项目来说,重新下载依赖浪费的时间还是很多的。


四、为什么 Go 编译要写 CGO_ENABLED=1?

这里还有一句:

CGO_ENABLED=1GOOS=linux go build

很多 Go 项目里更常见的是:

CGO_ENABLED=0

因为这样容易编译成完全静态的二进制文件。

但是 ShiyuAdmin 同时考虑了 SQLite 场景,所以 Dockerfile 安装了:

gcc musl-dev sqlite-dev

运行环境里也安装了:

sqlite-libs

这就是一个非常典型的例子:

Dockerfile 怎么写,不是看网上模板,而是看项目真正依赖什么。

如果项目依赖 CGO,那么简单复制:

CGO_ENABLED=0

反而可能直接编译失败或者运行异常。


五、第三阶段才是真正的生产镜像

前面的 Node 和 Go 镜像都只是:

builder

真正运行程序的是:

FROM nginx:alpine

然后把前两个阶段产生的结果复制进来。

前端:

COPY --from=frontend-builder \ /app/dist \ /usr/share/nginx/html

后端:

COPY --from=backend-builder \ /server \ /app/server

配置文件:

COPY --from=backend-builder \ /app/configs \ /app/configs

这样最终镜像里根本不需要:

完整 Node.js 开发环境 完整 Go 编译器 项目全部源码 npm 编译工具链 Go 编译工具链

它真正需要的只是:

Nginx 前端 dist Go 二进制程序 配置文件 运行库

这就是多阶段构建最大的价值之一。


六、为什么不把 Node 和 Go 都塞进最终镜像?

假设不用多阶段构建,直接:

FROM node RUN 安装 Go RUN 安装 Nginx RUN npm install RUN go build COPY 所有源码

虽然也可能跑起来,但是最终镜像会非常臃肿。

因为生产环境根本不需要 Go 编译器。

也不需要:

node_modules npm 源码 编译工具

Multi-stage Build 的思想其实非常简单:

构建环境 ≠ 运行环境

构建的时候可以很重。

运行的时候应该尽可能简单。


七、一个容器里面为什么同时有 Nginx 和 Go?

ShiyuAdmin 的最终容器里面有两个进程:

Nginx Go Backend

Nginx 负责:

前端静态资源 + API 反向代理

Go 负责业务 API。

Nginx 配置核心就是:

location /api { proxy_pass http://127.0.0.1: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; }

浏览器请求:

http://localhost:18000/api/v1/user

首先进入 Docker:

宿主机 18000 ↓ 容器 80 ↓ Nginx ↓ 127.0.0.1:8080 ↓ Go Backend

这样前端和后端就可以统一使用一个入口。

用户根本不需要知道 Go 实际运行在:

8080

八、React 刷新 404 怎么解决?

Nginx 里还有一句非常重要:

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

如果 React 使用前端路由:

/user/list /system/config /dashboard

用户直接刷新:

/dashboard

Nginx 会尝试找:

/usr/share/nginx/html/dashboard

当然找不到。

于是就会出现:

404 Not Found

通过:

try_files $uri /index.html;

找不到真实文件的时候返回:

index.html

然后让 React Router 自己处理路由。

这是前端 SPA 部署到 Nginx 时非常常见的配置。


九、一个容器跑两个进程,谁负责管理?

这里又出现一个东西:

Supervisor

Dockerfile 最后不是:

CMD ["nginx"]

也不是:

CMD ["/app/server"]

而是:

CMD [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ]

Supervisor 再分别启动:

[program:backend] command=/app/server autostart=true autorestart=true

以及:

[program:nginx] command=/usr/sbin/nginx -g "daemon off;" autostart=true autorestart=true

于是变成:

Docker │ ▼ Supervisor ├── Go Backend └── Nginx

如果 Go 后端异常退出:

autorestart=true

Supervisor 可以重新启动它。

这是这个项目里一个比较有意思的 Docker 设计。


十、Docker Compose 才是整个项目真正的启动入口

单独有 Dockerfile 还不够。

因为我们的系统还需要:

PostgreSQL Redis

于是使用:

docker-compose.yml

统一编排。

PostgreSQL:

shiyu-postgres:image:postgres:15environment:POSTGRES_DB:shiyu_admin_scaffoldPOSTGRES_USER:shiyuPOSTGRES_PASSWORD:shiyu123ports:-"15432:5432"

Redis:

shiyu-redis:image:redis:7ports:-"16379:6379"

应用:

shiyu-app:image:${SHIYU_IMAGE:-ghcr.io/rodert/shiyuadmin:latest}ports:-"18000:80"

最终我们只需要:

dockercompose up-d

整个环境就可以启动。


十一、15432:5432 到底是什么意思?

Docker 新手最容易混淆的就是:

ports:-"15432:5432"

记一个规则:

宿主机端口:容器端口

也就是:

localhost:15432 ↓ PostgreSQL 容器 5432

Redis:

-"16379:6379"

表示:

localhost:16379 ↓ Redis 6379

应用:

-"18000:80"

表示:

localhost:18000 ↓ Nginx 80

因此浏览器最终访问:

http://localhost:18000

十二、为什么容器之间不应该使用 localhost?

这是另一个非常容易踩坑的问题。

假设 Go 后端要连接 PostgreSQL。

很多人会写:

localhost:5432

但是在 Docker 里面:

localhost

代表的是:

当前这个容器自己

Go 在:

shiyu-app

数据库却在:

shiyu-postgres

它们不是一个容器。

所以应该通过 Docker Compose 的 Service Name 通信:

shiyu-postgres:5432

Redis 同理:

shiyu-redis:6379

Compose 中还定义了:

networks:shiyu-network:driver:bridge

三个容器加入同一个 Docker Network 后,就可以通过服务名称互相发现。

可以把它理解成 Docker 自带了一个内部 DNS。

shiyu-app │ ├── shiyu-postgres │ └── shiyu-redis

这也是为什么在容器里一般不需要记数据库 IP 地址。


十三、depends_on 并不只是控制启动顺序

ShiyuAdmin 的 Compose 还有一个很值得学习的配置:

depends_on:shiyu-postgres:condition:service_healthyshiyu-redis:condition:service_healthy

什么意思?

应用启动之前:

PostgreSQL Redis

必须先通过健康检查。

PostgreSQL:

healthcheck:test:["CMD-SHELL","pg_isready -U shiyu -d shiyu_admin_scaffold"]interval:10stimeout:5sretries:5

Redis:

healthcheck:test:["CMD","redis-cli","ping"]interval:10stimeout:5sretries:5

这解决了一个很经典的问题:

Docker:数据库容器已经启动了 实际: PostgreSQL:我还在初始化……

如果后端马上连接数据库:

connection refused

就可能出现了。

所以真正重要的不是:

容器启动

而是:

服务 Ready

十四、应用本身也有健康检查

应用容器同样配置:

healthcheck:test:["CMD-SHELL","wget--quiet--tries=1--spider \ http://127.0.0.1/api/v1/system/health \||exit 1"]interval:30stimeout:10sretries:3start_period:40s

它不是简单判断:

进程还活着

而是真正访问:

/api/v1/system/health

只有 API 正常返回,才算:

healthy

这个思路在:

Docker Compose Kubernetes CI/CD 负载均衡 自动部署

里都非常重要。


十五、数据库容器删了,数据怎么办?

数据库最不能接受的事情就是:

dockerrm

然后:

数据没了

所以 PostgreSQL 使用了 Volume:

volumes:-shiyu-pg-data:/var/lib/postgresql/data

Redis:

volumes:-shiyu-redis-data:/data

底部定义:

volumes:shiyu-pg-data:driver:localshiyu-redis-data:driver:local

于是:

Container 生命周期 ≠ Data 生命周期

即使容器被重新创建:

dockercompose downdockercompose up-d

Volume 仍然存在。

数据也就还在。

但注意:

dockercompose down-v

这里多了:

-v

就代表连 Volume 一起删除。

数据库数据也会被清理。

生产环境执行这个命令之前一定要确认清楚。


十六、restart: unless-stopped 有什么用?

三个核心服务都配置了:

restart:unless-stopped

意思可以简单理解成:

容器异常退出或者 Docker 重启之后,自动尝试重新运行。

比如服务器重启:

Linux 重启 ↓ Docker 启动 ↓ PostgreSQL Redis ShiyuAdmin 自动恢复

但是如果你明确手工把它停止:

dockerstop shiyu-app

Docker 会尊重这个行为。

这就是:

unless-stopped

这个名字的来源。


十七、开发环境和生产环境怎么共用 Compose?

这个项目还有一个很实用的设计:

docker-compose.yml docker-compose.local.yml

生产环境:

image:${SHIYU_IMAGE:-ghcr.io/rodert/shiyuadmin:latest}

直接拉已经构建好的镜像。

而本地开发:

services:shiyu-app:build:context:.dockerfile:Dockerfileimage:shiyu-admin:localvolumes:-./backend/shiyu-admin-backend/configs:/app/configs:ro

然后:

dockercompose\-fdocker-compose.yml\-fdocker-compose.local.yml\up-d--build

Docker Compose 会把两个文件合并。

可以理解成:

docker-compose.yml + docker-compose.local.yml ↓ 最终配置

主配置负责:

PostgreSQL Redis 网络 Volume 端口 健康检查

local 文件只覆盖:

shiyu-app 镜像来源

从:

GHCR

变成:

本地 Dockerfile 构建

这种方式比维护:

docker-compose.dev.yml docker-compose.test.yml docker-compose.prod.yml

三份大量重复配置要舒服很多。


十八、实际把项目跑起来

首先克隆项目:

gitclone https://github.com/Rodert/ShiyuAdmin.gitcdShiyuAdmin

如果直接使用已经发布的镜像:

dockercompose up-d

查看:

dockercomposeps

如果需要从当前源码重新构建:

dockercompose\-fdocker-compose.yml\-fdocker-compose.local.yml\up-d\--build

启动完成以后:

http://localhost:18000

即可访问应用。


十九、Docker 项目排错,先学会这几个命令

Docker 出问题以后,我最常用的并不是重装 Docker,而是先看:

dockercomposeps

如果发现:

shiyu-app

异常:

dockercompose logs shiyu-app

实时查看:

dockercompose logs-fshiyu-app

查看 PostgreSQL:

dockercompose logs-fshiyu-postgres

查看 Redis:

dockercompose logs-fshiyu-redis

进入应用容器:

dockerexec-itshiyu-appsh

进入 PostgreSQL:

dockerexec-itshiyu-postgres\psql\-Ushiyu\-dshiyu_admin_scaffold

查看 Docker 网络:

dockernetworkls

查看 Volume:

dockervolumels

这几个命令基本已经可以覆盖相当一部分 Docker 项目的日常排查。


二十、这套 Docker 配置还能继续优化什么?

目前这套配置作为开源项目和快速部署方案已经比较完整了。

如果真正用于生产环境,我还会继续做几件事。

首先,数据库密码不要直接硬编码:

POSTGRES_PASSWORD:shiyu123

可以改为:

POSTGRES_PASSWORD:${POSTGRES_PASSWORD}

然后放到:

.env

或者更专业的 Secret 管理系统里。

其次,生产环境可以不把 PostgreSQL:

5432

映射到宿主机。

如果只有:

shiyu-app

需要访问数据库,完全可以只通过 Docker 内网通信。

Redis 同理。

另外还可以继续增加:

CPU / Memory 限制 日志轮转 HTTPS 数据库备份 监控 告警 CI/CD 镜像版本固定 自动发布

再往后发展,其实就会慢慢进入:

Docker Compose ↓ CI/CD ↓ Kubernetes

这一整套工程化体系。


最后

Docker 真正难的地方,从来不是记住:

dockerpsdockerimagesdockerrun

而是理解:

程序 ↓ 镜像 ↓ 容器 ↓ 网络 ↓ Volume ↓ 服务编排 ↓ 健康检查 ↓ 部署

ShiyuAdmin 这个项目虽然规模不算特别庞大,但里面已经包含了不少非常典型的 Docker 实战知识:

Multi-stage Build Docker Layer Cache Node 前端构建 Go 二进制构建 Nginx 静态资源服务 Nginx API 反向代理 Supervisor 多进程管理 Docker Compose PostgreSQL Redis Docker Network Volume Healthcheck depends_on restart policy Compose override

所以如果你正在学 Docker,与其继续背几十条命令,不如直接找一个这样的完整项目:

gitclone https://github.com/Rodert/ShiyuAdmin.gitcdShiyuAdmindockercompose up-d

先把它跑起来。

然后一层一层把 Dockerfile 和 docker-compose.yml 拆开。

你会发现:

Docker 真正值得学的,不是怎么启动一个容器,而是怎么把一个完整的软件系统装进容器里,并且稳定地跑起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 15:47:37

res-downloader 十分钟实战:把视频号、抖音的视频快速存到本地

res-downloader 十分钟实战:把视频号、抖音的视频快速存到本地 【免费下载链接】res-downloader 视频号、小程序、抖音、快手、小红书、直播流、m3u8、酷狗、QQ音乐等常见网络资源下载! 项目地址: https://gitcode.com/GitHub_Trending/re/res-downloader re…

作者头像 李华
网站建设 2026/9/8 15:45:24

opencode 终端 AI 编程代理:从免费模型接入到 IDE 插件实战

最近一直在折腾终端里的 AI 编程助手,从早期的 Claude Code、Codex CLI 一路试过来,最后在一个开源项目里彻底停在了 opencode 上。一句话介绍:opencode 是一个跑在终端里的开源 AI 编码代理,它可以直接读你的项目代码、改文件、执…

作者头像 李华
网站建设 2026/9/8 15:44:49

智能任务自动化协同AI工作流技术文档

智能任务自动化协同AI工作流技术文档 1. 概述 智能任务自动化协同AI工作流,旨在打通多环节业务任务,依靠大模型能力实现任务解析、分发、执行、校验、结果汇总全链路自动化。该工作流支持多节点协同,可适配文本处理、数据解析、内容生成、结果…

作者头像 李华