Serverless Postgres
    Open Source

    Deploy Neon Serverless Postgres on a VPS

    Open-source serverless PostgreSQL with separated storage and compute, instant database branching, and MinIO object storage — the full Neon stack self-hosted with Docker Compose.

    At a Glance

    ProjectNeon (by Databricks)
    LicenseApache 2.0
    Recommended PlanRamNode Cloud VPS 4 GB+ (2 vCPUs)
    OSUbuntu 22.04 / 24.04 LTS
    StackDocker, Pageserver, Safekeeper, MinIO, Compute (Postgres)
    Estimated Setup Time25–35 minutes

    Architecture

    • Storage Broker: Lightweight pub-sub coordination (port 50051)
    • MinIO: S3-compatible object storage for durable WAL and page data
    • Pageserver: Core storage engine — receives WAL, serves pages on demand (ports 9898/6400)
    • Safekeeper: Redundant WAL service with quorum-based replication
    • Compute: Stateless PostgreSQL instance connected to the storage layer

    Prerequisites

    • A RamNode VPS with at least 4 GB RAM and 2 vCPUs
    • Ubuntu 22.04 or 24.04 LTS
    • Docker and Docker Compose installed
    • A domain name (optional, for remote access)
    1

    Install Docker and Docker Compose

    Install Docker
    sudo apt update && sudo apt upgrade -y
    sudo apt install -y ca-certificates curl gnupg lsb-release
    
    sudo mkdir -p /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
    
    echo \
      "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
      $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    
    sudo apt update
    sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
    Add user to Docker group
    sudo usermod -aG docker $USER
    newgrp docker
    docker --version
    docker compose version
    2

    Create the Project Directory

    Set up directory structure
    mkdir -p ~/neon-postgres && cd ~/neon-postgres
    mkdir -p pageserver_config compute_wrapper/var/db/postgres/configs compute_wrapper/shell
    3

    Create the Pageserver Configuration

    pageserver_config/pageserver.toml
    listen_pg_addr = '0.0.0.0:6400'
    listen_http_addr = '0.0.0.0:9898'
    
    broker_endpoint = 'http://storage_broker:50051'
    
    # WAL checkpointing settings
    checkpoint_distance = 268435456
    checkpoint_timeout = '10m'
    
    # Garbage collection
    gc_period = '1 hour'
    gc_horizon = 67108864
    
    # Remote storage (MinIO)
    [remote_storage]
    endpoint = 'http://minio:9000'
    bucket_name = 'neon'
    bucket_region = 'eu-north-1'
    prefix_in_bucket = '/pageserver/'
    4

    Create the Compute Node Startup Script

    This script waits for the Pageserver, creates a tenant and timeline, builds the compute spec, and launches Postgres.

    compute_wrapper/shell/compute.sh
    cat > compute_wrapper/shell/compute.sh << 'SCRIPT'
    #!/bin/bash
    set -eux
    
    PG_VERSION=${PG_VERSION:-16}
    SPEC_FILE=/tmp/spec.json
    
    echo "Waiting for pageserver to become ready..."
    while ! nc -z pageserver 6400; do sleep 1; done
    echo "Pageserver is ready."
    
    generate_id() {
      local -n resvar=$1
      printf -v resvar '%04x%04x%04x%04x%04x%04x%04x%04x' \
        $SRANDOM $SRANDOM $SRANDOM $SRANDOM \
        $SRANDOM $SRANDOM $SRANDOM $SRANDOM
    }
    
    if [ -z "${TENANT_ID:-}" ]; then
      generate_id TENANT_ID
      curl -sb -X POST -H "Content-Type: application/json" \
        -d "{\"new_tenant_id\": \"${TENANT_ID}\"}" \
        "http://pageserver:9898/v1/tenant/"
    fi
    
    if [ -z "${TIMELINE_ID:-}" ]; then
      generate_id TIMELINE_ID
      curl -sb -X POST -H "Content-Type: application/json" \
        -d "{\"new_timeline_id\": \"${TIMELINE_ID}\", \"pg_version\": ${PG_VERSION}}" \
        "http://pageserver:9898/v1/tenant/${TENANT_ID}/timeline/"
    fi
    
    # Build spec and launch compute_ctl
    SCRIPT
    
    chmod +x compute_wrapper/shell/compute.sh
    5

    Create the Docker Compose File

    docker-compose.yml
    services:
      storage_broker:
        image: ghcr.io/neondatabase/neon:latest
        restart: always
        ports:
          - "50051:50051"
        entrypoint:
          - "storage_broker"
          - "--listen-addr=0.0.0.0:50051"
    
      minio:
        image: quay.io/minio/minio:latest
        restart: always
        ports:
          - "9000:9000"
          - "9001:9001"
        environment:
          MINIO_ROOT_USER: minio
          MINIO_ROOT_PASSWORD: password
        command: server /data --console-address ":9001"
        volumes:
          - minio_data:/data
    
      minio_create_buckets:
        image: quay.io/minio/mc:latest
        depends_on:
          - minio
        entrypoint: >
          /bin/sh -c "
          until /usr/bin/mc alias set minio http://minio:9000 minio password; do
            sleep 1;
          done;
          /usr/bin/mc mb minio/neon --region=eu-north-1 --ignore-existing;
          exit 0;
          "
    
      pageserver:
        image: ghcr.io/neondatabase/neon:latest
        restart: always
        environment:
          - AWS_ACCESS_KEY_ID=minio
          - AWS_SECRET_ACCESS_KEY=password
        ports:
          - "9898:9898"
        entrypoint: ["/bin/sh", "-c"]
        command:
          - "/usr/local/bin/pageserver -D /data/.neon/
             -c \"broker_endpoint='http://storage_broker:50051'\"
             -c \"listen_pg_addr='0.0.0.0:6400'\"
             -c \"listen_http_addr='0.0.0.0:9898'\"
             -c \"remote_storage={endpoint='http://minio:9000',
                  bucket_name='neon', bucket_region='eu-north-1',
                  prefix_in_bucket='/pageserver/'}\" "
        volumes:
          - pageserver_data:/data
        depends_on:
          - storage_broker
          - minio_create_buckets
    
      safekeeper1:
        image: ghcr.io/neondatabase/neon:latest
        restart: always
        environment:
          - SAFEKEEPER_ADVERTISE_URL=safekeeper1:5454
          - SAFEKEEPER_ID=1
          - BROKER_ENDPOINT=http://storage_broker:50051
          - AWS_ACCESS_KEY_ID=minio
          - AWS_SECRET_ACCESS_KEY=password
        ports:
          - "7676:7676"
        entrypoint: ["/bin/sh", "-c"]
        command:
          - "safekeeper --listen-pg=${SAFEKEEPER_ADVERTISE_URL}
             --listen-http='0.0.0.0:7676'
             --id=${SAFEKEEPER_ID}
             --broker-endpoint=${BROKER_ENDPOINT} -D /data
             --remote-storage=\"{endpoint='http://minio:9000',
             bucket_name='neon', bucket_region='eu-north-1',
             prefix_in_bucket='/safekeeper/'}\" "
        volumes:
          - safekeeper1_data:/data
        depends_on:
          - storage_broker
    
      compute:
        build:
          args:
            - REPOSITORY=ghcr.io/neondatabase
            - COMPUTE_IMAGE=compute-node-v16
            - TAG=latest
          context: .
          dockerfile_inline: |
            ARG REPOSITORY
            ARG COMPUTE_IMAGE
            ARG TAG
            FROM ${REPOSITORY}/${COMPUTE_IMAGE}:${TAG}
            RUN apt-get update && apt-get install -y curl netcat-openbsd uuid-runtime jq
        restart: always
        ports:
          - "55433:55433"
          - "3080:3080"
        entrypoint: ["/shell/compute.sh"]
        volumes:
          - ./compute_wrapper/shell/:/shell/
        depends_on:
          - pageserver
          - safekeeper1
    
    volumes:
      minio_data:
      pageserver_data:
      safekeeper1_data:
    6

    Deploy the Stack

    Start all services
    cd ~/neon-postgres
    docker compose up -d --build
    Monitor startup
    docker compose logs -f
    docker compose ps

    You should see six containers: storage_broker, minio, minio_create_buckets (exited), pageserver, safekeeper1, and compute.

    7

    Connect to Your Database

    Install psql and connect
    sudo apt install -y postgresql-client
    psql postgresql://cloud_admin@localhost:55433/postgres
    Test the connection
    CREATE TABLE test_deployment (
        id SERIAL PRIMARY KEY,
        message TEXT NOT NULL,
        created_at TIMESTAMPTZ DEFAULT NOW()
    );
    
    INSERT INTO test_deployment (message) VALUES ('Neon is running on RamNode!');
    SELECT * FROM test_deployment;
    8

    Verify Object Storage

    Access the MinIO console at http://YOUR_VPS_IP:9001 (user: minio, password: password). Browse the neon bucket to verify pageserver/ and safekeeper/ directories exist.

    Important: Change default MinIO credentials before exposing to the internet. Update MINIO_ROOT_USER/MINIO_ROOT_PASSWORD and the corresponding AWS key variables.

    9

    Secure the Deployment

    Bind internal ports to localhost
    ports:
      - "127.0.0.1:9898:9898"   # pageserver API
      - "127.0.0.1:7676:7676"   # safekeeper API
      - "127.0.0.1:50051:50051" # storage broker
      - "127.0.0.1:9000:9000"   # minio API
      - "127.0.0.1:9001:9001"   # minio console
    Firewall rules
    sudo ufw allow 55433/tcp comment "Neon Postgres"
    sudo ufw allow 22/tcp comment "SSH"
    sudo ufw enable

    Set Postgres authentication by updating the encrypted_password field in the compute spec and creating application-specific database roles.

    10

    Working with Branching

    Neon's killer feature — create instant copy-on-write database branches without duplicating data.

    List tenants and timelines
    curl -s http://localhost:9898/v1/tenant | jq .
    curl -s http://localhost:9898/v1/tenant/YOUR_TENANT_ID/timeline | jq .
    Create a branch
    curl -X POST \
      -H "Content-Type: application/json" \
      -d '{
        "new_timeline_id": "NEW_TIMELINE_HEX_ID",
        "ancestor_timeline_id": "PARENT_TIMELINE_HEX_ID"
      }' \
      "http://localhost:9898/v1/tenant/YOUR_TENANT_ID/timeline/"

    Resource Recommendations

    • Dev/testing: 2 GB RAM, 1 vCPU — full stack with light query loads
    • Small production: 4 GB RAM, 2 vCPUs — moderate concurrent connections
    • Multi-tenant: 8 GB+ RAM, 4 vCPUs — multiple compute nodes or sustained write throughput

    Maintenance

    • Update: cd ~/neon-postgres && docker compose pull && docker compose up -d --build
    • Monitoring: Pageserver metrics at http://localhost:9898/metrics, Safekeeper at http://localhost:7676/metrics
    • Backups: Enable MinIO bucket versioning and replicate with mc mirror