When you run multiple servers, tailing logs over SSH stops scaling. Centralized logging with Loki and Vector ships every host’s logs to one place you can search. This walkthrough stands up Loki (the log store) and Grafana (the query UI) on a central server with Docker Compose, then installs Vector on a client host to forward its logs. By the end you will query logs from all your hosts in one Grafana search bar.
Prerequisites
- A central server with Docker and the Compose plugin installed.
- One or more client hosts whose logs you want to collect.
- Network access from clients to the central server on TCP 3100.
Step 1: run Loki and Grafana on the central server
Create a working directory and a Loki config:
mkdir -p ~/logging && cd ~/logging
cat > loki-config.yml <<'EOF'
auth_enabled: false
server:
http_listen_port: 3100
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
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
limits_config:
retention_period: 168h
EOFNow the Compose file:
cat > docker-compose.yml <<'EOF'
services:
loki:
image: grafana/loki:3.0.0
command: -config.file=/etc/loki/config.yml
ports:
- "3100:3100"
volumes:
- ./loki-config.yml:/etc/loki/config.yml
- loki-data:/loki
grafana:
image: grafana/grafana:11.1.0
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
volumes:
loki-data:
grafana-data:
EOF
docker compose up -dConfirm Loki is ready:
curl -s http://localhost:3100/readyIt returns ready once it has started (give it 30 seconds).
Step 2: install Vector on a client host
curl --proto '=https' --tlsv1.2 -sSf https://sh.vector.dev | bash
Configure it to read the journal and auth log, tag each line with the hostname, and ship to Loki. Replace the endpoint with your central server IP:
sudo tee /etc/vector/vector.yaml > /dev/null <<'EOF'
sources:
journal:
type: journald
current_boot_only: true
transforms:
tag_host:
type: remap
inputs: [journal]
source: |
.host = get_hostname!()
sinks:
loki:
type: loki
inputs: [tag_host]
endpoint: http://<central-server-ip>:3100
encoding:
codec: json
labels:
host: "{{ host }}"
unit: "{{ _SYSTEMD_UNIT }}"
source: vector
EOF
sudo systemctl enable --now vectorCheck Vector is running and shipping:
sudo systemctl status vector --no-pagerStep 3: Query the logs in Grafana
Open http://<central-server-ip>:3000 and log in (default admin/admin, change it on first login). Add a data source: Connections, Add data source, Loki, URL http://loki:3100, Save and test.
Go to Explore, pick the Loki data source, and run LogQL queries:
{source="vector"}
{host="agent01"} |= "error"
{unit="ssh.service"} |= "Failed password"The last one surfaces failed SSH logins across every host forwarding to Loki.
What goes wrong in centralized logging
Nothing shows up in Grafana. Vector cannot reach Loki. Confirm port 3100 is open from the client and that the endpoint IP is correct.
Loki rejects logs with an “out of order” or timestamp error. The client clock is skewed. Run NTP on every host.
Queries are slow or Loki uses too much memory. You put a high-cardinality value in labels (like a request ID or user ID). Labels must be low-cardinality (host, unit, environment). Put everything else in the log line and filter it with |= at query time.
Logs stop after a week. That is the retention_period of 168h in the config. Raise it if you need longer, and remember disk grows with it.