Building a Cost-Effective Datadog Alternative with Grafana, Loki, and Prometheus
A self-hosted observability stack can be a practical alternative to SaaS platforms such as Datadog and Splunk---particularly when log volume is high, retention requirements are long, and the team is comfortable operating infrastructure.
The main architectural advantage is control over where telemetry is processed and stored. Instead of paying a vendor to ingest and retain every log line, you can use Grafana Loki to index a small set of bounded labels, store compressed log chunks in S3-compatible object storage, and query those logs with LogQL. Prometheus handles metrics, while Grafana provides dashboards and a shared interface.
That does not make observability free. You still pay for compute, storage, requests, network transfer, backups, and the engineering time required to operate the platform. The objective is to replace variable SaaS charges with infrastructure costs you can measure and manage.
This guide explains the cost model, architecture, cardinality risks, a Docker Compose reference deployment, and how to size and place the infrastructure.
1. The Economics of Log Ingestion: Why Teams Migrate Off SaaS APM
Observability bills often grow faster than teams expect. The main reason is that telemetry volume and infrastructure complexity tend to increase together: more services produce more logs, more hosts generate more metrics, and longer retention keeps more data billable.
Where SaaS observability costs come from
The exact pricing model differs by vendor and contract, but these cost drivers are common:
Cost driver Why it grows What to review
Host or container Adding nodes and Which hosts and monitoring workloads can increase containers genuinely the monitored-host need full monitoring? count.
Custom metrics and Each unique label Are labels bounded? Are cardinality combination can create metrics duplicated or another time series and unnecessarily granular? may affect billing or
platform limits.
Log ingestion and High-volume logs can Which logs are useful, indexing create substantial and which can be recurring ingestion filtered or sampled? charges.
Log retention Keeping indexed data Which logs need for longer periods can searchable retention, increase the bill. and for how long?
Traces and add-on APM, profiling, Which features are features synthetics, and other actually used by the capabilities may be team? priced separately.
Before migrating, build a baseline from real invoices and usage reports. Include ingestion volume, indexed retention, monitored hosts, custom metrics, traces, and any committed-use discounts. A comparison against a public list price can be misleading if your organization has negotiated terms.
How Loki changes the storage model
Loki is designed around labels rather than full-text indexing every word in every log line.
At a high level, the pipeline works like this:
- Collect logs. Grafana Alloy reads logs from applications, containers, or supported system sources.
- Attach bounded labels. The collector adds metadata such as
service_name,environment, andcluster. - Write to Loki. Loki groups records into streams based on their label sets.
- Store chunks and index data. Loki stores compressed log chunks and the associated TSDB index objects in object storage.
- Query with LogQL. Loki uses labels to identify candidate streams, then reads and filters the relevant log data.
This is different from a conventional full-text search engine that builds an inverted index over the contents of every log line. Loki's approach can reduce indexing overhead and make object storage an economical place to keep log history.
There is a trade-off: a query that searches a broad time range across many services may need to read and scan a large amount of data. A cheap storage tier does not automatically make expensive queries cheap.
What kind of savings are realistic?
A reduction of 70--85% is sometimes used as a migration target, but it should not be presented as a guaranteed Loki outcome. Savings depend on the existing SaaS contract, the volume and type of telemetry, retention, query patterns, infrastructure prices, and the cost of operating the replacement.
Use a total-cost comparison:
- SaaS: ingestion, indexed retention, hosts, custom metrics, traces, and add-on features.
- Self-hosted: compute, object storage, storage requests, persistent volumes, network transfer, backups, and monitoring of the observability platform itself.
- Operations: upgrades, security patching, capacity planning, incident response, and on-call ownership.
For example, if a team currently spends €4,000 per month and the replacement costs €1,200 per month---including infrastructure and incremental operating costs---the modeled reduction is 70%. These figures are illustrative, not vendor quotes.
The honest business case is not "Loki is 85% cheaper." It is: "We measured our workload, modeled the full operating cost, and found that this architecture meets our requirements at a lower total cost."
2. System Architecture: Decoupled Storage and Stateless Ingestion
A useful observability architecture separates collection, ingestion, querying, and durable storage. That separation makes it easier to scale the busy parts without increasing every component's capacity.
The LGTM stack
LGTM traditionally stands for Loki, Grafana, Tempo, and Mimir. Prometheus is often used for metrics collection and alerting, either on its own or alongside a long-term metrics backend such as Mimir.
For a logs-and-metrics deployment, the core components are:
- Grafana: dashboards, exploration, and alert visualization.
- Loki: log ingestion, storage coordination, and LogQL queries.
- Grafana Alloy: collection and forwarding of logs and other telemetry.
- Prometheus: metrics scraping, local TSDB storage, and alert-rule evaluation.
- S3-compatible object storage: durable storage for Loki chunks and TSDB index objects.
Tempo is a separate tracing backend. Add it when distributed tracing is part of the requirements; it is not necessary for a logs-and-metrics deployment.
The data path
A typical log path looks like this:
- Alloy runs close to the workloads and collects application or container logs.
- Alloy attaches a controlled set of labels and sends batches to Loki.
- Loki accepts the data, groups it into streams, and writes chunks and index data.
- Loki uses object storage for durable chunks and TSDB index objects.
- Grafana sends LogQL queries to Loki and PromQL queries to Prometheus.
Prometheus has its own storage model. Loki's object-storage configuration does not automatically move Prometheus TSDB data into the same bucket. If long-term metrics retention or horizontal metrics scaling is required, evaluate Mimir or another Prometheus-compatible remote-write backend separately.
Loki deployment modes
Choose the deployment mode based on workload size and availability requirements.
Mode Suitable use Trade-off
Monolithic Evaluation, smaller Multiple installations, and responsibilities share simpler deployments one process and its resource limits.
Simple Scalable Existing installations Check the current Loki Deployment (SSD) that separate read, lifecycle and migration write, and backend guidance before targets choosing it for a new deployment.
Microservices Larger systems that More services, need independent configuration, scaling of Loki networking, and components operational work.
For a new installation, check the current Loki documentation before choosing SSD. Grafana's deployment guidance and lifecycle recommendations can change between releases. Do not build a long-lived platform around a deployment mode without checking its support status.
What "stateless" really means
Object storage lets Loki keep durable log history outside the compute nodes. That makes it easier to replace or scale some components without attaching a disk containing the entire log history.
It does not mean every Loki component is completely stateless. Depending on the deployment, components still need local working directories, caches, write-ahead logs, ring membership, or other state. Plan for those explicitly.
A production design should account for:
- WAL persistence and recovery.
- Ring membership and replication.
- Object-storage latency, throughput, and request limits.
- Local index and chunk caches.
- Compaction and retention backlogs.
- Network transfer between compute and storage.
- Redundancy across nodes and failure domains.
A bucket is a durable storage dependency, not a complete high-availability design. Availability also depends on replicated ingestion, redundant query capacity, component health, and tested recovery procedures.
3. Avoiding the High-Cardinality Label Trap
The most important Loki design rule is straightforward: do not put unbounded, request-specific values into stream labels.
Loki identifies a stream by its label set. Every distinct combination of label values can create another stream. If labels contain values that change on every request, the number of streams can grow rapidly and put pressure on the index, memory, and ingestion path.
Good labels and bad labels
Field Typical cardinality Recommendation
service_name Bounded by the number Good stream label of services
environment Usually very low Good stream label
cluster Bounded if cluster Usually suitable count is controlled
namespace Depends on cluster Suitable when practices controlled
level Small set such as Good stream label debug, info,warn, error
request_id Potentially unique per Keep in the log body or request structured metadata
user_id Potentially very large Do not use as a stream label
session_id Potentially unique per Do not use as a stream session label
client_ip Potentially large and Usually keep in the log variable body
trace_id Unique per trace Keep in the log body or structured metadata
A poor label design might look like this:
# Conceptual example: do not use request-specific values as stream labels.
labels:
service_name: checkout
environment: production
request_id: req-7f83a91d
user_id: user-583920
client_ip: 203.0.113.42A better label set is deliberately small:
labels:
service_name: checkout
environment: production
cluster: prod-eu
level: errorKeep the request-specific information in the log record instead:
{
"timestamp": "2026-11-11T10:30:00Z",
"level": "error",
"service_name": "checkout",
"request_id": "req-7f83a91d",
"user_id": "user-583920",
"client_ip": "203.0.113.42",
"message": "payment authorization failed",
"duration_ms": 842
}This preserves the information engineers need for debugging without making each request identifier part of the stream index.
Query request-specific fields with LogQL
For example, to find a particular request:
{service_name="checkout", environment="production"}
|= "payment authorization failed"
| json
| request_id="req-7f83a91d"To find errors associated with a particular user:
{service_name="checkout", environment="production"}
| json
| user_id="user-583920"
| level="error"These examples assume that the log lines contain valid JSON and the named fields are present.
There is a performance cost to parsing and filtering fields at query time. First narrow the search using indexed labels and a sensible time range. Then apply parsing and filters. A query that scans every service over several weeks can still be expensive, even if the labels are well designed.
For repeated investigations, structured metadata or carefully designed derived fields may improve the workflow. They should not be confused with stream labels, and they do not make every high-cardinality field suitable for indexing.
Make cardinality review part of the development process
For every proposed label:
- Estimate the number of possible values.
- Consider how it combines with existing labels.
- Check whether values can be unique per request, user, session, or trace.
- Test stream creation and ingestion under representative traffic.
- Review Loki's stream and ingestion metrics after deployment.
Do not rely on a simple "number of labels per stream" rule. A stream with only a few labels can still be problematic if one label has millions of possible values.
Apply the same discipline to Prometheus. Every unique combination of metric labels creates a time series, so unbounded dimensions can damage both systems.
4. Production Implementation: Docker Compose & Loki S3 Configuration
The following is a single-host reference deployment using Docker Compose, Loki, Prometheus, Grafana, and Alloy. It demonstrates the main configuration and data paths without requiring Kubernetes.
It is not a highly available production cluster. One host, one Loki process, and one local Prometheus TSDB remain failure domains. Use this setup to validate the architecture and workload before deciding whether to move to a distributed deployment.
4.1 Directory layout
Create this directory structure:
observability/
├── .env
├── .gitignore
├── docker-compose.yml
├── loki-config.yaml
├── prometheus.yml
├── alloy-config.alloy
├── grafana/
│ └── provisioning/
│ └── datasources/
│ └── datasources.yml
└── logs/
└── application.logThe logs/ directory is the input source for this example. In a real environment, configure Alloy for the actual sources: Kubernetes workloads, container logs, journald, or application files.
4.2 Object-storage credentials
Create a private .env file next to docker-compose.yml:
LOKI_S3_BUCKET=observability-prod-loki-logs
LOKI_S3_ENDPOINT=nyc3.digitaloceanspaces.com
LOKI_S3_REGION=nyc3
LOKI_S3_ACCESS_KEY_ID=replace-with-your-real-access-key
LOKI_S3_SECRET_ACCESS_KEY=replace-with-your-real-secret-key
GRAFANA_ADMIN_PASSWORD=replace-with-a-unique-strong-passwordThe endpoint and region above are an example for DigitalOcean Spaces. Change them to match the bucket and region you actually provision. Replace both credential values and the Grafana password before starting the services.
Add .env to .gitignore:
.envRestrict access to the file:
chmod 600 .envFor production, prefer a cloud secret manager or securely injected secrets rather than a long-lived credentials file. Use a private bucket and grant the Loki identity only the permissions it needs.
4.3 loki-config.yaml
This example uses schema v13, TSDB, S3-compatible storage, compaction, and a 14-day retention period.
auth_enabled: false
server:
http_listen_address: 0.0.0.0
http_listen_port: 3100
grpc_listen_port: 9096
log_level: info
common:
path_prefix: /loki
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: "2024-04-01"
store: tsdb
object_store: s3
schema: v13
index:
prefix: loki_index_
period: 24h
storage_config:
use_thanos_objstore: true
object_store:
s3:
bucket_name: ${LOKI_S3_BUCKET}
endpoint: ${LOKI_S3_ENDPOINT}
region: ${LOKI_S3_REGION}
access_key_id: ${LOKI_S3_ACCESS_KEY_ID}
secret_access_key: ${LOKI_S3_SECRET_ACCESS_KEY}
insecure: false
bucket_lookup_type: path
tsdb_shipper:
active_index_directory: /loki/index
cache_location: /loki/index_cache
cache_ttl: 24h
compactor:
working_directory: /loki/compactor
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
delete_request_store: s3
limits_config:
retention_period: 336h
max_query_lookback: 336h
allow_structured_metadata: true
analytics:
reporting_enabled: falseStart Loki with environment-variable expansion enabled:
-config.expand-env=trueImportant points:
auth_enabled: falsedisables Loki's tenant authentication. Do not expose this endpoint directly to untrusted networks. Put authentication and authorization in front of it.replication_factor: 1is for this single-instance reference deployment, not high availability.- The TSDB index period is 24 hours.
retention_period: 336hsets a 14-day retention period.- The Compactor performs retention processing asynchronously; deletion is not necessarily immediate when a log reaches its expiry time.
delete_request_store: s3configures the store used for deletion requests.insecure: falsekeeps TLS enabled for the remote object-storage endpoint.bucket_lookup_type: pathrequests path-style bucket addressing. Confirm that the chosen provider and endpoint support it.
Loki's object-storage configuration has changed across releases. Validate the configuration against the exact Loki version you deploy, especially when using a non-AWS S3-compatible provider. Consult the Loki storage documentation, schema documentation, and retention guide.
4.4 prometheus.yml
This configuration scrapes Prometheus itself and Loki's metrics endpoint:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets:
- prometheus:9090
- job_name: loki
metrics_path: /metrics
static_configs:
- targets:
- loki:3100Add application and infrastructure scrape jobs based on the exporters and services in your environment.
4.5 alloy-config.alloy
This configuration discovers files under /var/log/app/, attaches bounded labels, and forwards log records to Loki:
local.file_match "application_logs" {
path_targets = [
{
"__path__" = "/var/log/app/*.log",
"job" = "application",
"service_name" = "sample-app",
"environment" = "production",
},
]
}
loki.source.file "application_logs" {
targets = local.file_match.application_logs.targets
forward_to = [loki.write.local.receiver]
}
loki.write "local" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}The file source is intentionally simple. For Kubernetes, use a cluster-aware Alloy pipeline and apply the same cardinality controls to discovered metadata. See the Alloy component reference.
4.6 Grafana data-source provisioning
Create grafana/provisioning/datasources/datasources.yml:
apiVersion: 1
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
- name: Loki
uid: loki
type: loki
access: proxy
url: http://loki:3100
editable: falseGrafana can now query both services over the internal Compose network.
4.7 docker-compose.yml
This Compose file defines persistent volumes, internal networking, resource limits, and service configuration. The image versions are pinned examples, not a statement that these are the newest patch releases. Select and validate currently supported versions before deploying.
services:
loki:
image: grafana/loki:3.5.0
command:
- -config.file=/etc/loki/loki-config.yaml
- -config.expand-env=true
env_file:
- .env
volumes:
- ./loki-config.yaml:/etc/loki/loki-config.yaml:ro
- loki-data:/loki
ports:
- "127.0.0.1:3100:3100"
networks:
- observability
restart: unless-stopped
mem_limit: 4g
cpus: 2.0
prometheus:
image: prom/prometheus:v3.7.0
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=15d
- --web.enable-lifecycle
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
ports:
- "127.0.0.1:9090:9090"
networks:
- observability
restart: unless-stopped
mem_limit: 2g
cpus: 1.0
grafana:
image: grafana/grafana:12.1.1
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?set GRAFANA_ADMIN_PASSWORD}
GF_USERS_ALLOW_SIGN_UP: "false"
GF_ANALYTICS_REPORTING_ENABLED: "false"
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
ports:
- "127.0.0.1:3000:3000"
networks:
- observability
restart: unless-stopped
mem_limit: 1g
cpus: 1.0
depends_on:
- loki
- prometheus
alloy:
image: grafana/alloy:v1.8.3
command:
- run
- --server.http.listen-addr=0.0.0.0:12345
- /etc/alloy/config.alloy
volumes:
- ./alloy-config.alloy:/etc/alloy/config.alloy:ro
- ./logs:/var/log/app:ro
- alloy-data:/var/lib/alloy/data
networks:
- observability
restart: unless-stopped
mem_limit: 512m
cpus: 0.5
depends_on:
- loki
networks:
observability:
driver: bridge
volumes:
loki-data:
prometheus-data:
grafana-data:
alloy-data:The Loki, Prometheus, and Grafana ports are bound to loopback. They are not directly exposed on the public interface. For remote access, use a properly secured reverse proxy, VPN, or private network with authentication and TLS.
The resource limits are starting constraints, not sizing guarantees. Monitor memory, CPU throttling, disk usage, and query latency, then adjust them against the real workload.
4.8 Validate and start the stack
Create the log directory and a test record:
mkdir -p logs
printf '%s\n' \
'{"level":"info","message":"observability stack initialized"}' \
> logs/application.logValidate the Compose model:
docker compose config --quietStart the services:
docker compose up -dInspect service status and logs:
docker compose ps
docker compose logs --tail=100 loki
docker compose logs --tail=100 alloy
docker compose logs --tail=100 prometheusCheck Loki readiness:
curl --fail http://127.0.0.1:3100/readyIn Grafana Explore, select Loki and run:
{service_name="sample-app", environment="production"}A successful readiness response is not proof that the object-storage path works. Confirm that logs are uploaded, can be queried, and remain available after a Loki restart. Also test retention expiry and deletion against the actual bucket.
Production acceptance checks
Before calling the deployment production-ready, verify:
- Object-storage writes and reads succeed with the selected provider.
- Loki can recover after a process or host restart.
- Required WAL and local state survive the failure scenarios in your design.
- Retention and deletion work as expected.
- Compaction does not fall behind ingestion.
- Disk usage and free space are monitored.
- Queries perform acceptably at representative volume and concurrency.
- Credentials are private and have least-privilege permissions.
- Loki and Grafana are not exposed without appropriate access controls.
- Backups and recovery procedures have been tested.
This Compose deployment is a single-host reference architecture. For high availability, use a distributed Loki design with redundant components, appropriate replication, durable state, and failure-domain-aware placement.
5. Infrastructure Sizing & Storage Placement Recommendations
Disclosure: provider links in this section are affiliate links. We earn a commission if you sign up, at no cost to you. Evaluate against your own requirements and verify current pricing yourself.
The cheapest VM is not necessarily the cheapest observability platform. The useful comparison is the total cost of compute, object storage, requests, network transfer, backups, and operational effort.
For this architecture, size three areas separately:
- Compute: log ingestion, querying, compaction, Grafana, and metrics collection.
- Object storage: durable Loki chunks and TSDB index objects.
- Local storage: WALs, caches, temporary files, compaction working space, and Prometheus TSDB data.
Keep compute and object storage in a suitable nearby region, but verify the provider's actual network and billing rules rather than assuming that regional traffic is free.
5.1 Starting compute sizes
These are initial planning ranges, not fixed requirements. Actual resource use depends on log volume, compression, label cardinality, query patterns, concurrency, and retention.
Workload Initial compute Local storage starting allocation point
Lab or evaluation, 2--4 vCPU, 8 GB RAM 50--100 GB SSD below 5 GB/day
Small production, 5--20 4--8 vCPU, 16--32 GB 100--250 GB SSD GB/day RAM
Medium production, 8--16 vCPU, 32--64 GB 250--500 GB SSD/NVMe 20--100 GB/day RAM as a starting pool; per appropriately sized separate read/write node capacity when
measurements justify it
Larger workloads, above Benchmark a distributed Size each node for its 100 GB/day deployment and scale WAL, caches, temporary components files, and recovery independently needs
These ingestion bands refer to raw incoming log volume, not compressed object-storage usage. Two workloads with the same daily volume can have very different requirements if one produces many more streams or requires much more expensive queries.
Measure at least:
- Sustained and peak ingestion bytes per second.
- Log lines per second and batch sizes.
- Active streams and stream creation rate.
- Query concurrency, p95/p99 latency, and bytes scanned.
- Compaction throughput and backlog.
- Local disk latency, IOPS, and free capacity.
- Object-storage request latency, errors, and throttling.
5.2 Size write, read, and backend workloads separately
For a distributed Loki deployment, use the workload profile to guide capacity.
Write path: distributors and ingesters
A small-to-medium starting point might be 4--8 vCPU and 16--32 GB RAM for an appropriately sized worker group. Prioritize sustained ingestion throughput, memory headroom, predictable disk latency, and persistent storage for the WAL.
Read path: query frontends and queriers
Start benchmarking with 4--8 vCPU and 16--32 GB RAM for a small query worker group. Increase capacity when concurrent investigations, wide time ranges, or expensive parsing expressions cause query latency to rise. Query workers benefit from CPU, cache memory, and sufficient network throughput to object storage.
Backend: compaction and retention
A small backend component might start around 2--4 vCPU and 8--16 GB RAM. Size it against compaction duration, retention workload, and storage request rates. Provide enough scratch space and monitor deletion backlogs.
These are planning ranges, not vendor sizing guarantees. Benchmark with representative data before making a capacity commitment.
5.3 Estimate object-storage capacity
Use measured compression rather than assuming a universal ratio.
For example, consider 100 GB/day of raw log ingestion and 30 days of retention:
- Raw incoming volume: 3 TB.
- At an assumed 4:1 compression ratio, compressed chunks would use approximately 750 GB.
- Actual billed usage will differ because of index objects, chunk overhead, retention timing, and other stored objects.
A simple estimate is:
Estimated chunk storage ≈ (daily raw log volume × retention days) / compression ratioWhere:
Estimated chunk storageis the approximate storage used by compressed log chunks.Daily raw log volumeis the incoming, uncompressed log volume per day.Retention daysis the number of days logs are retained.Compression ratiois the measured ratio of raw log size to compressed chunk size. For example, a 4:1 ratio means 4 GB of raw logs becomes approximately 1 GB of compressed chunks.
Add measured index overhead, temporary retention overlap, and an appropriate safety margin separately.
Do not choose an object-storage tier based on capacity price alone. Loki also depends on request latency, read and write throughput, request limits, and API compatibility.
Benchmark the actual workload. Test sustained uploads, concurrent reads, request errors, throttling, and recovery behavior. Published throughput or IOPS limits are useful constraints, but they do not establish application-level Loki performance.
5.4 Cloud provider options
DigitalOcean: Droplets, DOKS, and Spaces
DigitalOcean is worth evaluating when the team wants relatively straightforward cloud infrastructure.
- Droplets: Run a Docker Compose deployment or dedicated Loki components.
- DOKS: Run Kubernetes workloads when independent deployments, scheduling, and scaling justify the additional operational complexity.
- Spaces: Store Loki chunks and index objects through an S3-compatible API.
Spaces offers S3 API compatibility, but compatibility is not identical to AWS S3 in every detail. Test the operations required by your Loki release and confirm the endpoint, region, addressing mode, and credentials.
Useful references:
Vultr: High Frequency Compute, VKE, and Object Storage
Vultr is another option to benchmark, particularly when its regional compute and storage offerings fit the deployment.
- High Frequency Compute: Evaluate for CPU-sensitive log processing and query workloads.
- VKE: Use managed Kubernetes when workload separation and orchestration justify it.
- Object Storage: Store durable Loki chunks and index objects, subject to compatibility and performance testing.
Review the storage endpoint, S3 compatibility, tier limits, throughput, IOPS, and network charges before selecting a tier.
Useful references:
AWS: EC2, EKS, and S3
AWS is a sensible baseline for organizations already using AWS networking, IAM, security controls, and operational tooling.
- EC2: Run a VM-based deployment or individual Loki components.
- EKS: Run a distributed observability platform on Kubernetes.
- S3: Store chunks and index objects using native AWS integration and IAM.
AWS integration can reduce the work involved in fitting the platform into an existing security and governance model. It does not eliminate the need to model compute, S3 requests, storage, persistent volumes, and network transfer.
5.5 Model network transfer explicitly
Do not compare compute and storage prices while ignoring the traffic between them.
Loki writes chunks to object storage, queries retrieve chunks, and compaction and retention generate additional object operations. Broad queries and repeated scans can generate significant reads.
Before selecting a provider:
- Choose the intended compute and object-storage regions.
- Confirm that the storage service supports the S3 operations required by your Loki version.
- Measure ingestion traffic and query-driven reads.
- Check which traffic is included, billed, or subject to limits.
- Estimate object requests, retention operations, and data transfer at the expected workload.
- Benchmark the full stack rather than comparing storage price per GB alone.
Keeping compute and storage geographically close generally helps latency and transfer efficiency. It does not guarantee that traffic will be free or classified as private-network traffic.
5.6 Choose a deployment pattern that matches the team
Pattern A: Single-host deployment
Run Docker Compose on one appropriately sized VM, use persistent local volumes, and store Loki's durable data in S3-compatible object storage. Restrict access to the observability interfaces and back up configuration off-host.
This is suitable for smaller workloads, initial migrations, and environments where downtime during a host failure is acceptable.
Pattern B: Kubernetes with separated services
Run Loki, Grafana, and the metrics backend in Kubernetes. Use persistent storage for required local state and object storage for durable Loki data.
This is suitable for teams already operating Kubernetes and needing independent scaling or controlled rollouts.
Pattern C: Distributed, highly available observability
Separate Loki's write, read, and backend responsibilities across redundant workloads. Design ring membership, replication, failure domains, storage access, and recovery procedures explicitly.
This is suitable for critical platforms where observability must remain available during individual node or workload failures.
For many teams, the sensible sequence is to start with a measured single-host deployment, establish real ingestion and query behavior, and then move to Kubernetes or a distributed design when the requirements justify it. Kubernetes is not automatically cheaper or simpler just because the applications being monitored already run on Kubernetes.
5.7 Production readiness checklist
Before treating the migration as complete, verify that:
- [ ] Stream labels have bounded cardinality.
- [ ] Ingestion, query, and compaction resource use has been measured.
- [ ] Loki retention is enabled and expiry has been tested.
- [ ] Object storage is private and credentials are securely managed.
- [ ] Required S3 operations work with the selected provider.
- [ ] Loki's local WAL and required state survive the intended failure scenarios.
- [ ] Prometheus retention and backup requirements are defined separately.
- [ ] Observability components have health checks and capacity alerts.
- [ ] Network access, authentication, and authorization match the threat model.
- [ ] The cost model includes compute, storage, requests, transfer, backups, and operations.
- [ ] Image versions are pinned, tested, and covered by an upgrade and rollback plan.
Conclusion
A Grafana, Loki, and Prometheus stack can be a strong alternative to SaaS observability for teams that need control over log retention, storage placement, and infrastructure costs.
The savings do not come from one configuration switch. They come from making several engineering decisions well: keep labels bounded, store durable log data in suitable object storage, control query scope, size compute from measurements, and account for network and operational costs.
DigitalOcean, Vultr, and AWS are all viable candidates, but the right choice depends on the actual workload, S3 compatibility, regional transfer economics, and the team's existing cloud expertise.
Start with a representative workload and a complete cost model. If the self-hosted stack meets the required query performance, retention, security, and availability targets at a lower total cost, the migration has a defensible engineering case.