Run PHP in Production with systemd

Khimananda Oli 7 min read Programming and Languages
Run PHP in Production with systemd

By Khimananda Oli | Last reviewed: August 2026

Most teams still treat PHP-FPM as a standalone daemon managed by legacy init scripts, missing the reliability and observability gains of modern Linux. When you run PHP in production with systemd, you gain automatic restarts, precise resource control via cgroups v2, and unified logging through journald. This guide replaces outdated SysVinit habits with a production-grade systemd configuration that integrates directly with your existing server monitoring stack.

Nginxphp-fpm.socket(systemd unit)php-fpm.service(Type=notify)journald + cgroupsHTTP RequestSocket ActivationOn-Demand Start
Systemd socket activation decouples Nginx from php-fpm lifecycle, enabling zero-downtime reloads and on-demand worker startup when you run PHP in production with systemd.

How do you configure php-fpm socket activation with systemd?

Socket activation is the single most impactful change when you run PHP in production with systemd. Instead of php-fpm binding its own socket at startup, systemd creates and owns the socket file, passing the open file descriptor to php-fpm only when needed. This eliminates race conditions during restarts and allows Nginx to queue requests briefly while workers spin up.

Create the socket unit file

Create /etc/systemd/system/php8.3-fpm.socket. The socket path must exactly match what Nginx expects in its fastcgi_pass directive. Set permissions explicitly so the www-data group can access it without world-readable bits.

[Unit]
Description=PHP 8.3 FPM Socket

[Socket]
ListenStream=/run/php/php8.3-fpm.sock
SocketUser=www-data
SocketGroup=www-data
SocketMode=0660
DirectoryMode=0755

[Install]
WantedBy=sockets.target

Adjust php-fpm pool configuration

In /etc/php/8.3/fpm/pool.d/www.conf, comment out or remove any existing listen = directive. Systemd injects the socket automatically when the service starts. Keep listen.owner and listen.group commented as well; the socket unit now owns those attributes. This prevents conflicting ownership that causes "permission denied" errors during deployment.

Enable and verify socket activation

  1. Reload systemd: sudo systemctl daemon-reload
  2. Enable the socket: sudo systemctl enable --now php8.3-fpm.socket
  3. Verify ownership: ls -la /run/php/php8.3-fpm.sock should show www-data:www-data with mode srw-rw----
  4. Test activation: sudo systemctl status php8.3-fpm.service should show inactive until a request arrives

A common mistake is leaving the old php8.3-fpm.service enabled without the socket unit. Disable the standalone service first with sudo systemctl disable php8.3-fpm.service to prevent both units fighting over the same socket path.

What systemd unit settings harden php-fpm for production?

The default php-fpm service unit shipped by Ubuntu or Debian prioritizes compatibility over security. When you run PHP in production with systemd, override these defaults in a drop-in file at /etc/systemd/system/php8.3-fpm.service.d/override.conf to avoid losing changes during package upgrades.

[Service]
Type=notify
NotifyAccess=all
ExecStart=
ExecStart=/usr/sbin/php-fpm8.3 --nodaemonize --fpm-config /etc/php/8.3/fpm/php-fpm.conf
Restart=on-failure
RestartSec=3s
TimeoutStopSec=30s

# Resource isolation
MemoryMax=2G
CPUQuota=80%
TasksMax=512

# Security hardening
ProtectSystem=strict
ReadWritePaths=/var/log/php /tmp /run/php
ProtectHome=yes
NoNewPrivileges=yes
PrivateTmp=yes
RestrictSUIDSGID=yes
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources

Type=notify is critical here. PHP-FPM supports sd_notify natively since version 7.3, signaling readiness only after workers are forked and listening. Without this, systemd marks the service active before it can actually handle requests, causing health checks to pass prematurely during deploys. Pair this with application-level health endpoints for complete readiness verification.

