Elasticsearch is a distributed search and analytics engine built on Apache Lucene. Applications use it for full-text search, log analytics and filtering large datasets in near real time. In this tutorial you will install Elasticsearch 9 on Ubuntu 24.04 from Elastic's official APT repository, run it as a secured single-node cluster, size the JVM heap for your server, and create, index and search your first documents over the REST API.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
  • At least 4 GB of RAM and 2 vCPUs. Elasticsearch runs on less, but it will be slow and the JVM heap will be too small for real data.
  • Free disk space for your indexes. Elasticsearch stops allocating data to a node once its disk is 85 percent full, so plan headroom.
  • A non-root user with sudo privileges.

You do not need to install Java: the Elasticsearch package includes its own bundled JDK.

Step 1 - Adding the Elastic APT repository

Elastic publishes signed packages for each major version. First install the tools needed to fetch the signing key:

sudo apt update
sudo apt install curl gpg

Download the Elastic GPG key and store it in /etc/apt/keyrings:

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /etc/apt/keyrings/elasticsearch.gpg

Add the repository for the 9.x release line, pinned to that key:

echo "deb [signed-by=/etc/apt/keyrings/elasticsearch.gpg] https://artifacts.elastic.co/packages/9.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-9.x.list

Refresh the package index and confirm that the elasticsearch package now comes from Elastic:

sudo apt update
apt-cache policy elasticsearch
elasticsearch:
  Installed: (none)
  Candidate: 9.x.x
  Version table:
     9.x.x 500
        500 https://artifacts.elastic.co/packages/9.x/apt stable/main amd64 Packages

Step 2 - Installing Elasticsearch

Install the package:

sudo apt install elasticsearch

During installation, Elasticsearch configures security automatically: it enables authentication, generates TLS certificates for the HTTP and transport layers, and creates a password for the built-in elastic superuser. The output includes a block like this:

--------------------------- Security autoconfiguration information ------------------------------

Authentication and authorization are enabled.
TLS for the transport and HTTP layers is enabled and configured.

The generated password for the elastic built-in superuser is : your_generated_password
...

Copy the password to a password manager now. If you miss it, you can generate a new one in Step 5.

The package does not start the service. You will configure it first.

Step 3 - Configuring the node

The main configuration file is /etc/elasticsearch/elasticsearch.yml. Open it:

sudo nano /etc/elasticsearch/elasticsearch.yml

Set a cluster name and a node name near the top of the file. Uncomment and edit these lines:

cluster.name: my-application
node.name: node-1

Leave network.host commented out, so the transport layer (node-to-node traffic) stays on the loopback interface.

Scroll to the end of the file. The installer appended a BEGIN SECURITY AUTO CONFIGURATION block with xpack.security.* settings, a cluster.initial_master_nodes line containing your hostname and this line:

http.host: 0.0.0.0

That setting makes the REST API on port 9200 listen on every interface, including the public one. When the application runs on the same server, change it so the API is only reachable locally:

http.host: 127.0.0.1

Leave the rest of the block as it is: it is what turns on TLS and authentication.

Save and close the file.

Step 4 - Setting the JVM heap size

Elasticsearch sizes its heap automatically based on total RAM, but setting it explicitly makes memory use predictable. The usual rule is half of the server's RAM, never above about 31 GB, leaving the other half for the operating system's file cache, which Lucene relies on heavily.

Create a heap options file. On a 4 GB server, use 2 GB:

sudo nano /etc/elasticsearch/jvm.options.d/heap.options
-Xms2g
-Xmx2g

Always set -Xms and -Xmx to the same value so the heap never resizes at runtime. Do not edit the main jvm.options file; files in jvm.options.d/ survive package upgrades.

The Debian package already sets vm.max_map_count to the value Elasticsearch needs. Verify it:

sysctl vm.max_map_count
vm.max_map_count = 262144

Step 5 - Starting Elasticsearch

Reload systemd, then enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now elasticsearch

Startup takes 20 to 60 seconds. Check the status:

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

If you did not save the generated password, reset it now. The tool prints a new random password:

sudo /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic

To avoid typing the password in each command, read it into a shell variable for this session:

read -rs ELASTIC_PASSWORD && export ELASTIC_PASSWORD

Now query the node over HTTPS. The --cacert option points to the CA certificate generated during installation, so curl can verify the connection:

sudo curl --cacert /etc/elasticsearch/certs/http_ca.crt -u "elastic:$ELASTIC_PASSWORD" https://localhost:9200
{
  "name" : "node-1",
  "cluster_name" : "my-application",
  "cluster_uuid" : "...",
  "version" : {
    "number" : "9.x.x",
    ...
  },
  "tagline" : "You Know, for Search"
}

sudo is needed only because the certificate directory is readable by root and the elasticsearch group. To use the certificate as your regular user, copy it:

sudo cp /etc/elasticsearch/certs/http_ca.crt ~/http_ca.crt
sudo chown "$USER": ~/http_ca.crt

Check the cluster health:

curl --cacert ~/http_ca.crt -u "elastic:$ELASTIC_PASSWORD" "https://localhost:9200/_cluster/health?pretty"
{
  "cluster_name" : "my-application",
  "status" : "green",
  "number_of_nodes" : 1,
  ...
}

