~/blog / new-relic-php-docker.md
Running the New Relic PHP agent in Docker
Application performance monitoring (APM) shows where a PHP application spends its time: slow transactions, database queries, external calls and errors. New Relic is one of the most widely used APM services, and its PHP integration has two parts:
- the agent, a PHP extension that collects data inside every PHP process;
- the daemon, a separate process that receives the data from the agent and forwards it to New Relic.
Outside containers, the agent starts the daemon itself. In Docker it is cleaner to run them separately, one process per container:
docker network "main"
┌──────────┐ ┌───────────────────────┐ ┌─────────────────┐ ┌───────────────┐
│ nginx │──▶│ php-fpm │──▶│ newrelic daemon │──HTTPS─▶│ New Relic │
│ :80 │ │ + newrelic extension │ │ :31339 │ │ collector │
└──────────┘ └───────────────────────┘ └─────────────────┘ └───────────────┘
FastCGI agent collects data batches & sends
The daemon port is only used inside the Docker network and is never exposed to the host.
1. The daemon container
New Relic publishes an official daemon image, so this part is just a service in compose.yaml:
services:
newrelic:
image: newrelic/php-daemon
networks: [main]
It listens on port 31339. The agent will reach it as newrelic:31339.
2. The PHP-FPM image with the agent
Extend the official PHP-FPM image and install the agent from New Relic's release tarball. Pin the agent version as a build argument, so builds are reproducible and upgrades are deliberate. You can find the current version in the release directory.
# docker/php/Dockerfile
FROM php:8.4-fpm
ARG NEWRELIC_AGENT_VERSION=12.12.0.41
RUN curl -fsSL "https://download.newrelic.com/php_agent/release/newrelic-php5-${NEWRELIC_AGENT_VERSION}-linux.tar.gz" \
| tar -C /tmp -xz \
&& NR_INSTALL_USE_CP_NOT_LN=1 NR_INSTALL_SILENT=1 \
/tmp/newrelic-php5-${NEWRELIC_AGENT_VERSION}-linux/newrelic-install install \
&& rm -rf /tmp/newrelic-php5-* /tmp/nrinstall*
COPY newrelic.ini /usr/local/etc/php/conf.d/zz-newrelic.ini
The installer writes a default newrelic.ini. Instead of patching it with sed, add a small override file that is loaded after it:
; docker/php/newrelic.ini
newrelic.license = "${NEW_RELIC_LICENSE_KEY}"
newrelic.appname = "${NEW_RELIC_APP_NAME}"
newrelic.daemon.address = "${NEW_RELIC_DAEMON_ADDRESS}"
; the daemon runs in its own container: never start one locally
newrelic.daemon.dont_launch = 3
; recommended for containers: wait for the daemon on startup
newrelic.daemon.app_connect_timeout = 15s
newrelic.daemon.start_timeout = 5s
PHP expands ${VARIABLE} in INI files from the environment when it starts. The licence key is supplied when the container runs, not when the image is built. Build arguments are stored in the image metadata, so anyone who can pull an image built with --build-arg NEWRELIC_KEY=... can read the key.
newrelic.daemon.dont_launch = 3 is the important setting. It tells the agent never to start its own daemon, because the daemon lives in the other container.
3. Wire it together
# compose.yaml
services:
newrelic:
image: newrelic/php-daemon
networks: [main]
php:
build: ./docker/php
environment:
NEW_RELIC_LICENSE_KEY: ${NEW_RELIC_LICENSE_KEY:?set it in .env}
NEW_RELIC_APP_NAME: "My Application (local)"
NEW_RELIC_DAEMON_ADDRESS: "newrelic:31339"
volumes:
- ./app:/var/www/html
networks: [main]
depends_on: [newrelic]
web:
image: nginx:stable
ports:
- "8080:80"
volumes:
- ./app:/var/www/html:ro
- ./docker/nginx/app.conf:/etc/nginx/conf.d/default.conf:ro
networks: [main]
depends_on: [php]
networks:
main:
Put the key in a .env file next to compose.yaml and keep that file out of Git:
echo 'NEW_RELIC_LICENSE_KEY=your-licence-key' > .env
echo '.env' >> .gitignore
Both php and web mount the application code: nginx serves the static files and PHP-FPM executes the scripts. A minimal nginx configuration for a front-controller application such as Symfony or Laravel:
# docker/nginx/app.conf
server {
listen 80;
root /var/www/html/public;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass php:9000;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
internal;
}
location ~ \.php$ {
return 404;
}
}
Only the front controller is executed. A request for any other .php file returns 404, so a stray script in public/ cannot be run directly.
4. Check that it works
To give the agent something to measure, create a test application, for example composer create-project symfony/skeleton app. Then start the stack:
docker compose up -d --build
docker compose exec php php -m | grep newrelic # the extension is loaded
docker compose exec php php -i | grep newrelic.appname
Open http://localhost:8080 a few times, then check the daemon log:
docker compose logs newrelic
Info: daemon listening on :31339
Info: Reporting to: https://rpm.newrelic.com/accounts/.../applications/...
Info: app 'My Application (local)' connected with run id '...'
After a minute or two, the application appears in New Relic APM with its transactions, response times and database calls.
Production notes
- Use a different
NEW_RELIC_APP_NAMEper environment, so staging traffic does not mix with production data. - One daemon container can serve many PHP-FPM containers. In Kubernetes, run it as a separate deployment with a service, or as a sidecar if you prefer one daemon per pod.
- Upgrade the agent by changing
NEWRELIC_AGENT_VERSIONand rebuilding. Keep the daemon image at a matching version.