Tutorial

Hosting Elasticsearch on a VPS: security and troubleshooting

Databases13 min read6 steps

Elasticsearch remains the reference for advanced full-text search, nested aggregations and large-scale log centralization. Managed cloud offerings charge per volume and network egress; on a properly sized VPS, you control the version, analysis plugins and cost. This guide covers step-by-step Docker deployment, xpack security, network hardening, Index Lifecycle Management (ILM), automated S3 snapshots, JVM heap monitoring, and the five failures almost everyone encounters on their first startup.

Contents· Why self-host Elasticsearch on your own VPS1/12
  1. 01Why self-host Elasticsearch on your own VPS
  2. 02What you gain by self-hosting Elasticsearch
  3. 03Precise prerequisites before launching the first container
  4. 04Step-by-step deployment
  5. 05xpack security: TLS, roles and network isolation
  6. 06Harden network access with UFW
  7. 07Index Lifecycle Management (ILM): managing data lifespan
  8. 08Snapshots to S3: backup and disaster recovery
  9. 09JVM heap monitoring: detecting OOM before it happens
  10. 10Kibana: index exploration and dashboards (optional)
  11. 11Troubleshooting: the 5 most common startup errors
  12. 12What if you want a fully open source alternative?

Why self-host Elasticsearch on your own VPS

Elasticsearch shines where simple search no longer suffices: configurable BM25 scoring, synonyms, custom linguistic analyzers (French, Arabic ICU), geo-queries, and a full ELK stack to centralize your application logs. Elastic Cloud and OpenSearch Service offerings quickly become costly once indexed volumes exceed a few gigabytes, with egress billing on top. On your VPS, you decide the deployed version, active plugins, retention duration and snapshot policy. It is also the only way to keep sensitive data — application logs, customer indexes — strictly within your own infrastructure, without dependency on a cloud tier.

On the licensing front, Elasticsearch became open source again in August 2024: since version 8.16, Elastic distributes the code under the AGPLv3 license (OSI-approved), alongside the Elastic License and SSPL. This return to open source changes the calculation for teams that had switched to OpenSearch in 2021. Current production versions are 8.19.x (long-term 8 branch) and 9.x (main branch since 2025).

What you gain by self-hosting Elasticsearch

  • Advanced full-text search: BM25 scoring, synonyms, custom linguistic analyzers (FR, AR ICU)
  • Complex aggregations and facets for e-commerce, BI or log centralization
  • Full ELK stack (Logstash, Beats, Kibana) with no software surcharge
  • Full control of the version, plugins and index lifecycle policies (ILM)
  • No billing per indexed volume or network egress fees
  • Automated snapshots to your own S3-compatible object storage via SLM
  • Sensitive data kept within your own infrastructure, under your sole jurisdiction

Precise prerequisites before launching the first container

Minimum RAM: 4 GB for a test environment, 8 GB (2-4 vCPU) for a light production instance, 16 GB as soon as you add Kibana or index several million documents. The key parameter is the JVM heap: set -Xms and -Xmx to 50% of available RAM, without exceeding 31 GB (beyond that, the JVM switches to a less efficient pointer compression mode; the exact limit is 31 GB with G1GC, not 32 GB). On an 8 GB RAM VPS, use -Xms4g -Xmx4g. Always set -Xms equal to -Xmx: a heap that is under-allocated at startup and then extended during operation causes long GC pauses.

On the network side, Elasticsearch uses two ports: 9200 (HTTP, REST API) and 9300 (inter-node transport). Never expose port 9200 directly on the public interface — this is the primary source of compromise seen on unsecured instances. On the storage side, an NVMe SSD is recommended: Elasticsearch performs many random read operations on Lucene segments; a magnetic disk or low-end SSD will saturate quickly on large indexes. Also plan for Docker and Docker Compose, and an es.yourdomain.com subdomain pointed at the VPS.

