Neo4j is a graph database: it stores data as nodes connected by typed relationships, and you query it with Cypher, a pattern-matching language designed for traversing those connections. It is a good fit for recommendation engines, fraud detection, access control graphs and any data where the relationships matter as much as the records. In this tutorial you will install Neo4j Community Edition on Ubuntu 24.04 from the official repository, set the admin password, size its memory, model a small graph with Cypher, and back it up.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, such as a CubePath VPS, with at least 2 GB of RAM (4 GB or more is recommended).
  • A non-root user with sudo privileges.
  • An SSH client on your local machine, used to reach Neo4j Browser through a tunnel.

Step 1 - Adding the Neo4j repository

Neo4j publishes Debian packages in its own APT repository. Download the signing key, convert it to the binary keyring format and store it in /etc/apt/keyrings:

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://debian.neo4j.com/neotechnology.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/neotechnology.gpg

Add the repository, pinned to that key:

echo "deb [signed-by=/etc/apt/keyrings/neotechnology.gpg] https://debian.neo4j.com stable latest" | sudo tee /etc/apt/sources.list.d/neo4j.list

Refresh the package index and confirm that APT sees the package:

sudo apt update
apt-cache policy neo4j

The output should show a candidate version coming from https://debian.neo4j.com stable/latest.

Step 2 - Installing Neo4j

Current Neo4j releases (calendar versions such as 2025.x) require Java 21, which APT installs automatically as a dependency together with cypher-shell:

sudo apt install neo4j

Check the installed version:

neo4j --version
neo4j 2025.xx.x

The package uses these locations:

PathContents
/etc/neo4j/neo4j.confMain configuration file
/var/lib/neo4j/dataDatabases and transaction logs
/var/lib/neo4j/pluginsPlugin JAR files
/var/log/neo4jLogs

Step 3 - Setting the initial password

The built-in neo4j user must get a password before the first login. Setting it before the first start avoids the forced password change prompt. The password must be at least 8 characters long; replace your_strong_password:

sudo neo4j-admin dbms set-initial-password your_strong_password
Changed password for user 'neo4j'. IMPORTANT: this change will only take effect if performed before the database is started for the first time.

Step 4 - Configuring memory and network

Neo4j performance depends mostly on two memory areas: the JVM heap (query execution and transaction state) and the page cache (the graph data kept in RAM). Ask Neo4j for a recommendation based on the server's memory:

sudo neo4j-admin server memory-recommendation
server.memory.heap.initial_size=1g
server.memory.heap.max_size=1g
server.memory.pagecache.size=512m
...

Open the configuration file:

sudo nano /etc/neo4j/neo4j.conf

Find the memory settings, uncomment them and paste the recommended values. Setting the initial and maximum heap to the same value avoids pauses caused by heap resizing:

server.memory.heap.initial_size=1g
server.memory.heap.max_size=1g
server.memory.pagecache.size=512m

By default Neo4j only listens on localhost, which is the safest option: the HTTP interface (port 7474) and the Bolt driver protocol (port 7687) stay private, and you reach them through an SSH tunnel. Leave server.default_listen_address commented out unless an application on another server must connect. In that case, set it to listen on all interfaces:

server.default_listen_address=0.0.0.0

and allow Bolt only from that application server's IP, for example 203.0.113.20:

sudo ufw allow from 203.0.113.20 to any port 7687 proto tcp

Step 5 - Starting Neo4j

Enable the service so it starts at boot, and start it now:

sudo systemctl enable --now neo4j

Check its status:

sudo systemctl status neo4j
● neo4j.service - Neo4j Graph Database
     Loaded: loaded (/usr/lib/systemd/system/neo4j.service; enabled; preset: enabled)
     Active: active (running) since ...

Startup takes a few seconds. Follow the log until you see Started.:

sudo journalctl -u neo4j -f

Then test a query with cypher-shell. It prompts for the password:

cypher-shell -u neo4j "RETURN 'connected' AS status;"
+-------------+
| status      |
+-------------+
| "connected" |
+-------------+

Step 6 - Opening Neo4j Browser through an SSH tunnel

Neo4j Browser is a web UI served on port 7474 that talks to the database over Bolt on port 7687. From your local machine, forward both ports over SSH (replace your_user and your_server_ip):

ssh -L 7474:localhost:7474 -L 7687:localhost:7687 your_user@your_server_ip

Keep that session open and browse to http://localhost:7474. Connect with the URL neo4j://localhost:7687, user neo4j and the password from Step 3.

Step 7 - Modeling data with Cypher

Open an interactive shell on the server:

cypher-shell -u neo4j

Before loading data, create uniqueness constraints. They prevent duplicate nodes and also create an index that makes lookups by that property fast:

CREATE CONSTRAINT person_email IF NOT EXISTS FOR (p:Person) REQUIRE p.email IS UNIQUE;
CREATE CONSTRAINT product_sku IF NOT EXISTS FOR (p:Product) REQUIRE p.sku IS UNIQUE;

Load a small purchase graph. MERGE creates a node or relationship only if it does not exist yet, so you can run the statement repeatedly without duplicates:

MERGE (alice:Person {email: '[email protected]'}) SET alice.name = 'Alice'
MERGE (bob:Person {email: '[email protected]'}) SET bob.name = 'Bob'
MERGE (carol:Person {email: '[email protected]'}) SET carol.name = 'Carol'
MERGE (laptop:Product {sku: 'LAP-01'}) SET laptop.name = 'Laptop'
MERGE (mouse:Product {sku: 'MOU-01'}) SET mouse.name = 'Mouse'
MERGE (dock:Product {sku: 'DCK-01'}) SET dock.name = 'Docking station'
MERGE (alice)-[:KNOWS]->(bob)
MERGE (alice)-[:BOUGHT]->(laptop)
MERGE (bob)-[:BOUGHT]->(laptop)
MERGE (bob)-[:BOUGHT]->(mouse)
MERGE (carol)-[:BOUGHT]->(laptop)
MERGE (carol)-[:BOUGHT]->(dock);
0 rows
ready to start consuming query after 45 ms, results consumed after another 0 ms
Added 6 nodes, Set 12 properties, Created 6 relationships, Added 6 labels

Query a pattern: who bought the laptop?

MATCH (p:Person)-[:BOUGHT]->(:Product {sku: 'LAP-01'})
RETURN p.name ORDER BY p.name;
+---------+
| p.name  |
+---------+
| "Alice" |
| "Bob"   |
| "Carol" |
+---------+

Now a query that is awkward in SQL but natural in a graph: recommend products to Alice based on what other buyers of her products also bought, ranked by how many of them bought it:

MATCH (alice:Person {email: '[email protected]'})-[:BOUGHT]->(:Product)<-[:BOUGHT]-(other:Person)-[:BOUGHT]->(rec:Product)
WHERE NOT EXISTS { (alice)-[:BOUGHT]->(rec) }
RETURN rec.name AS recommendation, count(DISTINCT other) AS score
ORDER BY score DESC;
+---------------------------+
| recommendation    | score |
+---------------------------+
| "Mouse"           | 1     |
| "Docking station" | 1     |
+---------------------------+

Use PROFILE in front of a query to see whether it uses an index. A lookup on email should start with a NodeUniqueIndexSeek operator instead of a full NodeByLabelScan:

PROFILE MATCH (p:Person {email: '[email protected]'}) RETURN p.name;

List the indexes and constraints you created, then leave the shell:

SHOW INDEXES;
SHOW CONSTRAINTS;
:exit

Step 8 - Enabling the APOC plugin (optional)

APOC Core adds hundreds of utility procedures and functions (data conversion, batch operations, path expansion). The package ships the JAR in the labs directory; copy it to plugins:

ls /var/lib/neo4j/labs/
sudo cp /var/lib/neo4j/labs/apoc-*-core.jar /var/lib/neo4j/plugins/
sudo systemctl restart neo4j

Verify that it loaded:

cypher-shell -u neo4j "RETURN apoc.version();"

The output shows the APOC version, which matches the Neo4j version.

Step 9 - Backing up and restoring

In Community Edition, backups are made with neo4j-admin database dump, which requires the database to be stopped. Create a directory owned by the neo4j user:

sudo install -d -o neo4j -g neo4j /var/backups/neo4j

Stop Neo4j, dump the neo4j database, and start it again:

sudo systemctl stop neo4j
sudo -u neo4j neo4j-admin database dump neo4j --to-path=/var/backups/neo4j
sudo systemctl start neo4j

Check that the dump file exists:

ls -lh /var/backups/neo4j
-rw-r--r-- 1 neo4j neo4j 12K Sep 25 10:30 neo4j.dump

To restore, stop the service and load the dump over the existing database:

sudo systemctl stop neo4j
sudo -u neo4j neo4j-admin database load neo4j --from-path=/var/backups/neo4j --overwrite-destination=true
sudo systemctl start neo4j

Copy the dump files to another server or object storage; a backup kept only on the same disk does not protect you from losing the server.

Troubleshooting

neo4j.service fails right after start: read sudo journalctl -u neo4j -n 50 and /var/log/neo4j/neo4j.log. The most common cause is a heap plus page cache larger than the available RAM; lower the values from Step 4.

The client is unauthorized due to authentication failure: set-initial-password only takes effect if it runs before the first start. If Neo4j had already started, the password is still the default neo4j; log in with it and cypher-shell will ask you to choose a new one.

Neo4j Browser connects but queries hang: the browser reaches port 7474 but not Bolt on 7687. Make sure the SSH tunnel forwards both ports.

Conclusion

You installed Neo4j Community Edition from the official repository, kept it private behind an SSH tunnel, sized its memory, built and queried a small graph with constraints and indexes, and set up dumps for backup. Next, you can connect your application with an official driver (Python, JavaScript, Java, Go or .NET), import larger datasets with LOAD CSV or neo4j-admin database import, and schedule the dump from Step 9 with a systemd timer.