Home

Awesome

dirtsimple/php-server

Overview

This is a docker image for an alpine nginx + php-fpm combo container, with support for:

Inspired by richarvey/nginx-php-fpm, this image supports most of that image's configuration flags, plus many, many enhancements and bug fixes like these:

Note: there are a few configuration options that must be specified in a different way than the richarvey image, or which have different defaults: see Backward-Compatibility Settings, below, for more info.

Contents

<!-- toc --> <!-- tocstop -->

Adding Your Code

This image assumes your primary application code will be found in the directory given by CODE_BASE (which defaults to /var/www/html). You can place it there via a volume mount, installation in a derived image, or by specifying a GIT_REPO environment variable targeting your code.

If a GIT_REPO is specified, the given repository will be cloned to the CODE_BASE directory at container startup, unless a .git subdirectory is already present (e.g. in the case of a restart, or a mounted checkout). If GIT_BRANCH is set, the specified branch will be used. You can also supply a base64-encoded SSH_KEY to access protected repositories (including any checkouts done by composer).

(Important: do not both mount your code as a volume and provide a GIT_REPO: your code will be erased unless it's already a git checkout, or you set REMOVE_FILES=false in your environment.)

Whether you're using a GIT_REPO or not, this image checks for the following things in the CODE_BASE directory during startup:

Note: if you are using a framework that exposes a subdirectory (like web or public) as the actual directory to be served by nginx, you must set the PUBLIC_DIR environment variable to that subdirectory (e.g. public). (Assuming you don't override the default web server configuration; see more below.)

Pulling Updates and Pushing Changes

You can pull updates from GIT_REPO to CODE_BASE by running the pull command via docker exec or docker-compose exec, as appropriate. If the pull is successful, the container will immediately shutdown so that it will reflect any changed configuration upon restart. If you're using a docker-compose container with restart: always, the container should automatically restart. Otherwise you will need to explicitly start the container again.

(Note that you must set GIT_NAME to a commiter name, and GIT_EMAIL to a committer email, in order for pull operations to work correctly.)

For compatibility with richarvey/nginx-php-fpm, there is also a push command that adds all non-gitignored files, commits them with a generic message, and pushes them to the origin. (You're probably better off looking at the script as a guide to implementing your own, unless those are your exact requirements.)

Permissions and the developer User

If you use any of the git or composer features of this image, they will be run using a special developer user that's created on-demand. This user is created inside the container, but you can set DEVELOPER_UID and/or DEVELOPER_GID so that the created user will have the right permissions to access or update files mounted from outside the container. The developer user is also added to the nginx group inside the container, so that it can read and write files created by the app. Once the user is created, ownership of the entire CODE_BASE directory tree is changed to developer.

If you need to run tasks inside the container as the developer user, you can use the as-developer script, e.g. as-developer composer install. (The push and pull commands and the container start script already use as-developer internally to run git and composer.)

Composer Configuration, PATH, and Tools

When running a command under as-developer, the PATH is expanded to include $COMPOSER_HOME/vendor/bin and $CODE_BASE/vendor/bin. This allows you to easily run project-specific tools, and also to override them with globally-installed tools using GLOBAL_REQUIRE.

Setting the GLOBAL_REQUIRE environment variable to a series of package specifiers causes them to be installed globally, just after templates are processed and before the project-level composer install. For example setting GLOBAL_REQUIRE to "psy/psysh wp-cli/wp-cli" would install both Psysh and the Wordpress command line tools as part of the container.

(You can also use GLOBAL_REQUIRE as a build-time argument, to specify packages to be built into the container.)

The COMPOSER_OPTIONS variable can also be set to change the command line options used by the default composer install run. It defaults to --no-dev --optimize-autoloader, but can be set to an empty string for a development environment. If you need finer control over the installation process, you can also disable automatic installation by setting SKIP_COMPOSER=true, and then running your own installation scripts with RUN_SCRIPTS (which are run right after the composer-install step.

Last but not least, you can set COMPOSER_CONFIG_JSON to a string of JSON to be placed in $COMPOSER_HOME/config.json. This can be useful for adding site-specific repository or authentication information to a project's composer.json. (New in version 1.2)

Configuration Templating

This image uses dockerize templates to generate arbitrary configuration files from templates. Templates are loaded first from the image's bundled/tpl directory, and then from the subdirectories of CODE_BASE identified by the DOCKERIZE_TEMPLATES environment variable.

If used, DOCKERIZE_TEMPLATES should be a space-separated series of source:destination directory name pairs. For example, if it were set to this:

.tpl:/ .nginx:/etc/nginx .supervisor:/etc/supervisor.d .periodic:/etc/periodic

it would mean that your project could contain a .tpl directory, whose contents would be recursively expanded to the root directory, an .nginx subdirectory expanded into /etc/nginx, and so on. Source and destination can both be absolute or relative; relative paths are interpreted relative to CODE_BASE.

The path of an output configuration file is derived from its relative path. So, for example, the default template for /etc/supervisord.conf can be found in /tpl/etc/supervisord.conf, and with the above DOCKERIZE_TEMPLATES setting it could be overrridden by a template in $CODE_BASE/.tpl/etc/supervisord.conf.

Templates found in the image-supplied /tpl are applied at the very beginning of container startup, before code is cloned or startup scripts are run. Templates in DOCKERIZE_TEMPLATES directories are applied just after the code checkout (if any), and just before composer install (if applicable).

Template files are just plain text, except that they can contain Go template code like {{.Env.DOMAIN}} to insert environment variables. Please see the dockerize documentation and Go Text Template language reference for more details, and this project's tpl subdirectory for examples.

(Note: for improved security, template files are not processed if they are writable by the nginx user. If even one template file is writable by the web server or php-fpm, the container will refuse to start.)

Nginx Configuration

Config Files

This image generates and uses the following configuration files in /etc/nginx, any or all of which can be replaced using mounts or template files:

For backwards compatibility with richarvey/nginx-php-fpm, you can include conf/nginx/nginx.conf, conf/nginx/nginx-site.conf, and/or conf/nginx/nginx-site-ssl.conf file(s) in your CODE_BASE directory. Doing this will, however, disable any features of app.conf that you don't copy into them. It's recommended that you use .nginx/app.conf instead, going forward.

Environment

In addition, the following environment variables control how the above configuration files behave:

Backward-Compatibility Settings

If you want extreme backward compatibility with the default settings of richarvey/nginx-php-fpm, you can use the following settings:

The following features of richarvey/nginx-php-fpm are not directly supported by this image, and must be configured in a different way:

PHP Front Controllers and PATH_INFO

Many PHP frameworks use a central entry point like index.php to process all dynamic paths in the application. If your app is like this, you can set PHP_CONTROLLER to true to get a default front controller of /index.php$is_args$args -- a value that works for correctly a wide variety of PHP applications and frameworks. If your front controller isn't index.php or needs different parameters, you can specify the exact URI to be used instead of true. (If the document root isn't the root of your code, you need to set PUBLIC_DIR as well.)

For example, if you are deploying a Laravel application, you need to set PUBLIC_DIR to public, and PHP_CONTROLLER to true. Then, any URLs that don't resolve to static files in public will be routed through /index.php instead of producing nginx 404 errors.

By default, PATH_INFO is disabled, meaning that you cannot add trailing paths after .php files. If you need this (e.g. for Moodle), you can set USE_PATH_INFO to true, and then you can access urls like /some/file.php/other/stuff. As long as /some/file.php exists, then it will be run with $_SERVER['PATH_INFO'] set to /other/stuff. If you also enable PHP_CONTROLLER, then the default PHP_CONTROLLER will be /index.php$uri?$args, so that the front controller gets PATH_INFO set as well. (You can override this by explicitly setting PHP_CONTROLLER to the exact expression desired.)

File Permissions

For security, you must specifically make files readable or writable by nginx and php, using the NGINX_READABLE and NGINX_WRITABLE variables. Each is a space-separated lists of file or directory globs (with ** recursion supported) which will be recursively chgrp'd to nginx and made group-readable or group-writable, respectively. Paths are interpreted relative to CODE_BASE, and the default NGINX_READABLE is ., meaning the entire code base is readable by default. If you are using a web framework that writes to the code base, you must add the affected directories and/or files to NGINX_WRITABLE. For frameworks like wordpress that require certain files to be owned by the web server, you can use NGINX_OWNED.

In some cases, it may be easier to specify what should not be readable or writable, or to carve out specific exceptions in a larger grant of access. For this purpose, you can remove group+world read/write/execute access via NGINX_NO_RWX, and group+world write access viaNGINX_NO_WRITE. These settings are applied after the readable/writable/owned ones, and so can carve out exceptions within them.

Both variables can be used to list file or directory paths that will be recursively chown'd to developer (creating the developer user if necessary), and then have the relevant permissions revoked. For simplicity's sake, existing file groups are not checked, so if you are sharing files outside the container this may not do what you want or expect.

Note: file permissions are applied prior to processing template files and running startup scripts, so if you make your entire codebase writable you will not be able to use configuration templates or startup scripts. To preserve separation of privileges within the container, you will need to explicitly list subdirectories of your code that do not include your templates or startup scripts, or else list those templates and startup scripts under NGINX_NO_WRITE.

In addition, please note that these permission changes are applied only to files and directories that actually exist: files created by startup scripts or nginx/php later will not magically have these permissions applied. To get the results you want in such cases, you may need to apply permissions to a parent directory, or pre-create the necessary files or directories yourself.

HTTPS and Let's Encrypt Support

HTTPS is automatically enabled if you set a DOMAIN and there's a private key in /etc/letsencrypt/live/$DOMAIN/.

If you want the key and certificate to be automatically generated, just set LETS_ENCRYPT to your desired registration email address, and certbot will automatically run at container start, either to perform the initial registration or renew the existing certificate. (You may want to make /etc/letsencrpt a named or local volume in order to ensure the certificate persists across container rebuilds.)

If your container isn't restarted often enough to ensure timely certificate renewals, you can set USE_CRON=true, and an automatic renewal attempt will also happen on the first of each month at approximately 5am UTC.

(Note: certbot uses the "webroot" method of authentication, so the document root of DOMAIN must be the server's default document root (i.e. $CODE_BASE/$PUBLIC_DIR), or else certificate authentication will fail. Once a certificate has been requested, the default document root directory must remain the same for all future renewals. Also note that the .well-known directory under the webroot should not be made inaccessible to the webserver; i.e., it needs to be NGINX_READABLE, at least.)

Adding Extensions

Additional alpine APKs, PHP core extensions, and pecl extensions can be installed using the EXTRA_APKS, EXTRA_EXTS, and EXTRA_PECL build-time arguments, respectively. For example, one might use this in a docker-compose.yml to build a server for Moodle:

version: '2'

services:
  moodle:
    build:
      context: https://github.com/dirtsimple/php-server.git
      args:
        - PHP_VER=7.2  # build from php:7.2-fpm-alpine3.10
        - OS_VER=3.10
        - EXTRA_APKS=ghostscript graphviz
        - EXTRA_EXTS=xmlrpc pspell ldap memcached
    environment:
      - GIT_REPO=https://github.com/moodle/moodle.git
      - GIT_BRANCH=MOODLE_33_STABLE
      - NGINX_WRITABLE=/moodledata
      - USE_PATH_INFO=true

For performance's sake, it's generally better to specify extras at build-time, but as a development convenience you can also pass them to the container as environment variables to be installed or built during container startup. (Which, of course, will be slower as a result.)

Specific versions of a PECL module can be forced by using a :, e.g. EXTRA_PECL=mcrypt:1.0.2. (A : is used in place of a - so that the version can be stripped from the extension name in generated PHP .ini file(s).)

As of the 2.x versions of this image, EXTRA_EXTS are built using mlocati/docker-php-extension-installer, so you no longer need to specify EXTRA_APKS for any of the extensions it supports, and you can build supported PECL extensions using EXTRA_EXTS, without needing EXTRA_PECL, unless you need to specify a particular module version, or need an extension that isn't supported by docker-php-extension-installer. (But in such cases, you must explicitly list any needed packages in EXTRA_APKS, since the automatic installer won't be handling it for you.)

Supervised Tasks

Any files named /etc/supervisor.d/*.ini are included as part of the supervisord configuration, so that you can add your own supervised tasks. (For example, if you wanted to add a mysql or openssh server.) This image's own tasks are there as well, and can be overridden by your own substitutions in /tpl/etc/supervisor.d or a DOCKERIZE_TEMPLATES directory:

You can override any of these with an empty file to disable the relevant functionality.

Scheduled Jobs (cron)

If you want to add cron jobs, you have two options:

Cron jobs listed in the /etc/crontabs/nginx file will run as the nginx user; scripts in /etc/periodic/ dirs run as root. You can preface commands in those scripts with as-nginx to run them as the nginx user, or as-developer to run them with developer privileges. (Note: the templates for scripts to be placed in /etc/periodic must have their executable bit set in order to run!)

As always, these configuration files can be generated by mounting templates in /tpl or via a DOCKERIZE_TEMPLATES directory inside your codebase.

Changed-File Jobs (modd)

The bundled modd tool lets you watch for changes to files, then run other tasks automatically (such as stopping, starting, or restarting other supervised processes). To enable this, just set MODD_CONF to the path of a modd config file, and a supervised modd process will be started automatically when the container starts. If the config file changes, modd will restart itself automatically to use the updated configuration.

You can also use MODD_OPTIONS to supply extra global options to modd, and MODD_DIR to set the directory where you want it to run, if it's not CODE_BASE. (If MODD_CONF is a relative path, it's relative to MODD_DIR or CODE_BASE.)

Please note that the modd process runs as root, which means that your config file must not be writable by nginx (or else the container will not start). This also means you should preface most commands in your modd config file with as-developer or as-nginx to set the appropriate user ID for the task in question. But if you need a modd job to stop or start other tasks, you can have it simply invoke supervisorctl with the appropriate options.

Webhooks (adnanh/webhook)

The bundled webhook tool optionally lets you run commands in the container upon receiving webhooks. These variables control where, how, and whether the webhooks are served:

The webhook tool is proxied by nginx, so its port does not need to be directly exposed, and so it listens only on 127.0.0.1:9000. If you have something else you need to run on port 9000 (and you also want to run webhooks), you can change the default port with WEBHOOK_PORT.

Security Considerations

To prevent privilege escalation from web-served PHP, the webhook tool's configuration file must not be writable by nginx (or else the container won't start).

The default WEBHOOK_USER is nginx, meaning the webhooks can't do anything that nginx or web-served PHP can't. If your webhooks need greater privileges than that, you can configure the tool to run as developer (if code needs to be updated), or root (if process control via supervisorctl is needed).

Note, however, that this tool is very powerful, and it is ridiculously easy to accidentally create vulnerabilities, especially if you use the developer or root user. For example, if your configuration file contains secrets (e.g. to verify a webhook is authorized), then you need to ensure it can't be read by the web server or php, not just written! (Otherwise, a PHP-level exploit can find out the secret, even if the file is not located in the container's PUBLIC_DIR.) Likewise, if a webhook doesn't have some sort of authorization check as part of its configuration, then any program run inside the container can invoke it by talking directly to port 9000. (Again allowing access by an exploited PHP process.)

Therefore, if a command can have detrimental effects (or if it merely accepts any input whatsoever!), it should probably be protected with some form of authorization check (such as request signatures or an authorization key field). Alternately, the command being run can perform the check, again with the caveat that such scripts should not be readable or writable by nginx or php. You may also need some form of concurrency protection (if webhooks arrive at the same time) and/or rate-limiting (to avoid denial of service or similar issues).

(In addition, if you're using root to run the webhook tool, you should also ensure webhooks that do not need root access downgrade their privileges with as-developer or as-nginx accordingly.)

In short, using this feature may require careful design consideration, as it can easily poke holes in the "defense in depth" architecture this container works so hard to create. (Especially with a non-default WEBHOOK_USER.)

Version Info

Builds of this image are tagged with multiple aliases to make it easy to pin specific revisions or to float by PHP version. For example, a PHP 7.3.13 image with release 2.1.1 of this container could be accessed via any of the following tags (if 2.1.1 were the latest release of this image):

Note that there is no latest tag for this image; you must explicitly select at least a PHP version such as 7.3 to get the latest version of this image for that PHP version.

Also note that although you can just specify a PHP version, major releases of this container may be incompatble with older releases due to e.g. changes in OS versions or other factors, so you should probably at least target a specific major release of this container, e.g. 7.3-2.x or 7.4-3.x.

Major Versions

Version Details

TagsPHPnginxmod luaalpineNotes
8.3-3.1.x8.3.11.24.00.10.243.18These versions are also tagged with an -alpine version suffix
8.2-3.1.x8.2.141.24.00.10.243.18
8.1-3.1.x8.1.271.24.00.10.243.18
8.0-3.1.x8.0.301.22.10.10.213.16
7.4-3.1.x7.4.331.22.10.10.213.16
7.3-3.1.x7.3.331.20.10.10.193.14
7.2-3.1.x7.2.341.18.00.10.153.12
7.1-3.1.x7.1.331.16.10.10.153.10
 
8.2-3.0.x8.2.31.22.10.10.213.16Composer 2
8.1-3.0.x8.1.161.22.10.10.213.16
8.0-3.0.x8.0.191.20.10.10.193.14
7.4-3.0.x7.4.221.20.10.10.193.14
7.3-3.0.x7.3.291.20.10.10.193.14
7.2-3.0.x7.2.341.18.00.10.153.12
7.1-3.0.x7.1.331.16.10.10.153.10
 
7.3-2.x7.3.131.14.20.10.153.9New extension build method for all 7.x-2.x versions
7.2-2.x7.2.261.14.20.10.153.9
7.1-2.x7.1.331.14.20.10.153.9
 
7.2.26-1.4.x7.2.261.14.20.10.153.9Old extension build method used from here down
7.1.33-1.4.x7.1.331.14.20.10.153.9
 
1.4.x7.1.331.14.20.10.153.9
1.4.07.1.321.14.20.10.153.9
1.0.x - 1.3.x7.1.121.13.70.10.113.6Based on upstream 1.3.10