A single node shows green until you create an index with replicas. Indexes with replicas show yellow, because a replica cannot live on the same node as its primary. Step 6 creates indexes with zero replicas to avoid that.

Step 6 - Indexing and searching documents

Create an index called products with one shard and no replicas, and define the field types:

curl --cacert ~/http_ca.crt -u "elastic:$ELASTIC_PASSWORD" -X PUT "https://localhost:9200/products" \
  -H 'Content-Type: application/json' -d '
{
  "settings": { "number_of_shards": 1, "number_of_replicas": 0 },
  "mappings": {
    "properties": {
      "name":        { "type": "text" },
      "category":    { "type": "keyword" },
      "price":       { "type": "float" },
      "description": { "type": "text" }
    }
  }
}'
{"acknowledged":true,"shards_acknowledged":true,"index":"products"}

Index a few documents in one request with the bulk API. The refresh=true parameter makes them searchable immediately:

curl --cacert ~/http_ca.crt -u "elastic:$ELASTIC_PASSWORD" -X POST "https://localhost:9200/products/_bulk?refresh=true" \
  -H 'Content-Type: application/x-ndjson' --data-binary '
{"index":{"_id":"1"}}
{"name":"Mechanical keyboard","category":"peripherals","price":89.9,"description":"Compact keyboard with brown switches"}
{"index":{"_id":"2"}}
{"name":"Wireless mouse","category":"peripherals","price":29.5,"description":"Ergonomic mouse with silent buttons"}
{"index":{"_id":"3"}}
{"name":"27 inch monitor","category":"displays","price":249.0,"description":"IPS monitor with adjustable stand"}
'

The response should contain "errors":false.

Run a full-text search for "keyboard" in the name and description fields, filtered by price:

curl --cacert ~/http_ca.crt -u "elastic:$ELASTIC_PASSWORD" "https://localhost:9200/products/_search?pretty" \
  -H 'Content-Type: application/json' -d '
{
  "query": {
    "bool": {
      "must":   { "multi_match": { "query": "keyboard", "fields": ["name", "description"] } },
      "filter": { "range": { "price": { "lte": 100 } } }
    }
  }
}'
  "hits" : {
    "total" : { "value" : 1, "relation" : "eq" },
    "max_score" : ...,
    "hits" : [
      {
        "_index" : "products",
        "_id" : "1",
        ...
        "_source" : { "name" : "Mechanical keyboard", ... }

Step 7 - Creating a user for your application

Do not use the elastic superuser in application code. Create an API key that can only read and write the products index:

curl --cacert ~/http_ca.crt -u "elastic:$ELASTIC_PASSWORD" -X POST "https://localhost:9200/_security/api_key?pretty" \
  -H 'Content-Type: application/json' -d '
{
  "name": "products-app",
  "role_descriptors": {
    "products_rw": {
      "indices": [
        { "names": ["products"], "privileges": ["read", "write", "view_index_metadata"] }
      ]
    }
  }
}'

The response contains an encoded value. Store it in your application's secrets and send it in the Authorization header:

curl --cacert ~/http_ca.crt -H "Authorization: ApiKey your_encoded_api_key" "https://localhost:9200/products/_count"
{"count":3,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0}}

Step 8 - Allowing access from an application server (optional)

Skip this step if the application runs on the same server.

To let another server on your private network connect, make the REST API listen on localhost and on this server's private IP. Edit /etc/elasticsearch/elasticsearch.yml and change the http.host line from Step 3, replacing your_private_ip:

http.host: [127.0.0.1, your_private_ip]

Restart the service and allow port 9200 only from the application server, replacing app_server_ip:

sudo systemctl restart elasticsearch
sudo ufw allow from app_server_ip to any port 9200 proto tcp

Copy http_ca.crt to the application server so its client can verify the TLS certificate. Never open port 9200 to the whole internet.

Troubleshooting

  • The service fails to start or times out: read the node log with sudo journalctl -u elasticsearch -n 50 --no-pager and sudo tail -n 100 /var/log/elasticsearch/my-application.log (the file is named after cluster.name). A heap larger than available RAM is a common cause.
  • curl: (60) SSL certificate problem: you are not passing the CA certificate. Add --cacert ~/http_ca.crt, and use https://, not http://.
  • security_exception with status 401: wrong password or an expired shell variable. Reset the password with elasticsearch-reset-password as shown in Step 5.
  • Cluster health is yellow: some index has replicas that cannot be allocated on a single node. Set them to zero with a PUT /index_name/_settings request containing {"index":{"number_of_replicas":0}}.
  • Writes fail with cluster_block_exception and read_only_allow_delete: the disk passed the flood-stage watermark (95 percent). Free disk space; the block is removed automatically once usage drops.

Conclusion

You installed Elasticsearch 9 on Ubuntu 24.04 from the official repository, kept its built-in TLS and authentication, set a fixed JVM heap and indexed and searched documents with a dedicated API key. Next, you can install Kibana from the same repository to explore your data visually, use an official client library (Python, Node.js, PHP) instead of curl, or register a snapshot repository so you can back up your indexes.