Docker Compose for Development: Local Infrastructure

Docker Compose for local development: PostgreSQL, Redis, Authentik, networking, health checks, volume management, and environment-specific configurations for TypeScript monorepos.

E
ECOSIRE Research and Development Team
|2026年3月19日5 分钟阅读1.1k 字数|

Docker Compose 开发:本地基础设施

愉快的入门体验(“克隆并运行 pnpm dev:infra”)和痛苦的入门体验(“首先设置 PostgreSQL,然后配置 Redis,然后......”)之间的区别取决于您的 Docker Compose 设置如何很好地满足您的基础设施要求。精心设计的 docker-compose.dev.yml 可以让任何机器上的任何开发人员在几分钟内运行完全相同的基础设施。

本指南涵盖了生产质量本地开发堆栈的模式:服务配置、运行状况检查、网络、卷管理以及与应用程序启动序列的集成。

要点

  • 在本地使用 PostgreSQL 的非默认端口 (5433) 以避免与系统安装发生冲突
  • 对服务依赖项的健康检查可防止“连接被拒绝”启动错误
  • 命名卷在容器重新启动之间保留数据库数据 - 绑定安装在 Windows 上无法可靠工作
  • 使用 env_file 将环境变量从 .env.local 文件加载到容器中
  • docker-compose.dev.ymldocker-compose.prod.yml 分开 — 它们有不同的用途
  • depends_on.condition: service_healthy 模式等待实际准备就绪,而不仅仅是容器启动
  • 使用 profiles 选择可选服务(电子邮件、监控)
  • 运行 docker compose (v2) 而不是 docker-compose (v1) — 插件语法是最新的

完整的开发堆栈

# infrastructure/docker-compose.dev.yml
name: ecosire-dev

services:
  # ─── PostgreSQL ─────────────────────────────────────────────────
  postgres:
    image: postgres:17-alpine
    container_name: ecosire-postgres
    environment:
      POSTGRES_DB: ecosire_dev
      POSTGRES_USER: ecosire
      POSTGRES_PASSWORD: dev_password_change_in_prod
    ports:
      - "5433:5432"  # 5433 externally — avoids conflicts with system PostgreSQL
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d  # Run SQL on first start
    command: >
      postgres
        -c shared_buffers=256MB
        -c effective_cache_size=1GB
        -c work_mem=16MB
        -c maintenance_work_mem=128MB
        -c checkpoint_completion_target=0.9
        -c wal_buffers=16MB
        -c max_connections=100
        -c log_min_duration_statement=100
        -c log_statement=ddl
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ecosire -d ecosire_dev"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s
    restart: unless-stopped

  # ─── Redis ──────────────────────────────────────────────────────
  redis:
    image: redis:7-alpine
    container_name: ecosire-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: >
      redis-server
        --maxmemory 512mb
        --maxmemory-policy allkeys-lru
        --appendonly yes
        --appendfsync everysec
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  # ─── Authentik (Identity Provider) ──────────────────────────────
  authentik-server:
    image: ghcr.io/goauthentik/server:2024.12
    container_name: ecosire-authentik
    command: server
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgres
      AUTHENTIK_POSTGRESQL__USER: ecosire
      AUTHENTIK_POSTGRESQL__PASSWORD: dev_password_change_in_prod
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_SECRET_KEY: dev-secret-key-change-in-production-32chars
      AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
      AUTHENTIK_DISABLE_STARTUP_ANALYTICS: "true"
    volumes:
      - authentik_media:/media
      - authentik_certs:/certs
    ports:
      - "9000:9000"  # HTTP
      - "9443:9443"  # HTTPS
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "ak healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s  # Authentik takes time to initialize
    restart: unless-stopped

  authentik-worker:
    image: ghcr.io/goauthentik/server:2024.12
    container_name: ecosire-authentik-worker
    command: worker
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgres
      AUTHENTIK_POSTGRESQL__USER: ecosire
      AUTHENTIK_POSTGRESQL__PASSWORD: dev_password_change_in_prod
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_SECRET_KEY: dev-secret-key-change-in-production-32chars
    volumes:
      - authentik_media:/media
      - authentik_certs:/certs
      - /var/run/docker.sock:/var/run/docker.sock  # For Authentik's proxy
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  # ─── Mailpit (Email Testing) ─────────────────────────────────────
  mailpit:
    image: axllent/mailpit:latest
    container_name: ecosire-mailpit
    ports:
      - "1025:1025"  # SMTP
      - "8025:8025"  # Web UI
    environment:
      MP_MAX_MESSAGES: 200
      MP_SMTP_AUTH_ACCEPT_ANY: true
      MP_SMTP_AUTH_ALLOW_INSECURE: true
    restart: unless-stopped
    profiles:
      - email  # Optional — use `docker compose --profile email up`

  # ─── pgAdmin (Database GUI) ─────────────────────────────────────
  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: ecosire-pgadmin
    environment:
      PGADMIN_DEFAULT_EMAIL: [email protected]
      PGADMIN_DEFAULT_PASSWORD: admin
      PGADMIN_CONFIG_SERVER_MODE: "False"
      PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
    ports:
      - "5050:80"
    volumes:
      - pgadmin_data:/var/lib/pgadmin
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped
    profiles:
      - tools  # Optional

networks:
  default:
    name: ecosire-dev-network

volumes:
  postgres_data:
    name: ecosire-postgres-data
  redis_data:
    name: ecosire-redis-data
  authentik_media:
    name: ecosire-authentik-media
  authentik_certs:
    name: ecosire-authentik-certs
  pgadmin_data:
    name: ecosire-pgadmin-data

Package.json 脚本

将 Docker Compose 命令连接到 monorepo 脚本中:

{
  "scripts": {
    "dev:infra": "docker compose -f infrastructure/docker-compose.dev.yml up -d",
    "dev:infra:down": "docker compose -f infrastructure/docker-compose.dev.yml down",
    "dev:infra:logs": "docker compose -f infrastructure/docker-compose.dev.yml logs -f",
    "dev:infra:reset": "docker compose -f infrastructure/docker-compose.dev.yml down -v && pnpm dev:infra",
    "dev:infra:email": "docker compose -f infrastructure/docker-compose.dev.yml --profile email up -d",
    "dev:infra:tools": "docker compose -f infrastructure/docker-compose.dev.yml --profile tools up -d"
  }
}

--profile 标志允许可选服务(使用 Mailpit 进行电子邮件测试、使用 pgAdmin 进行数据库 GUI)保持休眠状态,直到明确请求为止。


数据库初始化脚本

将 SQL 文件放入 infrastructure/init-scripts/ — 它们在第一个容器启动时运行:

-- infrastructure/init-scripts/01-create-databases.sql
-- Create all databases Authentik needs separately from the app DB
CREATE DATABASE authentik;
GRANT ALL PRIVILEGES ON DATABASE authentik TO ecosire;

-- Create test database for CI
CREATE DATABASE ecosire_test;
GRANT ALL PRIVILEGES ON DATABASE ecosire_test TO ecosire;
-- infrastructure/init-scripts/02-extensions.sql
-- Enable PostgreSQL extensions
\c ecosire_dev;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";  -- Trigram search
CREATE EXTENSION IF NOT EXISTS "btree_gin"; -- Composite GIN indexes

\c ecosire_test;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

初始化脚本按字母顺序运行。 \c database_name psql 元命令切换活动数据库。


环境变量集成

您的应用程序从 monorepo 根目录中的 .env.local 读取。 Docker 服务需要知道如何使用服务名称(而不是 localhost)相互连接:

# .env.local (monorepo root)

# PostgreSQL — use 5433 externally (host) or 5432 internally (container network)
DATABASE_URL=postgresql://ecosire:dev_password_change_in_prod@localhost:5433/ecosire_dev

# Redis
REDIS_URL=redis://localhost:6379

# Authentik — use 9000 for external calls from your dev machine
AUTHENTIK_URL=http://localhost:9000
# Use service name for server-to-server calls within Docker network
AUTHENTIK_INTERNAL_URL=http://authentik-server:9000

# Email (Mailpit SMTP)
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_SECURE=false

# Application
NODE_ENV=development

对于在 Docker 内运行且需要与其他服务通信的应用程序,请使用服务名称。对于在主机上运行的应用程序(开发模式下的 NestJS、Next.js),请将 localhost 与主机映射端口一起使用。


健康检查深入探讨

健康检查可防止级联启动失败。 depends_on.condition: service_healthy 等待实际准备就绪,而不仅仅是容器启动:

# Without health checks — can fail because PostgreSQL isn't ready
depends_on:
  - postgres

# With health checks — waits for PostgreSQL to accept connections
depends_on:
  postgres:
    condition: service_healthy

为您自己的服务定制健康检查:

// apps/api/src/health/health.controller.ts
@Get()
@Public()
@HealthCheck()
async check() {
  return this.health.check([
    () => this.db.isHealthy('database'),
    () => this.redis.isHealthy('redis'),
  ]);
}
# If your API is also dockerized
api:
  image: ecosire-api:latest
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:3001/api/health"]
    interval: 30s
    timeout: 10s
    retries: 3
    start_period: 30s

卷管理

命名卷在重新启动之间保留数据。了解何时使用每种卷类型:

类型坚持性能用于
命名卷是的优秀数据库数据
绑定安装是的好 (Linux),差 (macOS)源代码热重载
tmpfs没有优秀临时文件,秘密
# Use bind mounts for source code (enables hot reload)
volumes:
  - ./apps/api/src:/app/src  # Code changes reflected immediately

# Use named volumes for data
volumes:
  - postgres_data:/var/lib/postgresql/data

# Use tmpfs for ephemeral data
volumes:
  - type: tmpfs
    target: /tmp

在具有 Docker Desktop 的 macOS 上,绑定挂载使用 gRPC FUSE,这比 Linux 上慢得多。对于 NestJS 和 Next.js 开发服务器,直接在主机上(而不是在 Docker 中)运行它们以获得本机文件系统性能。


