阶段2(下):运维专题 Docker/Nginx/Linux/CI-CD/Git 的课程、企业场景、面试题和可运行示例

This commit is contained in:
Shikui Wang committed 2026-10-23 15:36:06 +08:00
1 parent 5eb67c5d08
commit b8010a74f3
42 files changed
+3533 -55

No files matched your search

@@ -0,0 +1,4 @@
.git
*.md
*_test.go
compose.yaml
@@ -0,0 +1,19 @@
# syntax=docker/dockerfile:1
# ---------- 构建阶段:带完整 Go 工具链 ----------
FROM golang:1.26-alpine AS build
WORKDIR /src
# 先只复制依赖描述文件,依赖不变时这一层可以复用缓存
COPY go.mod ./
RUN go mod download
COPY . .
# 静态编译,去掉调试信息减小体积
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/api .
# ---------- 运行阶段:只放二进制 ----------
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/api /api
ENV PORT=8080
EXPOSE 8080
USER nonroot:nonroot
# exec 形式,进程是 PID 1,能直接收到 SIGTERM
ENTRYPOINT ["/api"]
@@ -0,0 +1,36 @@
# 本地开发环境:docker compose up -d --build
# 镜像版本仅作示例,按项目需要调整
services:
api:
build: .
ports:
- "8080:8080"
environment:
DATABASE_URL: postgres://app:app@db:5432/app?sslmode=disable # 服务名 db 就是主机名
REDIS_URL: redis://redis:6379/0
depends_on:
db:
condition: service_healthy # 等数据库健康检查通过再启动
redis:
condition: service_started
restart: unless-stopped
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app # 仅限本地开发;生产用密钥管理
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data # 命名卷:容器删除后数据仍在
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7-alpine
volumes:
pgdata:
@@ -0,0 +1,3 @@
module example/goapi
go 1.22
@@ -0,0 +1,54 @@
// 一个最小的 Go HTTP 服务,用来演示多阶段构建、健康检查和优雅退出。
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func newMux() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
mux.HandleFunc("GET /api/hello", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message":"hello from container"}`))
})
return mux
}
func main() {
addr := ":" + getenv("PORT", "8080")
srv := &http.Server{Addr: addr, Handler: newMux(), ReadHeaderTimeout: 5 * time.Second}
// 容器停止时 Docker 先发 SIGTERM,等待一段时间(默认 10 秒)后才 SIGKILL
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
log.Printf("listening on %s", addr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { // 不再接新请求,等正在处理的请求完成
log.Printf("shutdown: %v", err)
}
log.Println("bye")
}
func getenv(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
@@ -0,0 +1,22 @@
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestRoutes(t *testing.T) {
srv := httptest.NewServer(newMux())
defer srv.Close()
for path, want := range map[string]int{"/healthz": 200, "/api/hello": 200, "/nope": 404} {
resp, err := http.Get(srv.URL + path)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != want {
t.Errorf("%s = %d, want %d", path, resp.StatusCode, want)
}
}
}