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
| Project | Neon (by Databricks) |
| License | Apache 2.0 |
| Recommended Plan | RamNode Cloud VPS 4 GB+ (2 vCPUs) |
| OS | Ubuntu 22.04 / 24.04 LTS |
| Stack | Docker, Pageserver, Safekeeper, MinIO, Compute (Postgres) |
| Estimated Setup Time | 25–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)
Install Docker and Docker Compose
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-pluginsudo usermod -aG docker $USER
newgrp docker
docker --version
docker compose versionCreate the Project Directory
mkdir -p ~/neon-postgres && cd ~/neon-postgres
mkdir -p pageserver_config compute_wrapper/var/db/postgres/configs compute_wrapper/shellCreate the Pageserver Configuration
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/'Create the Compute Node Startup Script
This script waits for the Pageserver, creates a tenant and timeline, builds the compute spec, and launches Postgres.
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.shCreate the Docker Compose File
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:Deploy the Stack
cd ~/neon-postgres
docker compose up -d --builddocker compose logs -f
docker compose psYou should see six containers: storage_broker, minio, minio_create_buckets (exited), pageserver, safekeeper1, and compute.
Connect to Your Database
sudo apt install -y postgresql-client
psql postgresql://cloud_admin@localhost:55433/postgresCREATE 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;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.
Secure the Deployment
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 consolesudo ufw allow 55433/tcp comment "Neon Postgres"
sudo ufw allow 22/tcp comment "SSH"
sudo ufw enableSet Postgres authentication by updating the encrypted_password field in the compute spec and creating application-specific database roles.
Working with Branching
Neon's killer feature — create instant copy-on-write database branches without duplicating data.
curl -s http://localhost:9898/v1/tenant | jq .
curl -s http://localhost:9898/v1/tenant/YOUR_TENANT_ID/timeline | jq .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 athttp://localhost:7676/metrics - Backups: Enable MinIO bucket versioning and replicate with
mc mirror
