SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor

Log Management for One VPS: Wetin You Really Need

Run logs for one VPS with journald and logrotate, or choose Loki and OpenSearch. See the RAM floors, search trade-offs, and retention rules before you deploy.

Wetin self-hosted log management for one VPS really dey cost

Self-hosted log management for one VPS (virtual private server) dey depend on one question: you need search cluster, or you only need rotation and grep? Most vendor guides dey start with three nodes and 12 GB of RAM before dem ship even one log line. For one server, that answer no useful, so the comparison below na about wetin each option demand from small server before e store anything.

If you dey run one or two servers and you want know wetin happen last Tuesday, systemd-journald and logrotate already fit do that work, and you fit stop after the next section. If several machines must send their logs go one place with search across weeks, Grafana Loki fit work for small server, because e dey index labels, not the text inside the lines. Elasticsearch and OpenSearch give you real full text search, but dem need plenty memory for am, because the JVM (Java virtual machine) heap get minimum size wey you no fit reduce pass.

Start with journald, because most people stop here

systemd-journald don already dey run for any current Ubuntu or Debian server. E dey capture standard output of every service unit, kernel messages, and anything wey dem send go syslog. Four commands dey cover most incidents.

journalctl -u nginx.service --since "2026-08-14 09:00" --until "2026-08-14 10:00"
journalctl -p err -b
journalctl -f -u ssh.service
journalctl --disk-usage

The last one go print line like Archived and active journals take up 1.1G in the file system.. Na this number dey decide whether you need anything else. If e show few hundred megabytes and you fit find wetin you need with -u and --since, you don finish.

Whether journal go survive reboot depend on Storage= and whether /var/log/journal dey exist. With the common Storage=auto setting, journald go write to /var/log/journal when that directory dey present, and to /run/log/journal when e no dey. /run dey backed by memory, so for machine wey no get that directory, every log go wipe during reboot. Na that time you suppose read dem. Ubuntu images dey ship with the directory. Minimal and container based images often no get am.

ls -d /var/log/journal
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald
journalctl --disk-usage

After the restart, journalctl --disk-usage suppose report size wey dey below /var/log/journal instead of /run. The defaults don already get limits, and na the main reason journald be serious solution, not fallback. The journald.conf man page set SystemMaxUse= to 10% of file system size and SystemKeepFree= to 15%, and e cap each calculated default at 4G. SystemMaxFileSize= default na one eighth of SystemMaxUse=, with cap at 128M, so normally you go keep seven rotated files. MaxRetentionSec= default na 0, wey turn off deletion based on age. Read that last default again: out of the box, journal get limit by size only, never by age.

[Journal]
Storage=persistent
SystemMaxUse=2G
MaxRetentionSec=30day

Write that to /etc/systemd/journald.conf.d/99-size.conf, restart journald, then check say journalctl --disk-usage don move toward your new ceiling. To reclaim space now instead of waiting for next rotation, run sudo journalctl --vacuum-size=500M or sudo journalctl --vacuum-time=14d. Both go print every file wey dem remove, so if nothing show, e mean say nothing dey to delete.

Everything outside the journal, like /var/log/nginx/access.log, na logrotate work, and e dey run daily from a systemd timer. One failure dey worth knowing because e fit look like bug for df. After rotation, the old file don disappear from directory listing while daemon still dey hold am open. So df -h go report say disk full while du -sh /var/log go report much less. The space go return only when process reopen its log, and na wetin the postrotate reload line for config dey do. sudo lsof -nP +L1 list deleted files wey process still dey hold open, and e show the process wey dey hold each one. Test a rule without touching anything with sudo logrotate -d /etc/logrotate.d/nginx.

Sending logs from several servers to one collector

Once you get more than one box, managing several Linux servers at once dey easier when all dem logs dey enter one place. Most distributions don already install rsyslog, so the cheapest central collector na one file for each sender.

*.* action(type="omfwd" target="logs.example.com" port="514" protocol="tcp")

Save am as /etc/rsyslog.d/50-forward.conf, check am with sudo rsyslogd -N1. E go validate the configuration and exit without starting anything. Then restart rsyslog. For the collector, enable the TCP input.

module(load="imtcp")
input(type="imtcp" port="514")

Two warnings, and both na mechanical issues. Plain syslog no get encryption or authentication, so anything wey fit reach port 514 fit inject log lines wey go look exactly like your own. Bind am to private network or VPN, and firewall the port. Second, the default action queue dey live for memory. So when collector no dey reachable, the queue go fill and messages go drop without keeping any copy. rsyslog document disk assisted queue for this situation for its reliable forwarding tutorial.

ELK stack no fit for small VPS

ELK mean Elasticsearch for storage and search, Logstash for ingestion pipeline, and Kibana for interface. The main limit na JVM heap, and system set am before any log arrive.

Elastic documentation talk say make you set heap no pass 50% of the total memory wey each Elasticsearch node get. The process still dey use off heap buffers, and e depend on operating system file cache to read index files fast. So 2 GB heap mean say you need 4 GB machine, before Kibana and before the work wey you actually buy the server to do. Elastic still talk say Elasticsearch dey size the heap automatically based on the node roles and total memory. This mean say small machine go get small heap, then e go spend most of the time dey do garbage collection.

Logstash na the part wey dey break small budget completely. Elastic own JVM settings page recommend heap wey no be less than 4GB and no pass 8GB for normal ingestion. That one process for the middle of the pipeline fit use the whole 4 GB VPS.

ChartDocumented JVM heap settings, from each project's own docs
The data behind this chart
[
  {
    "label": "Loki plus Alloy (no JVM)",
    "documented_heap_mb": 0
  },
  {
    "label": "OpenSearch demo compose",
    "documented_heap_mb": 512
  },
  {
    "label": "OpenSearch production example",
    "documented_heap_mb": 2048
  },
  {
    "label": "Logstash recommended minimum",
    "documented_heap_mb": 4096
  }
]

Those values na the ones wey each project publish for its own documentation. Dem no come from measurements for test machine, and your workload fit change dem. OpenSearch sample compose file set 512 MB per node for demo and 2048 MB for its production example, while Logstash recommended lower limit na 4096 MB. The heap column show 0 for Loki and Alloy because dem na Go programs wey no get JVM heap to reserve. Na the whole difference be this one number: JVM component go reserve the memory whether logs dey arrive or not.

If you still want run Elastic stack for one small server, remove Logstash and send logs direct into Elasticsearch with light collector. Logstash dey parse and transform data for high volume. For one server, you fit do that work for the edge or skip am.

Both Elasticsearch and OpenSearch still need vm.max_map_count raised to 262144, because dem dey memory-map index files and Linux default limit too low for dem. If container exit seconds after startup for fresh machine, na usually this issue and nothing else.

OpenSearch or Elasticsearch: which one you fit deploy?

The short licence history, because na e decide wetin you fit run. For January 2021 Elastic move Elasticsearch and Kibana comot from Apache 2.0 go dual SSPL (server side public license) and Elastic License 2.0 model. AWS fork the last Apache 2.0 code as OpenSearch, wey remain Apache 2.0. For September 2024 Elastic add AGPLv3 (GNU Affero General Public License version 3) as another option for the free source code. If na one person dey self-host for one VPS, all those licences allow wetin you dey do. The licences matter when you offer the software to other people as managed service.

The practical difference for small box no big pass wetin the history suggest, because both use the same engine underneath. The names different: index lifecycle na ISM (index state management) for OpenSearch, and ILM (index lifecycle management) for Elasticsearch. As of August 2026, OpenSearch 2.12 and later no go start unless you set admin password for first run.

sudo sysctl -w vm.max_map_count=262144
printf 'vm.max_map_count = 262144\n' | sudo tee /etc/sysctl.d/99-opensearch.conf
docker run -d -p 9200:9200 -p 9600:9600 -e "discovery.type=single-node" \
  -e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=<custom-admin-password>" \
  opensearchproject/opensearch:latest

The sysctl -w line apply the setting now, and the file for /etc/sysctl.d/ na the part wey go survive reboot. Check say the container don come up with curl -k -u admin:<password> https://localhost:9200. E dey answer over https with demo certificate, so -k skip verification, and healthy reply na small JSON block wey name the cluster and the version. The OpenSearch install page still tell Docker Desktop users make dem allow the host at least 4 GB memory. This show the kind resources the process expect.

How Loki small dey remain: labels instead of full text index

Loki keep one index for labels and store log lines as compressed chunks. Query first select streams, then e filter text. {unit="ssh.service"} |= "Failed password" use the label take pick stream, then scan those chunks for the string. Nothing index the body of any line, so ingestion cost stay low and no inverted index dey to keep for memory. The cost shift go query time, and na good trade-off when you normally know which service you dey check.

Grafana documentation talk say monolithic mode, meaning all of Loki dey inside one process with -target=all, fit handle small read and write volume up to approximately 20GB per day. One VPS dey well inside this range.

The main trap na label cardinality. Every different combination of label values na one stream, and stream count dey drive Loki memory use and index size. Label wey hold client IP address or request identifier go create one stream for every value. So busy web server fit generate tens of thousands of streams for one day, and the process go grow until kernel stop am. Keep labels to values wey you fit count on paper: unit, host, job, level. Put the variable details inside the line itself, where filter expression fit find am during query time.

Install Loki and Alloy for one VPS

Na two processes dey do the work. Loki dey store logs and answer queries. Grafana Alloy dey read logs and push dem go Loki. Promtail na the shipper before, but e reach end of life on 2 March 2026, so new installs dey use Alloy, and Loki own Docker example now dey ship with Alloy config.

wget https://raw.githubusercontent.com/grafana/loki/v3.7.0/cmd/loki/loki-local-config.yaml -O loki-config.yaml

Read that file before you use am. E set path_prefix: /tmp/loki with chunks under /tmp/loki/chunks, and that one correct for demo but wrong for server: nothing wey dey under the container /tmp go survive when dem recreate the container, so your history go disappear for the next image update. Point am to a path wey you mount.

common:
  instance_addr: 127.0.0.1
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory
docker volume create loki-data
docker run --name loki -d \
  -v $(pwd):/mnt/config -v loki-data:/loki \
  -p 127.0.0.1:3100:3100 \
  grafana/loki:3.7.0 -config.file=/mnt/config/loki-config.yaml
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3100/ready

That last command suppose print 200, because /ready dey return HTTP 200 once Loki ready to accept traffic. Anything else mean say the process still dey start or dem reject the config, and docker logs loki go show which one. Two details for the run command dey intentional. Dem publish the port for 127.0.0.1 only, because the sample config get auth_enabled: false and Loki no ship user authentication by itself, so anything wey fit reach port 3100 fit read every log and write fake ones. Keep am for loopback, or put am behind VPN or reverse proxy wey dey authenticate users. The named volume matter because the image dey run as user loki with UID 10001, so container no fit write to bind-mounted host directory wey root own.

sudo apt-get update && sudo apt-get install -y gpg wget
sudo mkdir -p /etc/apt/keyrings/
sudo wget -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key
echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" \
  | sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get install -y alloy

Alloy dey read /etc/alloy/config.alloy. This one dey take the system journal and one set of files, then push both go the local Loki.

loki.write "local" {
  endpoint {
    url = "http://127.0.0.1:3100/loki/api/v1/push"
  }
}

loki.relabel "journal" {
  forward_to = []
  rule {
    source_labels = ["__journal__systemd_unit"]
    target_label  = "unit"
  }
}

loki.source.journal "read" {
  forward_to    = [loki.write.local.receiver]
  relabel_rules = loki.relabel.journal.rules
  labels        = {job = "systemd-journal", host = "app-01"}
}

local.file_match "nginx" {
  path_targets = [{"__path__" = "/var/log/nginx/*.log", "job" = "nginx", "host" = "app-01"}]
}

loki.source.file "nginx" {
  targets    = local.file_match.nginx.targets
  forward_to = [loki.write.local.receiver]
}

The relabel rule dey copy journal field __journal__systemd_unit into label wey dem name unit, and na this one make {unit="ssh.service"} work later. Without that rule, the unit name dey inside the entry instead of label, so you no fit select with am and every query go scan everything.

sudo systemctl reload alloy
systemctl show -p User alloy
sudo journalctl -n 5
sudo -u alloy journalctl -n 5

Na here most setups dey stop. Alloy dey run with its own service account, not root, and reading the system journal need membership of systemd-journal group, while files under /var/log/nginx belong to group adm for Debian and Ubuntu. Put the account wey systemctl show print inside the last command. If e return entries wey far fewer pass the root run, that account no fit read the system journal, and Loki go remain empty no matter how correct your config be. Add the groups and restart with sudo usermod -aG systemd-journal,adm alloy followed by sudo systemctl restart alloy.

curl -G -s "http://127.0.0.1:3100/loki/api/v1/query_range" \
  --data-urlencode 'query={job="systemd-journal"}' \
  --data-urlencode 'limit=5' | jq '.data.result | length'

A number wey pass 0 mean say streams dey with that label and dem get entries. A 0 mean say nothing don arrive under that label yet. One default dey explain common false alarm: loki.source.journal dey set max_age to 7h, so fresh start go read the last seven hours of the journal and nothing older. For human interface, run Grafana for the same box and point Loki data source to http://127.0.0.1:3100.. Container logs need different source: Alloy dey discover Docker containers wey dey run and tail dem, na wetin Loki own getting started example dey do. For one-node k3s cluster for a VPS, that job dey move go the pod log directory wey kubelet dey write.

Retention: choose the day wey your logs go delete

Almost nobody dey choose retention period until disk don fill up. Then dem go choose am for 3 a.m. while service dey down. Decide am from day one based on two questions: how far back you really dey check, and wetin you still need to get during incident review next month. For one server, 14 to 30 days dey answer both questions.

Loki no dey delete anything until you enable compactor. Retention dey off by default. This one dey surprise people wey volume don fill while retention_period just dey inside config without doing anything.

limits_config:
  retention_period: 744h

compactor:
  working_directory: /loki/retention
  compaction_interval: 10m
  retention_enabled: true
  retention_delete_delay: 2h
  retention_delete_worker_count: 150
  delete_request_store: filesystem

744h na 31 days. Four documented rules dey control that block:

  • Compactor dey apply retention, and Grafana documentation say make you run compactor as one instance. For one VPS, this one happen by itself.
  • The minimum retention period na 24h, and retention only dey work when index period na 24h. The sample schema_config already dey use period: 24h, so leave am as e be.
  • delete_request_store dey required once retention_enabled don become true. E names the store wey dey hold delete requests, so for filesystem-backed single node e match object_store: filesystem wey already dey inside schema.
  • Chunks dey marked first and removed after retention_delete_delay, wey be 2h for here. So free space go return later than the policy dey suggest. No judge the setting with df five minutes after reload.

OpenSearch dey delete whole indexes instead of individual lines. Na why dem dey create log indexes for each day. ISM policy dey move an index through different states and delete am once e don old reach the required age. An ism_template attaches the policy to new indexes, so you no need remember to do am.

ISM policy wey dey delete log indexes after 14 days
{
  "policy": {
    "description": "delete log indexes after 14 days",
    "default_state": "hot",
    "states": [
      {
        "name": "hot",
        "actions": [],
        "transitions": [
          { "state_name": "delete", "conditions": { "min_index_age": "14d" } }
        ]
      },
      {
        "name": "delete",
        "actions": [ { "delete": {} } ],
        "transitions": []
      }
    ],
    "ism_template": { "index_patterns": ["logs-*"], "priority": 100 }
  }
}

Create am with PUT to _plugins/_ism/policies/logs-retention. The template dey apply to indexes wey dem create after the policy don exist. Anything wey already dey for disk need you attach the policy by hand.

Any system wey you run, retention number only good as the free space check wey dey support am. Deleting logs after 14 days no go save you if 10 days of logs don already fill the volume. So pair the policy with disk health monitoring for one VPS and alert when usage reach 80%.

How much disk per GB of logs

The correct answer depend on your log lines and fields. Measure am for your own data instead of trusting any published ratio. The storage mechanisms different enough to show the general direction. OpenSearch and Elasticsearch dey write inverted index for every indexed field together with the stored document. So the data wey land for disk go pass the raw text. Every replica go multiply the size. For one-node setup, set replica count to 0. Replica shard for the same node no fit survive if that node fail. If you leave am at 1, e go double the disk usage and keep cluster health at yellow forever. Loki dey write compressed chunks plus small label index. So e footprint dey follow the compressed size of the log lines.

sudo du -sh /var/lib/docker/volumes/loki-data/_data
curl -k -u admin:<password> "https://localhost:9200/_cat/indices?v&h=index,docs.count,store.size"

Run the method wey apply on 2 consecutive days. The difference na your daily growth. Multiply am by your retention days. Add about 30% headroom for compaction and merges. Then compare the result with the volume capacity. If the data no fit, reduce retention before you buy more disk. Bigger volume only go push the same problem forward by a few weeks.

Wetin dey break first for small box

Memory dey go first. Kernel OOM (out of memory) killer go pick one big process, and for logging box, JVM na usually the biggest process. journalctl -k | grep -i "killed process" dey show the kill with the process name inside brackets. The victim no always be the log stack: sshd or your database fit get picked instead. Na so logging experiment fit bring down the application wey you wan collect logs from. Give containers clear memory ceilings so failure go happen for the place wey you choose. Na wetin memory limits for Docker Compose dey help you do.

Disk dey go second, and search engines dey fail for one clear and recognisable way. Elasticsearch and OpenSearch dey monitor disk usage for different levels. Low watermark dey at 85% and high watermark dey at 90%. When disk reach flood stage of 95%, every index wey get shard for that node go receive the block index.blocks.read_only_allow_delete, and writes go then fail with blocked by: [FORBIDDEN/12/index read-only / allow delete (api)]. The block go clear once usage fall below high watermark. Free space first, then clear the block by hand only if e remain.

curl -k -u admin:<password> -X PUT "https://localhost:9200/_all/_settings" \
  -H 'Content-Type: application/json' \
  -d '{"index.blocks.read_only_allow_delete": null}'

Loki dey fail more quietly. E no get read-only mode wey e fit enter, so full volume go show as failed pushes for sender and gaps for query results. Cardinality problem go show as memory wey dey rise slowly instead of an error. Monitor the size of the chunks directory on a schedule, no be after incident happen.

The last failure na putting wrong data inside. Log system no be metrics system: CPU load wey you sample every 10 seconds and store as text dey expensive to keep and e hard to graph. That work belong to something like Zabbix monitoring server for Ubuntu 24.04. Application exceptions need grouping, deduplication and stack trace view. Na the work of self-hosted error tracker be that. Knowing say the site don go down na another separate work, and uptime and status page like Uptime Kuma fit answer am. Keep log system for lines of text wey person go read.

FAQ

I need Elasticsearch to search my server logs?

For one or two servers, no. journalctl don already filter by unit, priority, boot and time range, and rotated files dey respond to grep and zgrep. Search cluster dey worth the memory when you get many machines, when you need free text search across all of dem at once, or when several people need shared interface. Below that level, journald with size limit and retention time dey do the same work without extra RAM.

How much RAM I need for self-hosted log management?

Use the figures wey each project publish, instead of using guesswork. Loki and Alloy na Go programs wey no need reserve heap upfront, and Grafana document monolithic Loki up to approximately 20GB per day. OpenSearch sample compose set 512 MB heap for demo and 2 GB for production example, while Elastic talk say heap must remain at or below 50% of total memory. So, 2 GB heap mean 4 GB machine before Kibana. Logstash documentation recommend at least 4GB heap by itself. These na documented settings, no be benchmarks, so measure your own load before you size plan.

Wetin be the real difference between Loki and OpenSearch for logs?

Na the index model. Loki indexes labels only and keeps log body as compressed chunks wey e scan when query run, so writes cheap but broad queries cost more. OpenSearch indexes the content of fields, so arbitrary full text search fast, while both memory and disk pay for the index. Choose Loki when you know the service and time window wey you want. Choose OpenSearch when you need search text wey you no fit predict beforehand.

How long I suppose keep logs for VPS?

Choose the number before disk choose am for you. Set am for exactly one place per system: MaxRetentionSec= and SystemMaxUse= for journald, retention_period with compactor enabled for Loki, and ISM policy with min_index_age for OpenSearch. For most single-server setups, 14 to 30 days dey cover debugging and incident review. Anything wey you must keep longer suppose dey inside copy stored off the box, because log wey dey only on the server wey fail no be reliable record.

Promtail still be the way to ship logs to Loki?

No. Promtail reach end of life on 2 March 2026, and Grafana Alloy don replace am. Loki own Docker install example now ship Alloy configuration, and Grafana provide converter wey turn existing Promtail config into Alloy syntax. Existing Promtail install go continue to run, but e no dey receive fixes, so treat migration as maintenance, instead of upgrade wey you fit postpone forever.

#logging#loki#opensearch#journald#monitoring