Configuring Single-Port SNI Routing for PuppyGraph Behind Nginx TLS Proxy
Summary
In this tutorial, you will:
-
Deploy PuppyGraph with Nginx as a TLS termination proxy using Docker Compose
-
Configure Nginx to route both HTTPS and Bolt protocol traffic through a single port (443)
-
Secure your PuppyGraph deployment with TLS/SSL certificates
-
Access PuppyGraph Web UI and Bolt endpoint through secure connections
Prerequisites
Docker
Please ensure that docker and docker-compose are available. The installation can be verified by running:
See https://www.docker.com/get-started/ for more details on Docker installation.
OpenSSL
OpenSSL is required to generate SSL certificates. Verify installation:
Most systems have OpenSSL pre-installed. If not, install it using your system's package manager.
Hardware
Please see System Requirements for the minimum hardware requirements.
Architecture Overview
This deployment uses Nginx as a reverse proxy with TLS termination.
All external traffic enters through port 443, where Nginx terminates the TLS connection and routes traffic based on the hostname (SNI - Server Name Indication).
Traffic Flow
When a client connects to port 443, Nginx examines the hostname in the TLS handshake:
-
If the hostname is bolt.puppygraph.local, traffic is routed to the Bolt protocol endpoint (PuppyGraph port 7687)
-
If the hostname is puppygraph.local, traffic is routed to the HTTP API endpoint (PuppyGraph port 8081)
After TLS termination, Nginx forwards unencrypted traffic to PuppyGraph over the private Docker network.
Key Features
-
Single port (443) for all traffic
-
SNI-based routing to distinguish HTTP and Bolt protocols
-
TLS encryption for all external connections
-
Backend communication on private unencrypted network
Setup Instructions
Step 1: Create Project Directory
Create a directory for this tutorial deployment:
Step 2: Create Docker Compose Configuration
Create a file named docker-compose.yml:
services:
nginx_proxy:
image: nginx:alpine
container_name: nginx-proxy
ports:
- "443:443"
networks:
puppygraph_net:
volumes:
- ./certs:/etc/nginx/ssl:ro
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- puppygraph
puppygraph:
image: puppygraph/puppygraph:stable
container_name: puppygraph
pull_policy: always
environment:
- PUPPYGRAPH_PASSWORD=puppygraph123
- QUERY_TIMEOUT=5m
ports:
- "8081:8081"
- "8182:8182"
- "7687:7687"
volumes:
- puppygraph-data:/data/storage
networks:
puppygraph_net:
networks:
puppygraph_net:
volumes:
puppygraph-data:
Step 3: Create Nginx Configuration
Create a file named nginx.conf and put it under folder nginx:
events {
worker_connections 1024;
}
# Stream module - Routes based on SNI WITHOUT terminating TLS
stream {
map $ssl_preread_server_name $backend {
bolt.puppygraph.local 127.0.0.1:7688; # Route to bolt TLS terminator
~^bolt\. 127.0.0.1:7688;
default 127.0.0.1:8443; # Route to HTTP TLS terminator
}
# Main entry point - reads SNI and routes
server {
listen 443;
proxy_pass $backend;
ssl_preread on;
proxy_timeout 3600s;
}
# Bolt TLS termination endpoint (internal)
upstream bolt_backend {
server puppygraph:7687;
}
server {
listen 127.0.0.1:7688 ssl;
proxy_pass bolt_backend;
proxy_timeout 3600s;
# Terminate TLS here for Bolt traffic
ssl_certificate /etc/nginx/ssl/puppygraph.crt;
ssl_certificate_key /etc/nginx/ssl/puppygraph.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
}
}
# HTTP module - Terminates TLS for HTTP traffic
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
upstream http_backend {
server puppygraph:8081;
}
# HTTP TLS termination endpoint (internal)
server {
listen 127.0.0.1:8443 ssl http2;
server_name _;
# Terminate TLS here for HTTP traffic
ssl_certificate /etc/nginx/ssl/puppygraph.crt;
ssl_certificate_key /etc/nginx/ssl/puppygraph.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=31536000" always;
location / {
proxy_pass http://http_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}
Step 4: Generate SSL Certificates
For development and testing, generate self-signed certificates:
mkdir certs
openssl req -x509 -nodes -sha256 -newkey rsa:4096 -keyout certs/puppygraph.key -out certs/puppygraph.crt -days 3650 -subj "/C=US/ST=CA/L=Redwood/O=PuppyGraph/CN=bolt.puppygraph.local" -addext "subjectAltName=DNS:puppygraph.local,DNS:bolt.puppygraph.local"
Production Note: In production environments, replace self-signed certificates with valid certificates from a Certificate Authority (CA).
Step 5: Configure Local DNS
For local testing, add entries to /etc/hosts (Linux/Mac) or C:\Windows\System32\drivers\etc\hosts (Windows):
This allows SNI-based routing to work correctly with different hostnames.
Step 6: Start the Services
Start the docker compose stack:
By using docker compose ps, you should be able to see both nginx-proxy and puppygraph containers in the Up state.
Access the PuppyGraph
Access the Web UI
Open your browser and navigate to: https://puppygraph.local
Note: Since we're using a self-signed certificate, your browser will show a security warning. This is expected for development environments. Click "Advanced" and proceed to the site.
In this tutorial, we'll be utilizing the demo data supplied by PuppyGraph. Click on Use example schema/data, and the UI will show that loading is underway.
Once the schema is loaded, the page visualizes the schema of the graph.
Access via Bolt Protocol
After we've set up the schema, we can use the following python test script to connect to PuppyGraph using the Bolt protocol with TLS:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import neo4j
import argparse
import json
import time
# Default connection settings
DEFAULT_URI = "bolt+ssc://bolt.puppygraph.local:443"
DEFAULT_USERNAME = "puppygraph"
DEFAULT_PASSWORD = "puppygraph123"
# Test query
QUERY = "RETURN 1 AS ok"
def run_query(uri: str, user: str, password: str):
"""Execute a simple Cypher query and return results + timings."""
driver = neo4j.GraphDatabase.driver(uri, auth=(user, password))
try:
with driver.session() as session:
t0 = time.perf_counter()
result = session.run(QUERY)
rows = [dict(record) for record in result]
summary = result.consume()
t1 = time.perf_counter()
timings = {
"server_result_available_ms": summary.result_available_after,
"server_result_consumed_ms": summary.result_consumed_after,
"client_wall_ms": round((t1 - t0) * 1000, 2),
}
return rows, timings
finally:
driver.close()
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--uri", default=DEFAULT_URI, help="Bolt URI (bolt:// or bolt+ssc://)")
ap.add_argument("--user", default=DEFAULT_USERNAME, help="Username")
ap.add_argument("--password", default=DEFAULT_PASSWORD, help="Password")
args = ap.parse_args()
rows, timings = run_query(args.uri, args.user, args.password)
print(json.dumps({
"params": {"uri": args.uri, "user": args.user},
"count": len(rows),
"results": rows,
"timings": timings
}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
Important:
-
You must use bolt.puppygraph.local as the hostname for Bolt connections. This allows Nginx to route the traffic correctly via SNI.
-
You must use
bolt+sscprotocol. or useboltprotocol with ssl context as demonstrated below:
# Create an SSL context that trusts the self-signed cert
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
driver = neo4j.GraphDatabase.driver(uri, auth=(user, password), encrypted=True, ssl_context=ssl_context)
Cleaning Up
Stop Services
To stop services while preserving data:
To wipe everything: