nginx logrotate Troubleshooting
Why did this take so long...
Overview
During work, I needed to manage nginx access logs by date. I thought it would be easy, but there was a lot of troubleshooting. I'll introduce the troubleshooting process and solution.
Busy people, just look at the conclusion.
Environment
- Azure Managed Kubernetes
- PV mounted with nfs
- /var/log/nginx mounted
- nginx: 1.27.1
First Attempt (Failed)
Looking on the internet, there were many examples of using the logrotate package on Linux.
The gist is as follows:
-
logrotate is simply a package that renames files. It has no scheduling function, so install cron to set logrotate to run every day.
-
When cron runs logrotate, the access.log file is renamed to access.log-yyyymmdd.
-
Since logrotate simply renames files, nginx continues writing logs to the renamed file. We want a new access.log to be created, so we send a signal to nginx with the
kill -USR1command to reopen the file.
# dockerfile
FROM nginx:1.27.1
RUN apt-get install -y logrotate cron
COPY ./logrotate.conf /etc/logrotate.conf
RUN sh -c cron && nginx -g 'daemon off'# logrotate.conf
/var/log/nginx/*.log {
daily
rotate 365
dateext
dateyesterday
missingok
compress
delaycompress
ifempty
sharedscripts
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 `cat /var/run/nginx.pid`
fi
endscript
}When implemented this way, there were problems:
- It continued recording logs to access.log-yyyymmdd files. -> nginx doesn't reopen the file... I didn't know why at the time.
- After container restart, logrotate didn't run on the first day.
-> logrotate records the last-rotate time in
/var/lib/logrotate/status, but on the first day (after container restart), the file doesn't exist, so rotation doesn't happen. - I wanted it to run at 00:00, but it executed at 06:25. -> cron.daily's default execution time is 06:25...
Second Attempt (Thought it was solved but failed)
Problem 1 Countermeasure
Unable to identify the cause, I tried using logrotate's copytruncate option so that rotation could happen even if nginx didn't reopen the file. The previous method was to rename the file being written to. copytruncate creates a new file and copies the log content to the new file. Then it deletes that much history from the original file.
Therefore, even without opening a new file, nginx can continue writing to the same file.
Problem 2 Countermeasure
Use logrotate -f option to force rotation.
/usr/sbin/logrotate -v -f /home/logrotate.conf
Problem 3 Countermeasure
Set cron to execute at 0 0 * * *.
Idea
Since using copytruncate doesn't require sending signals to processes, there's no need to rotate in the same container! Since we're using Kubernetes, let's use a cronjob to run logrotate and rotate other nginx apps too! That was the idea.
dockerfile
I wanted logrotate to be executed by cronjob.
FROM alpine:3.14
USER root
RUN apk --update add --no-cache logrotate && \
rm -f /etc/logrotate.d/*
ADD logrotate.conf /etc/logrotate.conf
RUN chmod 0400 /etc/logrotate.conf
CMD ["/usr/sbin/logrotate", "-v", "-f", "--state","/tmp/logrotate.status", "/etc/logrotate.conf"]configmap
apiVersion: v1
kind: ConfigMap
metadata:
name: logrotate-config
namespace: wichan
data:
nginx-logrotate.conf: |
/var/log/app/nginx/*/log/*.log {
daily
rotate 365
dateext
dateyesterday
missingok
compress
delaycompress
ifempty
copytruncate
endscript
}cronjob
apiVersion: batch/v1
kind: CronJob
metadata:
name: job-logrotate
namespace: wichan
spec:
schedule: "0 0 * * *"
timeZone: Asia/Seoul
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
containers:
- name: container
image: my-logrotate:latest
imagePullPolicy: Always
volumeMounts:
- name: logrotate-conf
mountPath: /etc/logrotate.d
- name: nfs
mountPath: /var/log/app
subPath: App
- name: timezone
mountPath: /etc/localtime
imagePullSecrets:
- name: ghcrsecret
volumes:
- name: logrotate-conf
configMap:
name: logrotate-config
- name: nfs
persistentVolumeClaim:
claimName: pvc-wichan
- name: timezone
hostPath:
path: /usr/share/zoneinfo/Asia/Seoul
restartPolicy: NeverWith this, /var/log/app/nginx/*/log rotated well at 00:00 every day, and I thought it was successful.
New Problem
One day, I noticed that the logs were full of null characters like ^@^@^@.
When logrotate cuts up to the nth line while nginx is writing logs, nginx stacks the next log on the n+1th line, and at this time, lines 0 to n are replaced with null characters.
Third Attempt (Solved)
I realized that sending a reopen signal to the nginx process is essential for a fundamental solution.
However, when running with a logrotate container, since we can't access the nginx process, I abandoned the method using cronjob (2nd attempt).
Returning to the 1st attempt, I checked why the nginx process couldn't reopen the file... -> It turned out that due to nfs mount, the nginx process didn't have permission to open the file.
I tried to change the ownership of the nfs mounted directory to nginx, but nfs can't change ownership. So I changed the permission to 777, but an error occurred.
because parent directory has insecure permissions (It's world writable or writable by group which is not "root") Set "su" directive in config file to tell logrotate which user/group should be used for rotation.
If the rotation directory has world-writable permissions, it fails rotation for security reasons.
After countless troubleshooting... I set the nginx process to run with root privileges.
user root; # here
worker_processes 1;
http {
...
}Final Solution
# logrotate.conf
/var/log/nginx/*.log {
daily
rotate 365
dateext
dateyesterday
missingok
compress
delaycompress
ifempty
sharedscripts
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 `cat /var/run/nginx.pid`
fi
endscript
}# nginx.conf
user root; # here
worker_processes 1;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
include /etc/nginx/conf.d/*.conf;
}# Dockerfile
FROM nginx:latest
# Timezone setting
RUN ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
# Package installation
RUN apt-get update
RUN apt-get -y install logrotate cron
# Build file copy
COPY ./build /usr/share/nginx/html
# nginx config copy
RUN rm /etc/nginx/nginx.conf
COPY ./nginx.conf /home/tmp/nginx.conf
RUN tr -d '\r' < /home/tmp/nginx.conf > /etc/nginx/nginx.conf
RUN rm /etc/nginx/conf.d/default.conf
COPY ./default.conf /home/tmp/default.conf
RUN tr -d '\r' < /home/tmp/default.conf > /etc/nginx/conf.d/default.conf
# nginx-logrotate config copy
COPY ./logrotate.conf /home/tmp/logrotate.conf
RUN tr -d '\r' < /home/tmp/logrotate.conf > /home/logrotate.conf
# logrotate cron file creation
RUN echo "0 0 * * * /usr/sbin/logrotate -v -f /home/logrotate.conf" > /etc/cron.d/logrotate-nginx
RUN chmod 0644 /etc/cron.d/logrotate-nginx
# Register cron in crontab
RUN crontab /etc/cron.d/logrotate-nginx
EXPOSE 1020
CMD ["sh", "-c", "cron && nginx -g 'daemon off;'"]The key points are as follows:
- Give nginx root privileges so it can write files to /var/log/nginx
- Send reopen signal to nginx through postrotate script
- Make rotation happen even without logrotate status file (or mount status to manage persistently)
- Set cron to execute at 00:00
Side Note
The requirement was for LogStash to be able to read nginx logs by date. I didn't know that something that seemed like nothing would be this frustrating.
I hope this post helps someone whose name I don't know. Don't troubleshoot, my friend....