The MemoryMax directive uses cgroups v2 to enforce a hard ceiling. Unlike memory_limit in php.ini, which applies per-request and can be bypassed by extensions or subprocesses, systemd's limit covers the entire process tree including OPcache shared memory. Set this based on your worker count: if you run 20 workers with 80MB each plus 200MB overhead, target ~1.8G. Monitor actual usage with systemd-cgtop before tightening.

systemd managerExecStartphp-fpm masterfork workersWorkers Readysd_notify(READY=1)Active (ready)watchdog pingFailure / TimeoutRestart=on-failureAuto-Restart← No traffic accepted← Traffic now accepted← Within RestartSec=3s
The Type=notify handshake ensures systemd only routes traffic after php-fpm signals readiness, preventing failed requests during cold starts or restarts.

How does systemd improve php-fpm logging and observability?

Legacy php-fpm setups scatter logs across /var/log/php*-fpm.log, pool-specific error logs, and slowlog files. When you run PHP in production with systemd, all output flows through journald with structured metadata. This unifies debugging with the rest of your infrastructure and enables log aggregation via tools like Graylog or Loki without custom parsers.

Redirect php-fpm logs to journald

In your pool configuration (www.conf), set:

error_log = syslog
syslog.facility = daemon
syslog.ident = php-fpm
catch_workers_output = yes
decorate_workers_output = no

The decorate_workers_output = no setting is essential. By default, php-fpm prepends "[pool-name pid]" to every log line, which breaks structured parsing. Let journald handle process metadata instead. Query logs with:

  • All php-fpm output: journalctl -u php8.3-fpm.service --since "1 hour ago"
  • Errors only: journalctl -u php8.3-fpm.service -p err..emerg
  • Specific worker crashes: journalctl -u php8.3-fpm.service SYSLOG_IDENTIFIER=php-fpm | grep "segfault\|fatal"

Add structured context for production debugging

Journald supports custom fields that survive aggregation. Create a wrapper script or use EnvironmentFile to inject deployment metadata:

[Service]
Environment=DEPLOY_VERSION=2026.08.19-a3f2c1d
Environment=ENVIRONMENT=production
Environment=HOST_ROLE=web-tier

These appear as DEPLOY_VERSION=, ENVIRONMENT= in journal entries, letting you correlate errors with specific releases without parsing log messages. Combine this with structured logging patterns in your application code for end-to-end traceability.

How does systemd compare to supervisord and Docker for PHP process management?

Teams often ask whether they should use Supervisord, Docker, or bare systemd when they run PHP in production with systemd. Each has trade-offs rooted in operational complexity, not just features.

Criteriasystemd (native)SupervisordDocker / Container
Process isolationcgroups v2, namespacesNone (shared PID namespace)Full container isolation
Socket activationNative .socket unitsNot supportedRequires proxy or host networking
Log integrationjournald (structured)Flat files, custom tailContainer runtime logs + driver
Resource limitsMemoryMax, CPUQuota, TasksMaxBasic ulimit onlycgroups via runtime
Restart policyon-failure, watchdog, rate-limitautorestart=true (simple)restart policy + orchestrator
Operational overheadZero extra packagesExtra daemon + configImage build + registry + runtime
Best forBare-metal / VM productionLegacy apps, mixed processesMicroservices, CI parity

For traditional LEMP stacks on Ubuntu VPS or EC2 instances, systemd wins on simplicity and integration. Supervisord made sense in 2015 when Upstart lacked dependency management; today it adds a failure domain without adding capability. Containers excel when you need environment parity between dev and prod, but introduce image pipeline complexity that many PHP monoliths don't justify. If you're already running Kubernetes, see Kubespray deployment guide; otherwise, systemd gives you 90% of the operational benefits with none of the container tax.

Isolation Level →Operational Complexity →systemdLow ops, good isolationSupervisordMedium ops, weak isolationDockerHigh ops, full isolationSweet spot for PHP VPS
Systemd occupies the optimal balance for most PHP production workloads: sufficient isolation without container orchestration overhead.