生产 Docker Compose

生产撰写文件在结构上有所不同 - 无本地端口、重启策略、生产资源限制:

# infrastructure/docker-compose.prod.yml
name: ecosire-prod

services:
  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    # No port mapping — only accessible within Docker network
    restart: always
    deploy:
      resources:
        limits:
          memory: 2G
        reservations:
          memory: 512M

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 1gb
    volumes:
      - redis_data:/data
    restart: always
    # No port mapping — internal only

volumes:
  postgres_data:
  redis_data:

生产环境不会向外部公开端口——应用程序通过内部 Docker 网络进行连接。 Nginx 处理外部流量。


常见陷阱和解决方案

陷阱1:端口与系统服务冲突

PostgreSQL、Redis 和其他服务通常作为系统服务运行。始终映射到开发中的非标准端口:

  • PostgreSQL:5433:5432(不是5432:5432
  • Redis:保留 6379:6379 (很少发生冲突)
  • 运行 lsof -i :5432 检查什么正在使用默认端口

陷阱 2:Linux 上的卷权限问题

Linux 上的 Docker 卷默认使用 root 所有权。如果您的容器用户不是 root,请设置正确的所有权:

postgres:
  image: postgres:17-alpine
  user: "999:999"  # postgres user's UID:GID
  # Or use init container to fix permissions

陷阱 3:Authentik 初始化需要 60 秒以上

Authentik 在首次启动时运行数据库迁移。健康检查中的 start_period: 60s 为其提供了时间。如果依赖服务在 Authentik 准备就绪之前启动,它们将会失败。使用 service_healthy 条件并为其提供足够的 start_period

陷阱 4:Mac 上的 Docker 桌面资源限制

默认 Docker Desktop 分配 2 个 CPU 和 2GB RAM — 不足以同时运行 PostgreSQL + Redis + Authentik。将 Docker 桌面设置 > 资源增加到至少 4 个 CPU 和 6GB RAM。

陷阱 5:docker-composedocker compose

旧的 docker-compose (v1,用 Python 编写)已被弃用。使用 docker compose (v2,插件)。检查您的版本:docker compose version。如果您看到 Docker Compose version v2.x.x,则您正在使用 v2。


常见问题

开发期间我应该在 Docker 中运行应用程序服务(NestJS、Next.js)吗?

通常不需要 - 对于主动开发,请在主机上运行应用程序服务,以实现更快的热重载和更轻松的调试。仅将 Docker 用于稳定且不需要频繁重启的基础设施服务(数据库、缓存、身份提供商)。例外情况是,如果您的应用程序具有开发操作系统和生产环境之间不同的本机依赖项。

如何在 Docker Compose 工作流程中处理数据库迁移?

启动基础架构后从主机运行迁移:pnpm dev:infra && pnpm db:migrate。开发期间不要在 Docker 容器内运行迁移 - 您将失去使 Drizzle 迁移安全的类型检查和 IDE 集成。对于初始数据库创建,请使用 Docker 的 initdb.d 脚本。

如何备份和恢复本地 Docker 卷?

使用 docker run --rm -v postgres_data:/data -v $(pwd):/backup alpine tar czf /backup/postgres-backup.tar.gz /data 进行备份。使用 tar xzf 使用相同的方法进行恢复。对于开发,您还可以使用 pg_dump 转储并使用 psql 恢复,因为您已经暴露了端口。

如何与其他团队成员共享 Docker Compose 状态?

Docker Compose 文件通过 git 共享,但卷中的数据是本地的。每个开发人员都从一个空数据库开始,并运行迁移/种子来填充它。使用种子脚本(提交到存储库)创建一致的测试数据。共享的 docker-compose.dev.yml 确保每个人都使用相同的服务版本和配置。

为什么在开发中使用 Mailpit 而不是真正的电子邮件?

Mailpit 是一个本地 SMTP 服务器,它捕获所有外发电子邮件并提供 Web UI 来查看它们。它可以防止在开发过程中意外向真实用户发送真实电子邮件,不需要 SMTP 凭据,并且允许您验证电子邮件模板而无需检查收件箱。将您的应用程序配置为使用 SMTP_HOST=localhost SMTP_PORT=1025 并访问 http://localhost:8025 以查看捕获的电子邮件。


后续步骤

用于本地开发的精心设计的 Docker Compose 设置是一项投资,每次新开发人员加入或启动新机器时都会带来回报。 ECOSIRE 在 Docker Compose 中运行 PostgreSQL 17、Redis 7 和 Authentik,以便整个团队进行本地开发。

需要帮助设计您的本地开发基础设施或将您的应用程序容器化以用于生产? 探索我们的 DevOps 服务 以了解我们如何提供帮助。

E

作者

ECOSIRE Research and Development Team

在 ECOSIRE 构建企业级数字产品。分享关于 Odoo 集成、电商自动化和 AI 驱动商业解决方案的洞见。

通过 WhatsApp 聊天