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:
- An Ubuntu 24.04 VPS. A Cloud VPS with 1 vCPU / 1 GB RAM is sufficient for most logging setups. If you're running multiple services that produce heavy log volume, consider a plan with more storage.
- Root or sudo access. If you haven't configured your server yet, follow our Ubuntu VPS setup guide and security hardening guide first.
- Basic familiarity with the command line. You should be comfortable editing configuration files and restarting services.
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:
- Binary storage format — you must use
journalctlto read it - Structured metadata — each log entry includes fields like unit name, PID, priority, timestamp
- Automatic rate limiting — prevents log floods from crashing the system
- Built-in rotation and size management
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:
- Plain-text log files — easy to read with standard tools (
cat,grep,tail) - Facility/priority-based routing — different log sources go to different files
- Network forwarding — can send logs to remote syslog servers
- Requires external rotation via
logrotate
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:
- If the file meets rotation criteria (size, age, or schedule), rename it (e.g.,
syslogbecomessyslog.1) - Create a new empty log file with correct permissions
- Compress the rotated file (if configured)
- Delete old rotated files beyond the retention count
- 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
- Ubuntu VPS monitoring setup — system metrics, Prometheus, Grafana dashboards, and alerting
- Security hardening guide — firewall rules, SSH hardening, fail2ban (which relies heavily on log analysis)
- Automatic backups guide — back up your log archives along with your application data
- Performance optimization guide — tune I/O scheduling and filesystem settings that affect log write performance
- LEMP stack guide — includes Nginx log configuration that pairs with the logrotate rules above