Every GraphGuru engagement starts the same way: a throwaway Neo4j instance on a laptop so we can model, load a sample, and argue about Cypher before anyone touches a real server. Since Neo4j moved to calendar versioning in January 2025, the fastest way to get a current instance — one that speaks Cypher 25 and matches what you will run in production — is the official Docker image. This tutorial gets you from nothing to a queryable Neo4j 2026.x database with APOC installed in about ten minutes.
What you need
- Docker Desktop (or Docker Engine + Compose v2) on macOS, Windows, or Linux
- Roughly 2 GB of free RAM for the container
- A terminal
We will use the 2026.05 image throughout. Neo4j ships a new CalVer release every month (YYYY.MM.patch), so substitute the newest tag from Docker Hub if you are reading this later; everything below works on any 2025.06+ release.
Step 1 — the Compose file
Create a working directory and save this as compose.yaml:
services:
neo4j:
image: neo4j:2026.05
container_name: neo4j-dev
ports:
- "7474:7474" # HTTP / Neo4j Browser
- "7687:7687" # Bolt / drivers
environment:
NEO4J_AUTH: neo4j/change-me-please # min 8 characters
NEO4J_PLUGINS: '["apoc"]'
NEO4J_server_memory_heap_initial__size: 1G
NEO4J_server_memory_heap_max__size: 1G
NEO4J_server_memory_pagecache_size: 512M
volumes:
- neo4j-data:/data
- neo4j-logs:/logs
- ./import:/var/lib/neo4j/import
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:7474"]
interval: 10s
timeout: 5s
retries: 12
volumes:
neo4j-data:
neo4j-logs:
Three things worth understanding in that file:
NEO4J_AUTHsets the initial password for theneo4juser. Neo4j refuses passwords shorter than eight characters, and the value is only read on the first start of an empty/datavolume. Change it later withALTER CURRENT USER SET PASSWORD.NEO4J_PLUGINS: '["apoc"]'tells the image's entrypoint to download the matching APOC Core release and enable it. Note the distinction: APOC Core ships with Neo4j and is what you get here; APOC Extended is a separate community project that you would have to mount into/pluginsyourself. Most of the APOC functions people reach for out of habit (apoc.create.uuid,apoc.date.*helpers,apoc.text.join) now have native Cypher equivalents (randomUUID(), temporal functions,reduce/stringfunctions), so check the Cypher manual before you depend on a procedure.- Config via environment uses the
NEO4J_prefix with_for.and__for_, soserver.memory.heap.max_sizebecomesNEO4J_server_memory_heap_max__size. Every setting in the operations manual can be set this way.
This is the Community edition. For Enterprise features (multi-database, Autonomous Clustering, RBAC) use neo4j:2026.05-enterprise and add NEO4J_ACCEPT_LICENSE_AGREEMENT: "yes"; a free developer licence is fine for local use.
Step 2 — start it
mkdir -p import
docker compose up -d
docker compose logs -f neo4j
Wait for the line Started. and then open http://localhost:7474. Log in with neo4j / change-me-please. You are in Neo4j Browser, connected over Bolt on 7687.
If you prefer the terminal, the image bundles cypher-shell:
docker exec -it neo4j-dev cypher-shell -u neo4j -p change-me-please
Step 3 — confirm the version and APOC
CALL dbms.components() YIELD name, versions, edition
RETURN name, versions, edition;
You should see Neo4j Kernel, ["2026.05.0"], community. Then:
SHOW FUNCTIONS YIELD name
WHERE name STARTS WITH 'apoc.'
RETURN count(*) AS apocFunctions;
A non-zero count means the plugin loaded. If it is zero, check docker compose logs neo4j for a download failure — corporate proxies are the usual culprit.
Step 4 — your first Cypher 25 queries
Since 2025.06, every Neo4j release carries two Cypher language versions side by side: Cypher 5 (the default, for compatibility) and Cypher 25, the GQL-aligned evolution that new features land in. You pick per query with a prefix, or per database with a default.
Create a tiny graph first:
CREATE (a:Person {name: 'Ada', born: 1815})
CREATE (g:Person {name: 'Grace', born: 1906})
CREATE (l:Person {name: 'Linus', born: 1969})
CREATE (a)-[:MENTORED {year: 1843}]->(g)
CREATE (g)-[:MENTORED {year: 1991}]->(l);
Now a Cypher 25 query that uses two additions you will not find in Cypher 5 — LET to bind expressions and FILTER as a standalone clause:
CYPHER 25
MATCH (p:Person)
LET age = 2026 - p.born
FILTER age > 60
RETURN p.name AS name, age
ORDER BY age DESC;
And a quantified path pattern for "chains of mentoring, 1 to 3 hops", which is the modern replacement for variable-length *1..3 syntax:
CYPHER 25
MATCH (start:Person {name: 'Ada'}) (()-[:MENTORED]->()){1,3} (end:Person)
RETURN end.name;
To make Cypher 25 the default so you can drop the prefix:
ALTER DATABASE neo4j SET DEFAULT LANGUAGE CYPHER 25;
Drivers inherit that default, which is exactly what you want when a team is standardising on the new dialect. Keep Cypher 5 as the default on any database that still serves legacy application code until you have run its queries through the deprecations list.
Step 5 — connect from Python
The neo4j driver 6.x is the current major line and the one that understands the native Vector type introduced in 2025.10.
pip install "neo4j>=6"
from neo4j import GraphDatabase
URI = "bolt://localhost:7687"
AUTH = ("neo4j", "change-me-please")
with GraphDatabase.driver(URI, auth=AUTH) as driver:
driver.verify_connectivity()
records, summary, keys = driver.execute_query(
"""
CYPHER 25
MATCH (p:Person)
LET age = 2026 - p.born
RETURN p.name AS name, age
ORDER BY age DESC
""",
database_="neo4j",
)
for r in records:
print(r["name"], r["age"])
print(f"{summary.result_available_after} ms")
execute_query is the right entry point for almost everything: it handles sessions, retries, and routing for you. Reach for explicit session.execute_write / execute_read only when you need multi-statement transactions.
Step 6 — load a CSV from the import directory
The Compose file mounted ./import into the container, so any file you drop there is reachable as file:///name.csv:
cat > import/people.csv <<'CSV'
name,born
Margaret,1936
Barbara,1949
CSV
LOAD CSV WITH HEADERS FROM 'file:///people.csv' AS row
MERGE (p:Person {name: row.name})
SET p.born = toInteger(row.born);
For anything larger than a few million rows you will want neo4j-admin database import or the Data Importer — we cover both in Importing Relational Data Without the (Dead) ETL Tool.
Resetting, upgrading, and cleaning up
- Wipe everything:
docker compose down -vremoves the container and the named volumes. - Move to a newer monthly release: change the image tag and
docker compose up -d. Within the CalVer line, stores upgrade automatically on start; Neo4j's upgrade guide lists the few cases that needneo4j-admin database migrate. - Keep it out of your RAM budget:
docker compose stopwhen you are done for the day.
Where this goes next
You now have the same stack our consultants use on day one of a project. From here, the natural next steps are modelling your own domain, loading real data, and — for most 2026 projects — adding a vector index so the graph can serve retrieval for an LLM application. Our GraphRAG pipeline tutorial picks up from exactly this container.
If you would rather have a senior Neo4j engineer set this up inside your environment — with the right memory settings, clustering topology, and security model for production — talk to us.