├── .gitignore ├── rootfs ├── var │ └── www │ │ └── html │ │ ├── index.php │ │ └── test.html ├── docker-entrypoint-init.d │ └── 01-uname.sh ├── etc │ ├── service │ │ ├── php │ │ │ └── run │ │ └── nginx │ │ │ └── run │ ├── php84 │ │ ├── conf.d │ │ │ └── custom.ini.tpl │ │ └── php-fpm.d │ │ │ └── www.conf │ └── nginx │ │ └── nginx.conf └── bin │ └── docker-entrypoint.sh ├── run_tests.sh ├── renovate.json ├── docker-compose.test.yml ├── .gitattributes ├── .github ├── dependabot.yml ├── scripts │ └── update-readme.sh └── workflows │ └── build.yml ├── LICENSE ├── Dockerfile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .aider* 2 | .env 3 | -------------------------------------------------------------------------------- /rootfs/var/www/html/index.php: -------------------------------------------------------------------------------- 1 | &1 5 | exec php-fpm84 -F -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /rootfs/etc/service/nginx/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # pipe stderr to stdout and run nginx omiting ENV vars to avoid security leaks 4 | exec 2>&1 5 | exec env - PATH=$PATH nginx -g 'daemon off;' -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | app: 4 | build: . 5 | sut: 6 | image: alpine:latest 7 | depends_on: 8 | - app 9 | command: /tmp/run_tests.sh 10 | volumes: 11 | - "./run_tests.sh:/tmp/run_tests.sh:ro" 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ensure shell scripts retain executable bit 2 | *.sh text eol=lf 3 | *.sh binary 4 | 5 | # Ensure entrypoint scripts retain executable bit 6 | **/docker-entrypoint.sh text eol=lf 7 | **/docker-entrypoint.sh binary 8 | 9 | # Default text files to use LF 10 | * text=auto eol=lf 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "docker" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ernesto Serrano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rootfs/etc/php84/conf.d/custom.ini.tpl: -------------------------------------------------------------------------------- 1 | [Date] 2 | 3 | 4 | allow_url_fopen = $allow_url_fopen 5 | allow_url_include= $allow_url_include 6 | display_errors= $display_errors 7 | file_uploads= $file_uploads 8 | max_execution_time= $max_execution_time 9 | max_input_time= $max_input_time 10 | max_input_vars= $max_input_vars 11 | memory_limit= $memory_limit 12 | post_max_size= $post_max_size 13 | upload_max_filesize= $upload_max_filesize 14 | zlib.output_compression= $zlib_output_compression 15 | date.timezone= "$date_timezone" 16 | intl.default_locale= "$intl_default_locale" 17 | 18 | ; Recommended OPcache settings for Symfony 19 | ; https://symfony.com/doc/current/performance.html 20 | 21 | opcache.enable=1 22 | opcache.enable_cli=1 23 | 24 | ; The amount of memory to use for OPcache, in megabytes. 25 | opcache.memory_consumption=$opcache_memory_consumption 26 | 27 | ; The maximum number of files to cache. 28 | opcache.max_accelerated_files=$opcache_max_accelerated_files 29 | 30 | ; How often to check for changed files, in seconds. 31 | ; 1 means always check, which is ideal for development. 32 | ; In production, you should set this to 0 and use a cache warmer. 33 | opcache.validate_timestamps=$opcache_validate_timestamps 34 | 35 | ; If enabled, OPcache will save comments from PHP source files. 36 | ; This is required for some libraries, like Doctrine Annotations. 37 | opcache.save_comments=1 38 | 39 | ; Preloading configuration 40 | opcache.preload=$opcache_preload 41 | opcache.preload_user=nobody 42 | 43 | ; Realpath cache configuration 44 | realpath_cache_size=$realpath_cache_size 45 | realpath_cache_ttl=$realpath_cache_ttl 46 | -------------------------------------------------------------------------------- /.github/scripts/update-readme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | 4 | MAINTAINED_MINORS="${MAINTAINED_MINORS:-3}" 5 | REPO="${GITHUB_REPOSITORY:-erseco/alpine-php-webserver}" 6 | README="${README:-README.md}" 7 | 8 | [ -n "$REPO" ] || { echo "GITHUB_REPOSITORY is not defined (owner/repo)" >&2; exit 1; } 9 | 10 | BLOCK_START="" 11 | BLOCK_END="" 12 | 13 | # 1) Get all 3.x.y tags sorted 14 | TAGS=$(git tag -l '3.*.*' | sort -V) 15 | [ -n "$TAGS" ] || { echo "No 3.x.y tags found" >&2; exit 0; } 16 | 17 | # 2) For each minor, keep the highest patch version 18 | BEST=$(echo "$TAGS" | awk -F. ' 19 | { 20 | if ($1 != 3) next 21 | m = $1 "." $2 22 | last[m] = $0 23 | } 24 | END { 25 | for (m in last) print m, last[m] 26 | } 27 | ' | sort -V -k1,1) 28 | 29 | # 3) Select the last N minors and reverse the order 30 | SEL=$(echo "$BEST" | tail -n "$MAINTAINED_MINORS" | awk '{ lines[NR]=$0 } END { for (i=NR;i>0;i--) print lines[i] }') 31 | 32 | [ -n "$SEL" ] || { echo "No minors selected" >&2; exit 0; } 33 | 34 | # 4) Build the block 35 | BLOCK="" 36 | first=1 37 | 38 | echo "$SEL" | while read -r minor full; do 39 | if [ "$first" -eq 1 ]; then 40 | # newest minor → include all aliases: latest, 3, minor, full 41 | line="- \`latest\`, \`3\`, \`$minor\`, \`$full\`" 42 | url="https://github.com/${REPO}/blob/${full}/Dockerfile" 43 | echo "${line} ([Dockerfile](${url}))" 44 | first=0 45 | else 46 | # older minors → only minor and full 47 | url="https://github.com/${REPO}/blob/${full}/Dockerfile" 48 | echo "- \`${minor}\`, \`${full}\` ([Dockerfile](${url}))" 49 | fi 50 | done > supported-tags.tmp 51 | 52 | 53 | # 5) Replace the block in README 54 | awk -v start="$BLOCK_START" -v end="$BLOCK_END" ' 55 | BEGIN { inblk=0 } 56 | { 57 | if ($0 ~ start) { 58 | print $0 59 | while ((getline line < "supported-tags.tmp") > 0) print line 60 | inblk=1 61 | next 62 | } 63 | if ($0 ~ end) { 64 | inblk=0 65 | } 66 | if (!inblk) print $0 67 | } 68 | ' "$README" > "${README}.new" 69 | 70 | mv "${README}.new" "$README" 71 | rm -f supported-tags.tmp 72 | 73 | echo "README updated." 74 | -------------------------------------------------------------------------------- /rootfs/bin/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | shutdown() { 4 | echo "shutting down container" 5 | 6 | # first shutdown any service started by runit 7 | for _srv in $(ls -1 /etc/service); do 8 | sv force-stop $_srv 9 | done 10 | 11 | # shutdown runsvdir command 12 | kill -HUP $RUNSVDIR 13 | wait $RUNSVDIR 14 | 15 | # give processes time to stop 16 | sleep 0.5 17 | 18 | # kill any other processes still running in the container 19 | for _pid in $(ps -eo pid | grep -v PID | tr -d ' ' | grep -v '^1$' | head -n -6); do 20 | timeout 5 /bin/sh -c "kill $_pid && wait $_pid || kill -9 $_pid" 21 | done 22 | exit 23 | } 24 | 25 | # Replace ENV vars in nginx configuration files 26 | if [ "$DISABLE_DEFAULT_LOCATION" = "true" ]; then 27 | sed -i '/location \/ {/,/}/ s/^/#/' /etc/nginx/nginx.conf 28 | fi 29 | 30 | tmpfile=$(mktemp) 31 | cat /etc/nginx/nginx.conf | envsubst "$(env | cut -d= -f1 | sed -e 's/^/$/')" | tee "$tmpfile" > /dev/null 32 | mv "$tmpfile" /etc/nginx/nginx.conf 33 | 34 | # Replace ENV vars in php configuration files 35 | tmpfile=$(mktemp) 36 | cat /etc/php84/conf.d/custom.ini.tpl | envsubst "$(env | cut -d= -f1 | sed -e 's/^/$/')" | tee "$tmpfile" > /dev/null 37 | mv "$tmpfile" /etc/php84/conf.d/custom.ini 38 | 39 | tmpfile=$(mktemp) 40 | cat /etc/php84/php-fpm.d/www.conf | envsubst "$(env | cut -d= -f1 | sed -e 's/^/$/')" | tee "$tmpfile" > /dev/null 41 | mv "$tmpfile" /etc/php84/php-fpm.d/www.conf 42 | 43 | echo "Starting startup scripts in /docker-entrypoint-init.d ..." 44 | for script in $(find /docker-entrypoint-init.d/ -executable -type f | sort); do 45 | 46 | echo >&2 "*** Running: $script" 47 | $script 48 | retval=$? 49 | if [ $retval != 0 ]; 50 | then 51 | echo >&2 "*** Failed with return value: $?" 52 | exit $retval 53 | fi 54 | 55 | done 56 | echo "Finished startup scripts in /docker-entrypoint-init.d" 57 | 58 | echo "Starting runit..." 59 | exec runsvdir -P /etc/service & 60 | 61 | RUNSVDIR=$! 62 | echo "Started runsvdir, PID is $RUNSVDIR" 63 | echo "wait for processes to start...." 64 | 65 | sleep 5 66 | for _srv in $(ls -1 /etc/service); do 67 | sv status $_srv 68 | done 69 | 70 | # If there are additional arguments, execute them 71 | if [ $# -gt 0 ]; then 72 | exec "$@" 73 | fi 74 | 75 | # catch shutdown signals 76 | trap shutdown SIGTERM SIGHUP SIGQUIT SIGINT 77 | wait $RUNSVDIR 78 | 79 | shutdown 80 | -------------------------------------------------------------------------------- /rootfs/etc/php84/php-fpm.d/www.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | ; Log to stderr 3 | error_log = /dev/stderr 4 | 5 | [www] 6 | ; The address on which to accept FastCGI requests. 7 | ; Valid syntaxes are: 8 | ; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on 9 | ; a specific port; 10 | ; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on 11 | ; a specific port; 12 | ; 'port' - to listen on a TCP socket to all addresses 13 | ; (IPv6 and IPv4-mapped) on a specific port; 14 | ; '/path/to/unix/socket' - to listen on a unix socket. 15 | ; Note: This value is mandatory. 16 | listen = /run/php-fpm.sock 17 | 18 | ; Set permissions for unix socket, if one is used. In Linux, read/write 19 | ; permissions must be set in order to allow connections from a web server. Many 20 | ; BSD-derived systems allow connections regardless of permissions. The owner 21 | ; and group can be specified either by name or by their numeric IDs. 22 | ; Default Values: user and group are set as the running user 23 | ; mode is set to 0660 24 | listen.owner = nobody 25 | listen.group = nobody 26 | ;listen.mode = 0660 27 | ; When POSIX Access Control Lists are supported you can set them using 28 | ; these options, value is a comma separated list of user/group names. 29 | ; When set, listen.owner and listen.group are ignored 30 | ;listen.acl_users = 31 | ;listen.acl_groups = 32 | 33 | ; Enable status page 34 | pm.status_path = /fpm-status 35 | 36 | ; Ondemand process manager 37 | pm = ondemand 38 | 39 | ; The number of child processes to be created when pm is set to 'static' and the 40 | ; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'. 41 | ; This value sets the limit on the number of simultaneous requests that will be 42 | ; served. Equivalent to the ApacheMaxClients directive with mpm_prefork. 43 | ; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP 44 | ; CGI. The below defaults are based on a server without much resources. Don't 45 | ; forget to tweak pm.* to fit your needs. 46 | ; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand' 47 | ; Note: This value is mandatory. 48 | pm.max_children = 100 49 | 50 | ; The number of seconds after which an idle process will be killed. 51 | ; Note: Used only when pm is set to 'ondemand' 52 | ; Default Value: 10s 53 | pm.process_idle_timeout = 10s; 54 | 55 | ; The number of requests each child process should execute before respawning. 56 | ; This can be useful to work around memory leaks in 3rd party libraries. For 57 | ; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS. 58 | ; Default Value: 0 59 | pm.max_requests = 1000 60 | 61 | ; Make sure the FPM workers can reach the environment variables for configuration 62 | clear_env = $clear_env 63 | 64 | ; Catch output from PHP 65 | catch_workers_output = yes 66 | 67 | ; Remove the 'child 10 said into stderr' prefix in the log and only show the actual message 68 | decorate_workers_output = no 69 | 70 | ; Enable ping page to use in healthcheck 71 | ping.path = /fpm-ping 72 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ARCH= 2 | FROM ${ARCH}alpine:3.23 3 | 4 | LABEL org.opencontainers.image.authors="Ernesto Serrano " \ 5 | org.opencontainers.image.description="Lightweight container with Nginx & PHP-FPM based on Alpine Linux." 6 | 7 | # Set pipefail to catch errors in piped commands 8 | SHELL ["/bin/ash", "-eo", "pipefail", "-c"] 9 | 10 | # Install packages 11 | RUN apk --no-cache add \ 12 | php84 \ 13 | php84-ctype \ 14 | php84-curl \ 15 | php84-dom \ 16 | php84-exif \ 17 | php84-fileinfo \ 18 | php84-fpm \ 19 | php84-gd \ 20 | php84-iconv \ 21 | php84-intl \ 22 | php84-json \ 23 | php84-mbstring \ 24 | php84-mysqli \ 25 | php84-opcache \ 26 | php84-openssl \ 27 | php84-pecl-apcu \ 28 | php84-pdo \ 29 | php84-pdo_mysql \ 30 | php84-pgsql \ 31 | php84-phar \ 32 | php84-session \ 33 | php84-simplexml \ 34 | php84-soap \ 35 | php84-sodium \ 36 | php84-tokenizer \ 37 | php84-xml \ 38 | php84-xmlreader \ 39 | php84-zip \ 40 | php84-zlib \ 41 | nginx \ 42 | runit \ 43 | curl \ 44 | # Bring in gettext so we can get `envsubst`, then throw 45 | # the rest away. To do this, we need to install `gettext` 46 | # then move `envsubst` out of the way so `gettext` can 47 | # be deleted completely, then move `envsubst` back. 48 | && apk add --no-cache --virtual .gettext gettext \ 49 | && mv /usr/bin/envsubst /tmp/ \ 50 | && runDeps="$( \ 51 | scanelf --needed --nobanner /tmp/envsubst \ 52 | | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ 53 | | sort -u \ 54 | | xargs -r apk info --installed \ 55 | | sort -u \ 56 | )" \ 57 | && apk add --no-cache $runDeps \ 58 | && apk del .gettext \ 59 | && mv /tmp/envsubst /usr/local/bin/ \ 60 | # Remove alpine cache 61 | && rm -rf /var/cache/apk/* \ 62 | # Remove default server definition 63 | && rm /etc/nginx/http.d/default.conf \ 64 | # Make sure files/folders needed by the processes are accessable when they run under the nobody user 65 | && mkdir -p /run /var/lib/nginx /var/www/html /var/log/nginx \ 66 | && chown -R nobody:nobody /run /var/lib/nginx /var/www/html /var/log/nginx 67 | 68 | # Add configuration files 69 | COPY --chown=nobody rootfs/ / 70 | 71 | # Switch to use a non-root user from here on 72 | USER nobody 73 | 74 | # Add application 75 | WORKDIR /var/www/html 76 | 77 | # Expose the port nginx is reachable on 78 | EXPOSE 8080 79 | 80 | # Let runit start nginx & php-fpm 81 | # Ensure /bin/docker-entrypoint.sh is always executed 82 | ENTRYPOINT ["/bin/docker-entrypoint.sh"] 83 | 84 | 85 | # Configure a healthcheck to validate that everything is up&running 86 | HEALTHCHECK --timeout=10s CMD curl --silent --fail http://127.0.0.1:8080/fpm-ping || exit 1 87 | 88 | ENV nginx_root_directory=/var/www/html \ 89 | client_max_body_size=2M \ 90 | clear_env=no \ 91 | allow_url_fopen=On \ 92 | allow_url_include=Off \ 93 | display_errors=Off \ 94 | file_uploads=On \ 95 | max_execution_time=0 \ 96 | max_input_time=-1 \ 97 | max_input_vars=1000 \ 98 | memory_limit=128M \ 99 | post_max_size=8M \ 100 | upload_max_filesize=2M \ 101 | zlib_output_compression=On \ 102 | date_timezone=UTC \ 103 | intl_default_locale=en_US \ 104 | fastcgi_read_timeout=60s \ 105 | fastcgi_send_timeout=60s \ 106 | # Recommended OPcache settings for Symfony 107 | opcache_memory_consumption=256 \ 108 | opcache_max_accelerated_files=20000 \ 109 | opcache_validate_timestamps=0 \ 110 | opcache_preload="" \ 111 | realpath_cache_size=4096K \ 112 | realpath_cache_ttl=600 113 | -------------------------------------------------------------------------------- /rootfs/etc/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | error_log stderr warn; 3 | pid /run/nginx.pid; 4 | 5 | events { 6 | worker_connections 1024; 7 | } 8 | 9 | http { 10 | include mime.types; 11 | default_type application/octet-stream; 12 | 13 | # Define custom log format to include reponse times 14 | log_format main_timed '$remote_addr - $remote_user [$time_local] "$request" ' 15 | '$status $body_bytes_sent "$http_referer" ' 16 | '"$http_user_agent" "$http_x_forwarded_for" ' 17 | '$request_time $upstream_response_time $pipe $upstream_cache_status'; 18 | 19 | access_log /dev/stdout main_timed; 20 | error_log /dev/stderr notice; 21 | 22 | keepalive_timeout 65; 23 | 24 | # Write temporary files to /tmp so they can be created as a non-privileged user 25 | client_body_temp_path /tmp/client_temp; 26 | proxy_temp_path /tmp/proxy_temp_path; 27 | 28 | # ------------------------------------------------------------------ 29 | # FastCGI timeout configuration for large file processing 30 | # ------------------------------------------------------------------ 31 | # 32 | # These timeouts are essential for handling large file uploads 33 | # and long-running processing operations. 34 | # 35 | # We set these to match PHP's max_execution_time 36 | 37 | # Maximum time to wait for PHP-FPM to process a request 38 | fastcgi_read_timeout $fastcgi_read_timeout; 39 | 40 | # Maximum time to send a request to PHP-FPM 41 | fastcgi_send_timeout $fastcgi_send_timeout; 42 | 43 | # Buffer configuration for handling large responses 44 | fastcgi_buffering on; 45 | fastcgi_buffer_size 16k; 46 | fastcgi_buffers 16 16k; 47 | fastcgi_busy_buffers_size 32k; 48 | fastcgi_temp_path /tmp/fastcgi_temp; 49 | uwsgi_temp_path /tmp/uwsgi_temp; 50 | scgi_temp_path /tmp/scgi_temp; 51 | 52 | # Default server definition 53 | server { 54 | listen 8080 default_server; 55 | server_name _; 56 | 57 | sendfile off; 58 | 59 | # Set the forwarded_scheme variable based on the X-Forwarded-Proto header 60 | # This is used to maintain the original protocol used by the client 61 | # This is important when behind a reverse proxy that handles SSL termination 62 | set $forwarded_scheme "http"; 63 | if ($http_x_forwarded_proto = "https") { 64 | set $forwarded_scheme "https"; 65 | } 66 | 67 | 68 | # Increase proxy buffers for large requests 69 | proxy_buffer_size 128k; 70 | proxy_buffers 4 256k; 71 | proxy_busy_buffers_size 256k; 72 | 73 | # Upload limit 74 | client_max_body_size ${client_max_body_size}; 75 | client_body_buffer_size 128k; 76 | 77 | root ${nginx_root_directory}; 78 | index index.php index.html; 79 | 80 | location / { 81 | # First attempt to serve request as file, then 82 | # as directory, then fall back to index.php 83 | try_files $uri $uri/ /index.php$is_args$args; 84 | } 85 | 86 | # Redirect server error pages to the static page /50x.html 87 | error_page 500 502 503 504 /50x.html; 88 | location = /50x.html { 89 | root /var/lib/nginx/html; 90 | } 91 | 92 | # Pass the PHP scripts to PHP-FPM listening on socket 93 | location ~ [^/]\.php(/|$) { 94 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 95 | fastcgi_pass unix:/run/php-fpm.sock; 96 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 97 | fastcgi_param SCRIPT_NAME $fastcgi_script_name; 98 | fastcgi_param PATH_INFO $fastcgi_path_info; 99 | fastcgi_index index.php; 100 | include fastcgi_params; 101 | 102 | # Pass the original forwarded_scheme and HTTPS status to the PHP backend 103 | fastcgi_param HTTP_X_FORWARDED_PROTO $forwarded_scheme; 104 | fastcgi_param HTTPS $https if_not_empty; 105 | 106 | } 107 | 108 | location ~* \.(jpg|jpeg|gif|png|css|js|ico|xml)$ { 109 | expires 5d; 110 | } 111 | 112 | # Deny access to . files, for security 113 | location ~ /\. { 114 | log_not_found off; 115 | deny all; 116 | } 117 | 118 | # Allow fpm ping and status from localhost 119 | location ~ ^/(fpm-status|fpm-ping)$ { 120 | access_log off; 121 | allow 127.0.0.1; 122 | deny all; 123 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 124 | include fastcgi_params; 125 | fastcgi_pass unix:/run/php-fpm.sock; 126 | } 127 | 128 | # Include additional server-specific configurations 129 | include /etc/nginx/server-conf.d/*.conf; 130 | 131 | } 132 | 133 | # Include other server configs 134 | include /etc/nginx/conf.d/*.conf; 135 | 136 | gzip on; 137 | gzip_proxied any; 138 | # Based on CloudFlare's recommended settings https://developers.cloudflare.com/speed/optimization/content/brotli/content-compression/ 139 | gzip_types text/richtext text/plain text/css text/x-script text/x-component text/x-java-source text/x-markdown application/javascript application/x-javascript text/javascript text/js image/x-icon image/vnd.microsoft.icon application/x-perl application/x-httpd-cgi text/xml application/xml application/rss+xml application/vnd.api+json application/x-protobuf application/json multipart/bag multipart/mixed application/xhtml+xml font/ttf font/otf font/x-woff image/svg+xml application/vnd.ms-fontobject application/ttf application/x-ttf application/otf application/x-otf application/truetype application/opentype application/x-opentype application/font-woff application/eot application/font application/font-sfnt application/wasm application/javascript-binast application/manifest+json application/ld+json application/graphql+json application/geo+json; 140 | gzip_vary on; 141 | gzip_disable "msie6"; 142 | 143 | } 144 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: buildx 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # beta 7 | tags: 8 | # Match semantic version tags like 3.22, 3.22.1, 3.22.1-1, 3.22-beta, v3.22.1-rc1, etc. 9 | - '[0-9]+\.[0-9]+(\.[0-9]+)?(-[0-9A-Za-z]+)?' 10 | pull_request: 11 | release: 12 | types: [ published ] 13 | 14 | jobs: 15 | buildx: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | packages: write 20 | security-events: write 21 | 22 | steps: 23 | # Checkout the code 24 | - name: Checkout 25 | uses: actions/checkout@v6 26 | with: 27 | fetch-depth: 0 # tags + full history 28 | 29 | # Detect whether the tag commit is also on main 30 | - name: Check main ancestry 31 | id: mainline 32 | if: ${{ github.ref_type == 'tag' }} 33 | run: | 34 | git fetch origin main --depth=1 35 | if git merge-base --is-ancestor origin/main "$GITHUB_SHA"; then 36 | echo "is_main=true" >> "$GITHUB_OUTPUT" 37 | else 38 | echo "is_main=false" >> "$GITHUB_OUTPUT" 39 | fi 40 | 41 | # Extract metadata and prepare tags 42 | - name: Docker metadata 43 | id: meta 44 | uses: docker/metadata-action@v5 45 | with: 46 | images: | 47 | ghcr.io/${{ github.repository }} 48 | docker.io/${{ github.repository }} 49 | tags: | 50 | # Always from a tag push -> 3.20.7 y 3.20 51 | type=semver,pattern={{version}},enable=${{ github.ref_type == 'tag' }} 52 | type=semver,pattern={{major}}.{{minor}},enable=${{ github.ref_type == 'tag' }} 53 | # latest only if tag belongs to main 54 | type=raw,value=latest,enable=${{ github.ref_type == 'tag' && steps.mainline.outputs.is_main == 'true' }} 55 | # beta on each push to main (no tag) 56 | type=raw,value=beta,enable=${{ github.ref_name == 'main' }} 57 | 58 | 59 | # Set up QEMU for multi-platform builds 60 | - name: Set up QEMU 61 | uses: docker/setup-qemu-action@v3 62 | with: 63 | platforms: all 64 | 65 | # Set up Docker Buildx 66 | - name: Set up Docker Buildx 67 | id: buildx 68 | uses: docker/setup-buildx-action@v3 69 | 70 | # Login to DockerHub and GHCR 71 | - name: Login to DockerHub 72 | if: github.event_name != 'pull_request' 73 | uses: docker/login-action@v3 74 | with: 75 | username: ${{ secrets.DOCKER_USERNAME }} 76 | password: ${{ secrets.DOCKER_PASSWORD }} 77 | 78 | - name: Login to GHCR 79 | if: github.event_name != 'pull_request' 80 | uses: docker/login-action@v3 81 | with: 82 | registry: ghcr.io 83 | username: ${{ github.actor }} 84 | password: ${{ secrets.GITHUB_TOKEN }} 85 | 86 | # Lint Dockerfile 87 | - name: Hadolint Action 88 | uses: hadolint/hadolint-action@v3.3.0 89 | with: 90 | format: sarif 91 | output-file: hadolint-results.sarif 92 | no-fail: true 93 | 94 | - name: Upload SARIF results 95 | uses: github/codeql-action/upload-sarif@v4 96 | with: 97 | sarif_file: hadolint-results.sarif 98 | category: hadolint-dockerfile 99 | 100 | 101 | # Debug Build for PRs 102 | - name: Debug Build 103 | if: github.event_name == 'pull_request' 104 | run: | 105 | docker buildx build --load . 106 | 107 | # Test the built image 108 | - name: Test 109 | run: | 110 | docker compose version 111 | docker compose --file docker-compose.test.yml up --exit-code-from sut --timeout 10 --build 112 | 113 | # Build and Push to both registries in one step 114 | - name: Build and push 115 | if: github.event_name != 'pull_request' 116 | uses: docker/build-push-action@v6 117 | with: 118 | context: . 119 | push: true 120 | tags: ${{ steps.meta.outputs.tags }} 121 | platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le,linux/s390x 122 | cache-from: type=gha 123 | cache-to: type=gha,mode=max 124 | 125 | # Run Trivy vulnerability scanner ── only tags on main 126 | - name: Run Trivy vulnerability scanner 127 | if: github.ref_type == 'tag' && steps.mainline.outputs.is_main == 'true' 128 | uses: aquasecurity/trivy-action@master 129 | with: 130 | image-ref: ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.version }} 131 | format: 'table' 132 | exit-code: '0' 133 | severity: 'CRITICAL,HIGH' 134 | 135 | - name: Generate Supported Tags block 136 | if: github.ref_name == 'main' || (github.ref_type == 'tag' && steps.mainline.outputs.is_main == 'true') 137 | env: 138 | GITHUB_REPOSITORY: ${{ github.repository }} 139 | MAINTAINED_MINORS: "3" 140 | run: | 141 | .github/scripts/update-readme.sh 142 | 143 | - name: Commit README if changed 144 | if: github.ref_name == 'main' || (github.ref_type == 'tag' && steps.mainline.outputs.is_main == 'true') 145 | run: | 146 | if ! git diff --quiet -- README.md; then 147 | git config user.name "github-actions" 148 | git config user.email "github-actions@users.noreply.github.com" 149 | git add README.md 150 | git commit -m "chore(readme): update supported tags" 151 | git push 152 | else 153 | echo "No README changes to commit." 154 | fi 155 | 156 | # Update Docker Hub Description 157 | - name: Docker Hub Description 158 | if: startsWith(github.ref, 'refs/tags/') && contains(join(steps.meta.outputs.tags, '\n'), 'latest') 159 | uses: peter-evans/dockerhub-description@v5 160 | with: 161 | username: ${{ secrets.DOCKER_USERNAME }} 162 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 163 | short-description: ${{ github.event.repository.description }} 164 | readme-filepath: ./README.md 165 | 166 | 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker PHP-FPM 8.4 & Nginx 1.28 on Alpine Linux 3.23 2 | 3 | [![Docker Pulls](https://img.shields.io/docker/pulls/erseco/alpine-php-webserver.svg)](https://hub.docker.com/r/erseco/alpine-php-webserver/) 4 | ![Docker Image Size](https://img.shields.io/docker/image-size/erseco/alpine-php-webserver) 5 | ![alpine 3.23](https://img.shields.io/badge/alpine-3.23-brightgreen.svg) 6 | ![nginx 1.28](https://img.shields.io/badge/nginx-1.28-brightgreen.svg) 7 | ![php 8.4](https://img.shields.io/badge/php-8.4-brightgreen.svg) 8 | ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg) 9 | 10 | Example PHP-FPM 8.4 & Nginx 1.28 setup for Docker, build on [Alpine Linux](https://www.alpinelinux.org/). 11 | The image is only +/- 25MB large. 12 | 13 | Repository: https://github.com/erseco/alpine-php-webserver 14 | 15 | * Built on the lightweight and secure Alpine Linux distribution 16 | * Very small Docker image size (+/-25MB) 17 | * Uses PHP 8.4 for better performance, lower cpu usage & memory footprint 18 | * Multi-arch support: 386, amd64, arm/v6, arm/v7, arm64, ppc64le, s390x 19 | * Optimized for 100 concurrent users 20 | * Optimized to only use resources when there's traffic (by using PHP-FPM's ondemand PM) 21 | * Use of runit instead of supervisord to reduce memory footprint 22 | * The servers Nginx, PHP-FPM run under a non-privileged user (nobody) to make it more secure 23 | * The logs of all the services are redirected to the output of the Docker container (visible with `docker logs -f `) 24 | * Follows the KISS principle (Keep It Simple, Stupid) to make it easy to understand and adjust the image to your needs 25 | 26 | ## Supported tags and respective Dockerfile links 27 | 28 | - `latest`, `3`, `3.23`, `3.23.0` ([Dockerfile](https://github.com/erseco/alpine-php-webserver/blob/3.23.0/Dockerfile)) 29 | - `3.22`, `3.22.2` ([Dockerfile](https://github.com/erseco/alpine-php-webserver/blob/3.22.2/Dockerfile)) 30 | - `3.21`, `3.21.5` ([Dockerfile](https://github.com/erseco/alpine-php-webserver/blob/3.21.5/Dockerfile)) 31 | 32 | 33 | > **Note**: The `main` branch ([Dockerfile](https://github.com/erseco/alpine-php-webserver/blob/main/Dockerfile)) is automatically pushed with the tag **`beta`**. 34 | > Use this tag for testing purposes before stable releases are published. 35 | 36 | ## Usage 37 | 38 | Start the Docker container: 39 | 40 | docker run -p 80:8080 erseco/alpine-php-webserver 41 | 42 | See the PHP info on http://localhost, or the static html page on http://localhost/test.html 43 | 44 | Or mount your own code to be served by PHP-FPM & Nginx 45 | 46 | docker run -p 80:8080 -v ~/my-codebase:/var/www/html erseco/alpine-php-webserver 47 | 48 | ## Running with Docker Compose 49 | 50 | Easily serve your local PHP files using Docker Compose. This setup mounts your `./php` directory and binds it to port 8080 on your local machine, allowing for immediate reflection of changes in your PHP files through a web server. It's perfect for local development and testing. 51 | 52 | ### Docker Compose Configuration 53 | 54 | Here's a simple `docker-compose.yml` example to get you started: 55 | 56 | ```yaml 57 | services: 58 | webserver: 59 | image: erseco/alpine-php-webserver 60 | ports: 61 | - 8080:8080 62 | volumes: 63 | - ./php:/var/www/html 64 | restart: unless-stopped 65 | ``` 66 | 67 | - **image**: Uses `erseco/alpine-php-webserver`, optimized for PHP applications. 68 | - **ports**: Maps port 8080 from the container to your local machine, accessible at `http://localhost:8080`. 69 | - **volumes**: Mounts your local `./php` directory to `/var/www/html` in the container, enabling live updates to your PHP files. 70 | - **restart**: Ensures the container automatically restarts unless manually stopped, for better reliability. 71 | 72 | ### How to Use 73 | 74 | 1. Save the above `docker-compose.yml` in your project directory. 75 | 2. Run `docker compose up -d` in your terminal, within the same directory. 76 | 3. Access your PHP application at `http://localhost:8080`. 77 | 78 | This method ensures a seamless development process, allowing you to focus on coding rather than setup complexities. 79 | 80 | ## Adding additional daemons 81 | You can add additional daemons (e.g. your own app) to the image by creating runit entries. You only have to write a small shell script which runs your daemon, and runit will keep it up and running for you, restarting it when it crashes, etc. 82 | 83 | The shell script must be called `run`, must be executable, and is to be placed in the directory `/etc/service/`. 84 | 85 | Here's an example showing you how a memcached server runit entry can be made. 86 | 87 | #!/bin/sh 88 | ### In memcached.sh (make sure this file is chmod +x): 89 | # `chpst -u memcache` runs the given command as the user `memcache`. 90 | # If you omit that part, the command will be run as root. 91 | exec 2>&1 chpst -u memcache /usr/bin/memcached 92 | 93 | ### In Dockerfile: 94 | RUN mkdir /etc/service/memcached 95 | ADD memcached.sh /etc/service/memcached/run 96 | 97 | Note that the shell script must run the daemon **without letting it daemonize/fork it**. Usually, daemons provide a command line flag or a config file option for that. 98 | 99 | 100 | ## Running scripts during container startup 101 | You can set your own scripts during startup, just add your scripts in `/docker-entrypoint-init.d/`. The scripts are run in lexicographic order. 102 | 103 | All scripts must exit correctly, e.g. with exit code 0. If any script exits with a non-zero exit code, the booting will fail. 104 | 105 | The following example shows how you can add a startup script. This script simply logs the time of boot to the file /tmp/boottime.txt. 106 | 107 | #!/bin/sh 108 | ### In logtime.sh (make sure this file is chmod +x): 109 | date > /tmp/boottime.txt 110 | 111 | ### In Dockerfile: 112 | ADD logtime.sh /docker-entrypoint-init.d/logtime.sh 113 | 114 | 115 | ## Nginx Configuration 116 | 117 | The Nginx configuration is designed to be flexible and easy to customize. By default, the main configuration file is located at `rootfs/etc/nginx/nginx.conf`. 118 | 119 | ### Adding Custom Configurations 120 | 121 | You can add custom configurations in two ways: 122 | 123 | 1. **Global Configurations**: Place your configuration files in `/etc/nginx/conf.d/`. These configurations are included globally and affect all server blocks. 124 | 125 | 2. **Server-Specific Configurations**: For configurations specific to a particular server block, place your files in `/etc/nginx/server-conf.d/`. These are included within the server block, allowing for more granular control. 126 | 127 | ### Example 128 | 129 | To add a custom configuration, create a `.conf` file in the appropriate directory. For example, to add a server-specific rule, you might create a file named `custom-server.conf` in `/etc/nginx/server-conf.d/` with the following content: 130 | 131 | ```nginx 132 | # Example custom server configuration 133 | location /custom { 134 | return 200 'Custom server configuration is working!'; 135 | add_header Content-Type text/plain; 136 | } 137 | ``` 138 | 139 | This setup allows you to easily manage and customize your Nginx configurations without modifying the main `nginx.conf` file. 140 | In [rootfs/etc/](rootfs/etc/) you'll find the default configuration files for Nginx, PHP and PHP-FPM. 141 | If you want to extend or customize that you can do so by mounting a configuration file in the correct folder; 142 | 143 | Nginx configuration: 144 | 145 | docker run -v "`pwd`/nginx-server.conf:/etc/nginx/conf.d/server.conf" erseco/alpine-php-webserver 146 | 147 | PHP configuration: 148 | 149 | docker run -v "`pwd`/php-setting.ini:/etc/php8/conf.d/settings.ini" erseco/alpine-php-webserver 150 | 151 | PHP-FPM configuration: 152 | 153 | docker run -v "`pwd`/php-fpm-settings.conf:/etc/php8/php-fpm.d/server.conf" erseco/alpine-php-webserver 154 | 155 | ## Environment variables 156 | 157 | You can define the next environment variables to change values from NGINX and PHP 158 | 159 | | Server | Variable Name | Default | description | 160 | |--------|-------------------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 161 | | NGINX | nginx_root_directory | /var/www/html | Sets the root directory for the NGINX server, which specifies the location from which files are served. This is the directory where your web application's public files should reside. | 162 | | NGINX | client_max_body_size | 2m | Sets the maximum allowed size of the client request body, specified in the “Content-Length” request header field. | 163 | | NGINX | fastcgi_read_timeout | 60s | Defines a timeout for reading a response from the FastCGI server. | 164 | | NGINX | fastcgi_send_timeout | 60s | Sets a timeout for transmitting a request to the FastCGI server. | 165 | | NGINX | DISABLE_DEFAULT_LOCATION | false | If set to "true", this variable disables the default `location /` block in the Nginx configuration. This allows you to mount a custom configuration in `/etc/nginx/server-conf.d/` without conflicts. | 166 | | PHP8 | clear_env | no | Clear environment in FPM workers. Prevents arbitrary environment variables from reaching FPM worker processes by clearing the environment in workers before env vars specified in this pool configuration are added. | 167 | | PHP8 | allow_url_fopen | On | Enable the URL-aware fopen wrappers that enable accessing URL object like files. Default wrappers are provided for the access of remote files using the ftp or http protocol, some extensions like zlib may register additional wrappers. | 168 | | PHP8 | allow_url_include | Off | Allow the use of URL-aware fopen wrappers with the following functions: include(), include_once(), require(), require_once(). | 169 | | PHP8 | display_errors | Off | Determine whether errors should be printed to the screen as part of the output or if they should be hidden from the user. | 170 | | PHP8 | file_uploads | On | Whether or not to allow HTTP file uploads. | 171 | | PHP8 | max_execution_time | 0 | Maximum time in seconds a script is allowed to run before it is terminated by the parser. This helps prevent poorly written scripts from tying up the server. The default setting is 30. | 172 | | PHP8 | max_input_time | -1 | Maximum time in seconds a script is allowed to parse input data, like POST, GET and file uploads. | 173 | | PHP8 | max_input_vars | 1000 | Maximum number of input variables allowed per request and can be used to deter denial of service attacks involving hash collisions on the input variable names. | 174 | | PHP8 | memory_limit | 128M | Maximum amount of memory in bytes that a script is allowed to allocate. This helps prevent poorly written scripts for eating up all available memory on a server. Note that to have no memory limit, set this directive to -1. | 175 | | PHP8 | post_max_size | 8M | Max size of post data allowed. This setting also affects file upload. To upload large files, this value must be larger than upload_max_filesize. Generally speaking, memory_limit should be larger than post_max_size. | 176 | | PHP8 | upload_max_filesize | 2M | Maximum size of an uploaded file. | 177 | | PHP8 | zlib_output_compression | On | Whether to transparently compress pages. If this option is set to "On" in php.ini or the Apache configuration, pages are compressed if the browser sends an "Accept-Encoding: gzip" or "deflate" header. | 178 | | PHP8 | date_timezone | UTC | Sets the PHP timezone configuration (date.timezone) in custom.ini. Accepts standard PHP timezone identifiers (e.g., 'America/New_York', 'Europe/London'). See [PHP timezones](https://www.php.net/manual/en/timezones.php) for valid values. | 179 | | PHP8 | intl_default_locale | en_US | If you want to change the [PHP locale](https://www.php.net/manual/en/class.locale.php) for the entire application or server globally (e.g. in php.ini), you can set the intl.default_locale directives (e.g. en_US or de_DE) | 180 | | PHP8 | opcache_memory_consumption | 256 | The size of the OPcache shared memory storage in megabytes. | 181 | | PHP8 | opcache_max_accelerated_files | 20000 | The maximum number of keys (and files) in the OPcache hash table. | 182 | | PHP8 | opcache_validate_timestamps | 0 | When set to `0`, OPcache doesn't check if files have changed, which is recommended for production. After each deployment, the cache must be cleared manually. Set to `1` for development. | 183 | | PHP8 | opcache_preload | *(empty)* | Specifies a PHP script to be compiled and executed at server start-up, improving performance. See [PHP Preloading](https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.preload). Example: `/var/www/html/config/preload.php` | 184 | | PHP8 | realpath_cache_size | 4096K | The size of the realpath cache to be used by PHP. | 185 | | PHP8 | realpath_cache_ttl | 600 | The time-to-live for the realpath cache. | 186 | 187 | ## Adding composer 188 | 189 | If you need [Composer](https://getcomposer.org/) in your project, here's an easy way to add it. 190 | 191 | ```dockerfile 192 | FROM erseco/alpine-php-webserver:latest 193 | USER root 194 | # Install composer from the official image 195 | RUN apk add --no-cache composer 196 | USER nobody 197 | # Run composer install to install the dependencies 198 | RUN composer install --optimize-autoloader --no-interaction --no-progress 199 | ``` 200 | 201 | ### Building with Composer 202 | 203 | If you are building an image that includes your source code and uses Composer to manage dependencies, it's recommended to install the dependencies during the image build process. 204 | 205 | Although Composer (`/usr/bin/composer`) is required to install the dependencies, it does not need to remain in the final image. If you're building for production and want a leaner image, you can uninstall it after running `composer install` using `apk del`. 206 | 207 | ```Dockerfile 208 | FROM erseco/alpine-php-webserver 209 | 210 | # Switch to root to install Composer 211 | USER root 212 | 213 | # Install Composer and required tools 214 | RUN apk add --no-cache composer 215 | 216 | # Copy application source code 217 | COPY ./ /var/www/html 218 | WORKDIR /var/www/html 219 | 220 | # Switch to a non-root user for running Composer 221 | USER nobody 222 | 223 | # Install PHP dependencies (requires composer.json) 224 | RUN composer install \ 225 | --no-dev \ 226 | --optimize-autoloader \ 227 | --no-interaction \ 228 | --no-progress 229 | 230 | # Optional: remove Composer to reduce image size 231 | USER root 232 | RUN apk del composer 233 | USER nobody 234 | ``` 235 | 236 | This keeps the final image clean, reduces its size, and minimizes the available tooling that could be misused in production environments. 237 | 238 | ## Running Commands as Root 239 | 240 | In certain situations, you might need to run commands as `root` within your Moodle container, for example, to install additional packages. You can do this using the `docker compose exec` command with the `--user root` option. Here's how: 241 | 242 | ```bash 243 | docker compose exec --user root alpine-php-webserver sh 244 | ``` 245 | --------------------------------------------------------------------------------