Deployment guide

Centralize Docker logs on your VPS with Loki and Grafana

Deploy on a VPS Cloud →

Security & Monitoring10 min read

Centralize Docker logs on your VPS with Loki and Grafana

Jumping from one SSH session to another to read each container's logs takes time — the time needed to locate a 502 error that appeared across multiple services at once. Loki solves this by centralizing all your log streams into a single Grafana dashboard, with memory usage compatible with a standard VPS, where an ELK stack would require a dedicated server. This guide covers the complete deployment: Loki, Promtail, Grafana, and post-install configuration.

Why centralize your logs on a VPS instead of staying with SSH

When an error hits multiple services simultaneously — a timeout on the application side, a 502 on nginx, an exception in a worker — no SSH tool lets you correlate these events without reading them one by one. docker compose logs -f app, then docker compose logs -f nginx, then docker compose logs -f worker: that's serial diagnosis, on a single server. On two VPS instances, the time doubles. On three, it triples. Loki is an open source log aggregation system developed by Grafana Labs and listed in the CNCF catalog. Unlike Elasticsearch, which indexes the full content of every log line, Loki only indexes labels — lightweight metadata such as the container name, service name, or environment. The logs themselves are compressed and stored as-is. This design choice has a direct consequence: Loki runs comfortably on a 2 GB RAM VPS, where a basic ELK stack requires at least 8 to 16 GB to remain stable. For a developer or sysadmin managing multiple self-hosted applications without an enterprise APM budget, that is the difference between a feasible solution and an impractical one.

What this stack delivers concretely

  • Unified search across all containers — a single LogQL query covers all services across all your VPS instances, without SSH or distributed grep.
  • Temporal correlation — Grafana displays Loki (logs) and Prometheus (metrics) on the same time axis: you see the 502 error and the CPU spike that preceded it in a single view.
  • No ingestion quota — you store as many logs as your disk allows, without a subscription or monthly limit imposed by a third party.
  • Contained memory footprint — Loki consumes significantly less RAM than Elasticsearch at equivalent log volume, thanks to label-only indexing.
  • Configurable retention — set retention duration per label (chunk_retain_period, retention_period) according to your disk constraints and compliance requirements.
  • Native Grafana integration — Loki is a first-class data source in Grafana: no third-party plugin, no intermediate API.
  • Alerts on log content — Grafana can trigger an alert when a regular expression matches in a Loki stream, without going through an external service.
  • Data hosted on your infrastructure — logs stay on your VPS, without transiting through a cloud service.

Prerequisites before deploying

The Loki + Promtail + Grafana stack is lightweight, but it has its own requirements. Here is what you need before starting. Recommended VPS resources: 2 vCPU and 2 GB of RAM are a reasonable minimum for single-server usage with a few dozen active containers. If you collect logs from multiple VPS instances or a large number of services, aim for 4 GB of RAM. Loki does not keep logs in memory: it compresses them and writes them to disk, which keeps the memory footprint stable over time. Plan for 20 to 50 GB of SSD storage depending on your log volume and retention period. Required software: Docker and Docker Compose v2 installed on the VPS, a dedicated subdomain (logs.your-domain.com) pointing to the VPS IP address, and ports 3100 (Loki) and 3000 (Grafana) accessible internally. Port 3100 must not be exposed publicly — Loki has no native authentication layer, only Grafana is exposed via reverse proxy.

Step-by-step deployment

01

Create the directory structure

Connect to your VPS and create a dedicated directory: mkdir -p /opt/loki-stack/{loki,promtail} && cd /opt/loki-stack. This folder will hold the Loki and Promtail configuration files along with the docker-compose.yml.

02

Write the Loki configuration

Create /opt/loki-stack/loki/loki-config.yaml with the following content: auth_enabled: false — disables multi-tenant authentication, sufficient for single-VPS usage. server: { http_listen_port: 3100 } ingester: { lifecycler: { address: 127.0.0.1, ring: { kvstore: { store: inmemory }, replication_factor: 1 } }, chunk_idle_period: 5m, chunk_retain_period: 30s } schema_config: { configs: [ { from: 2020-10-24, store: boltdb-shipper, object_store: filesystem, schema: v11, index: { prefix: index_, period: 24h } } ] } storage_config: { boltdb_shipper: { active_index_directory: /loki/boltdb-shipper-active, cache_location: /loki/boltdb-shipper-cache, shared_store: filesystem }, filesystem: { directory: /loki/chunks } } limits_config: { retention_period: 720h }