Step-by-step deployment

  1. Prepare the Linux kernel

    Before launching the container, apply two mandatory system settings. First, increase the memory-mapped zone limit: sysctl -w vm.max_map_count=262144. Persist this setting by adding vm.max_map_count=262144 to /etc/sysctl.conf — without it, Elasticsearch refuses to start with a max virtual memory areas vm.max_map_count [65530] is too low error. Then, disable swap on the VPS (swapoff -a and comment out the swap line in /etc/fstab), or configure bootstrap.memory_lock=true in elasticsearch.yml so the JVM is never paged to disk, which would catastrophically degrade performance.

  2. Write the docker-compose.yml file

    Create a working directory, then a docker-compose.yml file with the Elasticsearch service: image docker.elastic.co/elasticsearch/elasticsearch:8.19.4, environment variable ES_JAVA_OPTS=-Xms4g -Xmx4g (adapt to the VPS), a named volume mounted on /usr/share/elasticsearch/data, and the port 9200 bound to 127.0.0.1 only (127.0.0.1:9200:9200). Also add discovery.type=single-node for a single-node deployment. Never publish 0.0.0.0:9200:9200 in production.

  3. Enable xpack.security and start

    In elasticsearch.yml, verify that xpack.security.enabled: true and xpack.security.http.ssl.enabled: true are active. Since version 8.x, security is enabled by default, but a legacy configuration file may disable it explicitly. Start the cluster: docker compose up -d. On first startup, wait 2 to 3 minutes — initialization of system indexes (.security-*, .kibana_*) takes time. Check the logs: docker compose logs -f elasticsearch.

  4. Create users and retrieve the elastic password

    Once the container is started, reset the elastic superuser password: docker exec -it elasticsearch bin/elasticsearch-reset-password -u elastic. Store this password in a secrets manager. Then create the kibana_system system user if you add Kibana: docker exec -it elasticsearch bin/elasticsearch-users useradd kibana_system -r kibana_system. This account must never be used for application queries: create dedicated users per application, with minimum necessary roles.

  5. Verify the cluster with curl

    Test the connection from the VPS (not from outside): curl -u elastic:<PASSWORD> https://localhost:9200 --cacert /usr/share/elasticsearch/config/certs/http_ca.crt. A JSON response with cluster_name and status: green or yellow confirms the cluster is operational. A yellow status on a single-node cluster is normal: shard replicas cannot be allocated without a second node.

  6. Expose via HTTPS reverse proxy with Nginx

    Install Nginx on the VPS and configure a virtual host for es.yourdomain.com. The reverse proxy forwards requests to https://127.0.0.1:9200 and presents a Let's Encrypt certificate to the client. Add Nginx basic authentication as an additional protection layer if the API must be accessible from outside. Only forward routes necessary for your application — avoid exposing /_cat/* or /_cluster/* publicly.

xpack security: TLS, roles and network isolation

Elasticsearch's xpack security covers three layers. Inter-node TLS (xpack.security.transport.ssl.enabled: true) encrypts traffic between nodes on port 9300 — essential as soon as a second node joins the cluster. HTTP TLS (xpack.security.http.ssl.enabled: true) encrypts port 9200; without it, passwords transit in clear even on a private network. Role control: Elasticsearch provides predefined roles (read, write, monitor, kibana_system, logstash_writer). Assign the minimum required role to each application: a service that only reads one index does not need the superuser role. Avoid using the elastic account in production — reserve it for initial administration. Last point: the network.host parameter in elasticsearch.yml. Its default value is _local_ (loopback only). Switching to 0.0.0.0 to listen on all interfaces without having configured xpack security exposes your cluster to the entire internet.

Harden network access with UFW

After verifying that Elasticsearch listens only on 127.0.0.1, lock down the firewall: ufw deny 9200/tcp and ufw deny 9300/tcp. Only the Nginx reverse proxy (port 443) should be accessible. If multiple nodes communicate with each other, explicitly allow node IPs on port 9300 (ufw allow from <NODE_2_IP> to any port 9300), and block everything else. A ufw status after configuration gives you the exact view of what is open.

Index Lifecycle Management (ILM): managing data lifespan

ILM automates the lifecycle of your indexes based on age or size criteria, preventing disk saturation and keeping queries fast on recent data. A typical ILM policy breaks down into four phases:

Hot phase: the index receives new data. Configure automatic rollover to create a new index once it reaches a certain size (max_size: 50gb) or age (max_age: 7d). Hot indexes live on your fastest NVMe SSDs.

Warm phase: the index is no longer written to but remains queried. Elasticsearch reduces replicas to 1 and performs a force-merge of Lucene segments (forcemerge: max_num_segments: 1) — this reduces memory used by open segments and speeds up reads.

Cold phase: rarely queried data. Replicas drop to 0 and the index can be mounted read-only from object storage (searchable snapshots), eliminating replication overhead.

Delete phase: automatic removal after the defined retention period (example: 90 days for application logs).

Create the policy via the REST API and attach it to an index template so it applies automatically to all new indexes:

PUT _ilm/policy/logs-policy
{
  "policy": {
    "phases": {
      "hot":  { "actions": { "rollover": { "max_age": "7d", "max_size": "50gb" } } },
      "warm": { "min_age": "7d",  "actions": { "forcemerge": { "max_num_segments": 1 }, "shrink": { "number_of_shards": 1 } } },
      "cold": { "min_age": "30d", "actions": { "freeze": {} } },
      "delete": { "min_age": "90d", "actions": { "delete": {} } }
    }
  }
}

On a VPS where disk space is at a premium, ILM is what prevents your logs from saturating the SSD and keeps queries fast on hot data.

Snapshots to S3: backup and disaster recovery

Elasticsearch snapshots allow you to back up the complete state of your indexes to S3-compatible object storage. Combine them with a Snapshot Lifecycle Policy (SLM) to automate backups and their retention, without manual intervention.

1. Install the S3 plugin and configure the repository:

docker exec -it elasticsearch bin/elasticsearch-plugin install repository-s3

Then declare the access keys in the Elasticsearch keystore (never in plain text in elasticsearch.yml):

docker exec -it elasticsearch bin/elasticsearch-keystore add s3.client.default.access_key
docker exec -it elasticsearch bin/elasticsearch-keystore add s3.client.default.secret_key

Register the repository:

PUT _snapshot/s3-backup
{
  "type": "s3",
  "settings": {
    "bucket": "my-elasticsearch-bucket",
    "region": "eu-west-1",
    "base_path": "snapshots/production"
  }
}

2. Create the Snapshot Lifecycle Policy:

PUT _slm/policy/daily-snapshots
{
  "schedule": "0 30 2 * * ?",
  "name": "<daily-snap-{now/d}>",
  "repository": "s3-backup",
  "config": { "indices": ["*"], "ignore_unavailable": true },
  "retention": { "expire_after": "30d", "min_count": 5, "max_count": 50 }
}

This policy triggers a snapshot every day at 2:30 AM, automatically names each snapshot with the date, and purges snapshots older than 30 days while keeping a minimum of 5 versions. Check snapshot status with GET _slm/policy/daily-snapshots and test restore from a staging environment before you need it.

JVM heap monitoring: detecting OOM before it happens

On a VPS, the Linux kernel OOM killer can terminate the Elasticsearch process without warning. Proactive JVM heap monitoring prevents 80% of production incidents.

Metrics to monitor via the _nodes/stats API:

- jvm.mem.heap_used_percent: trigger an alert above 75% on average, and a critical alert above 85% over 5 minutes. Sustained saturation at 90%+ indicates a memory leak or undersized heap.
- jvm.gc.collectors.old.collection_count and collection_time_in_millis: a rapid increase in old-gen (major GC) count and times > 2 s per collection signal that the JVM is spending more time collecting than working.

Prometheus + Grafana integration (Beats stack):

Add metricbeat to your Compose to collect Elasticsearch metrics and push them to Prometheus or directly into Elasticsearch for visualization in Kibana. The official image docker.elastic.co/beats/metricbeat:8.19.4 is preconfigured with an Elasticsearch module.

Operational rules:

1. If heap_used_percent exceeds 75% in steady state, increase the heap (or VPS RAM) before experiencing the first OOM.
2. Do not increase the heap beyond 31 GB — past this threshold, the garbage collector disables compressed object pointers (compressed oops) and the JVM consumes more memory per object, which is counterproductive.
3. Regularly verify that bootstrap.memory_lock: true is active with GET _nodes?filter_path=**.mlockall: a false value indicates swap is still active on the VPS.

Kibana: index exploration and dashboards (optional)

Kibana is the official graphical interface for Elasticsearch to explore indexes, build dashboards and configure alerts. It is not required for programmatic use (REST API), but it simplifies managing ILM, SLM and indexes in general.

Add Kibana to the same Compose file, connecting it to the internal Docker network:

kibana:
  image: docker.elastic.co/kibana/kibana:8.19.4
  environment:
    - ELASTICSEARCH_HOSTS=https://elasticsearch:9200
    - ELASTICSEARCH_USERNAME=kibana_system
    - ELASTICSEARCH_PASSWORD=<KIBANA_SYSTEM_PASSWORD>
    - ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES=/usr/share/kibana/config/certs/http_ca.crt
  volumes:
    - /path/to/http_ca.crt:/usr/share/kibana/config/certs/http_ca.crt:ro
  depends_on:
    - elasticsearch

Expose Kibana behind an Nginx reverse proxy on kibana.yourdomain.com with authentication. Never expose Kibana directly on the internet without authentication: it provides full cluster management access. From Kibana, access Stack Management → Index Lifecycle Policies and Snapshot and Restore to visually manage ILM and SLM, without writing manual API calls.

Troubleshooting: the 5 most common startup errors

1. OOM Killer kills the Elasticsearch process. Symptom: the container stops without an error message in logs, dmesg | grep -i killed reveals a Killed process. Cause: the -Xmx heap is too high for available RAM, or other processes are saturating memory. Fix: reduce -Xmx to 50% of actual free RAM (max 31 GB), and monitor memory consumption with docker stats.

2. max_map_count too low. Symptom: Elasticsearch refuses to start with the error max virtual memory areas vm.max_map_count [65530] is too low. Fix: sysctl -w vm.max_map_count=262144 then add vm.max_map_count=262144 to /etc/sysctl.conf.

3. Permission denied on /usr/share/elasticsearch/data. Symptom: AccessDeniedException error appears in logs when mounting the volume. Cause: the host directory belongs to root but the container runs with UID 1000 (user elasticsearch). Fix: chown -R 1000:1000 <host_volume_path> before running docker compose up.

4. Connection refused on port 9200. Symptom: curl localhost:9200 returns Connection refused. Frequent cause: network.host is misconfigured in elasticsearch.yml (value _site_ or an IP that does not match the Docker interface). On a single-node Docker cluster, leave network.host at its default value (_local_) and access via 127.0.0.1:9200 from the container or host. Also check that the container is running: docker ps.

5. Slow startup: normal on first initialization. Symptom: the cluster takes 2 to 3 minutes to respond on first launch. This is not a failure. Elasticsearch initializes system indexes (.security-7, .kibana_1, default mappings). Wait until logs show mode [basic], reason [security is enabled] or Cluster health status changed from [RED] to [GREEN] before sending queries.

What if you want a fully open source alternative?

OpenSearch is the community fork of Elasticsearch, born in 2021 when Elastic changed its license to SSPL (not OSI-approved). OpenSearch maintains an Apache 2.0 license, offers a largely compatible REST API, and includes advanced security features in its free distribution (role-based access control, audit logging, encryption at rest). Since Elastic reintroduced AGPLv3 in August 2024 with version 8.16, both projects are once again under OSI-approved licenses — the choice now depends more on ecosystem (plugins, integrations, commercial support) than on license constraints. Docker deployment follows the same pattern, with the opensearchproject/opensearch image instead.

A VPS built for Elasticsearch

Generous RAM, SSD storage and adjustable kernel settings from day one: the ServOrbit Cloud VPS gives you the foundation an Elasticsearch JVM requires in production.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab