DEV Community

bertrand HARTWIG
bertrand HARTWIG

Posted on

Running PostgreSQL Correctly with Docker Compose

This guide explains how to run a PostgreSQL instance with Docker Compose using a configuration that provides a solid baseline for a properly configured PostgreSQL deployment. (excluding backup and monitoring).

The goal is not only to start PostgreSQL, but to start it with sane defaults, persistent storage, proper health checks, query observability, and memory settings aligned with the resources allocated to the container.

The example below is generated by pgAssistant, which provides this kind of PostgreSQL Docker configuration for free.

Why this configuration matters

A PostgreSQL container should not be treated as a simple throwaway service when it stores application data. Several Docker Compose options have a direct impact on reliability, performance, security, and observability.

The most important points are:

  • restart: always
  • shm_size
  • persistent volumes
  • container CPU and memory limits
  • POSTGRES_INITDB_ARGS
  • a proper health check
  • pg_stat_statements
  • autovacuum=on
  • PostgreSQL tuning parameters generated from pgTune

Example docker-compose.yml

services:
  northwind-db:
    restart: always
    image: postgres:17-alpine
    shm_size: 1GB

    # Optional: Use tmpfs for shared memory when running in Swarm mode
    # volumes:
    #   - type: tmpfs
    #     target: /dev/shm
    #     tmpfs:
    #       size: 1073741824

    volumes:
      - northwind_data:/var/lib/postgresql/data

    ports:
      - "5432:5432"

    deploy:
      resources:
        limits:
          cpus: "6.0"
          memory: 4GB

    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=xxxxx
      - POSTGRES_DB=northwind
      - POSTGRES_INITDB_ARGS=--auth-local=scram-sha-256 --auth-host=scram-sha-256

    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

    command: >
      postgres
        -c shared_preload_libraries='pg_stat_statements'
        -c autovacuum=on
        -c max_connections=200
        -c shared_buffers='1GB'
        -c effective_cache_size='3GB'
        -c maintenance_work_mem='256MB'
        -c checkpoint_completion_target=0.9
        -c wal_buffers='16MB'
        -c default_statistics_target=100
        -c random_page_cost=1.1
        -c effective_io_concurrency=200
        -c work_mem='5242kB'
        -c min_wal_size='1GB'
        -c max_wal_size='4GB'
        -c huge_pages='off'
        -c max_worker_processes=6
        -c max_parallel_workers=6
        -c max_parallel_maintenance_workers=3
        -c max_parallel_workers_per_gather=3

volumes:
  northwind_data:
Enter fullscreen mode Exit fullscreen mode

restart: always

restart: always
Enter fullscreen mode Exit fullscreen mode

This option tells Docker to automatically restart the PostgreSQL container if it stops unexpectedly.

For a database service, this is important because PostgreSQL is usually a critical dependency for the application. Without a restart policy, the database may remain stopped after a crash, host reboot, or Docker daemon restart.

restart: always does not replace monitoring, backups, replication, or high availability, but it provides a basic resilience layer that should almost always be enabled for a database container.

Shared memory and shm_size

shm_size: 1GB
Enter fullscreen mode Exit fullscreen mode

PostgreSQL relies on shared memory for several internal operations. Docker containers have a default shared memory size that is often too small for a properly tuned PostgreSQL instance.

A good rule is to align shm_size with the PostgreSQL shared_buffers value.

For example:

shm_size: 512MB
Enter fullscreen mode Exit fullscreen mode

should be consistent with:

-c shared_buffers='512MB'
Enter fullscreen mode Exit fullscreen mode

In the example generated by pgAssistant, both values are set to 1GB:

shm_size: 1GB
-c shared_buffers='1GB'
Enter fullscreen mode Exit fullscreen mode

This is important because shared_buffers defines how much memory PostgreSQL uses for its own shared buffer cache. If Docker shared memory is undersized compared to PostgreSQL configuration, PostgreSQL may fail to start or behave poorly under load.

Persistent volume

volumes:
  - northwind_data:/var/lib/postgresql/data
Enter fullscreen mode Exit fullscreen mode

mount point is diffrent with postgresql 18 :

volumes:
  - northwind_data:/var/lib/postgresql
Enter fullscreen mode Exit fullscreen mode

A PostgreSQL database must use persistent storage.

The directory /var/lib/postgresql/data or /var/lib/postgresql with v18 is where the official PostgreSQL Docker image stores the database cluster. If this directory is not backed by a Docker volume or another persistent storage mechanism, the data may be lost when the container is removed.

The named volume:

volumes:
  northwind_data:
Enter fullscreen mode Exit fullscreen mode

ensures that database files survive container recreation.

This is one of the most important parts of the configuration.

Container resources

deploy:
  resources:
    limits:
      cpus: "6.0"
      memory: 4GB
Enter fullscreen mode Exit fullscreen mode

The CPU and memory allocated to the container are not just Docker-level constraints. They are also the values that should be used to calculate PostgreSQL tuning parameters.

Tools such as pgTune need to know how many CPUs and how much memory are available to PostgreSQL. The values used in pgTune must match the resources allocated to the container.

For example, if the container is limited to:

cpus: "6.0"
memory: 4GB
Enter fullscreen mode Exit fullscreen mode

then pgTune should be configured using:

  • 6 CPUs
  • 4 GB of RAM

Using host-level resources instead of container-level resources would produce an incorrect PostgreSQL configuration.

POSTGRES_INITDB_ARGS

POSTGRES_INITDB_ARGS=--auth-local=scram-sha-256 --auth-host=scram-sha-256
Enter fullscreen mode Exit fullscreen mode

POSTGRES_INITDB_ARGS allows passing arguments to initdb when the PostgreSQL data directory is initialized for the first time.

In this example, it enables SCRAM-SHA-256 authentication for both local and host connections:

--auth-local=scram-sha-256
--auth-host=scram-sha-256
Enter fullscreen mode Exit fullscreen mode

This is important because authentication settings are part of the initial database cluster setup.

Note that these options only apply when the PostgreSQL data directory is empty. If the persistent volume already contains an initialized database, changing POSTGRES_INITDB_ARGS will not reinitialize the database.

Health check

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
  interval: 10s
  timeout: 5s
  retries: 5
Enter fullscreen mode Exit fullscreen mode

A health check allows Docker to determine whether PostgreSQL is actually ready to accept connections.

This is different from simply checking whether the container process is running. PostgreSQL may be running but not ready yet, especially during startup, crash recovery, or initialization.

The command:

pg_isready -U postgres
Enter fullscreen mode Exit fullscreen mode

checks the readiness of the PostgreSQL server.

A proper health check is important when other services depend on the database. It allows orchestration tools, deployment scripts, and dependent containers to wait until PostgreSQL is ready before connecting.

pg_stat_statements

-c shared_preload_libraries='pg_stat_statements'
Enter fullscreen mode Exit fullscreen mode

pg_stat_statements is essential for PostgreSQL query optimization.

It tracks SQL execution statistics, including how often queries run, how long they take, and how much load they generate. This makes it possible to identify expensive queries that need indexes, rewriting, caching, or schema improvements.

Enabling it requires two steps.

First, it must be loaded at server startup:

-c shared_preload_libraries='pg_stat_statements'
Enter fullscreen mode Exit fullscreen mode

Second, the extension must be created inside the target database:

CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
Enter fullscreen mode Exit fullscreen mode

A common way to do this with Docker is to add an initialization SQL script under /docker-entrypoint-initdb.d/, for example:

CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
Enter fullscreen mode Exit fullscreen mode

Without pg_stat_statements, database optimization is mostly guesswork. With it, you can focus on the queries that actually consume time and resources.

Useful example query:

SELECT
  query,
  calls,
  total_exec_time,
  mean_exec_time,
  rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
Enter fullscreen mode Exit fullscreen mode

autovacuum=on

-c autovacuum=on
Enter fullscreen mode Exit fullscreen mode

Autovacuum should remain enabled.

PostgreSQL uses MVCC, which means updates and deletes leave behind dead tuples. Autovacuum cleans up these dead tuples and helps prevent table and index bloat.

Disabling autovacuum is dangerous for most applications. It can lead to degraded performance, excessive storage growth, transaction ID wraparound risks, and poor query plans due to outdated statistics.

In most cases, the right approach is not to disable autovacuum, but to tune it if the workload requires it.

pgTune-generated PostgreSQL parameters

The remaining PostgreSQL parameters passed to the postgres command are generated from pgTune:

-c max_connections=200
-c shared_buffers='1GB'
-c effective_cache_size='3GB'
-c maintenance_work_mem='256MB'
-c checkpoint_completion_target=0.9
-c wal_buffers='16MB'
-c default_statistics_target=100
-c random_page_cost=1.1
-c effective_io_concurrency=200
-c work_mem='5242kB'
-c min_wal_size='1GB'
-c max_wal_size='4GB'
-c huge_pages='off'
-c max_worker_processes=6
-c max_parallel_workers=6
-c max_parallel_maintenance_workers=3
-c max_parallel_workers_per_gather=3
Enter fullscreen mode Exit fullscreen mode

These values should not be copied blindly from another machine.

They depend on:

  • allocated memory
  • available CPUs
  • storage type
  • expected workload
  • maximum number of connections
  • PostgreSQL version

pgTune provides a strong baseline configuration based on the resources allocated to the PostgreSQL container. It does not replace workload-specific tuning, but it gives a much better starting point than PostgreSQL defaults for many real-world deployments.

Important relationship between Docker resources and pgTune

The values used in pgTune must match the resources actually available to PostgreSQL.

For example, this Docker resource limit:

cpus: "6.0"
memory: 4GB
Enter fullscreen mode Exit fullscreen mode

must be reflected in the pgTune input.

That is why pgAssistant generates both:

  1. the Docker resource limits, and
  2. the PostgreSQL runtime parameters derived from those limits.

This avoids a common mistake: tuning PostgreSQL for the host machine while the container is limited to fewer resources.

About pgAssistant

pgAssistant generates this kind of PostgreSQL Docker Compose configuration for free.

It helps create a more reliable and better-tuned PostgreSQL setup

This provides a solid minimum configuration for running PostgreSQL properly with Docker.

Top comments (0)