---
title: "Deploy PGvector & PGadmin on Docker and Ditch Pinecone"
description: "Deploy PGvector & PGadmin on Docker Compose and run your own vector database. Self-host embeddings, skip Pinecone's $50/month minimum, and save big."
date: 2026-07-29
categories: ["self-hosting"]
tags: ["pgvector","postgresql","docker"]
---

import Button from "../../components/widgets/Button.astro";
import { Picture } from "astro:assets";
import YouTubeEmbed from "../../components/widgets/YouTubeEmbed.astro";
import Notice from "../../components/widgets/Notice.astro";
import ListCheck from "../../components/widgets/ListCheck.astro";
import Accordion from "../../components/widgets/Accordion.astro";
import imag1 from "../../assets/images/24/01/cloudflare-tunel-setup.png";

**PGvector** turns PostgreSQL into a self-hosted vector database. It lets you store, index, and query embeddings right inside Postgres instead of paying for a separate managed service. **PGAdmin** gives you a web UI to manage it. Together on Docker, you get a vector database stack running on a cheap VPS in under 10 minutes.

The original guide used the `ankane/pgvector` Docker image (abandoned since 2023) and an outdated Docker Compose format. This update fixes all of that: the official `pgvector/pgvector:pg17` image, current Compose syntax, security improvements, and verification steps the original lacked.

If you've been paying for Pinecone or another managed vector database, this is the guide that lets you cancel that bill.

<Notice type="info" title="About the video below">
The video was recorded with an older version of the stack (`ankane/pgvector` image, Docker Compose v1). The written guide below has the fully updated steps.
</Notice>

<YouTubeEmbed
  url="https://www.youtube.com/embed/WSkP9EkBsh0"
  label="How to Deploy PGvector and PGadmin on Docker and Ditch Pinecone"
/>

## What is PGvector? PostgreSQL as a vector database

PGvector is a Postgres extension that adds vector similarity search to any PostgreSQL database. It supports exact and approximate nearest neighbor search with HNSW and IVFFlat indexing, distance operators for L2, cosine, and inner product, and up to 16,000 dimensions per vector (HNSW indexing supports up to 2,000 for full precision, 4,000 with `halfvec`, 64,000 with `bit`).

With 22,000+ GitHub stars, pgvector has become the default choice for teams that want vector search without running a separate service. The "one database for everything" argument is strong: your relational data and your embeddings live in the same Postgres instance. Same backup strategy, same access controls, same monitoring stack. No separate billing, no separate API keys.

Common use cases:

- **RAG pipelines**: store OpenAI or local LLM embeddings and retrieve relevant context for augmenting AI responses.
- **Semantic search**: find similar documents, products, or support tickets by vector distance instead of keyword matching.
- **Recommendations**: match users to items based on embedding similarity.
- **Image/audio search**: compare multimodal embeddings from CLIP or similar models.

For higher scale, TimescaleDB's `pgvectorscale` extension adds StreamingDiskANN indexing on top of pgvector. Not needed for most setups, but it's there when you outgrow basic HNSW.

<Notice type="info" title="PostgreSQL version support">
pgvector 0.8.5 (July 2026) supports PostgreSQL 13 and later (including PG 18). PostgreSQL 12 support was dropped in pgvector 0.8.0. This guide uses the `pgvector/pgvector:pg17` image, which bundles PostgreSQL 17 with pgvector 0.8.5.
</Notice>

## Why ditch Pinecone? Updated pricing and cost comparison

The "ditch Pinecone" angle was relevant in 2024 when this article was first published. It's gotten stronger since. In October 2025, Pinecone introduced a **$50/month minimum** on their Standard plan. That change effectively killed the hobby-tier use case and sent the vector database community looking for alternatives.

### Pinecone's $50/month minimum vs self-hosted pgvector

Here's the cost math:

