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.
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
- Snapshots to your own object storage for controlled disaster recovery
- 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 32 GB (beyond that, the JVM switches to a less efficient pointer compression mode). On an 8 GB RAM VPS, use -Xms4g -Xmx4g. 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
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.
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.13.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.
Enable xpack.security and start
In elasticsearch.yml, add xpack.security.enabled: true and xpack.security.http.ssl.enabled: true. Since version 8.x, security is enabled by default, but verify that the configuration file does not explicitly disable it. 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.
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.
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.
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.
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, 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). If your constraint is strictly the open source license or the absence of dependency on Elastic BV, OpenSearch is a direct alternative to evaluate. Docker deployment follows the same pattern, with the opensearchproject/opensearch image instead.