├── .dockerignore ├── .github └── workflows │ ├── docker-hub.yml │ └── main.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config ├── fpm-pool.conf ├── nginx.conf ├── php.ini └── supervisord.conf ├── docker-compose.test.yml ├── docker-compose.yml ├── run_tests.sh ├── scripts └── ssh.sh └── src ├── index.php └── test.html /.dockerignore: -------------------------------------------------------------------------------- 1 | data/ 2 | src-compose/ -------------------------------------------------------------------------------- /.github/workflows/docker-hub.yml: -------------------------------------------------------------------------------- 1 | name: Docker Hub build and push 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Set up Docker Buildx 14 | uses: docker/setup-buildx-action@v1 15 | - 16 | name: Login to DockerHub 17 | uses: docker/login-action@v1 18 | with: 19 | username: ${{ secrets.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_TOKEN }} 21 | - 22 | name: Build and push 23 | id: docker_build 24 | uses: docker/build-push-action@v2 25 | with: 26 | push: true 27 | tags: khromov/alpine-nginx-php8:latest 28 | platforms: linux/amd64,linux/arm64 -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | env: 10 | REGISTRY: ghcr.io 11 | IMAGE_NAME: ${{ github.repository }} 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | permissions: write-all 17 | 18 | steps: 19 | - name: Checkout Repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v3 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3 27 | 28 | - name: Extract metadata (tags, labels) for Docker 29 | id: meta 30 | uses: docker/metadata-action@v5 31 | with: 32 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 33 | 34 | - name: Login to GHCR 35 | uses: docker/login-action@v3 36 | with: 37 | registry: ghcr.io 38 | username: ${{ github.actor }} 39 | password: ${{ secrets.GITHUB_TOKEN }} 40 | 41 | - name: Build and Push Docker Image 42 | uses: docker/build-push-action@v3 43 | with: 44 | context: . 45 | platforms: linux/amd64, linux/arm64 46 | push: true 47 | tags: ghcr.io/khromov/alpine-nginx-php8/alpine-nginx-php8:latest 48 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | src-compose/ 3 | /vendor/ 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.21 2 | LABEL Maintainer="Stanislav Khromov " \ 3 | Description="Lightweight container with Nginx 1.26 & PHP-FPM 8.4 based on Alpine Linux." 4 | 5 | ARG PHP_VERSION="8.4.5-r0" 6 | 7 | # https://github.com/wp-cli/wp-cli/issues/3840 8 | ENV PAGER="more" 9 | 10 | # Install packages and remove default server definition 11 | RUN apk --no-cache add php84=${PHP_VERSION} \ 12 | php84-ctype \ 13 | php84-curl \ 14 | php84-dom \ 15 | php84-exif \ 16 | php84-fileinfo \ 17 | php84-fpm \ 18 | php84-gd \ 19 | php84-iconv \ 20 | php84-intl \ 21 | php84-mbstring \ 22 | php84-mysqli \ 23 | php84-opcache \ 24 | php84-openssl \ 25 | php84-pecl-imagick \ 26 | php84-pecl-redis \ 27 | php84-phar \ 28 | php84-session \ 29 | php84-simplexml \ 30 | php84-soap \ 31 | php84-xml \ 32 | php84-xmlreader \ 33 | php84-zip \ 34 | php84-zlib \ 35 | php84-pdo \ 36 | php84-xmlwriter \ 37 | php84-tokenizer \ 38 | php84-pdo_mysql \ 39 | php84-pdo_sqlite \ 40 | nginx supervisor curl tzdata htop mysql-client dcron 41 | 42 | # Symlink php8 => php 43 | RUN ln -s /usr/bin/php84 /usr/bin/php 44 | 45 | RUN ls /usr/bin 46 | 47 | # Install PHP tools 48 | RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && chmod +x wp-cli.phar && mv wp-cli.phar /usr/local/bin/wp 49 | RUN php84 -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && php84 composer-setup.php --install-dir=/usr/local/bin --filename=composer 50 | 51 | # Configure nginx 52 | COPY config/nginx.conf /etc/nginx/nginx.conf 53 | 54 | # Configure PHP-FPM 55 | COPY config/fpm-pool.conf /etc/php84/php-fpm.d/www.conf 56 | COPY config/php.ini /etc/php84/conf.d/custom.ini 57 | 58 | # Configure supervisord 59 | COPY config/supervisord.conf /etc/supervisor/conf.d/supervisord.conf 60 | 61 | # Setup document root 62 | RUN mkdir -p /var/www/html 63 | 64 | # Make sure files/folders needed by the processes are accessable when they run under the nobody user 65 | RUN chown -R nobody:nobody /var/www/html && \ 66 | chown -R nobody:nobody /run && \ 67 | chown -R nobody:nobody /var/lib/nginx && \ 68 | chown -R nobody:nobody /var/log/nginx 69 | 70 | # Switch to use a non-root user from here on 71 | USER nobody 72 | 73 | # Add application 74 | WORKDIR /var/www/html 75 | COPY --chown=nobody src/ /var/www/html/ 76 | 77 | # Expose the port nginx is reachable on 78 | EXPOSE 8080 79 | 80 | # Let supervisord start nginx & php-fpm 81 | CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] 82 | 83 | # Configure a healthcheck to validate that everything is up&running 84 | HEALTHCHECK --timeout=10s CMD curl --silent --fail http://127.0.0.1:8080/fpm-ping 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Stanislav Khromov, Tim de Pater 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker PHP-FPM 8.4 & Nginx 1.26 on Alpine Linux 2 | 3 | - Built on the lightweight and secure Alpine Linux distribution 4 | - Uses PHP 8.4 for better performance, lower CPU usage & memory footprint 5 | - Optimized to use low amount of resources when there's no traffic 6 | - The servers Nginx, PHP-FPM and supervisord run under a non-privileged user (nobody) to make it more secure 7 | - The logs of all the services are redirected to the output of the Docker container (visible with `docker logs -f `) 8 | - Follows the KISS principle (Keep It Simple, Stupid) to make it easy to understand and adjust the image to your needs 9 | 10 | ![nginx 1.26.0](https://img.shields.io/badge/nginx-1.22-brightgreen.svg) 11 | ![php 8.4](https://img.shields.io/badge/php-8.4-brightgreen.svg) 12 | ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg) 13 | 14 | ### Includes 15 | 16 | - Composer 17 | - WP-CLI 18 | - GD2 19 | - Various other extensions (like SimpleXML) 20 | - MySQL CLI 21 | 22 | This image is built on GitHub actions and hosted on the GitHub Docker images repo. It is also available under `khromov/alpine-nginx-php8` on [Docker Hub](https://hub.docker.com/r/khromov/alpine-nginx-php8). 23 | 24 | ### Usage 25 | 26 | Fetch the prebuilt image in your custom images: 27 | 28 | GitHub (preferred): 29 | 30 | ``` 31 | docker pull ghcr.io/khromov/alpine-nginx-php8/alpine-nginx-php8:latest 32 | ``` 33 | 34 | If you get "no basic auth credentials", see [this page](https://docs.github.com/en/free-pro-team@latest/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages). 35 | 36 | Docker Hub: 37 | 38 | ``` 39 | docker pull khromov/alpine-nginx-php8 40 | ``` 41 | 42 | #### Start Nginx, PHP and MySQL via docker-compose 43 | 44 | This is convenient for developing Laravel, WordPress or Drupal sites. It includes MySQL and phpMyAdmin 45 | 46 | ``` 47 | docker-compose up 48 | ``` 49 | 50 | Now you can access your site at http://localhost:8080 and the MySQL database at `db:3306`. 51 | 52 | The folder `./src-compose` will be created and you can put your project files there. 53 | 54 | The urls are: 55 | 56 | - Web: http://localhost:8080 57 | - phpMyAdmin: http://localhost:8081 58 | 59 | ###### File permission issues 60 | 61 | If you copied files into `./src-compose` you need to run: 62 | 63 | ``` 64 | sudo chown -R nobody:nogroup ./src-compose 65 | sudo chmod -R 777 ./src-compose 66 | ``` 67 | 68 | This makes sure that the files have the correct owner inside the container but remain writable outside of it. 69 | 70 | #### Quick build / run 71 | 72 | ``` 73 | docker build . -t php8 74 | docker run -p 8080:8080 -t php8 75 | ``` 76 | 77 | Go to: 78 | http://localhost:8080/ 79 | 80 | ## Configuration 81 | 82 | In [config/](config/) you'll find the default configuration files for Nginx, PHP and PHP-FPM. 83 | If you want to extend or customize that you can do so by mounting a configuration file in the correct folder. 84 | 85 | ## Acknowledgements 86 | 87 | This image was inspired by [TrafeX/docker-php-nginx](https://github.com/TrafeX/docker-php-nginx) and [this subsequent fork](https://github.com/khromov/docker-php-nginx). 88 | -------------------------------------------------------------------------------- /config/fpm-pool.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 = 127.0.0.1:9000 17 | 18 | ; Enable status page 19 | pm.status_path = /fpm-status 20 | 21 | ; Ondemand process manager 22 | pm = dynamic 23 | 24 | ; The number of child processes to be created when pm is set to 'static' and the 25 | ; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'. 26 | ; This value sets the limit on the number of simultaneous requests that will be 27 | ; served. Equivalent to the ApacheMaxClients directive with mpm_prefork. 28 | ; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP 29 | ; CGI. The below defaults are based on a server without much resources. Don't 30 | ; forget to tweak pm.* to fit your needs. 31 | ; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand' 32 | ; Note: This value is mandatory. 33 | pm.max_children=20 34 | ; The number of child processes to start when PHP-FPM starts. 35 | pm.start_servers=2 36 | ;The minimum number of idle child processes PHP-FPM will create. More are created if fewer than this number are available. 37 | pm.min_spare_servers=1 38 | ;The maximum number of child processes PHP-FPM will create. More are created if more than this number are needed. 39 | pm.max_spare_servers=4 40 | 41 | ; The number of seconds after which an idle process will be killed. 42 | ; Note: Used only when pm is set to 'ondemand' 43 | ; Default Value: 10s 44 | pm.process_idle_timeout = 10s; 45 | 46 | ; The number of requests each child process should execute before respawning. 47 | ; This can be useful to work around memory leaks in 3rd party libraries. For 48 | ; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS. 49 | ; Default Value: 0 50 | pm.max_requests = 1000 51 | 52 | ; Make sure the FPM workers can reach the environment variables for configuration 53 | clear_env = no 54 | 55 | ; Catch output from PHP 56 | catch_workers_output = yes 57 | 58 | ; Remove the 'child 10 said into stderr' prefix in the log and only show the actual message 59 | decorate_workers_output = no 60 | 61 | ; Enable ping page to use in healthcheck 62 | ping.path = /fpm-ping 63 | -------------------------------------------------------------------------------- /config/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes auto; 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 | # Max body size 25 | client_max_body_size 192M; 26 | 27 | # Write temporary files to /tmp so they can be created as a non-privileged user 28 | client_body_temp_path /tmp/client_temp; 29 | proxy_temp_path /tmp/proxy_temp_path; 30 | fastcgi_temp_path /tmp/fastcgi_temp; 31 | uwsgi_temp_path /tmp/uwsgi_temp; 32 | scgi_temp_path /tmp/scgi_temp; 33 | 34 | # Default server definition 35 | server { 36 | listen [::]:8080 default_server; 37 | listen 8080 default_server; 38 | server_name _; 39 | 40 | # When redirecting from /url to /url/, use non-absolute redirects to avoid issues with 41 | # protocol and ports (eg. when running the Docker service on 8080 but serving in production on 443) 42 | # https://stackoverflow.com/a/49638652 43 | absolute_redirect off; 44 | 45 | sendfile off; 46 | 47 | root /var/www/html; 48 | index index.php index.html; 49 | 50 | # Add support for "WebP Converter for Media" WordPress plugin 51 | # https://wordpress.org/plugins/webp-converter-for-media/ 52 | location ~ ^/wp-content/(?.+)\.(?jpe?g|png|gif)$ { 53 | if ($http_accept !~* "image/webp") { 54 | break; 55 | } 56 | 57 | expires 180d; 58 | add_header Vary Accept; 59 | try_files /wp-content/uploads-webpc/$path.$ext.webp $uri =404; 60 | } 61 | 62 | location / { 63 | # First attempt to serve request as file, then 64 | # as directory, then fall back to index.php 65 | try_files $uri $uri/ /index.php?$args; 66 | } 67 | 68 | # Redirect server error pages to the static page /50x.html 69 | error_page 500 502 503 504 /50x.html; 70 | location = /50x.html { 71 | root /var/lib/nginx/html; 72 | } 73 | 74 | # Pass the PHP scripts to PHP-FPM listening on 127.0.0.1:9000 75 | location ~ \.php$ { 76 | try_files $uri =404; 77 | 78 | fastcgi_buffers 16 16k; 79 | fastcgi_buffer_size 32k; 80 | 81 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 82 | fastcgi_pass 127.0.0.1:9000; 83 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 84 | fastcgi_param SCRIPT_NAME $fastcgi_script_name; 85 | fastcgi_index index.php; 86 | include fastcgi_params; 87 | } 88 | 89 | location ~* \.(jpg|jpeg|gif|png)$ { 90 | expires 180d; 91 | } 92 | 93 | location ~* \.(css|js|ico)$ { 94 | expires 1d; 95 | } 96 | 97 | # Deny access to . files, for security 98 | location ~ /\. { 99 | log_not_found off; 100 | deny all; 101 | } 102 | 103 | # Allow fpm ping and status from localhost 104 | location ~ ^/(fpm-status|fpm-ping)$ { 105 | access_log off; 106 | allow 127.0.0.1; 107 | deny all; 108 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 109 | include fastcgi_params; 110 | fastcgi_pass 127.0.0.1:9000; 111 | } 112 | } 113 | 114 | gzip on; 115 | gzip_proxied any; 116 | gzip_types 117 | text/plain 118 | text/css 119 | text/js 120 | text/xml 121 | text/html 122 | text/javascript 123 | application/javascript 124 | application/x-javascript 125 | application/json 126 | application/xml 127 | application/xml+rss 128 | application/rss+xml 129 | image/svg+xml/javascript; 130 | gzip_vary on; 131 | gzip_disable "msie6"; 132 | 133 | # Include other server configs 134 | include /etc/nginx/conf.d/*.conf; 135 | } 136 | -------------------------------------------------------------------------------- /config/php.ini: -------------------------------------------------------------------------------- 1 | [PHP] 2 | file_uploads = On 3 | upload_max_filesize = 256M 4 | post_max_size = 256M 5 | 6 | [Date] 7 | date.timezone="UTC" 8 | 9 | [opcache] 10 | opcache.enable=1 11 | opcache.memory_consumption=128 12 | opcache.max_accelerated_files=30000 13 | opcache.revalidate_freq=0 14 | opcache.revalidate_path=1 15 | #opcache.file_update_protection=30 16 | #opcache.consistency_checks=1 17 | 18 | # Logging 19 | # opcache.log_verbosity_level=4 20 | 21 | # https://github.com/docker-library/php/issues/772 22 | # https://stackoverflow.com/a/21291587 23 | #opcache.optimization_level=0x00000000 24 | opcache.optimization_level=0xFFFFFBFF 25 | 26 | # JIT - due to crashes on WordPress Upgrades, we can't use the tracing jit mode 27 | opcache.jit_buffer_size=64M 28 | opcache.jit=function 29 | 30 | #opcache.jit=disable 31 | #opcache.jit_debug=1048576 -------------------------------------------------------------------------------- /config/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | logfile=/dev/null 4 | logfile_maxbytes=0 5 | pidfile=/run/supervisord.pid 6 | 7 | [program:php-fpm] 8 | command=php-fpm84 -F 9 | stdout_logfile=/dev/stdout 10 | stdout_logfile_maxbytes=0 11 | stderr_logfile=/dev/stderr 12 | stderr_logfile_maxbytes=0 13 | autorestart=false 14 | startretries=0 15 | 16 | [program:nginx] 17 | command=nginx -g 'daemon off;' 18 | stdout_logfile=/dev/stdout 19 | stdout_logfile_maxbytes=0 20 | stderr_logfile=/dev/stderr 21 | stderr_logfile_maxbytes=0 22 | autorestart=false 23 | startretries=0 24 | -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | app: 4 | build: . 5 | sut: 6 | image: alpine:3.12 7 | depends_on: 8 | - app 9 | command: /tmp/run_tests.sh 10 | volumes: 11 | - "./run_tests.sh:/tmp/run_tests.sh:ro" 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | web: 5 | container_name: khromov_php8 6 | depends_on: 7 | - db 8 | volumes: 9 | - ./src-compose:/var/www/html 10 | build: . 11 | ports: 12 | - "8080:8080" 13 | environment: 14 | FOO: bar 15 | db: 16 | image: mysql:8 17 | command: --default-authentication-plugin=mysql_native_password 18 | volumes: 19 | - ./data:/var/lib/mysql 20 | environment: 21 | MYSQL_ROOT_PASSWORD: app 22 | MYSQL_DATABASE: app 23 | MYSQL_USER: app 24 | MYSQL_PASSWORD: app 25 | cap_add: 26 | - SYS_NICE # CAP_SYS_NICE 27 | pma: 28 | depends_on: 29 | - db 30 | image: phpmyadmin/phpmyadmin 31 | ports: 32 | - "8081:80" 33 | environment: 34 | PMA_HOST: db 35 | PMA_USER: app 36 | PMA_PASSWORD: app 37 | MYSQL_ROOT_PASSWORD: app -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | apk --no-cache add curl 3 | echo "Waiting for process to start..." 4 | sleep 10 5 | curl --silent --fail http://app:8080 | grep 'PHP 8.0.30' 6 | -------------------------------------------------------------------------------- /scripts/ssh.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker exec -it khromov_php8 sh -------------------------------------------------------------------------------- /src/index.php: -------------------------------------------------------------------------------- 1 |