How to view logs for a specific systemd service

Introduction

journalctl -u nginx prints nothing? The unit name is usually wrong.

That is the most common reason, and it is not the only one. This guide covers reading one service’s logs with journalctl, the flags worth knowing, the five things that make the output empty, and what to do when one service on one host stops being enough.

The command

journalctl -u nginx

-u takes a unit name. The .service suffix is optional, so -u nginx and -u nginx.service are the same command.

Get the unit name right

Guessing the name is where most of the wasted time goes. Ask systemd instead:

systemctl list-units --type=service            # running services
systemctl list-units --type=service --all      # including stopped ones
systemctl list-unit-files --type=service       # everything installed

The names surprise people. Cron is cron on Debian and Ubuntu and crond on RHEL and Fedora. PHP-FPM carries its version: php8.2-fpm.service. Docker is docker.service and its socket is a separate unit.

A glob saves you from remembering:

journalctl -u 'php*'

Several units at once interleave in time order, which is what you want when a request crossed both of them:

journalctl -u nginx -u php8.2-fpm

Follow it live

journalctl -u nginx -f              # new lines as they arrive
journalctl -u nginx -n 100 -f       # the last 100, then follow

Ctrl-C stops it.

Narrow by time

journalctl -u nginx --since "1 hour ago"
journalctl -u nginx --since today
journalctl -u nginx --since "2026-08-01 09:00" --until "2026-08-01 10:00"

Narrow by severity

journalctl -u nginx -p err

-p is a ceiling, not an exact match. -p err gives you err, crit, alert and emerg. The numbers behind the names:

Level Name
0 emerg
1 alert
2 crit
3 err
4 warning
5 notice
6 info
7 debug

Search the message text

journalctl -u nginx -g timeout

-g is case-insensitive while your pattern is all lowercase. Add one capital letter and it becomes case-sensitive.

Five reasons it prints nothing

The unit name is wrong. Check it with systemctl list-units as above. A name journald has never seen produces no error, just no output.

The service does not log to the journal. Nginx writes its access log to /var/log/nginx/access.log by default, not to journald. The unit’s journal then holds only start, stop and crash messages. Point the service at stdout, or read its file.

You are not allowed to see it. An unprivileged user sees only their own messages. Use sudo journalctl -u nginx, or add yourself to the systemd-journal group.

It is a user unit. Services started by systemctl --user need journalctl --user -u myapp.

The journal was not persistent. If /var/log/journal does not exist, systemd keeps the journal in /run/log/journal and the whole thing is erased on reboot. Create the directory, or set Storage=persistent in /etc/systemd/journald.conf and restart systemd-journald.

Cron jobs are a special case

journalctl -u cron shows that cron started your job. It does not show what the job printed. Cron mails that to the user, and on a server with no mail transfer agent installed it goes nowhere at all.

That is a separate problem with a separate fix — see monitoring cron jobs.

One host at a time

Every flag above is scoped to the machine you are typing on.

Three app servers behind a load balancer means three SSH sessions and three journalctl -u myapp -f windows, and you still have to work out which one took the request. -f across all of them at once is not something journalctl does. Neither is a query over last month, once the journal has been vacuumed.

The same filters, across every host, in one query

Point the CL Agent at a Central Logging instance and every journal entry arrives as the same JSON object journalctl prints. The unit is in _SYSTEMD_UNIT, so -u nginx becomes a WHERE clause:

SELECT datetime(CAST(json_extract(msg, '$.__REALTIME_TIMESTAMP') AS INTEGER) / 1000000, 'unixepoch') AS at,
       json_extract(msg, '$._HOSTNAME') AS host,
       json_extract(msg, '$.MESSAGE')   AS message
FROM logs
WHERE json_valid(msg)
  AND json_extract(msg, '$._SYSTEMD_UNIT') = 'nginx.service'
ORDER BY at DESC
LIMIT 100;

That is journalctl -u nginx, over every host running the agent, with the hostname in a column so you can tell them apart.

The rest of the flags translate the same way:

journalctl SQL
-u nginx json_extract(msg, '$._SYSTEMD_UNIT') = 'nginx.service'
-p err CAST(json_extract(msg, '$.PRIORITY') AS INTEGER) <= 3
-g timeout json_extract(msg, '$.MESSAGE') LIKE '%timeout%'
--since "1 hour ago" CAST(json_extract(msg, '$.__REALTIME_TIMESTAMP') AS INTEGER) / 1000000 >= CAST(strftime('%s', 'now', '-1 hour') AS INTEGER)
one host json_extract(msg, '$._HOSTNAME') = 'web-01'

Three details make the difference between these working and not.

PRIORITY arrives as a string, because journalctl’s JSON output quotes every field. Compare it without the CAST and SQLite compares text to a number, which is never true.

strftime returns text too. Cast both sides or the comparison silently matches nothing.

Keep json_valid(msg) in front. json_extract raises an error on the first line that is not JSON, and it protects only the terms that come after it in the WHERE clause.

An error-level query across the fleet, put together:

SELECT datetime(CAST(json_extract(msg, '$.__REALTIME_TIMESTAMP') AS INTEGER) / 1000000, 'unixepoch') AS at,
       json_extract(msg, '$._HOSTNAME')     AS host,
       json_extract(msg, '$._SYSTEMD_UNIT') AS unit,
       json_extract(msg, '$.MESSAGE')       AS message
FROM logs
WHERE json_valid(msg)
  AND CAST(json_extract(msg, '$.PRIORITY') AS INTEGER) <= 3
  AND CAST(json_extract(msg, '$.__REALTIME_TIMESTAMP') AS INTEGER) / 1000000
      >= CAST(strftime('%s', 'now', '-1 hour') AS INTEGER)
ORDER BY at DESC;

Save that as an alert rule and the fleet tells you about its own errors.

When journalctl is enough

If you run one server, journalctl -u is the right tool and a log server is overhead you do not need. The flags on this page cover it.

The case for shipping the logs starts at the second host, and gets stronger with retention: journald vacuums old entries away, and the incident you want to read about is often older than what is left.

Where to go next

💌 Get notified on new features and updates

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