Docker Best Practices: Dockerfile и docker-compose для веб-сервисов
Правильная контейнеризация веб-сервисов — это искусство, требующее понимания не только Docker, но и специфики каждой технологии в стеке. В этой статье мы рассмотрим лучшие практики создания Dockerfile и docker-compose.yaml для современного веб-стека.
Общие принципы лучших практик Docker
1. Принцип единственной ответственности
Каждый контейнер должен решать одну задачу и делать её хорошо.
2. Минимизация размера образов
Используйте alpine образы, многостадийную сборку и .dockerignore.
3. Безопасность превыше всего
Не запускайте процессы от root, сканируйте на уязвимости, используйте секреты.
4. Оптимизация кэширования слоев
Структурируйте Dockerfile для максимального переиспользования слоев.
Правильный порядок инструкций в Dockerfile может сократить время сборки в разы за счет кэширования Docker слоев.
Базовые практики Dockerfile
Оптимизация порядка инструкций
# ❌ Плохо - зависимости пересобираются при каждом изменении кода
FROM node:18-alpine
COPY . /app
WORKDIR /app
RUN npm install
CMD ["npm", "start"]
# ✅ Хорошо - зависимости кэшируются
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["npm", "start"]
Использование .dockerignore
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.env.local
coverage
.coverage
.nyc_output
.cache
.next
.nuxt
dist
.DS_Store
Thumbs.db
*.log
.vscode
.idea
docker-compose*.yml
Dockerfile*
Многостадийная сборка
# Dockerfile.multistage
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine AS production
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
WORKDIR /app
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
Лучшие практики по технологиям
JavaScript/Node.js Service
# Dockerfile.nodejs
FROM node:18-alpine AS base
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create app directory
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Development stage
FROM base AS development
ENV NODE_ENV=development
COPY package*.json ./
RUN npm install
COPY . .
RUN chown -R nodejs:nodejs /app
USER nodejs
EXPOSE 3000
CMD ["dumb-init", "npm", "run", "dev"]
# Build stage
FROM base AS builder
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
RUN npm run build && \
npm run test -- --coverage --watchAll=false
# Production stage
FROM base AS production
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public
RUN chown -R nodejs:nodejs /app
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["dumb-init", "node", "dist/server.js"]
JavaScript с линтерами и тестами
# Dockerfile.js-full
FROM node:18-alpine AS base
RUN apk add --no-cache dumb-init git
WORKDIR /app
# Dependencies stage
FROM base AS dependencies
COPY package*.json ./
RUN npm ci && npm cache clean --force
# Linting stage
FROM dependencies AS linting
COPY . .
RUN npm run lint && \
npm run type-check
# Testing stage
FROM dependencies AS testing
COPY . .
RUN npm run test -- --coverage --watchAll=false --ci
RUN npm run test:e2e -- --ci
# Build stage
FROM dependencies AS builder
COPY . .
RUN npm run build
# Production stage
FROM base AS production
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
USER nodejs
EXPOSE 3000
HEALTHCHECK CMD curl -f http://localhost:3000/health || exit 1
CMD ["dumb-init", "node", "dist/index.js"]
PHP Service с тестами
# Dockerfile.php
FROM php:8.2-fpm-alpine AS base
# Install system dependencies
RUN apk add --no-cache \
git \
curl \
libpng-dev \
libxml2-dev \
zip \
unzip \
oniguruma-dev \
icu-dev \
freetype-dev \
libjpeg-turbo-dev
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg && \
docker-php-ext-install \
pdo_mysql \
mbstring \
exif \
pcntl \
bcmath \
gd \
intl \
opcache
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Create non-root user
RUN addgroup -g 1000 www && \
adduser -u 1000 -G www -s /bin/sh -D www
WORKDIR /var/www
# Development stage
FROM base AS development
# Install Xdebug for development
RUN pecl install xdebug && \
docker-php-ext-enable xdebug
COPY php.ini-development /usr/local/etc/php/php.ini
COPY xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini
COPY --chown=www:www . .
RUN composer install --dev --no-scripts
USER www
CMD ["php-fpm"]
# Testing stage
FROM base AS testing
COPY --chown=www:www . .
RUN composer install --dev --no-scripts --no-autoloader && \
composer dump-autoload --optimize
# Run tests
RUN ./vendor/bin/phpunit --configuration phpunit.xml --coverage-text && \
./vendor/bin/phpstan analyse --level=8 src/ && \
./vendor/bin/php-cs-fixer fix --dry-run --diff
# Production stage
FROM base AS production
# Production PHP configuration
COPY php.ini-production /usr/local/etc/php/php.ini
COPY opcache.ini /usr/local/etc/php/conf.d/opcache.ini
COPY --chown=www:www composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts
COPY --chown=www:www . .
RUN composer dump-autoload --optimize --no-dev
USER www
EXPOSE 9000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD php-fpm-healthcheck || exit 1
CMD ["php-fpm"]
Go Service
# Dockerfile.go
FROM golang:1.21-alpine AS base
RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /app
# Dependencies stage
FROM base AS dependencies
COPY go.mod go.sum ./
RUN go mod download && go mod verify
# Development stage
FROM dependencies AS development
RUN go install github.com/cosmtrek/air@latest
COPY . .
EXPOSE 8080
CMD ["air", "-c", ".air.toml"]
# Testing stage
FROM dependencies AS testing
COPY . .
# Run linting
RUN go install golang.org/x/tools/cmd/goimports@latest && \
go install honnef.co/go/tools/cmd/staticcheck@latest && \
gofmt -l . && \
goimports -l . && \
staticcheck ./...
# Run tests with race detection
RUN CGO_ENABLED=1 go test -race -coverprofile=coverage.out ./... && \
go tool cover -html=coverage.out -o coverage.html
# Build stage
FROM dependencies AS builder
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags='-w -s -extldflags "-static"' \
-a -installsuffix cgo \
-o main ./cmd/server
# Production stage
FROM scratch AS production
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /app/main /main
USER 1000:1000
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/main", "healthcheck"]
CMD ["/main"]
Nginx Service
# Dockerfile.nginx
FROM nginx:1.25-alpine AS base
# Install additional tools
RUN apk add --no-cache \
curl \
tzdata
# Remove default nginx website
RUN rm /etc/nginx/conf.d/default.conf
# Development stage
FROM base AS development
COPY nginx/dev.conf /etc/nginx/conf.d/
COPY nginx/nginx.conf /etc/nginx/
EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"]
# Production stage
FROM base AS production
# Security headers and optimizations
COPY nginx/prod.conf /etc/nginx/conf.d/
COPY nginx/nginx.conf /etc/nginx/
COPY nginx/ssl-params.conf /etc/nginx/
# Create non-root user
RUN addgroup -g 101 -S nginx && \
adduser -S -D -H -u 101 -h /var/cache/nginx -s /sbin/nologin -G nginx nginx
# Copy static files
COPY --chown=nginx:nginx dist/ /usr/share/nginx/html/
EXPOSE 80 443
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/health || exit 1
CMD ["nginx", "-g", "daemon off;"]
Конфигурация docker-compose
Базовый docker-compose.yml
# docker-compose.yml
version: '3.8'
x-common-variables: &common-variables
NODE_ENV: ${NODE_ENV:-production}
DATABASE_URL: mysql://user:password@mysql:3306/app
REDIS_URL: redis://redis:6379
JWT_SECRET: ${JWT_SECRET}
services:
nginx:
build:
context: ./nginx
dockerfile: Dockerfile
target: ${BUILD_TARGET:-production}
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./ssl:/etc/nginx/ssl
- static_files:/usr/share/nginx/html
depends_on:
- frontend
- api
restart: unless-stopped
networks:
- web
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
target: ${BUILD_TARGET:-production}
args:
- NODE_ENV=${NODE_ENV:-production}
- API_URL=${API_URL:-http://api:3000}
environment:
<<: *common-variables
volumes:
- static_files:/app/dist
depends_on:
- api
restart: unless-stopped
networks:
- web
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
api:
build:
context: ./api
dockerfile: Dockerfile
target: ${BUILD_TARGET:-production}
environment:
<<: *common-variables
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- web
- internal
volumes:
- ./api/storage:/app/storage
secrets:
- db_password
- jwt_secret
php-app:
build:
context: ./php
dockerfile: Dockerfile
target: ${BUILD_TARGET:-production}
environment:
- DB_HOST=mysql
- DB_DATABASE=app
- DB_USERNAME=user
- DB_PASSWORD_FILE=/run/secrets/db_password
- REDIS_HOST=redis
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- ./php/storage:/var/www/storage
- ./php/bootstrap/cache:/var/www/bootstrap/cache
restart: unless-stopped
networks:
- internal
secrets:
- db_password
go-service:
build:
context: ./go-service
dockerfile: Dockerfile
target: ${BUILD_TARGET:-production}
environment:
- DATABASE_URL=mysql://user:password@mysql:3306/app
- REDIS_URL=redis://redis:6379
- PORT=8080
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- internal
secrets:
- db_password
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
MYSQL_DATABASE: app
MYSQL_USER: user
MYSQL_PASSWORD_FILE: /run/secrets/db_password
volumes:
- mysql_data:/var/lib/mysql
- ./mysql/init:/docker-entrypoint-initdb.d
- ./mysql/conf.d:/etc/mysql/conf.d
restart: unless-stopped
networks:
- internal
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$(cat /run/secrets/db_root_password)"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
secrets:
- db_password
- db_root_password
command: --default-authentication-plugin=mysql_native_password
redis:
image: redis:7.2-alpine
command: redis-server --appendonly yes --requirepass $${REDIS_PASSWORD}
environment:
- REDIS_PASSWORD_FILE=/run/secrets/redis_password
volumes:
- redis_data:/data
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf
restart: unless-stopped
networks:
- internal
healthcheck:
test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "$$(cat /run/secrets/redis_password)", "ping"]
interval: 10s
timeout: 5s
retries: 3
secrets:
- redis_password
volumes:
mysql_data:
driver: local
redis_data:
driver: local
static_files:
driver: local
networks:
web:
driver: bridge
internal:
driver: bridge
internal: true
secrets:
db_password:
file: ./secrets/db_password.txt
db_root_password:
file: ./secrets/db_root_password.txt
redis_password:
file: ./secrets/redis_password.txt
jwt_secret:
file: ./secrets/jwt_secret.txt
Разработческий docker-compose.dev.yml
# docker-compose.dev.yml
version: '3.8'
services:
nginx:
build:
target: development
volumes:
- ./nginx/dev.conf:/etc/nginx/conf.d/default.conf
ports:
- "8080:80"
frontend:
build:
target: development
environment:
- NODE_ENV=development
- CHOKIDAR_USEPOLLING=true
volumes:
- ./frontend:/app
- /app/node_modules
ports:
- "3000:3000"
command: npm run dev
api:
build:
target: development
environment:
- NODE_ENV=development
- DEBUG=app:*
volumes:
- ./api:/app
- /app/node_modules
ports:
- "3001:3000"
- "9229:9229" # Node.js debugger
command: npm run dev:debug
php-app:
build:
target: development
environment:
- APP_ENV=local
- APP_DEBUG=true
- XDEBUG_MODE=debug
- XDEBUG_CONFIG=client_host=host.docker.internal client_port=9003
volumes:
- ./php:/var/www
- ./php/docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
ports:
- "9003:9003" # Xdebug port
go-service:
build:
target: development
environment:
- GO_ENV=development
- CGO_ENABLED=1
volumes:
- ./go-service:/app
ports:
- "8080:8080"
- "2345:2345" # Delve debugger
command: air -c .air.toml
mysql:
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=rootpass
- MYSQL_PASSWORD=password
volumes:
- mysql_dev_data:/var/lib/mysql
redis:
ports:
- "6379:6379"
command: redis-server --appendonly yes
volumes:
- redis_dev_data:/data
# Development tools
mailhog:
image: mailhog/mailhog
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI
networks:
- internal
phpmyadmin:
image: phpmyadmin:latest
environment:
PMA_HOST: mysql
PMA_USER: root
PMA_PASSWORD: rootpass
ports:
- "8081:80"
depends_on:
- mysql
networks:
- internal
redis-commander:
image: rediscommander/redis-commander:latest
environment:
- REDIS_HOSTS=local:redis:6379
ports:
- "8082:8081"
depends_on:
- redis
networks:
- internal
volumes:
mysql_dev_data:
redis_dev_data:
Тестовый docker-compose.test.yml
# docker-compose.test.yml
version: '3.8'
services:
frontend-test:
build:
context: ./frontend
dockerfile: Dockerfile
target: testing
environment:
- NODE_ENV=test
- CI=true
volumes:
- ./frontend/coverage:/app/coverage
command: npm run test:ci
api-test:
build:
context: ./api
dockerfile: Dockerfile
target: testing
environment:
- NODE_ENV=test
- DATABASE_URL=mysql://test:test@mysql-test:3306/test_db
- REDIS_URL=redis://redis-test:6379
depends_on:
- mysql-test
- redis-test
volumes:
- ./api/coverage:/app/coverage
command: npm run test:integration
php-test:
build:
context: ./php
dockerfile: Dockerfile
target: testing
environment:
- APP_ENV=testing
- DB_HOST=mysql-test
- DB_DATABASE=test_db
- DB_USERNAME=test
- DB_PASSWORD=test
depends_on:
- mysql-test
volumes:
- ./php/coverage:/var/www/coverage
go-test:
build:
context: ./go-service
dockerfile: Dockerfile
target: testing
environment:
- GO_ENV=test
- DATABASE_URL=mysql://test:test@mysql-test:3306/test_db
depends_on:
- mysql-test
volumes:
- ./go-service/coverage:/app/coverage
mysql-test:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: roottest
MYSQL_DATABASE: test_db
MYSQL_USER: test
MYSQL_PASSWORD: test
tmpfs:
- /var/lib/mysql
command: --default-authentication-plugin=mysql_native_password
redis-test:
image: redis:7.2-alpine
command: redis-server --appendonly no
tmpfs:
- /data
# E2E tests
e2e:
build:
context: ./e2e
dockerfile: Dockerfile
environment:
- BASE_URL=http://nginx
depends_on:
- nginx
- frontend
- api
volumes:
- ./e2e/screenshots:/app/screenshots
- ./e2e/videos:/app/videos
profiles:
- e2e
Безопасность и производство
Конфигурационные файлы
# secrets/db_password.txt
super_secure_db_password_here
# secrets/db_root_password.txt
ultra_secure_root_password_here
# secrets/redis_password.txt
redis_password_here
# secrets/jwt_secret.txt
jwt_secret_key_here
Nginx конфигурация для production
# nginx/prod.conf
upstream frontend {
server frontend:3000 max_fails=3 fail_timeout=30s;
}
upstream api {
server api:3000 max_fails=3 fail_timeout=30s;
}
upstream php_backend {
server php-app:9000 max_fails=3 fail_timeout=30s;
}
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL configuration
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
include /etc/nginx/ssl-params.conf;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# API routes
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://api/;
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;
# Timeout settings
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# PHP application
location /app/ {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php_backend;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Static files with caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri @frontend;
}
# Frontend application
location / {
proxy_pass http://frontend;
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;
}
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
MySQL конфигурация
# mysql/conf.d/my.cnf
[mysqld]
# Performance
innodb_buffer_pool_size = 256M
innodb_log_file_size = 64M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
# Security
local-infile = 0
skip-show-database
bind-address = 0.0.0.0
# Logging
general_log = 0
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/slow.log
long_query_time = 2
# Character set
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
[client]
default-character-set = utf8mb4
Мониторинг и логирование
docker-compose.monitoring.yml
# docker-compose.monitoring.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--storage.tsdb.retention.time=200h'
- '--web.enable-lifecycle'
ports:
- "9090:9090"
networks:
- monitoring
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=grafana
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/provisioning:/etc/grafana/provisioning
ports:
- "3001:3000"
networks:
- monitoring
loki:
image: grafana/loki:latest
container_name: loki
restart: unless-stopped
volumes:
- ./monitoring/loki.yml:/etc/loki/local-config.yaml
- loki_data:/loki
ports:
- "3100:3100"
networks:
- monitoring
promtail:
image: grafana/promtail:latest
container_name: promtail
restart: unless-stopped
volumes:
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./monitoring/promtail.yml:/etc/promtail/config.yml
networks:
- monitoring
node_exporter:
image: prom/node-exporter:latest
container_name: node_exporter
restart: unless-stopped
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.rootfs=/rootfs'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
ports:
- "9100:9100"
networks:
- monitoring
volumes:
prometheus_data:
grafana_data:
loki_data:
networks:
monitoring:
driver: bridge
Автоматизация и CI/CD
Makefile для управления проектом
# Makefile
.PHONY: help build up down logs test clean lint security-scan
# Variables
COMPOSE_DEV := docker-compose -f docker-compose.yml -f docker-compose.dev.yml
COMPOSE_TEST := docker-compose -f docker-compose.yml -f docker-compose.test.yml
COMPOSE_PROD := docker-compose -f docker-compose.yml -f docker-compose.prod.yml
help: ## Show this help message
@echo 'Usage: make [target]'
@echo ''
@echo 'Targets:'
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
build: ## Build all services
$(COMPOSE_DEV) build
up: ## Start development environment
$(COMPOSE_DEV) up -d
@echo "Development environment started"
@echo "Frontend: http://localhost:3000"
@echo "API: http://localhost:3001"
@echo "Nginx: http://localhost:8080"
down: ## Stop all services
$(COMPOSE_DEV) down
$(COMPOSE_TEST) down
$(COMPOSE_PROD) down
logs: ## Show logs for all services
$(COMPOSE_DEV) logs -f
test: ## Run all tests
$(COMPOSE_TEST) up --build --abort-on-container-exit
$(COMPOSE_TEST) down
test-unit: ## Run unit tests only
$(COMPOSE_TEST) run --rm frontend-test npm run test:unit
$(COMPOSE_TEST) run --rm api-test npm run test:unit
$(COMPOSE_TEST) run --rm php-test ./vendor/bin/phpunit
$(COMPOSE_TEST) run --rm go-test go test ./...
test-integration: ## Run integration tests
$(COMPOSE_TEST) run --rm api-test npm run test:integration
lint: ## Run linting for all services
$(COMPOSE_DEV) run --rm frontend npm run lint
$(COMPOSE_DEV) run --rm api npm run lint
$(COMPOSE_DEV) run --rm php-app ./vendor/bin/php-cs-fixer fix --dry-run
$(COMPOSE_DEV) run --rm go-service golangci-lint run
security-scan: ## Run security scans
docker run --rm -v $(PWD):/app securecodewarrior/docker-image-scanner /app
$(COMPOSE_DEV) run --rm php-app ./vendor/bin/security-checker security:check
$(COMPOSE_DEV) run --rm frontend npm audit
$(COMPOSE_DEV) run --rm api npm audit
prod-deploy: ## Deploy to production
$(COMPOSE_PROD) pull
$(COMPOSE_PROD) up -d --no-deps --build
$(COMPOSE_PROD) exec api npm run migrate:prod
backup: ## Backup databases
docker exec mysql mysqldump -u root -p$$(cat secrets/db_root_password.txt) --all-databases > backup_$$(date +%Y%m%d_%H%M%S).sql
docker exec redis redis-cli --rdb /data/dump_$$(date +%Y%m%d_%H%M%S).rdb
clean: ## Clean up containers, images, and volumes
docker container prune -f
docker image prune -f
docker volume prune -f
docker network prune -f
restart: down up ## Restart all services
shell-%: ## Open shell in service (e.g., make shell-api)
$(COMPOSE_DEV) exec $* sh
GitHub Actions Workflow
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: |
frontend/package-lock.json
api/package-lock.json
- name: Install dependencies
run: |
cd frontend && npm ci
cd ../api && npm ci
- name: Lint Frontend
run: cd frontend && npm run lint
- name: Lint API
run: cd api && npm run lint
- name: Type Check
run: |
cd frontend && npm run type-check
cd ../api && npm run type-check
test:
runs-on: ubuntu-latest
needs: lint
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: test_db
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
ports:
- 3306:3306
redis:
image: redis:7.2-alpine
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Run tests
run: |
docker-compose -f docker-compose.yml -f docker-compose.test.yml up --build --abort-on-container-exit
docker-compose -f docker-compose.yml -f docker-compose.test.yml down
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
files: ./frontend/coverage/lcov.info,./api/coverage/lcov.info
flags: unittests
name: codecov-umbrella
security:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results.sarif'
build-and-push:
runs-on: ubuntu-latest
needs: [test, security]
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop'
strategy:
matrix:
service: [frontend, api, php-app, go-service, nginx]
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-${{ matrix.service }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./${{ matrix.service }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
target: production
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
runs-on: ubuntu-latest
needs: build-and-push
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: |
echo "Deploying to production server..."
# Add your deployment logic here
Оптимизация производительности
Многостадийная сборка с кэшированием
# Dockerfile.optimized
# syntax=docker/dockerfile:1.4
FROM node:18-alpine AS base
WORKDIR /app
RUN apk add --no-cache dumb-init
# Dependencies stage with cache mount
FROM base AS deps
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production && npm cache clean --force
# Development dependencies stage
FROM deps AS deps-dev
RUN --mount=type=cache,target=/root/.npm \
npm install
# Build stage
FROM deps-dev AS builder
COPY . .
RUN --mount=type=cache,target=/root/.npm \
--mount=type=cache,target=.next/cache \
npm run build
# Production stage
FROM base AS production
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public
COPY package*.json ./
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
USER nextjs
EXPOSE 3000
CMD ["dumb-init", "node", "dist/server.js"]
Оптимизация размера образов
# Dockerfile.minimal
FROM alpine:3.18 AS base
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
FROM golang:1.21-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags='-w -s -extldflags "-static"' \
-a -installsuffix cgo \
-o app ./cmd/main.go
# Distroless final image
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /build/app /
EXPOSE 8080
USER nonroot:nonroot
CMD ["/app"]
Управление конфигурацией
Использование переменных окружения
# .env.example
# Database
DB_HOST=mysql
DB_PORT=3306
DB_NAME=app
DB_USER=user
DB_PASSWORD=
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=
# Application
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com
API_URL=https://api.example.com
# Security
JWT_SECRET=
ENCRYPTION_KEY=
SESSION_SECRET=
# Services
MAIL_HOST=
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
# Monitoring
SENTRY_DSN=
NEW_RELIC_LICENSE_KEY=
Конфигурация для разных сред
# docker-compose.override.yml (automatically loaded)
version: '3.8'
services:
frontend:
environment:
- NODE_ENV=development
- REACT_APP_API_URL=http://localhost:3001
volumes:
- ./frontend:/app
- /app/node_modules
ports:
- "3000:3000"
api:
environment:
- NODE_ENV=development
- DEBUG=api:*
volumes:
- ./api:/app
- /app/node_modules
ports:
- "3001:3000"
- "9229:9229" # Debug port
Заключение
Правильная контейнеризация веб-сервисов требует понимания множества аспектов:
Ключевые принципы:
- Безопасность первична - используйте non-root пользователей, сканируйте на уязвимости
- Оптимизация размера - многостадийная сборка, alpine образы, .dockerignore
- Кэширование слоев - правильный порядок инструкций в Dockerfile
- Мониторинг и логирование - healthchecks, structured logging
- Автоматизация - CI/CD пайплайны, автоматическое тестирование
Лучшие практики по технологиям:
- JavaScript/Node.js: используйте dumb-init, кэшируйте node_modules, включайте линтинг в сборку
- PHP: настройте OPcache, используйте PHP-FPM, включайте Xdebug только для разработки
- Go: используйте scratch/distroless образы, статическую компиляцию, race detector для тестов
- Nginx: настройте gzip, кэширование, security headers, rate limiting
- MySQL: оптимизируйте конфигурацию, используйте health checks
- Redis: настройте persistence, мониторинг памяти
Инструменты разработчика:
- docker-compose profiles для условного запуска сервисов
- Makefile для автоматизации типичных задач
- GitHub Actions для CI/CD
- Мониторинг с Prometheus и Grafana
- Логирование с ELK/Loki стеком
Следование этим практикам поможет создать надежную, масштабируемую и безопасную инфраструктуру для ваших веб-сервисов.
Интересуетесь другими аспектами DevOps? Читайте наши статьи о Docker и контейнеризации.