PHP sessions filling up disk

Hello,

we are running a Docker Passbolt CE installation for many years (always updated to latest version), however every few months I need to enter the volume and delete php session files manually in /var/lib/php/sessions, because these fill up the filesystem completely (no more inodes).

The specificity of this instance is that it’s running behind HAProxy. Initially I thought that using / as the healthcheck path was causing PHP to create a session for every hit, so I changed the haproxy healthcheck to /healthcheck/status. That works fine, but sessions are still created and fill up the disk.

I haven’t found any mentions of similar issues but I suppose we shouldn’t be the only company to run Passbolt behind HAProxy… Thanks in advance for any feedback that the community can provide.

HI @tanji,

Iam also facing similar problem like you explained above. In the passbolt app docker container after few hours i see a message as “Cannot make/remove an entry for the specified session” and when i do a simple restart of container the app works again for few more hours.

Do you have any thoughts here ? did you find any resolution for your above query..

G’day Prasad.

The error Cannot make/remove an entry for the specified session comes from PHP’s session save handler when it cannot create a session file. The most common cause is the filesystem running out of inodes or free space, which fits tanji’s original symptom of /var/lib/php/sessions growing without bound, but it can also be a permissions or locking issue depending on your setup. Can you give us a bit of an idea what’s happening on your container?

The Debian PHP package ships a cron job at /etc/cron.d/php that runs /usr/lib/php/sessionclean every 30 minutes and deletes session files older than session.gc_maxlifetime (24 minutes by default). If that cron is healthy and gc_maxlifetime hasn’t been raised, the directory should hold steady rather than grow over weeks or months.

Could you run this inside the passbolt container and share the output?

docker exec <your-passbolt-container> sh -c '
  echo -n "cron running:        "; pgrep -a cron || echo "no cron process"
  echo -n "/etc/cron.d/php:     "; [ -f /etc/cron.d/php ] && echo "present" || echo "missing"
  echo -n "/run/systemd/system: "; [ -d /run/systemd/system ] && echo "present (sessionclean cron skips when this exists)" || echo "absent"
  echo -n "gc_maxlifetime:      "; php -r "echo ini_get(\"session.gc_maxlifetime\");"; echo
  echo -n "save_path:           "; php -r "echo ini_get(\"session.save_path\") ?: \"/var/lib/php/sessions (default)\";"; echo
  echo -n "session count:       "; ls /var/lib/php/sessions/ 2>/dev/null | wc -l
  echo -n "newest session:      "; ls -lt /var/lib/php/sessions/ 2>/dev/null | sed -n "2p"
  echo -n "oldest session:      "; ls -lt /var/lib/php/sessions/ 2>/dev/null | tail -1
  echo -n "free inodes on /:    "; df -i / | tail -1
  echo -n "free space on /:     "; df -h / | tail -1
'

If your sessions live on a separate Docker volume, replace / with the mount point in the last two lines.

For reference, here’s the output from my demo container :

cron running:        1296 cron -f -l
/etc/cron.d/php:     present
/run/systemd/system: absent
gc_maxlifetime:      1440
save_path:           /var/lib/php/sessions
session count:       0
free inodes on /:    overlay        6553600 282619 6270981    5% /
free space on /:     overlay          98G   15G   79G  16% /

The newest/oldest lines are blank above because the directory was empty at the time. On a busy instance you’d see a non-zero count and timestamps within roughly the last gc_maxlifetime window if cleanup is keeping up.

It would also help to see your HAProxy backend config for passbolt, in particular the option httpchk line and the check interval. HAProxy probes do not carry session cookies, so depending on the route they hit they may or may not contribute to the file count, and that’s worth checking against your actual setup rather than guessing.

These are general workarounds that are safe regardless of root cause:

  • Free the directory by hand: docker exec <container> find /var/lib/php/sessions -name 'sess_*' -type f -delete (this only removes session files; active users will be logged out and need to sign in again)
  • If you can tolerate sessions being lost on container restart, mount /var/lib/php/sessions as a tmpfs in your compose file, the directory then lives in RAM and cannot exhaust real inodes
  • Move sessions out of files entirely by setting SESSION_DEFAULTS=database in the passbolt environment (and loading config/schema/sessions.sql into your DB), or to a cache backend like Redis

Happy to give you more specific solutions if you can provide more info.

Chat soon.

Cheers,
Gareth

Hi Gareth,

apologies but your answer is partly misoriented, given that we are in container context.
Passbolt container doesn’t ship with system cron. You’re getting “cron running” because they ship their own cron to process emails, but that is not executing /etc/cron.d/php

Now I can share my fix with others if interested:

Create this script in /usr/local/bin, for example; In my case, we’re assuming that the passbolt docker-compose file is in /opt/passbolt ; feel free to change that to reflect your own install

#!/usr/bin/bash
cd /opt/passbolt
docker-compose exec --user root passbolt /bin/sh -c "
save_path='/var/lib/php/sessions';
gc_maxlifetime=24;
for pid in \$(pidof php-fpm8.4); do
echo \"Touching open session files for php-fpm8.2 process \$pid...\";
find \"/proc/\$pid/fd\" -ignore_readdir_race -lname \"\$save_path/sess_*\" -exec touch -c {} \; 2>/dev/null;
done;
echo \"Deleting all session files older than \$gc_maxlifetime minutes...\";
find -O3 \"\$save_path/\" -ignore_readdir_race -depth -mindepth 1 -name 'sess_*' -type f -cmin \"+\$gc_maxlifetime\" -delete;
"

Run this script in a systemd timer, or in cron if you wish.

Another possibility would be to enable the php cron in the container but this solution would only work between upgrades.

Best

Guillaume

G’day Guillaume,

Yep, spot on.

Your script works. There’s also a one-liner that calls sessionclean directly:

docker compose exec --user root passbolt /usr/lib/php/sessionclean

Same logic, picks up whatever PHP version and gc_maxlifetime are actually configured rather than pinning them (e.g. $(pidof php-fpm8.4)).

To skip the cron problem altogether, move sessions out of files. Two options:

Either way /var/lib/php/sessions stops mattering.

Prasad, you’re hitting the same root cause at a faster rate. Errors every few hours means something is creating sessions much faster than normal user load, like a polling integration or a misconfigured probe. The DB-backed sessions switch above fixes it either way.

Cheers
Gareth