| Setup | Monthly Cost | What You Get |
|-------|-------------|--------------|
| Pinecone Starter (free) | $0 | 2 GB storage, 1M read units/mo, 5 indexes, community support only |
| Pinecone Builder | $20/mo | Usage-capped, solo dev tier |
| Pinecone Standard | $50/mo | Production use, per-unit pricing on top: $8.25/M reads, $2.00/M writes |
| **Self-hosted pgvector on [Hetzner](https://go.bitdoze.com/hetzner) CX22** | **~€5/mo** | Unlimited queries, unlimited indexes, your data on your server |

A [Hetzner](https://go.bitdoze.com/hetzner) CX22 (2 vCPU, 4 GB RAM, 40 GB NVMe) at ~€5/month or a [Hostinger VPS](https://go.bitdoze.com/hostinger-vps) KVM plan handles pgvector for thousands of embeddings with no per-query charges. No usage caps. No surprise invoices. You can also check [Vultr](https://go.bitdoze.com/vultr) for global datacenter options.

The savings compound: Pinecone's serverless pricing adds $0.33/GB/month for storage plus per-read/write units. At moderate traffic (10M reads/month), you're looking at $82.50 in read charges alone on top of the base fee. pgvector on a VPS? Same flat €5–15/month regardless of query volume.

<Button text="Deploy on Hetzner" link="https://go.bitdoze.com/hetzner" variant="solid" color="green" size="md" icon="arrow-right" />

### Vendor lock-in, latency, and other hidden costs

Beyond price, there's the operational reality:

- **Network latency**: every vector query to Pinecone crosses the internet. A local Postgres query hits localhost. For RAG pipelines making dozens of lookups per request, that adds up fast.
- **API key management**: Pinecone requires API keys, rate limit awareness, and error handling for service outages. pgvector is just a Postgres connection.
- **Data sovereignty**: your embeddings (which often encode sensitive business knowledge) stay on your infrastructure. No third-party data processing agreements needed.
- **Pricing risk**: Pinecone already changed their pricing once. With self-hosted, your cost is whatever your VPS provider charges, and you can move providers anytime.

If you're already running Docker for your app stack, you can also [self-host your own infrastructure](https://www.bitdoze.com/dokploy-install/) with Dokploy or use a [self-hosted platform as a service](https://www.bitdoze.com/coolify-install-heroku-alternative/) like Coolify. The point is: you control the bill.

## Prerequisites

Before you start, you need:

<ListCheck>
<ul>
<li>Linux VPS with Docker support. Any provider works: [Hetzner](https://go.bitdoze.com/hetzner), [Hostinger VPS](https://go.bitdoze.com/hostinger-vps), [Vultr](https://go.bitdoze.com/vultr), DigitalOcean, or your own hardware</li>
<li>Docker Engine 24+ and Docker Compose v2+ installed. Verify with `docker compose version`</li>
<li>Basic terminal/SSH access</li>
<li>(Optional) A domain or subdomain for Cloudflare Tunnel remote access</li>
<li>(Optional) [Dockge](https://www.bitdoze.com/dockge-install/) or another container manager for easier compose file management</li>
</ul>
</ListCheck>

No Kubernetes, no external databases, no cloud accounts needed. Docker Compose handles everything.

If you're looking for a good VPS deal, check [Hetzner](https://go.bitdoze.com/hetzner) for cheap EU instances or [Hostinger VPS](https://go.bitdoze.com/hostinger-vps) for budget global options. For more container ideas, see our guide to [self-hosted Docker containers for your home server](https://www.bitdoze.com/docker-containers-home-server/).

## Environment variables (.env file)

Create a `.env` file next to your compose file. These credentials configure both Postgres and PGAdmin:

```
POSTGRES_USER=pguser
POSTGRES_PASSWORD=change-me-use-a-strong-password
POSTGRES_DB=vectors
PGADMIN_DEFAULT_EMAIL=admin@example.com
PGADMIN_DEFAULT_PASSWORD=change-me-too
```

Replace the placeholder passwords with real ones. The `POSTGRES_*` variables configure the database; the `PGADMIN_*` variables set your PGAdmin login.

The compose file uses `${VAR:-default}` syntax, so if you miss a variable it falls back to a default value. But don't rely on defaults for credentials. Set them explicitly.

For more on how Docker handles environment variables, see [environment variables in Docker Compose](https://www.bitdoze.com/docker-env-vars/).

<Notice type="warning" title="Don't commit .env to version control">
Add `.env` to your `.gitignore`. For production deployments, consider [securing your Docker Compose credentials](https://www.bitdoze.com/docker-compose-secrets/) with Docker secrets or a vault.
</Notice>

## Docker Compose file for pgvector and PGAdmin

This is the core deliverable. Copy this into `docker-compose.yml` (or `compose.yml`, both work):

### Complete docker-compose.yml

```yaml
services:
  db:
    image: pgvector/pgvector:pg17
    container_name: pgvector_db
    restart: unless-stopped
    shm_size: '256mb'
    ports:
      - "127.0.0.1:5432:5432"
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-vector}
      POSTGRES_USER: ${POSTGRES_USER:-user}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./local_pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-user} -d ${POSTGRES_DB:-vector}"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10s

  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: pgadmin4
    restart: unless-stopped
    ports:
      - "5016:80"
    environment:
      PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
    user: "${UID:-5050}:${GID:-5050}"
    volumes:
      - ./pgadmin-data:/var/lib/pgadmin
    depends_on:
      db:
        condition: service_healthy
```

### What changed from the previous version

If you're migrating from the old article's compose file, here's every change and why:

| Change | Why |
|--------|-----|
| `ankane/pgvector` → `pgvector/pgvector:pg17` | The `ankane/pgvector` image is abandoned (last update: Oct 2023, pgvector 0.5.1). The official image moved to the `pgvector` Docker Hub org with pgvector 0.6.0. |
| Removed `version: "3.8"` | The `version` field is deprecated in Compose v2+ and ignored entirely. Modern compose files start with `services:` directly. |
| Added `shm_size: '256mb'` | Parallel HNSW index builds (pgvector 0.6.0+) use shared memory. Default Docker `/dev/shm` is 64 MB, which causes crashes during large index builds. |
| Port binding `0.0.0.0:5432` → `127.0.0.1:5432` | Don't expose Postgres to the internet. PGAdmin connects via Docker's internal network anyway. |
| Removed `POSTGRES_HOST_AUTH_METHOD=trust` | This disabled password authentication entirely. Anyone who could reach port 5432 could connect without credentials. |
| Added `depends_on` with `condition: service_healthy` | PGAdmin now waits for Postgres to be ready before starting. No more race conditions on fresh deploys. |
| Added `start_period: 10s` to healthcheck | Avoids false "unhealthy" reports during Postgres initialization. |
| Removed `hostname` | Not needed with modern Docker networking. `container_name` is sufficient for identification. |
| `docker-compose` (hyphen) → `docker compose` (space) | Docker Compose v1 (`docker-compose` with hyphen) was removed in April 2025. The command is now `docker compose`. |
| `${VAR:-default}` fallbacks | The compose file won't break if an env var is missing. It uses sensible defaults. |

<Notice type="error" title="POSTGRES_HOST_AUTH_METHOD=trust was a security risk">
The old compose file included `POSTGRES_HOST_AUTH_METHOD=trust`, which disabled password authentication entirely. Anyone who could reach port 5432 could connect without a password. The updated file removes it. If you're migrating from the old setup, make sure `POSTGRES_PASSWORD` is set in your `.env` file and update any connection strings that relied on passwordless auth.
</Notice>

<Notice type="warning" title="The ankane/pgvector image is abandoned">
The `ankane/pgvector` Docker image hasn't been updated since October 2023 (pgvector 0.5.1). The official image is now `pgvector/pgvector` on Docker Hub. Always use the official org image.
</Notice>

<Accordion label="Why bind Postgres to 127.0.0.1?" group="faq">
By default, Docker maps published ports to `0.0.0.0`. That means anyone on the internet can reach your Postgres port if no firewall is in place. Binding to `127.0.0.1` means only processes on the host machine (and other Docker containers on the same network) can connect.

This is fine because PGAdmin connects to the `db` service via Docker's internal network, not through the host port. If you need remote Postgres access from another server, use an SSH tunnel or a VPN. Never expose port 5432 directly to the internet.
</Accordion>

## Deploy pgvector and PGAdmin with Docker Compose

With the compose file and `.env` in place, deploy:

```bash
# Pull the latest images
docker compose pull

# Start in detached mode
docker compose up -d

# Verify both containers are running
docker compose ps

# Check Postgres logs for errors
docker compose logs db

# Check PGAdmin logs
docker compose logs pgadmin
```

**What success looks like:** `docker compose ps` shows both `pgvector_db` and `pgadmin4` as "Up". The `db` service should show `(healthy)` in the status column after a few seconds.

**What failure looks like:** If `db` shows "unhealthy" or keeps restarting, check `docker compose logs db`. Common causes are port 5432 already in use on the host, or the data volume from a previous Postgres install with incompatible settings.

For more Docker commands and troubleshooting, see [essential Docker commands](https://www.bitdoze.com/docker-commands/).

> If you are interested to monitor server resources like CPU, memory, disk space you can check: [How To Monitor Server and Docker Resources](https://www.bitdoze.com/sever-monitoring/)

## Accessing PGAdmin and connecting to pgvector

1. Open your browser to `http://your-vps-ip:5016`.
2. Log in with the `PGADMIN_DEFAULT_EMAIL` and `PGADMIN_DEFAULT_PASSWORD` from your `.env` file.
3. In PGAdmin, right-click **Servers** in the left panel → **Register** → **Server**.
4. In the **General** tab, give it a name (e.g., "pgvector").
5. In the **Connection** tab, fill in:
   - **Host name/address**: `pgvector_db` (the container name, Docker resolves it via internal network)
   - **Port**: `5432`
   - **Username**: the value of `POSTGRES_USER` from your `.env`
   - **Password**: the value of `POSTGRES_PASSWORD` from your `.env`
6. Click **Save**. The database should appear in PGAdmin's browser panel.

**Verify:** Open a Query Tool (right-click the database → Query Tool) and run `SELECT version();` to confirm the Postgres version.

<Notice type="info" title="PGAdmin 9.x workspace layouts">
PGAdmin 9.x uses Workspace layouts by default. If you prefer the classic interface, switch via the user menu in the top-right corner.
</Notice>

## After deployment: verify pgvector and create your first index

The original article stopped at "deploy." That's not enough. You need to confirm pgvector actually works, and understand how to index your data for real workloads.

### Verify the pgvector extension is working

Open a Query Tool in PGAdmin (or run via `docker compose exec db psql -U pguser -d vectors`) and execute:

```sql
-- Enable the extension (first time per database)
CREATE EXTENSION IF NOT EXISTS vector;

-- Check the version
SELECT extversion FROM pg_extension WHERE extname = 'vector';
-- Should return '0.8.5' or similar

-- Test a simple vector operation
SELECT '[1,2,3]'::vector <-> '[4,5,6]'::vector AS distance;
-- Should return: 5.196152422706632
```

If the version query returns a value >= 0.8.2 and the distance query returns a number, pgvector is operational.

### Create an HNSW index for fast similarity search

For any real use case, you need an index. Without one, pgvector does a sequential scan on every query. Fine for 100 rows, terrible for 100,000.

```sql
-- Create a table with a vector column
CREATE TABLE items (
    id bigserial PRIMARY KEY,
    content text,
    embedding vector(1536)  -- OpenAI ada-002 dimension
);

-- HNSW index (recommended for most use cases)
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);

-- For production on large tables: create concurrently to avoid blocking writes
CREATE INDEX CONCURRENTLY ON items USING hnsw (embedding vector_cosine_ops);

-- Increase maintenance_work_mem for faster index builds
SET maintenance_work_mem = '2GB';
```

### HNSW vs IVFFlat: which index should you use?

| | HNSW | IVFFlat |
|---|------|---------|
| **Query performance** | Better | Lower |
| **Build speed** | Slower (but parallel since 0.6.0) | Faster |
| **Memory during build** | Higher | Lower |
| **Works on empty table** | Yes | No, needs data first |
| **Default choice** | **Yes — use this** | Only if build time/memory is the bottleneck |

**Default recommendation:** Use HNSW. It's the better choice for almost every workload. IVFFlat is only worth considering when you have a very large dataset where index build time is the bottleneck and you can tolerate slightly lower query accuracy.

<Notice type="info" title="Parallel HNSW builds in pgvector 0.6.0+">
pgvector 0.6.0+ supports parallel HNSW index builds, up to 30x faster than before. The `shm_size: '256mb'` setting in the compose file ensures this works correctly in Docker. If you increase `maintenance_work_mem` beyond 256 MB, bump `shm_size` to match.
</Notice>

<Accordion label="What about pgvectorscale and DiskANN?" group="pgvectorscale">
TimescaleDB's `pgvectorscale` extension adds StreamingDiskANN indexing with Statistical Binary Quantization (SBQ) on top of pgvector. For workloads that need higher scale than native pgvector offers, it's worth evaluating.

```sql
CREATE EXTENSION IF NOT EXISTS vectorscale CASCADE;
CREATE INDEX ON items USING diskann (embedding vector_cosine_ops);
```

Benchmark claims suggest 28x lower p95 latency and 16x higher throughput vs Pinecone's s1 index at 99% recall, at 75% less cost self-hosted. Not needed for most setups, but it's the upgrade path when you outgrow basic HNSW.
</Accordion>

## Configure Cloudflare Tunnel for secure remote access

Instead of opening PGAdmin's port directly to the internet, you can expose it through a Cloudflare Tunnel. This gives you SSL, DDoS protection, and Cloudflare Access policies — without exposing your VPS IP address.

1. In the Cloudflare dashboard, go to **Access → Tunnels**.
2. Select your existing tunnel (or create a new one).
3. Add a hostname mapping your subdomain (e.g., `pgadmin.yourdomain.com`) to the service `http://localhost:5016`.
4. Save and wait for the tunnel to connect.

<Picture src={imag1} alt="Cloudflare Tunnel configuration connecting a subdomain to pgvector PGAdmin Docker container on port 5016" />

Once the tunnel is active, access PGAdmin at `https://pgadmin.yourdomain.com` with full Cloudflare protection.

<Notice type="info" title="Alternative reverse proxies">
You can also [set up Traefik as your reverse proxy](https://www.bitdoze.com/traefik-proxy-docker/) or [use CloudPanel as a reverse proxy with Docker](https://www.bitdoze.com/cloudpanel-setup-dockge/) instead of Cloudflare Tunnel. Pick whichever fits your stack.
</Notice>

## Security best practices for your pgvector Docker setup

The updated compose file already addresses the biggest security issues from the original article. Here's the full picture:

### Remove POSTGRES_HOST_AUTH_METHOD=trust

Already done in the updated compose file. The old file included this setting, which disabled password authentication entirely. Anyone who could reach port 5432 could connect without credentials. If you're migrating from the old setup, verify that `POSTGRES_PASSWORD` is set and update any connection strings.

### Bind Postgres to localhost

The compose file binds `127.0.0.1:5432:5432` instead of `0.0.0.0:5432:5432`. PGAdmin connects via Docker's internal network (not the host port), so this only affects external access. If you need to reach Postgres from another server, use SSH tunnels or a VPN.

### Use strong passwords and secure your .env

Don't use `pass` or `pgpass` as your Postgres password. Generate random strings:

```bash
# Generate a random password
openssl rand -base64 32
```

Add `.env` to `.gitignore`. For production, consider [securing your Docker Compose credentials](https://www.bitdoze.com/docker-compose-secrets/) with Docker secrets or a vault.

### Keep pgvector updated

<Notice type="error" title="CVE-2026-3172: critical security fix">
pgvector 0.8.0–0.8.1 had a heap buffer overflow vulnerability (CVE-2026-3172) in parallel HNSW index builds. This was fixed in 0.8.2. The `pgvector/pgvector:pg17` tag currently ships 0.8.5, which is safe. If you're running an older version, update immediately:

```bash
docker compose pull && docker compose up -d
```
</Notice>

Periodically check for updates. The `pgvector/pgvector:pg17` tag follows the latest pgvector release for PostgreSQL 17. A `docker compose pull && docker compose up -d` gets you the latest patch.

## Back up your vector database

The original article didn't mention backups. For a self-hosted database, this is critical.

```bash
# Backup a single database
docker compose exec db pg_dump -U pguser vectors > backup_$(date +%F).sql

# Backup the entire cluster (all databases)
docker compose exec db pg_dumpall -U pguser > full_backup_$(date +%F).sql

# Restore from backup
docker compose exec -T db psql -U pguser vectors < backup_2026-07-21.sql
```

For automated backups, add `pg_dump` to a cron job and sync the output to S3-compatible storage. The `./local_pgdata` volume directory can also be backed up directly — but stop the container first for a consistent snapshot, or use `pg_dump` for a live backup.

<Notice type="warning" title="Never back up the raw data directory while Postgres is running">
File-level copies of `./local_pgdata` while Postgres is up can produce inconsistent snapshots. Use `pg_dump` for live backups, or `docker compose stop db` before copying the volume.
</Notice>

## Scaling beyond the basics: halfvec, pgvectorscale and more

Once you have the basic setup running, pgvector has several levers for when your workload grows:

**`halfvec` type** (pgvector 0.7.0+) — stores vectors at half precision: 2 bytes per dimension instead of 4. OpenAI 1536-dim embeddings go from ~6 KB to ~3 KB each. Halves storage and memory costs.

**`sparsevec` type** (pgvector 0.7.0+) — sparse vectors for high-dimensional data where most values are zero. Useful for text search embeddings that use sparse representations.

**Binary quantization** via `binary_quantize()` — compress vectors to 1-bit per dimension. Massive storage savings at the cost of some recall accuracy. Good for initial filtering before a more precise reranking step.

**`ef_search` tuning** — HNSW parameter that trades query speed for recall accuracy. Higher values = better recall, slower queries. Default is 40; bump to 100+ for critical accuracy requirements.

<Accordion label="Install pgvectorscale alongside pgvector" group="scaling">
For higher-scale workloads, TimescaleDB's `pgvectorscale` adds StreamingDiskANN indexing:

```sql
CREATE EXTENSION IF NOT EXISTS vectorscale CASCADE;
CREATE INDEX ON items USING diskann (embedding vector_cosine_ops);
```

This installs alongside pgvector (the `CASCADE` flag handles dependencies). DiskANN is designed for datasets in the tens of millions of vectors where HNSW memory usage becomes prohibitive.
</Accordion>

## Conclusion

You now have a self-hosted vector database running PostgreSQL 17 with pgvector 0.8.5, managed through a PGAdmin web interface — all on Docker Compose. Total cost: whatever your VPS runs you (€5–15/month on [Hetzner](https://go.bitdoze.com/hetzner) or [Hostinger VPS](https://go.bitdoze.com/hostinger-vps)). No per-query charges, no vendor lock-in, no pricing surprises.

Start with the basic setup. Verify the extension works, create an HNSW index, and run a few similarity queries. When you outgrow the basics, reach for `halfvec` or pgvectorscale.

If you're building out your self-hosted stack, explore more [self-hosted Docker containers for your home server](https://www.bitdoze.com/docker-containers-home-server/) or check out [self-hosted server management panels](https://www.bitdoze.com/best-self-hosted-panels/) for managing multiple services.

<Button text="Get a VPS and start" link="https://go.bitdoze.com/hetzner" variant="solid" color="green" size="lg" icon="arrow-right" />