03

Write the Promtail configuration

Create /opt/loki-stack/promtail/promtail-config.yaml. Promtail is the agent that collects logs from Docker containers and pushes them to Loki: server: { http_listen_port: 9080, grpc_listen_port: 0 } positions: { filename: /tmp/positions.yaml } clients: [ { url: http://loki:3100/loki/api/v1/push } ] scrape_configs: [ { job_name: docker, docker_sd_configs: [ { host: unix:///var/run/docker.sock, refresh_interval: 5s } ], relabel_configs: [ { source_labels: ['__meta_docker_container_name'], regex: '/(.*)', target_label: container }, { source_labels: ['__meta_docker_container_log_stream'], target_label: stream } ] } ] This configuration uses Docker auto-discovery (docker_sd_configs), which detects every started container without manual intervention.

04

Write the docker-compose.yml file

Create /opt/loki-stack/docker-compose.yml: version: '3.8' services: loki: image: grafana/loki:3.0.0 ports: ['127.0.0.1:3100:3100'] volumes: [loki-data:/loki, ./loki/loki-config.yaml:/etc/loki/local-config.yaml] command: -config.file=/etc/loki/local-config.yaml restart: unless-stopped promtail: image: grafana/promtail:3.0.0 volumes: [/var/run/docker.sock:/var/run/docker.sock:ro, ./promtail/promtail-config.yaml:/etc/promtail/config.yml, /var/log:/var/log:ro] command: -config.file=/etc/promtail/config.yml restart: unless-stopped depends_on: [loki] grafana: image: grafana/grafana:latest ports: ['127.0.0.1:3000:3000'] volumes: [grafana-data:/var/lib/grafana] environment: [GF_SECURITY_ADMIN_PASSWORD=change-me] restart: unless-stopped depends_on: [loki] volumes: loki-data: grafana-data: Note that Loki (3100) and Grafana (3000) are bound to 127.0.0.1 only — they are not directly accessible from outside.

05

Start the stack

From /opt/loki-stack, run: docker compose up -d. Wait 20 to 30 seconds then check that all three containers are running: docker compose ps. Then verify the Loki logs to confirm it started without errors: docker compose logs -f loki. You should see a line msg="Loki started" in the output.

06

Configure the reverse proxy for Grafana

Expose Grafana via nginx with a subdomain and TLS certificate. Create /etc/nginx/sites-available/grafana.conf: server { listen 443 ssl; server_name logs.your-domain.com; ssl_certificate /etc/letsencrypt/live/logs.your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/logs.your-domain.com/privkey.pem; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } Obtain the certificate with: certbot certonly --nginx -d logs.your-domain.com, then enable the site: ln -s /etc/nginx/sites-available/grafana.conf /etc/nginx/sites-enabled/ && nginx -t && systemctl reload nginx.

07

Add Loki as a data source in Grafana

Open https://logs.your-domain.com in your browser. Log in with the admin username and the password you set in GF_SECURITY_ADMIN_PASSWORD. Go to Connections → Data sources → Add data source, choose Loki, and enter the URL: http://loki:3100. Click Save & test — you should see the message Data source connected and labels found.

08

Query your first logs

In Grafana, open the Explore tab and select the Loki source. Use the Log Browser to pick a container label and filter by container name. For example, to see all logs from the nginx container: {container="nginx"}. To filter on a string: {container="nginx"} |= "error". To count errors by container over the last hour: sum by (container) (count_over_time({container=~".+"} |= "error" [1h])).

09

Collect logs from a second VPS

To centralize logs from a second VPS into the same Loki instance, deploy only Promtail on that secondary VPS. In its configuration, replace the client URL with the internal address of your first VPS: url: http://<Loki-VPS-IP>:3100/loki/api/v1/push. Open port 3100 only between the two VPS instances (firewall or private network), never publicly. Add a static label to distinguish logs by server: static_configs: [ { labels: { host: vps2 } } ].

Post-install configuration: retention, backup, and alerts

Once the stack is running, three adjustments are important before considering the deployment complete. Retention. The retention_period: 720h value in loki-config.yaml corresponds to 30 days. Adjust this parameter based on your available disk space (docker system df to check volume usage). Loki applies retention in the background with no noticeable performance impact. Backup. The loki-data volume contains the BoltDB indexes and compressed chunks — it is the only element to back up. A daily snapshot of the Docker volume (docker run --rm -v loki-data:/data -v /backup:/backup alpine tar czf /backup/loki-$(date +%F).tar.gz /data) is sufficient for standard usage. Do not back up Grafana without its grafana-data volume (it contains your dashboards and alerts). Alerts on log content. In Grafana, create a Grafana managed alert on a Loki query: for example, trigger a notification when the number of lines containing FATAL exceeds 0 over the last 5 minutes. Configure a notification channel (email, Slack, webhook) in Alerting → Contact points before creating the rule.

Loki exposes no native authentication mechanism on port 3100. If you need to expose the Loki API to an untrusted network (for remote Promtail agents, for example), place an nginx reverse proxy with client certificate authentication or basic auth upstream. Never leave port 3100 open on 0.0.0.0 on a production VPS. On the Grafana side, enable strong authentication: GF_AUTH_ANONYMOUS_ENABLED=false and GF_USERS_ALLOW_SIGN_UP=false in the container environment variables.

Troubleshooting: common errors and how to resolve them

Here are the most frequent issues encountered when deploying the Loki + Promtail + Grafana stack.

Common errors

  • msg="error creating ingester" err="context deadline exceeded" on Loki startup — the Loki container does not have write access to the mounted volume. Check the permissions of the directory corresponding to the Docker volume (docker inspect loki-data to find the actual path) and ensure the container user (UID 10001 for recent Grafana images) can write to it: chown -R 10001:10001 /path/to/the/volume.
  • Data source connected and labels found absent in Grafana, replaced by connection refused — Grafana cannot reach Loki. Verify that the URL entered in the data source is http://loki:3100 (the Docker service name, not localhost) and that both containers are in the same Docker network (docker inspect loki-grafana-stack_default).
  • Promtail collects no logs, docker compose logs -f promtail shows component=discovery.docker msg="refreshing targets" in a loop with no progress — Promtail does not have access to the Docker socket. Verify that the volume /var/run/docker.sock:/var/run/docker.sock:ro is declared in the Promtail service and that the socket exists on the host: ls -la /var/run/docker.sock.
  • err="entry out of order for stream" in Loki logs — logs are arriving with out-of-order timestamps (a restarted container resending old logs, for example). Add max_stream_label_count: 0 under limits_config and unordered_writes: true under ingester in loki-config.yaml to accept out-of-sequence entries.
  • Grafana shows no data on a valid LogQL query — first check the time range selected in the top right (the most common trap: range set to last 5 minutes while Promtail has not yet collected recent logs). Then verify that the label used in your query actually exists: {container="nginx"} fails if the container is named nginx-1 — use the Log Browser to explore available labels.

A dashboard to correlate logs and metrics

The main benefit of Loki in an existing Grafana environment is correlation with Prometheus metrics. If you already have a Prometheus source (see the article on VPS monitoring with Grafana and Prometheus), you can create a mixed dashboard: a row of metric panels (CPU, memory, HTTP request rate) at the top, and a Loki log panel filtered on the same service at the bottom. When a CPU spike appears at 2:37 PM, you immediately see which logs were emitted at that exact moment, without switching tools or running another SSH command. This type of correlation is the use case that justifies deploying both stacks rather than one or the other: Prometheus for "what can be measured", Loki for "what gets narrated".

Loki vs ELK vs SSH logs — which approach for which use case

CriterionSSH + docker logsELK Stack (Elastic)Loki + Grafana
RAM requiredNone (no service)8–16 GB minimum per node2–4 GB for a standard VPS
IndexingNoneFull-text indexing (Elasticsearch)Label-only indexing
Multi-server searchImpossible without scriptingYes, nativelyYes, via multi-host Promtail
Correlation with metricsManualWith Kibana + APM (complex)Native in Grafana
Software costNoneBasic license free, features limitedFully open source, no quota

What you have just put into orbit

You now have a working log aggregation stack: Loki stores and indexes by labels, Promtail automatically collects streams from all your Docker containers, and Grafana provides a search and alerting interface. The stack runs entirely on your VPS, without a third-party service, without an ingestion quota, and without additional software cost. Natural next steps: connect this stack to your existing Prometheus monitoring for metrics/logs correlation, enable distributed tracing with Tempo if your applications emit OpenTelemetry traces, and explore Grafana alerts on log content to be notified before your users report a problem.

A VPS ready for your observability stack

Deploy Loki, Promtail, and Grafana on a ServOrbit Cloud VPS with full root access, dedicated IPv4, and NVMe SSD storage. No port restrictions, no software surcharge.

Need help?

Browse our help center and FAQ, or write to our team — support in French, English and Arabic.