A PostgreSQL 16 Docker image with the RDKit cartridge pre-installed and optimized for chemical informatics workloads.
This image inherits from the official postgres image, and therefore has all the same environment variables for configuration, and can be extended by adding entrypoint scripts to the /docker-entrypoint-initdb.d directory to be run on first launch.
- Multi-architecture support: Built for both AMD64 and ARM64 platforms
- Optimized configuration: Pre-configured
postgresql.conf, tuned and measured against a 26 GB GPCRdb database on a Mac development machine (see Configuration) - Planner cost hints included: ships
rdkit-tuning.sql, which lets bulk molecule builds use every core instead of one — measured 55.5 s to 6.9 s on 222,036 ligands - Automated builds: Images are automatically built and published via GitHub Actions
- Modern Dockerfile: Multi-stage build for smaller image size and faster builds
- PostgreSQL 16: Based on the latest PostgreSQL 16 (bookworm) image
- RDKit 2025.03.1: The latest release compatible with Debian 12 (Bookworm) and its system Boost libraries (v1.74), ensuring maximum stability without experimental dependencies.
Follow these steps to get PostgreSQL with RDKit running in minutes. Choose the instructions for your operating system.
mkdir -p ~/GPCRdb && cd ~/GPCRdbdocker network create gpcrdb
docker volume create postgres_dataCopy and paste this entire block into your terminal:
cat > docker-compose.yml << 'EOF'
services:
db:
image: ghcr.io/protwis/postgres_rdkit_docker:latest
container_name: postgres-rdkit
restart: always
shm_size: 2g
environment:
- POSTGRES_USER=protwis
- POSTGRES_PASSWORD=protwis
- POSTGRES_DB=protwis
- PGDATA=/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- gpcrdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U protwis"]
interval: 10s
timeout: 5s
retries: 5
adminer:
image: adminer
container_name: adminer
restart: always
ports:
- "8888:8080"
environment:
- ADMINER_DEFAULT_SERVER=db
networks:
- gpcrdb
depends_on:
db:
condition: service_healthy
networks:
gpcrdb:
external: true
volumes:
postgres_data:
external: true
EOFdocker compose up -d- PostgreSQL: Connect to
localhost:5432with userprotwisand passwordprotwis - Adminer: Open http://localhost:8888 in your browser to manage the database
Adminer server: on macOS, set
Servertohost.docker.internal; on Windows, setServertopostgres-rdkit(the DB container name).
mkdir $env:USERPROFILE\Desktop\GPCRdb
cd $env:USERPROFILE\Desktop\GPCRdbdocker network create gpcrdb
docker volume create postgres_dataCopy and paste this entire block into PowerShell:
@'
services:
db:
image: ghcr.io/protwis/postgres_rdkit_docker:latest
container_name: postgres-rdkit
restart: always
shm_size: 2g
environment:
- POSTGRES_USER=protwis
- POSTGRES_PASSWORD=protwis
- POSTGRES_DB=protwis
- PGDATA=/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- gpcrdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U protwis"]
interval: 10s
timeout: 5s
retries: 5
adminer:
image: adminer
container_name: adminer
restart: always
ports:
- "8888:8080"
environment:
- ADMINER_DEFAULT_SERVER=db
networks:
- gpcrdb
depends_on:
db:
condition: service_healthy
networks:
gpcrdb:
external: true
volumes:
postgres_data:
external: true
'@ | Out-File -Encoding utf8 docker-compose.ymldocker compose up -d- PostgreSQL: Connect to
localhost:5432with userprotwisand passwordprotwis - Adminer: Open http://localhost:8888 in your browser to manage the database
Adminer server: on macOS, set
Servertohost.docker.internal; on Windows, setServertopostgres-rdkit(the DB container name).
# Stop the services
docker compose down
# To also remove the data volume (WARNING: deletes all data!)
docker volume rm postgres_dataDownload the latest GPCRdb database dump from files.gpcrdb.org:
curl -L https://files.gpcrdb.org/protwis_sp.sql.gz -o ~/protwis.sql.gzIf you're loading the database for the first time (fresh postgres_data volume):
time gunzip -c ~/protwis.sql.gz | docker exec -i postgres-rdkit psql -U protwis -d protwis -q -1Tip
The -q flag suppresses output, and -1 wraps the entire import in a single transaction for better performance and atomicity.
If you've previously loaded data and want to start fresh:
-
Stop all services using the database:
docker compose down
-
Remove the existing data volume:
docker volume rm postgres_data
-
Recreate the volume and restart services:
docker volume create postgres_data docker compose up -d
-
(Optional) Restart Docker Desktop to clear memory (recommended on macOS/Windows)
-
Wait for the database to be ready, then load the dump:
# Wait for healthcheck to pass docker compose ps # Load the dump time gunzip -c ~/protwis.sql.gz | docker exec -i postgres-rdkit psql -U protwis -d protwis -q -1
Windows does not have gunzip or time by default. Use PowerShell instead:
# Download to your Downloads folder
curl.exe -L -o "$env:USERPROFILE\Downloads\protwis.sql.gz" "https://files.gpcrdb.org/protwis_sp.sql.gz"Using gzip via Docker (no extra software needed):
# Copy the gzipped file into the container
docker cp "$env:USERPROFILE\Downloads\protwis.sql.gz" postgres-rdkit:/tmp/protwis.sql.gz
# Decompress and load inside the container
docker exec postgres-rdkit bash -c "gunzip -c /tmp/protwis.sql.gz | psql -U protwis -d protwis -q -1"
# Clean up
docker exec postgres-rdkit rm /tmp/protwis.sql.gzThis image exposes port 5432 (the postgres port), so standard container linking will make it automatically available to the linked containers.
POSTGRES_PASSWORD: Superuser password for PostgreSQL (usePOSTGRES_PASSWORD_FILEfor secrets instead).POSTGRES_USER: Superuser username (defaultpostgres).POSTGRES_DB: Default database that is created when the image is first started.PGDATA: Location for the database files (default/var/lib/postgresql/data).
See the official postgres image for more details.
Images are automatically built and published to GitHub Container Registry (ghcr.io) via GitHub Actions when:
- Code is pushed to the
mainbranch - The workflow is manually triggered
The build process:
- Builds multi-architecture images (AMD64 and ARM64)
- Tags images with both
latestand date-based versions (YYYY.MM.DD format) - Publishes to GitHub Container Registry as
ghcr.io/protwis/postgres_rdkit_docker
See .github/workflows/build_multi_arch.yml for the build configuration.
To build the image manually:
docker build -t ghcr.io/protwis/postgres_rdkit_docker:latest .The Dockerfile uses a multi-stage build:
- Stage 1 (Builder): Compiles RDKit from source on
postgres:16-bookworm - Stage 2 (Final): Creates the final image with only runtime dependencies
Build arguments:
RDKIT_VERSION: RDKit version to build (default:Release_2025_03_1)
Example with custom RDKit version:
docker build \
--build-arg RDKIT_VERSION=Release_2024_09_2 \
-t ghcr.io/protwis/postgres_rdkit_docker:custom .The image includes a pre-configured postgresql.conf optimized for RDKit workloads.
What the defaults target. This is a development image, and its default configuration is tuned for a single developer on a Mac with Docker Desktop given ~12 CPUs and 16 GB RAM — the environment in which the values below were measured, against a 26 GB GPCRdb database. It also suits comparable workstations (Apple Silicon, Oracle Cloud ARM) with ~10–16 GB available to the container.
If your environment differs, override the settings — see Adapting the configuration.
Two defaults in particular assume a single developer and will bite a multi-user deployment:
max_connections = 20. Ample for a dev server pluspsqlplus Adminer, but a multi-user or multi-stack setup will fail withFATAL: sorry, too many clients already.work_mem = 1GBwithhash_mem_multiplier = 8. These limits apply per sort/hash node, per connection, so they are only safe because the connection count is small. If you raisemax_connections, lowerwork_memin the same change.On a small instance (e.g. 2 GB) you must lower
shared_buffers,effective_cache_sizeandwork_memor the server will be OOM-killed. On a machine with few cores, lowermax_parallel_workers_per_gather.
shared_buffers = 2560MB: measured optimum for a 16 GB container on a 26 GB database. Raising it was slower — memory reserved here is taken from the VM page cache, which serves a mostly-sequential working set bettereffective_cache_size = 7GB: measured optimum (4 GB and 12 GB both tested and slower)work_mem = 1GBandhash_mem_multiplier = 8: worth ~1.6× on aDISTINCTover 24 million rows, where spilling dropped from ~300,000 blocks to zero. Safe here only in combination withmax_connections = 20— see the note abovemaintenance_work_mem = 512MB: for vacuum and index creation
random_page_cost = 2.0: chosen by sweeping 1.1 / 1.5 / 2.0 / 3.0 / 4.0. Below ~1.5 the planner switches RDKit GiST scans from Bitmap Heap Scan to plain Index Scan, which measured slower; 2.0 was best across both chemistry and relational querieseffective_io_concurrency = 200: prefetch depth for NVMemin_wal_size = 4GB,max_wal_size = 8GB: reduce checkpoint frequency during bulk loadscheckpoint_completion_target = 0.9: spread checkpoint I/O load
max_worker_processes = 16,max_parallel_workers = 12max_parallel_workers_per_gather = 8: the single most effective setting for GPCRdb's workload. Raising it from 4 to 8 was worth 1.6–2.8× on large scans, aggregations and multi-table joins. Lower this on machines with fewer than ~8 cores.max_parallel_maintenance_workers = 4: helps B-tree index builds. Note it has no effect on RDKit GiST index builds, because PostgreSQL has no parallel GiST build
default_statistics_target = 100: PostgreSQL's own default. Raising it to 200 madeANALYZE~2.5× slower with no measurable query benefit on this workloadjit = off: heavy aggregate queries measured 4.5–7.9% faster with JIT disabled. Set toonif your workload is dominated by long analytical queries that benefit from expression compilation
max_connections = 20: sized for a single developer. A Django dev server holds single-digit connections (Django's defaultCONN_MAX_AGE = 0opens and closes one per request), leaving room forpsqland Adminer. Raise this for any multi-user deployment — and lowerwork_memwhen you do.synchronous_commit = on: ensure data durabilityfull_page_writes = on: protect against partial page writeslisten_addresses = '*': listen on all interfaces
The quickest way to change a few settings is to append -c flags to the command — no rebuild and
no config file needed:
# docker-compose.yml
services:
db:
image: ghcr.io/protwis/postgres_rdkit_docker:latest
command: >
postgres -c config_file=/etc/postgresql/postgresql.conf
-c shared_buffers=512MB
-c effective_cache_size=1536MB
-c work_mem=16MB
-c max_parallel_workers_per_gather=2Suggested starting points for environments other than the default. These follow standard
PostgreSQL sizing practice (shared_buffers ≈ 25 % of available RAM, effective_cache_size
≈ 65–75 %, work_mem scaled down as max_connections goes up); only the 16 GB column was
measured on this image.
| Setting | 2 GB container | 8 GB container | 16 GB (default, measured) | 32 GB, many clients |
|---|---|---|---|---|
shared_buffers |
512MB |
2GB |
2560MB |
8GB |
effective_cache_size |
1536MB |
6GB |
7GB |
24GB |
work_mem |
16MB |
256MB |
1GB |
32MB |
hash_mem_multiplier |
2 |
4 |
8 |
2 |
max_connections |
20 |
20 |
20 |
200 |
maintenance_work_mem |
128MB |
512MB |
512MB |
1GB |
max_parallel_workers_per_gather |
2 |
4 |
8 |
8 |
max_parallel_workers |
4 |
8 |
12 |
16 |
min_wal_size / max_wal_size |
256MB / 1GB |
1GB / 4GB |
4GB / 8GB |
4GB / 16GB |
Note how work_mem and hash_mem_multiplier move down in the 32 GB column even though the
machine is larger: that column assumes 200 concurrent clients, and the limits are per node per
connection. Memory sizing follows the connection count, not the machine.
Two rules of thumb worth stating explicitly, because both were measured here:
- More
shared_buffersis not better. On a 16 GB container with a 26 GB database, 4 GB and 6 GB were both slower than 2560 MB. Memory given to PostgreSQL is taken away from the kernel page cache, and a large mostly-sequential working set is served better by the latter. max_parallel_workers_per_gathershould track core count, not RAM. It is the highest-value setting for this workload on a many-core machine and a liability on a small one.
For a heavier rewrite, mount your own file:
docker run -d \
--name postgres-rdkit \
-v /path/to/custom/postgresql.conf:/etc/postgresql/postgresql.conf \
-e POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password \
ghcr.io/protwis/postgres_rdkit_docker:latest \
postgres -c config_file=/etc/postgresql/postgresql.confThe image ships /usr/local/share/postgresql/rdkit-tuning.sql. Run it once per database, after
CREATE EXTENSION rdkit (for a Django project, after the migration that creates the extension):
docker exec -i <container> psql -U protwis -d protwis \
-f /usr/local/share/postgresql/rdkit-tuning.sqlIt is idempotent, changes no data and no query results, and is a no-op in a database without the extension.
Why it matters. The cartridge declares its parsing functions PARALLEL SAFE and IMMUTABLE
but gives most of them no COST, so PostgreSQL prices them like an integer comparison. Measured on
this image, mol_from_smiles(cstring) costs ~123 µs per call — an underestimate of roughly five
orders of magnitude — so a bulk molecule build plans as a serial sequential scan even on a
12-core machine. On 222,036 real ligands:
| time | workers | |
|---|---|---|
| without the hints | 55.5 s | 0 |
with COST applied |
14.1 s | 3 |
with COST + relation-level parallel_workers (both in the file) |
6.9 s | 8 |
The script cannot run automatically at container start, because the extension is created by the
application's migrations rather than at initdb time.
The included postgresql.conf is already optimized for RDKit workloads. However, if you need to further optimize for specific use cases:
If you're doing bulk inserts and building indexes, you can temporarily adjust these settings (at the cost of data safety):
-- WARNING: These settings reduce data safety
ALTER SYSTEM SET synchronous_commit = 'off';
ALTER SYSTEM SET full_page_writes = 'off';
SELECT pg_reload_conf();Warning:
synchronous_commit = off: Speeds normal operation but increases the chance of losing commits if PostgreSQL crashes. Commits will be reported as executed even if not stored and flushed to durable storage.full_page_writes = off: Speeds normal operation but might lead to unrecoverable or silent data corruption after a system failure.
Recommendation: Only use these settings during initial data loading. To revert to production safety:
-- Revert to safe production defaults
ALTER SYSTEM RESET synchronous_commit;
ALTER SYSTEM RESET full_page_writes;
SELECT pg_reload_conf();The default configuration already includes optimized memory settings:
shared_buffers = 2560MB: PostgreSQL's dedicated RAMwork_mem = 1GB,hash_mem_multiplier = 8: maximum RAM per sort/hash node, per connection, before spilling to disk
These settings increase the RAM requirements for PostgreSQL. Ensure your container has sufficient memory allocated.
It is, for the single-developer profile this image targets — and the reasoning is worth stating, because the arithmetic looks alarming at first.
The theoretical worst case is work_mem × hash_mem_multiplier × (workers + 1) per node, which with
these defaults is far more memory than a 16 GB container has. In practice PostgreSQL's hash nodes
are budget-aware and spill to disk rather than exceeding their allowance. Measured on this image
with a deliberately hostile query — a forced parallel HashAggregate with array_agg over
24 million rows — the query used 1.27 GB of memory plus 744 MB of disk spill, partitioned itself
into 8 batches, and peaked at 6.11 GiB of the container's 15.6 GiB with no OOM and every backend
surviving. hash_mem_multiplier raises the ceiling, not the actual usage.
What makes it safe is max_connections = 20. The limit is per node per connection, so the
safety argument is about concurrency, not about the machine's size.
For a multi-user deployment, either scale work_mem down as you raise max_connections, or
keep the connection count high and grant the memory to a single role:
CREATE ROLE analytics LOGIN CONNECTION LIMIT 2;
ALTER ROLE analytics SET work_mem = '1GB';
ALTER ROLE analytics SET hash_mem_multiplier = 8;To verify your own configuration under load, read Memory Usage, Batches and Disk Usage from
EXPLAIN (ANALYZE, BUFFERS) and check the container's memory.events oom counter.
Source: RDKit Cartridge Configuration
For more information, see the RDKit PostgreSQL Cartridge documentation.
- Base Image:
postgres:16-bookworm - RDKit Version: Release_2025_03_1
- PostgreSQL Version: 16
- Architectures: linux/amd64, linux/arm64
- Image Size: ~262 MB compressed (what
docker pulltransfers), ~1.12 GB on disk after extraction
See LICENSE file for details.