Run PHP in Production with systemd: Next Steps

Migrating to systemd-managed php-fpm takes under an hour but pays dividends in reliability and debuggability for years. Start with socket activation and Type=notify, then layer in resource limits and journal-based logging. Test thoroughly in staging with systemd-analyze verify before deploying. If your team needs help auditing existing PHP deployments or building compliant infrastructure for SOC 2 or ISO 27001, reach out for a consultation.

Frequently Asked Questions

Systemd is built into modern Linux distributions, eliminating extra dependencies. It offers superior resource control via cgroups, integrated journal logging, and automatic restart policies without requiring separate daemon management overhead in 2026 production environments.

Create a unit file at /etc/systemd/system/laravel-worker.service specifying User, WorkingDirectory, and ExecStart pointing to php artisan queue:work. Set Restart=always and define EnvironmentFile for secrets. Run systemctl daemon-reload then enable and start the service.

No. Systemd manages the master PHP-FPM process only. Pool configuration remains in /etc/php/8.4/fpm/pool.d/ files. Systemd handles service lifecycle, restarts, and resource limits while FPM internally manages worker processes and pool settings.

Use Restart=on-failure with RestartSec=5 for queue workers. This prevents rapid restart loops during deployment or database outages while ensuring crashed workers recover automatically. Avoid Restart=always unless you need unconditional resurrection regardless of exit code.

Store credentials in /etc/systemd/system/laravel-worker.service.d/env.conf with restrictive 0600 permissions. Reference it using EnvironmentFile directive. Never embed secrets directly in unit files since they are world-readable by default through systemctl show commands.

Yes. Set TimeoutStopSec=30 and send SIGTERM via KillSignal=SIGTERM. Laravel queue workers finish current jobs before exiting on SIGTERM. Configure ExecStopPre hooks if additional cleanup is needed before the main process terminates gracefully.

Add MemoryMax=512M to the Service section. Systemd kills the process if it exceeds this limit, preventing runaway workers from crashing the server. Combine with MemoryHigh=400M for soft throttling before hitting the hard ceiling.

Journalctl captures stdout and stderr automatically. Query with journalctl -u laravel-worker.service --since today. Configure StandardOutput=journal and StandardError=journal explicitly. Avoid file-based logging duplication since journald provides structured, searchable, rotated logs natively.

Yes. Use template units named [email protected] and instantiate with systemctl enable --now laravel-worker@{1..4}. Each instance runs independently with shared configuration. Reference %i in ExecStart to pass instance identifiers to your PHP application.

Run systemctl reload-or-restart laravel-worker.service after deploying. Define ExecReload=/bin/kill -HUP $MAINPID in your unit file. Laravel workers will finish current jobs then restart cleanly without dropping messages or causing duplicate processing during deployments.

Generally no. Systemd excels at long-running daemons and workers. For cron-like tasks, use systemd timers instead of services. Timers provide better logging, dependency management, and randomized delays compared to traditional crontab entries for scheduled PHP execution.

Check systemctl status for exit codes and recent logs. Use journalctl -xeu laravel-worker.service for full context. Test ExecStart manually as the specified user. Validate syntax with systemd-analyze verify before reloading to catch configuration errors early.

Yes. PrivateTmp=true isolates /tmp and /var/tmp per service, preventing cross-service data leakage and tmp-based attacks. This hardens PHP workers without affecting application functionality since most frameworks use configurable temp paths anyway.

Use www-data or a dedicated app user, never root. Set User=www-data and Group=www-data in the Service section. Ensure file ownership matches. Running as root creates unnecessary privilege escalation risks if workers are compromised.

Systemd manages processes on bare metal or VMs with lower overhead. Docker adds containerization isolation but requires orchestration tooling. Choose systemd for simpler single-server deployments in 2026; use containers when you need reproducible environments across multiple hosts.