Render doesn’t have a “native PHP runtime” like it does for Node/Python, but it does have Docker. So we can use a Docker image that will set up the PHP runtime for us.
To make it happen, we will have to add these two files to our application:
Dockerfilethat imports and uses a prebuilt image from richarvey/nginx-php-fpm- Nginx configuration, for URL rewrites among other things
Dockerfile
Add a file with the name Dockerfile and below contents to your application root.
FROM richarvey/nginx-php-fpm:latest
WORKDIR /var/www/html
# Copy composer.json first to have it on a separate cache layer
COPY composer.json ./
RUN composer install --no-scripts --no-interaction --optimize-autoloader
# Copy the rest of the application
COPY . .
# Ensures writable folders and config/app_local.php before continuing
RUN composer run-script post-install-cmd --no-interaction
# Run the post-update scripts
RUN composer run-script post-update-cmd --no-interaction
# Environment variables for start.sh below
ENV WEBROOT /var/www/html/webroot
ENV REAL_IP_HEADER 1
ENV RUN_SCRIPTS 0
ENV PHP_ERRORS_STDERR 1
# Tell start.sh to skip composer because we handle it ourselves
ENV SKIP_COMPOSER 1
# Source: github.com/richarvey/nginx-php-fpm/blob/main/scripts/start.sh
# To debug: CMD ["/bin/bash", "-x", "/start.sh"]
CMD ["/start.sh"]
We start by copying only composer.json and running composer install right away. This allows Docker to cache the composer layer, and only reinstall the dependencies when they actually change.
We then copy everything else and run post-install-cmd and post-update-cmd hooks in a specific sequence - more on that below.
Next is the main script start.sh, provided by the richarvey/nginx-php-fpm image. We set up the environment variables it expects and finally run it. To see all possible environment variables and better understand what start.sh does, see its source code.
Important: On composer scripts sequence
We have to specifically ensure the order in which the composer scripts are executed. By default, it would attempt to run post-update-cmd without (or in parallel to) post-install-cmd. The issue is, post-install-cmd must come first and set the security salt and filesystem permissions, else we end up with the following errors:
[TypeError] Cake\Utility\Security::setSalt(): Argument #1 ($salt) must be of type string, null given, called in /var/www/html/config/bootstrap.php on line 188 in /var/www/html/vendor/cakephp/cakephp/src/Utility/Security.php on line 304
Script bin/cake plugin assets symlink handling the post-update-cmd event returned with error code 1
...
WARNING: [pool www] child 326 said into stderr: "NOTICE: PHP message: PHP Warning: file_put_contents(/var/www/html/logs/error.log): Failed to open stream: Permission denied in /var/www/html/vendor/cakephp/cakephp/src/Log/Engine/FileLog.php on line 137"
... and so on
Nginx config
The start.sh will check whether conf/nginx/nginx-site.conf exists and if so, use it as the main Nginx config, which is what we need.
The below configuration, among other things, ensures URL rewriting is enabled and points to index.php - so that CakePHP routing works.
# A part of richarvey/nginx-php-fpm setup
# start.sh expects Nginx configuration at conf/nginx/nginx-site.conf
server {
listen 80;
server_name _;
sendfile off;
charset utf-8;
root /var/www/html/webroot;
index index.html index.htm index.php;
access_log /dev/stdout;
error_log /dev/stdout info;
error_page 404 /index.php;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
location / {
try_files $uri $uri/ /index.php?$args;
}
location = /favicon.ico {
access_log off;
log_not_found off;
}
location = /robots.txt {
access_log off;
log_not_found off;
}
location ~* \.(jpg|jpeg|gif|png|css|js|ico|webp|tiff|ttf|svg)$ {
expires 5d;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
include fastcgi_params;
}
# Allow .well-known
location ^~ /.well-known/ {
allow all;
}
# Deny all other dotfiles
location ~ /\.(?!well-known/) {
deny all;
log_not_found off;
}
}