Log files are the foundation of observability on any Linux server. Every service — from the kernel and SSH daemon to Nginx and your application — writes logs that record what happened and when. On a VPS without managed logging, these logs grow unchecked until they consume all available disk space, causing service failures and potential data loss.

This guide covers the complete log management stack on Ubuntu 24.04: understanding how journald and rsyslog work together, configuring storage limits and retention policies, writing custom logrotate rules for application logs, mastering journalctl for efficient log searching, and forwarding logs to external services for long-term storage. By the end, your VPS will have a disciplined, space-efficient logging setup that keeps you informed without filling your disk.

Prerequisites

Before starting, you need:

MassiveGRID Ubuntu VPS — Ubuntu 24.04 LTS pre-installed, Proxmox HA cluster with automatic failover, Ceph 3x replicated NVMe storage, independent CPU/RAM/storage scaling, 12 Tbps DDoS protection, 4 global datacenter locations, 100% uptime SLA, and 24/7 human support rated 9.5/10. Deploy a self-managed VPS from $1.99/mo.

Understanding the Ubuntu Logging Architecture

Ubuntu 24.04 uses two logging systems that work in parallel. Understanding how they interact is essential before configuring either one.

systemd-journald

systemd-journald is the primary logging daemon on modern Ubuntu. It collects log messages from the kernel, systemd services, and anything that writes to stdout/stderr when managed by systemd. The journal is stored in a binary format at /var/log/journal/ (persistent) or /run/log/journal/ (volatile, lost on reboot).

Key characteristics of journald:

rsyslog

rsyslog is the traditional syslog daemon. It reads messages from /dev/log, the kernel log buffer, and (by default) forwards from journald. It writes plain-text log files to /var/log/ — the files you're used to seeing like /var/log/syslog, /var/log/auth.log, and /var/log/kern.log.

Key characteristics of rsyslog:

How They Work Together

On Ubuntu 24.04, the default flow is:

# Log flow on Ubuntu 24.04
Application/Service → journald (binary journal)
                          ↓
                      rsyslog (reads from journal)
                          ↓
                    /var/log/*.log (plain text files)
                          ↓
                    logrotate (rotation and cleanup)

Both systems capture the same messages by default. This means logs are stored twice — once in the journal and once in text files. This redundancy is intentional but consumes extra disk space. Later in this guide, we'll discuss strategies for reducing duplication.

Checking Current Log Disk Usage

Before configuring anything, check how much disk space your logs currently consume:

# Check journal disk usage
journalctl --disk-usage

Example output:

Archived and active journals take up 152.0M in the file system.

Check the traditional log directory:

sudo du -sh /var/log/
sudo du -sh /var/log/* | sort -rh | head -20

Example output:

480M    /var/log/
156M    /var/log/journal
98M     /var/log/syslog.1
67M     /var/log/auth.log.1
45M     /var/log/kern.log.1
32M     /var/log/nginx
28M     /var/log/syslog
15M     /var/log/auth.log

Check overall disk usage on the system:

df -h /

On a VPS with limited storage, logs can easily consume 10-20% of available disk space if left unmanaged. The configurations below will bring this under control.

Configuring journald Storage and Limits

The journald configuration file is /etc/systemd/journald.conf. Open it:

sudo nano /etc/systemd/journald.conf

The default file has all settings commented out (using defaults). Here's a production-ready configuration for a VPS:

[Journal]
# Store journals persistently (survive reboots)
Storage=persistent

# Maximum disk space the journal can use
SystemMaxUse=200M

# Maximum size of individual journal files
SystemMaxFileSize=50M

# Keep at least this much free disk space
SystemKeepFree=500M

# Maximum time to keep journal entries
MaxRetentionSec=30day

# Maximum size of log entries from a single process burst
RateLimitIntervalSec=30s
RateLimitBurst=10000

# Compress journal files (saves ~50% disk space)
Compress=yes

# Forward to syslog (rsyslog) — set to no if you only want journal
ForwardToSyslog=yes

# Do not forward to kernel log buffer, wall, or console
ForwardToKMsg=no
ForwardToConsole=no
ForwardToWall=yes

Explanation of the key settings:

Setting Value Purpose
Storage persistent Store journals in /var/log/journal/ (survives reboots)
SystemMaxUse 200M Hard cap on total journal disk usage
SystemMaxFileSize 50M Each journal file is rotated at this size
SystemKeepFree 500M Journal will shrink if free disk drops below this
MaxRetentionSec 30day Entries older than 30 days are purged
Compress yes Compress journal files to save disk space
ForwardToSyslog yes Forward entries to rsyslog for text file storage

Apply the changes by restarting journald:

sudo systemctl restart systemd-journald

Verify the new settings are active:

journalctl --disk-usage
systemctl status systemd-journald

Choosing Storage Limits for Your VPS

The right SystemMaxUse value depends on your VPS disk size and workload:

VPS Disk Size Recommended SystemMaxUse Recommended MaxRetentionSec
20 GB 100M 14day
40 GB 200M 30day
80 GB 500M 60day
160 GB+ 1G 90day

Reducing Duplicate Storage

If you only read logs via journalctl and don't need traditional text files, you can disable forwarding to rsyslog:

# In /etc/systemd/journald.conf
ForwardToSyslog=no

Then optionally stop rsyslog entirely:

sudo systemctl stop rsyslog
sudo systemctl disable rsyslog

This eliminates duplicate storage and can save hundreds of megabytes on log-heavy servers. However, some applications and log analysis tools expect files in /var/log/, so test carefully before disabling rsyslog.

Mastering journalctl

journalctl is the command-line tool for querying the systemd journal. It's far more powerful than grep-ing text files because it can filter by service, priority, time range, and structured fields.

Basic Usage

# View all logs (newest last)
journalctl

# View logs in reverse order (newest first)
journalctl -r

# Follow new entries in real time (like tail -f)
journalctl -f

# Show only the last 100 entries
journalctl -n 100

Filtering by Service

# Logs for a specific systemd unit
journalctl -u nginx
journalctl -u ssh
journalctl -u mysql

# Follow a specific service in real time
journalctl -u nginx -f

# Multiple services
journalctl -u nginx -u php8.3-fpm

Filtering by Time

# Logs since a specific time
journalctl --since "2026-02-27 10:00:00"

# Logs from the last hour
journalctl --since "1 hour ago"

# Logs from the last 30 minutes
journalctl --since "30 min ago"

# Logs between two times
journalctl --since "2026-02-26 00:00:00" --until "2026-02-27 00:00:00"

# Logs from the current boot
journalctl -b

# Logs from the previous boot
journalctl -b -1

Filtering by Priority

Syslog priority levels range from 0 (emergency) to 7 (debug). Filter to see only important messages:

# Only errors and above (emergency, alert, critical, error)
journalctl -p err

# Warnings and above
journalctl -p warning

# Only critical and above
journalctl -p crit

# Specific priority range
journalctl -p err..warning

Output Formatting

# JSON output (useful for parsing)
journalctl -u nginx -o json-pretty -n 5

# Short format with precise timestamps
journalctl -o short-precise

# Verbose format (shows all fields)
journalctl -u ssh -o verbose -n 1

# Export format (for forwarding)
journalctl -o export --since "1 hour ago"

Searching Log Content

# Search for a keyword (grep-style)
journalctl -g "error"
journalctl -g "failed password" -u ssh

# Case-insensitive search
journalctl -g "(?i)timeout"

# Combine filters: SSH failures in the last hour
journalctl -u ssh -g "Failed password" --since "1 hour ago"

Disk Management

# Show disk usage
journalctl --disk-usage

# Remove old entries, keeping only the last 200M
sudo journalctl --vacuum-size=200M

# Remove entries older than 14 days
sudo journalctl --vacuum-time=14d

# Remove all but the last 2 journal files
sudo journalctl --vacuum-files=2

# Verify journal integrity
journalctl --verify

Understanding Logrotate

logrotate manages the rotation, compression, and deletion of plain-text log files — the files in /var/log/ written by rsyslog and application-specific logging. It runs daily via a systemd timer (or cron on older systems).

How Logrotate Works

When logrotate runs, it checks each configured log file against its rules:

  1. If the file meets rotation criteria (size, age, or schedule), rename it (e.g., syslog becomes syslog.1)
  2. Create a new empty log file with correct permissions
  3. Compress the rotated file (if configured)
  4. Delete old rotated files beyond the retention count
  5. Optionally send a signal to the service to reopen its log file

Configuration File Hierarchy

/etc/logrotate.conf          # Global defaults
/etc/logrotate.d/            # Per-service configuration files
    ├── apt
    ├── dpkg
    ├── nginx
    ├── rsyslog
    ├── ufw
    └── ...                  # Your custom configs go here

View the main configuration:

cat /etc/logrotate.conf

Default contents on Ubuntu 24.04:

# rotate log files weekly
weekly

# keep 4 weeks worth of backlogs
rotate 4

# create new (empty) log files after rotating old ones
create

# use date as a suffix of the rotated file
dateext

# uncomment if you want to use the date as a suffix
#dateyesterday

# packages can drop log rotation information into this directory
include /etc/logrotate.d

# system-specific logs may also be configured here

Examining Existing Logrotate Configs

Look at how Ubuntu configures rotation for rsyslog:

cat /etc/logrotate.d/rsyslog

You'll see that rsyslog rotates weekly, keeps 4 copies, compresses old files, and runs a postrotate script to signal rsyslog to reopen its log files. This pattern — rotate, compress, notify the service — is the template for all custom logrotate configurations.

Logrotate Directive Reference

Directive Purpose
daily / weekly / monthly Rotation frequency
rotate N Keep N rotated files before deleting
size NM Rotate when file exceeds N megabytes
maxsize NM Rotate when file exceeds N megabytes, even if interval hasn't passed
minsize NM Don't rotate unless file is at least N megabytes
compress Compress rotated files with gzip
delaycompress Don't compress the most recent rotated file (needed for some apps)
missingok Don't error if the log file is missing
notifempty Don't rotate if the file is empty
create MODE OWNER GROUP Create new log file with specified permissions
copytruncate Copy the file then truncate (for apps that can't reopen logs)
postrotate ... endscript Run a command after rotation (e.g., reload the service)
sharedscripts Run postrotate script only once for all matched files
dateext Use date instead of number for rotated file suffix

Custom Logrotate Rules for Application Logs

Most applications write their own log files outside the system logging pipeline. You need custom logrotate configurations for these. Below are production-ready configurations for common applications.

Nginx Logs

Ubuntu's Nginx package includes a logrotate config, but you may want to customize it for higher-traffic sites:

sudo nano /etc/logrotate.d/nginx
/var/log/nginx/*.log {
    daily
    rotate 14
    missingok
    notifempty
    compress
    delaycompress
    create 0640 www-data adm
    sharedscripts
    postrotate
        if [ -f /var/run/nginx.pid ]; then
            kill -USR1 $(cat /var/run/nginx.pid)
        fi
    endscript
}

The kill -USR1 signal tells Nginx to reopen its log files without restarting. This is critical — without it, Nginx would continue writing to the old (rotated) file.

Apache Logs

sudo nano /etc/logrotate.d/apache2
/var/log/apache2/*.log {
    daily
    rotate 14
    missingok
    notifempty
    compress
    delaycompress
    create 640 root adm
    sharedscripts
    postrotate
        if invoke-rc.d apache2 status > /dev/null 2>&1; then
            invoke-rc.d apache2 reload > /dev/null
        fi
    endscript
}

Node.js / PM2 Application Logs

If your Node.js application writes logs to files (via winston, pino, or similar), and you're not using PM2's built-in log rotation:

sudo nano /etc/logrotate.d/nodeapp
/var/log/nodeapp/*.log
/var/www/myapp/logs/*.log {
    daily
    rotate 7
    missingok
    notifempty
    compress
    delaycompress
    copytruncate
    size 50M
    create 0644 deploy deploy
}

Important: We use copytruncate instead of the standard rename approach because Node.js applications typically keep their log file handles open. copytruncate copies the current log, then truncates the original file to zero bytes — the application continues writing to the same file descriptor without interruption.

PostgreSQL Logs

sudo nano /etc/logrotate.d/postgresql
/var/log/postgresql/*.log {
    weekly
    rotate 10
    missingok
    notifempty
    compress
    delaycompress
    create 0640 postgres postgres
    su postgres postgres
    postrotate
        /usr/lib/postgresql/16/bin/pg_ctl reload -D /var/lib/postgresql/16/main > /dev/null 2>&1 || true
    endscript
}

MySQL / MariaDB Logs

sudo nano /etc/logrotate.d/mysql-custom
/var/log/mysql/*.log {
    daily
    rotate 7
    missingok
    notifempty
    compress
    delaycompress
    create 0640 mysql adm
    sharedscripts
    postrotate
        if [ -f /var/run/mysqld/mysqld.pid ]; then
            kill -HUP $(cat /var/run/mysqld/mysqld.pid)
        fi
    endscript
}

Custom Application Logs (Generic Template)

For any application that writes to log files:

sudo nano /etc/logrotate.d/myapp
/var/log/myapp/*.log {
    daily
    rotate 14
    missingok
    notifempty
    compress
    delaycompress
    copytruncate
    maxsize 100M
    create 0644 appuser appuser
}

Testing Logrotate Configurations

Always test your configuration before waiting for the daily run:

# Dry run — shows what would happen without making changes
sudo logrotate -d /etc/logrotate.d/nginx

# Force rotation (actually rotates, even if criteria not met)
sudo logrotate -f /etc/logrotate.d/nginx

# Verbose output
sudo logrotate -v /etc/logrotate.conf

Check for configuration errors across all configs:

sudo logrotate -d /etc/logrotate.conf 2>&1 | grep error

Log Retention Policies for Compliance

Different regulatory frameworks have specific log retention requirements. Here's how to align your logrotate and journald settings with common compliance standards.

Standard Minimum Retention Recommended journald Setting Recommended logrotate Setting
PCI DSS 1 year MaxRetentionSec=365day rotate 365 (daily)
HIPAA 6 years Use external storage Forward to archival system
SOC 2 1 year MaxRetentionSec=365day rotate 365 (daily)
GDPR Varies (minimize) MaxRetentionSec=90day rotate 90 (daily)
Internal audit 90 days typical MaxRetentionSec=90day rotate 90 (daily)

For retention beyond what local disk can hold, you must forward logs to external storage. We cover this in the log forwarding section below.

Important: If compliance requires long retention periods, storing a full year of logs locally on a VPS is rarely practical. Forward logs to a centralized logging service or cloud storage for archival, and keep only recent logs (30-90 days) on the VPS for operational use.

Disk Space Management for Logs

Even with logrotate and journald limits configured, you should proactively monitor log disk usage. Here are practical techniques.

Finding Large Log Files

# Top 20 largest files in /var/log
sudo du -ah /var/log/ | sort -rh | head -20

# Find log files larger than 100MB anywhere on the system
sudo find /var/log -name "*.log" -size +100M -exec ls -lh {} \;

# Find uncompressed rotated logs (should have been compressed)
sudo find /var/log -name "*.log.[0-9]" -not -name "*.gz"

Cleaning Up Immediately

If your disk is critically low, here are safe emergency cleanup commands:

# Vacuum the journal to 100M immediately
sudo journalctl --vacuum-size=100M

# Truncate a large log file without removing it
# (safer than deleting — services may still have the file open)
sudo truncate -s 0 /var/log/syslog

# Remove old compressed log files
sudo find /var/log -name "*.gz" -mtime +30 -delete

# Force logrotate to run now
sudo logrotate -f /etc/logrotate.conf

Automated Disk Space Monitoring

Create a simple script that alerts you when log disk usage exceeds a threshold. Save it as /usr/local/bin/check-log-disk.sh:

#!/bin/bash
# Alert if /var/log exceeds the specified size in MB

THRESHOLD_MB=500
LOG_DIR="/var/log"
CURRENT_MB=$(du -sm "$LOG_DIR" | awk '{print $1}')

if [ "$CURRENT_MB" -gt "$THRESHOLD_MB" ]; then
    echo "WARNING: $LOG_DIR is using ${CURRENT_MB}MB (threshold: ${THRESHOLD_MB}MB)" | \
        mail -s "Log disk warning on $(hostname)" admin@yourdomain.com
fi
sudo chmod +x /usr/local/bin/check-log-disk.sh

Schedule it to run daily via cron:

sudo crontab -e

Add:

0 6 * * * /usr/local/bin/check-log-disk.sh

For a more comprehensive monitoring setup, see our Ubuntu VPS monitoring guide which covers system metrics, alerting dashboards, and integration with tools like Prometheus and Grafana.

Forwarding Logs to External Services

For long-term retention, centralized analysis, and multi-server correlation, forward your logs to an external logging service. Here are the most common approaches.

Option 1: rsyslog Remote Forwarding

rsyslog can forward logs to any syslog-compatible receiver. Edit the rsyslog configuration:

sudo nano /etc/rsyslog.d/50-remote.conf
# Forward all logs to remote syslog server via TCP
*.* @@logserver.yourdomain.com:514

# Or via UDP (less reliable but lower overhead)
# *.* @logserver.yourdomain.com:514

# Forward only auth logs
# auth,authpriv.* @@logserver.yourdomain.com:514

# Use a disk queue to prevent log loss if remote is down
$ActionQueueType LinkedList
$ActionQueueFileName remote-fwd
$ActionResumeRetryCount -1
$ActionQueueSaveOnShutdown on

Restart rsyslog:

sudo systemctl restart rsyslog

Option 2: Forwarding to a Logging Platform with Vector

Vector is a lightweight, high-performance log forwarder that can read from journald, transform logs, and send them to dozens of destinations (Elasticsearch, Loki, Datadog, S3, etc.).

Install Vector:

curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev | bash

Create a configuration file at /etc/vector/vector.yaml:

sources:
  journald:
    type: journald
    current_boot_only: false

  nginx_logs:
    type: file
    include:
      - /var/log/nginx/access.log
      - /var/log/nginx/error.log

transforms:
  parse_nginx:
    type: remap
    inputs:
      - nginx_logs
    source: |
      . = parse_nginx_log!(.message, "combined")

sinks:
  # Example: Send to Grafana Loki
  loki:
    type: loki
    inputs:
      - journald
      - parse_nginx
    endpoint: http://loki.yourdomain.com:3100
    labels:
      host: "{{ host }}"
      source: "{{ source_type }}"

  # Example: Send to S3 for archival
  s3_archive:
    type: aws_s3
    inputs:
      - journald
    bucket: your-log-bucket
    region: us-east-1
    key_prefix: "logs/{{ host }}/%Y/%m/%d/"
    compression: gzip
    encoding:
      codec: json

Enable and start Vector:

sudo systemctl enable vector
sudo systemctl start vector
sudo systemctl status vector

Option 3: journald Remote Forwarding with systemd-journal-remote

systemd includes native journal forwarding. On the receiving server:

sudo apt install -y systemd-journal-remote
sudo systemctl enable --now systemd-journal-remote.socket

On the sending VPS:

sudo apt install -y systemd-journal-remote

Configure the upload target:

sudo nano /etc/systemd/journal-upload.conf
[Upload]
URL=http://logserver.yourdomain.com:19532
sudo systemctl enable --now systemd-journal-upload

Advanced: Structured Logging for Applications

Plain-text log lines like Error processing request from 192.168.1.50 are difficult to search and aggregate. Structured logging outputs JSON, making logs machine-parseable.

Node.js with Pino

const pino = require('pino');
const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  timestamp: pino.stdTimeFunctions.isoTime,
});

// Structured log entry
logger.info({ userId: 1234, action: 'login', ip: '192.168.1.50' }, 'User logged in');

// Output:
// {"level":30,"time":"2026-02-27T10:30:00.000Z","userId":1234,"action":"login","ip":"192.168.1.50","msg":"User logged in"}

Python with structlog

import structlog

structlog.configure(
    processors=[
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.JSONRenderer()
    ]
)

logger = structlog.get_logger()
logger.info("user_login", user_id=1234, ip="192.168.1.50")

# Output:
# {"event": "user_login", "user_id": 1234, "ip": "192.168.1.50", "timestamp": "2026-02-27T10:30:00Z"}

When your application outputs JSON logs to stdout and runs as a systemd service, journald captures the raw JSON. You can then use Vector or Fluentd to parse and route these structured entries to your logging platform.

Practical Logging Recipes

Monitor SSH Login Attempts

# Failed SSH logins in the last 24 hours
journalctl -u ssh --since "24 hours ago" | grep "Failed password"

# Count failed logins by IP
journalctl -u ssh --since "24 hours ago" | grep "Failed password" | \
    awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -10

Track Service Restarts

# Find all service start/stop events today
journalctl --since today | grep -E "(Started|Stopped|Starting|Stopping)"

# Check how many times a service restarted
journalctl -u nginx --since "7 days ago" | grep "Started" | wc -l

Find Disk I/O Errors

journalctl -k -p err --since "7 days ago" | grep -i "i/o\|disk\|ext4\|xfs"

Audit sudo Usage

journalctl -u sudo --since "7 days ago"
# Or from auth.log
grep "sudo:" /var/log/auth.log | tail -20

Export Logs for Analysis

# Export the last 24 hours of logs as JSON
journalctl --since "24 hours ago" -o json > /tmp/logs-export.json

# Export specific service logs as plain text
journalctl -u nginx --since "7 days ago" --no-pager > /tmp/nginx-logs.txt

Putting It All Together: Complete VPS Log Management Setup

Here's a summary checklist of everything we configured:

# 1. Configure journald
sudo nano /etc/systemd/journald.conf
# Set: Storage=persistent, SystemMaxUse=200M, MaxRetentionSec=30day, Compress=yes
sudo systemctl restart systemd-journald

# 2. Verify logrotate is running on a timer
systemctl status logrotate.timer

# 3. Create custom logrotate configs for your applications
sudo nano /etc/logrotate.d/myapp
# Test: sudo logrotate -d /etc/logrotate.d/myapp

# 4. Set up disk monitoring
sudo nano /usr/local/bin/check-log-disk.sh
sudo chmod +x /usr/local/bin/check-log-disk.sh
sudo crontab -e  # Add daily check

# 5. (Optional) Configure log forwarding
sudo nano /etc/rsyslog.d/50-remote.conf
# Or install Vector for more flexibility

# 6. Verify everything
journalctl --disk-usage
sudo du -sh /var/log/
df -h /

Running multiple services that produce heavy log volume? If your VPS plan is running low on storage, you can independently scale storage on a MassiveGRID Cloud VPS without upgrading CPU or RAM. For production environments with strict compliance requirements, a Dedicated VPS (VDS) provides guaranteed I/O performance so log writes never compete with other tenants for disk throughput.

What's Next