强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 15 章:Docker 部署

第 15 章:Docker 部署

15.1 Docker 镜像选择

官方镜像

镜像说明推荐
postgis/postgis:16-3.4PostgreSQL 16 + PostGIS 3.4✅ 推荐
postgis/postgis:15-3.3PostgreSQL 15 + PostGIS 3.3兼容旧项目
postgis/postgis:17-3.5PostgreSQL 17 + PostGIS 3.5尝鲜
mdillon/postgis社区维护(已弃用)❌ 不推荐
kartoza/postgis含额外工具特殊需求

镜像标签规范

# 格式: postgis/postgis:{pg_version}-{postgis_version}
postgis/postgis:16-3.4        # 推荐
postgis/postgis:16-3.4-alpine # 轻量版
postgis/postgis:latest         # 最新版本(不推荐生产使用)

15.2 快速启动

基础启动

docker run -d \
  --name postgis \
  -e POSTGRES_USER=gisadmin \
  -e POSTGRES_PASSWORD=SecurePass123 \
  -e POSTGRES_DB=gisdb \
  -p 5432:5432 \
  postgis/postgis:16-3.4

带数据持久化

# 使用命名卷(推荐)
docker volume create pgdata

docker run -d \
  --name postgis \
  -e POSTGRES_PASSWORD=SecurePass123 \
  -v pgdata:/var/lib/postgresql/data \
  -p 5432:5432 \
  postgis/postgis:16-3.4

# 使用宿主机目录
docker run -d \
  --name postgis \
  -e POSTGRES_PASSWORD=SecurePass123 \
  -v /opt/pgdata:/var/lib/postgresql/data \
  -p 5432:5432 \
  postgis/postgis:16-3.4

连接测试

# 使用 psql 连接
docker exec -it postgis psql -U gisadmin -d gisdb

# 验证 PostGIS
SELECT PostGIS_Full_Version();

15.3 Docker Compose 完整配置

基础 Compose 文件

version: '3.8'

services:
  postgis:
    image: postgis/postgis:16-3.4
    container_name: postgis
    environment:
      POSTGRES_USER: gisadmin
      POSTGRES_PASSWORD: ${PG_PASSWORD:-SecurePass123}
      POSTGRES_DB: gisdb
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=en_US.utf8"
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U gisadmin -d gisdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  pgdata:
    driver: local

初始化脚本

init/01-extensions.sql:

-- 启用 PostGIS 扩展
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;
CREATE EXTENSION IF NOT EXISTS postgis_raster;
CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;

-- 创建只读用户
CREATE USER gisreader WITH PASSWORD 'ReadOnlyPass';
GRANT CONNECT ON DATABASE gisdb TO gisreader;
GRANT USAGE ON SCHEMA public TO gisreader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO gisreader;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO gisreader;

init/02-sample-data.sql:

-- 创建示例表
CREATE TABLE cities (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    province VARCHAR(50),
    population INTEGER,
    geom GEOMETRY(Point, 4326)
);

CREATE INDEX idx_cities_geom ON cities USING GIST(geom);

INSERT INTO cities (name, province, population, geom) VALUES
('北京', '北京', 21890000, ST_SetSRID(ST_MakePoint(116.4074, 39.9042), 4326)),
('上海', '上海', 24870000, ST_SetSRID(ST_MakePoint(121.4737, 31.2304), 4326));

15.4 生产级 Docker Compose

version: '3.8'

services:
  postgis:
    image: postgis/postgis:16-3.4
    container_name: postgis-production
    environment:
      POSTGRES_USER: gisadmin
      POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
      POSTGRES_DB: gisdb
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C"
    ports:
      - "127.0.0.1:5432:5432"  # 仅绑定本地
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d
      - ./backups:/backups
    secrets:
      - pg_password
    deploy:
      resources:
        limits:
          cpus: '4.0'
          memory: 8G
        reservations:
          cpus: '2.0'
          memory: 4G
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U gisadmin -d gisdb"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "3"
    restart: unless-stopped

  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: pgadmin
    environment:
      PGADMIN_DEFAULT_EMAIL: [email protected]
      PGADMIN_DEFAULT_PASSWORD: admin123
    ports:
      - "5050:80"
    volumes:
      - pgadmin_data:/var/lib/pgadmin
    depends_on:
      postgis:
        condition: service_healthy
    restart: unless-stopped

secrets:
  pg_password:
    file: ./secrets/pg_password.txt

volumes:
  pgdata:
  pgadmin_data:

15.5 PostgreSQL 配置调优

自定义 postgresql.conf

# 创建自定义配置文件
cat > postgresql-custom.conf << 'EOF'
# 内存配置
shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 256MB
maintenance_work_mem = 1GB

# 连接配置
max_connections = 200

# 查询优化
random_page_cost = 1.1
effective_io_concurrency = 200
max_parallel_workers_per_gather = 4
max_parallel_workers = 8

# WAL 配置
wal_buffers = 64MB
checkpoint_completion_target = 0.9

# 日志配置
log_min_duration_statement = 1000
log_checkpoints = on
log_lock_waits = on
EOF
# 在 Docker Compose 中挂载
services:
  postgis:
    volumes:
      - ./postgresql-custom.conf:/etc/postgresql/postgresql.conf
    command: postgres -c config_file=/etc/postgresql/postgresql.conf

15.6 数据备份与恢复

备份脚本

#!/bin/bash
# backup.sh

BACKUP_DIR="/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
CONTAINER="postgis"

# 全量备份
docker exec $CONTAINER pg_dump \
  -U gisadmin -d gisdb \
  -Fc -Z9 \
  -f /tmp/gisdb_${TIMESTAMP}.dump

# 复制到宿主机
docker cp $CONTAINER:/tmp/gisdb_${TIMESTAMP}.dump $BACKUP_DIR/

# 清理容器内临时文件
docker exec $CONTAINER rm /tmp/gisdb_${TIMESTAMP}.dump

# 删除 30 天前的备份
find $BACKUP_DIR -name "*.dump" -mtime +30 -delete

echo "Backup completed: gisdb_${TIMESTAMP}.dump"

恢复

# 恢复全量备份
docker exec -i postgis pg_restore \
  -U gisadmin -d gisdb \
  --clean --if-exists \
  < /backups/gisdb_20260510_120000.dump

# 仅恢复特定表
docker exec -i postgis pg_restore \
  -U gisadmin -d gisdb \
  --table=cities --table=stores \
  < /backups/gisdb_20260510_120000.dump

使用 WAL 进行增量备份

# docker-compose.yml 中启用 WAL 归档
services:
  postgis:
    environment:
      POSTGRES_INITDB_ARGS: "--data-checksums"
    command: >
      postgres
        -c wal_level=replica
        -c archive_mode=on
        -c archive_command='cp %p /backups/wal/%f'
        -c max_wal_senders=3
    volumes:
      - ./backups/wal:/backups/wal

15.7 主从复制

# docker-compose-replication.yml
version: '3.8'

services:
  postgis-primary:
    image: postgis/postgis:16-3.4
    container_name: postgis-primary
    environment:
      POSTGRES_USER: gisadmin
      POSTGRES_PASSWORD: primary_pass
      POSTGRES_DB: gisdb
    ports:
      - "5432:5432"
    volumes:
      - pg_primary:/var/lib/postgresql/data
    command: >
      postgres
        -c wal_level=replica
        -c max_wal_senders=3
        -c wal_keep_size=256MB
        -c hot_standby=on
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U gisadmin -d gisdb"]
      interval: 10s

  postgis-replica:
    image: postgis/postgis:16-3.4
    container_name: postgis-replica
    environment:
      PGUSER: replicator
      PGPASSWORD: replica_pass
    ports:
      - "5433:5432"
    volumes:
      - pg_replica:/var/lib/postgresql/data
    depends_on:
      postgis-primary:
        condition: service_healthy
    command: >
      bash -c "
        until pg_basebackup -h postgis-primary -D /var/lib/postgresql/data -U replicator -Fp -Xs -P -R; do
          echo 'Waiting for primary...'
          sleep 5
        done
        postgres
      "

