Перейти к основному содержимому

Docker и деплой Go-приложений

Multi-stage сборка

# Stage 1: Сборка
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Копируем зависимости
COPY go.mod go.sum ./
RUN go mod download

# Копируем код и собираем
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

# Stage 2: Production образ
FROM alpine:latest

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

WORKDIR /home/appuser

# Копируем бинарник
COPY --from=builder /app/app .

# Копируем конфигурацию (если есть)
# COPY config.yaml .

EXPOSE 8080

CMD ["./app"]

Альтернатива: scratch (минимальный образ)

FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -o app .

FROM scratch

COPY --from=builder /app/app /app
COPY --from=builder /etc/passwd /etc/passwd
# COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

EXPOSE 8080

USER 1000:1000

CMD ["/app"]

docker-compose.yml

version: '3.8'

services:
app:
build: .
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/app
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3

db:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped

cache:
image: redis:7-alpine
restart: unless-stopped

volumes:
postgres_data:

Настройка Go-приложения для Docker

package main

import (
"database/sql"
"fmt"
"log"
"net/http"
"os"
"time"

_ "github.com/lib/pq"
)

type Config struct {
Port int
DatabaseURL string
RedisURL string
Environment string
ReadTimeout time.Duration
WriteTimeout time.Duration
}

func loadConfig() *Config {
return &Config{
Port: getEnvInt("PORT", 8080),
DatabaseURL: os.Getenv("DATABASE_URL"),
RedisURL: os.Getenv("REDIS_URL"),
Environment: getEnv("ENVIRONMENT", "development"),
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
}

func getEnv(key, defaultValue string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return defaultValue
}

func getEnvInt(key string, defaultValue int) int {
if value, ok := os.LookupEnv(key); ok {
if parsed, err := fmt.Sscanf(value, "%d"); parsed == 1 && err == nil {
return parsed
}
}
return defaultValue
}

func main() {
cfg := loadConfig()

// Подключение к БД
db, err := sql.Open("postgres", cfg.DatabaseURL)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()

// Health check
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
if err := db.Ping(); err != nil {
http.Error(w, "DB unhealthy", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})

// Graceful shutdown
// ... (см. предыдущий урок)

log.Printf("Starting server on :%d", cfg.Port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", cfg.Port), nil))
}

K8s Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
name: go-app
labels:
app: go-app
spec:
replicas: 3
selector:
matchLabels:
app: go-app
template:
metadata:
labels:
app: go-app
spec:
containers:
- name: go-app
image: your-registry/go-app:latest
ports:
- containerPort: 8080
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
- name: PORT
value: "8080"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5

---
apiVersion: v1
kind: Service
metadata:
name: go-app
spec:
selector:
app: go-app
ports:
- port: 80
targetPort: 8080
type: ClusterIP

CI/CD пример (GitHub Actions)

name: Go CI/CD

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'

- name: Cache Go modules
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-

- name: Build
run: go build -v ./...

- name: Test
run: go test -v -race ./...

- name: Lint
uses: golangci/golangci-lint-action@v3
with:
version: latest

- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name == 'push' }}
tags: ${{ secrets.DOCKER_REGISTRY }}/go-app:${{ github.sha }}

- name: Deploy to K8s
if: github.ref == 'refs/heads/main'
run: |
kubectl set image deployment/go-app go-app=${{ secrets.DOCKER_REGISTRY }}/go-app:${{ github.sha }} -n default

Environment variables в production

package main

import (
"fmt"
"os"
"strconv"
"time"
)

type Config struct {
// Обязательные переменные
DatabaseURL string `required:"true"`

// Опциональные с defaults
Port int `default:"8080"`
Environment string `default:"development"`
LogLevel string `default:"info"`

// Тайм-ауты
RequestTimeout time.Duration `default:"30s"`
ShutdownTimeout time.Duration `default:"10s"`
}

func LoadConfig() (*Config, error) {
cfg := &Config{
Port: getEnvInt("PORT", cfg.Port),
Environment: getEnv("ENVIRONMENT", cfg.Environment),
LogLevel: getEnv("LOG_LEVEL", cfg.LogLevel),
RequestTimeout: getEnvDuration("REQUEST_TIMEOUT", cfg.RequestTimeout),
DatabaseURL: os.Getenv("DATABASE_URL"),
}

// Валидация
if cfg.DatabaseURL == "" {
return nil, fmt.Errorf("DATABASE_URL is required")
}

return cfg, nil
}

func getEnv(key, defaultValue string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultValue
}

func getEnvInt(key string, defaultValue int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return defaultValue
}

func getEnvDuration(key string, defaultValue time.Duration) time.Duration {
if v := os.Getenv(key); v != "" {
if d, err := time.ParseDuration(v); err == nil {
return d
}
}
return defaultValue
}

Итоги

ТехнологияНазначение
Multi-stage buildУменьшение размера образа
scratchМинимальный образ
docker-composeЛокальная разработка
K8sОркестрация в production
GitHub ActionsCI/CD пайплайн