Set up a self-hosted logging server

A working log server takes one binary and about fifteen minutes.

This page walks the whole path: download, TLS, service install, shipping logs from your hosts, the first useful query, then retention, backups and an alert. Every command below is one you can paste.

What you need first

  • A server. 1GB of RAM is enough for around 15GB of logs at 15-day retention. Scale from there.
  • A domain name with an A record pointing at that server. TLS is issued automatically, and that needs a real hostname.
  • Ports 80 and 443 open inbound.

No Docker. No database. No JVM. Nothing to install first.

Step 1: Download and run it

SSH to the server and grab the binary for your architecture:

wget https://downloads.eligian.com/centrallogging-linux-amd64.zip
unzip centrallogging-linux-amd64.zip

Let it bind the privileged ports without running as root:

sudo setcap CAP_NET_BIND_SERVICE=+eip centrallogging-linux-amd64

Then start it:

./centrallogging-linux-amd64

Step 2: Finish setup in the browser

Open your domain. You get a setup screen. Fill in the admin email, which is also the address Let’s Encrypt uses for the certificate.

Confirm HTTPS works before going further. If the certificate did not issue, the usual cause is DNS that has not propagated or port 80 blocked upstream.

Then stop it with Ctrl+C.

Step 3: Install it as a service

So it survives a reboot:

sudo ./centrallogging-linux-amd64 -install
sudo systemctl enable centrallogging
sudo systemctl start centrallogging

Behind a reverse proxy instead? Set PORT to anything other than :80 and terminate TLS yourself:

PORT=:8080 ./centrallogging-linux-amd64

Step 4: Create a log source

A log source is a stream of logs with its own database, its own retention setting, and its own token. Create one per application or per host group — whatever grouping you want to search separately.

Create it in the web UI. Copy the token it gives you. That token is what authenticates ingest, so treat it like a password.

Step 5: Ship logs to it

From an application, over HTTP

One line:

curl -X POST https://logs.example.com/api/v1/log/{log-source-token} -d "this is a log entry"

Structured JSON works the same way, and pays off later when you query:

curl -X POST https://logs.example.com/api/v1/log/{log-source-token} \
    -d '{"level": "error", "msg": "something went wrong"}'

For batches, send a file. Every newline becomes a log entry:

curl -X POST https://logs.example.com/api/v1/ingest_logs/{log-source-token} -d @mylogs.txt

Prefer a header to a token in the URL? Use a Bearer token:

curl -X POST https://logs.example.com/api/v1/ingest_logs \
    -H "Authorization: Bearer {log-source-token}" \
    -d @mylogs.txt

Gzip is supported for large batches:

curl -X POST https://logs.example.com/api/v1/ingest_logs/{log-source-token} --data-binary @mylogs.gz

From your servers, with the agent

The clagent binary ships system logs upstream on a schedule. On Linux it reads systemd via journalctl. On OpenBSD it reads syslogd.

Install it on each host you want logs from:

wget https://downloads.eligian.com/clagent-linux-amd64.tar.gz
tar xzf clagent-linux-amd64.tar.gz

Create /etc/clagent.toml:

URL = "https://logs.example.com/your-log-source-token"

Then install it as a service:

sudo ./clagent-linux-amd64 -install
sudo systemctl enable clagent
sudo systemctl start clagent

The agent pushes outbound over HTTPS. Nothing needs to reach into your hosts, which keeps the firewall rules simple. It also sends host check-ins, so you get host monitoring with no extra work.

For a fleet, use the Ansible playbook in the agent docs instead of doing this by hand.

Step 6: Query what arrived

Two modes, toggled from the search page.

Full-text search for the quick look:

description:water
+description:water -light beer
/light (beer|wine)/

SQL when you need real answers. Logs live in a logs table with timestamp and msg columns, and json_extract reaches into structured messages:

SELECT
    "timestamp",
    json_extract(logs.msg, '$.Data.method') AS method,
    json_extract(logs.msg, '$.Data.request_uri') AS request_uri,
    json_extract(logs.msg, '$.Data.addr') AS addr
FROM logs
WHERE json_valid(msg) AND request_uri LIKE '%/account%'
ORDER BY "timestamp" DESC
LIMIT 2001;

Which hosts are actually sending logs? This is the first query to run after any install:

select source, count(source) as log_count
from logs
group by source
order by log_count desc;

If a host you configured is missing from that list, the agent on it is not working.

More in useful log queries.

Step 7: Set retention before the disk fills

Each log source has a Retention Days setting. It defaults to 15. Raise it if you have disk and a reason, lower it if you are storing something sensitive.

Retention is the setting people forget until the disk is at 98%. Set it on day one.

Step 8: Turn on backups

Nightly backups upload the main database and every per-source log database to S3. Set them in the Settings page, or with environment variables:

AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
BACKUP_AWS_REGION=us-east-2
BACKUP_S3_BUCKET=cl-backups

Self-hosting the backups too? Point BACKUP_S3_ENDPOINT at a Garage or MinIO server:

BACKUP_S3_ENDPOINT=https://s3.example.com:3900
BACKUP_AWS_REGION=garage

Values saved in the web UI take precedence over the environment variables. Backups older than 15 days are removed automatically.

Step 9: Create one alert

A log server nobody looks at is a log server that tells you nothing. Create at least one alert on day one.

Alert rules run a SQL or FTS query every 5 minutes. Set a Max Frequency so a noisy condition does not page you forty times.

The condition can also fire when a query returns nothing. That is how you catch a job that stopped running rather than one that failed loudly.

Notifications go to Slack via an incoming webhook, or to Telegram via a bot. Both have a Test button. Use it — a notification channel you never tested is not a notification channel.

Full detail in the alerting docs.

Step 10: Monitor the things around the logs

You already have the server. These come with it:

  • Cron monitoring — append a curl to any job and get alerted when it stops reporting in:

    0 3 * * * /usr/local/bin/backup.sh && curl https://logs.example.com/p/<API-KEY>/nightly-backup?state=complete
    
  • Website monitoring — uptime, response time, keyword checks and TLS expiry warnings.

  • Metrics — scrape any Prometheus endpoint and chart it.

Keeping it running

Update the server:

./centrallogging-linux-amd64 -update
sudo systemctl restart centrallogging

Update an agent:

./clagent-linux-amd64 -update
sudo service clagent restart

Watch the disk. Watch that every source is still sending. An alert on “no logs from source X in an hour” catches most of what goes wrong.

When this setup stops being enough

Around 10GB/day you are at the edge of the design target. Storage is SQLite on one node, so there is no horizontal scale and no multi-node failover. If you need either, run Loki or ELK and budget for the operational work.

Where to go next

💌 Get notified on new features and updates

Only sent when a new version is released. Nothing else.