volumes:
  pg_primary:
  pg_replica:

15.8 导入外部数据到容器

# 复制 Shapefile 到容器
docker cp /data/shapefile.shp postgis:/tmp/
docker cp /data/shapefile.shx postgis:/tmp/
docker cp /data/shapefile.dbf postgis:/tmp/
docker cp /data/shapefile.prj postgis:/tmp/

# 在容器内导入
docker exec postgis shp2pgsql \
  -s 4326 -W UTF-8 -I \
  /tmp/shapefile.shp public.imported_table \
  | docker exec -i postgis psql -U gisadmin -d gisdb

# 直接从宿主机管道导入
shp2pgsql -s 4326 -W UTF-8 -I /data/shapefile.shp public.imported_table \
  | docker exec -i postgis psql -U gisadmin -d gisdb

# 导入 CSV
docker cp /data/pois.csv postgis:/tmp/
docker exec -i postgis psql -U gisadmin -d gisdb << 'SQL'
CREATE TEMP TABLE tmp_import (
    name TEXT, category TEXT, lat DOUBLE PRECISION, lng DOUBLE PRECISION
);
COPY tmp_import FROM '/tmp/pois.csv' WITH (FORMAT csv, HEADER true);
INSERT INTO pois (name, category, geom)
SELECT name, category, ST_SetSRID(ST_MakePoint(lng, lat), 4326)
FROM tmp_import;
DROP TABLE tmp_import;
SQL

15.9 监控容器健康

健康检查脚本

#!/bin/bash
# healthcheck.sh

# 检查容器状态
docker inspect --format='{{.State.Health.Status}}' postgis

# 检查 PostgreSQL 连接
docker exec postgis pg_isready -U gisadmin -d gisdb

# 检查 PostGIS 功能
docker exec postgis psql -U gisadmin -d gisdb -c "SELECT PostGIS_Version();"

# 检查磁盘使用
docker exec postgis du -sh /var/lib/postgresql/data

# 检查活跃连接数
docker exec postgis psql -U gisadmin -d gisdb -c \
  "SELECT count(*) FROM pg_stat_activity WHERE state = 'active';"

Prometheus + Grafana 监控

# docker-compose-monitoring.yml
services:
  postgres-exporter:
    image: prometheuscommunity/postgres-exporter
    container_name: pg-exporter
    environment:
      DATA_SOURCE_NAME: "postgresql://gisadmin:SecurePass123@postgis:5432/gisdb?sslmode=disable"
    ports:
      - "9187:9187"
    depends_on:
      - postgis

  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana
    ports:
      - "3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana

15.10 常见部署问题

问题原因解决方案
启动慢数据目录未持久化,每次重新初始化使用 volume 挂载数据
连接被拒端口绑定问题检查 -p 127.0.0.1:5432:5432
权限错误SELinux 或文件权限使用命名卷而非绑定挂载
内存不足PostgreSQL 默认配置太小调整 shared_buffers 等参数
磁盘满WAL 或数据增长设置 WAL 归档和监控
编码错误默认编码非 UTF-8POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
# 调试启动问题
docker logs postgis 2>&1 | head -50

# 进入容器排查
docker exec -it postgis bash

# 检查 PostgreSQL 进程
docker exec postgis ps aux | grep postgres

# 检查磁盘空间
docker system df
docker exec postgis df -h /var/lib/postgresql/data

15.11 本章小结

要点说明
镜像选择postgis/postgis:16-3.4
数据持久化必须使用 volume
配置优化自定义 postgresql.conf
备份策略pg_dump + WAL 归档
安全使用 secrets 管理密码
监控healthcheck + Prometheus

扩展阅读