commit 2688243d5917fe659b60cd56eab75a213d428926 Author: chris Date: Thu Mar 20 16:10:46 2025 +0100 Initial creation of image with php8.4-fpm with nginx diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..156bf50 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,66 @@ +name: Build nginx fpm 8.4 image + +on: + push: + branches: [ master ] + schedule: + # Run every Sunday at midnight + - cron: '0 0 * * 0' + +env: + IMAGE: /docker/nginx-fpm-8-4 + +jobs: + Build-and-release-image: + + runs-on: ubuntu-latest + + container: + image: catthehacker/ubuntu:act-latest + + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log into registry + uses: docker/login-action@v3 + with: + registry: ${{ secrets.REGISTRY_URL }} + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_PASS }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ secrets.REGISTRY_URL }}${{ env.IMAGE }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + env: + ACTIONS_RUNTIME_TOKEN: '' + with: + tags: ${{ secrets.REGISTRY_URL }}${{ env.IMAGE }}:latest + push: true + + - name: Scan image + uses: anchore/scan-action@v6 + id: scan + with: + image: ${{ secrets.REGISTRY_URL }}${{ env.IMAGE }}:latest + fail-build: false + output-format: table + severity-cutoff: critical + registry-username: ${{ secrets.REGISTRY_USER }} + registry-password: ${{ secrets.REGISTRY_PASS }} + grype-version: 'v0.90.0' + + - name: Inspect file + run: cat ${{ steps.scan.outputs.table }} + + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: scan-result + path: ${{ steps.scan.outputs.table }} + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9a62cc9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,60 @@ +FROM alpine:3.21 +LABEL Maintainer="Christian Steinle " +LABEL Description="Lightweight container with Nginx 1.26 & PHP 8.4 based on Alpine Linux." + +WORKDIR /var/www/html + +# Set environment variable to serve not from /var/www/html but subpath, for e.g. Laravel. +ENV SERVE_PATH= + +RUN apk update && \ + apk upgrade && \ + apk add --no-cache \ + curl \ + nginx \ + php84 \ + php84-bcmath \ + php84-ctype \ + php84-curl \ + php84-dom \ + php84-fileinfo \ + php84-fpm \ + php84-gd \ + php84-mbstring \ + php84-mysqli \ + php84-openssl \ + php84-pdo \ + php84-pdo_mysql \ + php84-pdo_pgsql \ + php84-pdo_sqlite \ + php84-session \ + php84-tokenizer \ + php84-xml \ + php84-xsl \ + php84-zip \ + supervisor + +# Link to php, for calling "php artisan serve". +RUN ln -s /usr/bin/php84 /usr/bin/php + +# Copy configuration and entrypoint. Entrypoint is needed to change nginx's document root if environement variable is given. +COPY config/nginx.conf /etc/nginx/nginx.conf +COPY config/fpm-pool.conf /etc/php84/php-fpm.d/www.conf +COPY config/php.ini /etc/php84/conf.d/custom.ini +COPY config/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY config/entrypoint.sh /bin/entrypoint.sh + +RUN chmod a+x /bin/entrypoint.sh + +EXPOSE 8080 + +RUN chown -R nobody:nobody /var/www/html /run /var/lib/nginx /var/log/nginx /etc/nginx + +ENTRYPOINT ["/bin/entrypoint.sh"] + +# Use unpriviledged user for security. +USER nobody + +# Add healthcheck to container. +HEALTHCHECK --timeout=10s CMD curl --silent --fail http://127.0.0.1:8080/fpm-ping + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ba692c --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Docker PHP-FPM & Nginx on Alpine Linux + +* Built on the lightweight and secure Alpine Linux distribution +* Small image size +* PHP 8.4 +* Optimized for 100 concurrent users +* Optimized to only use resources when there's traffic (PHP-FPM's on-demand process manager) +* Using a non-privileged user to make it more secure +* Logs of all services are redirected to the output of the container diff --git a/config/entrypoint.sh b/config/entrypoint.sh new file mode 100755 index 0000000..15fb7b2 --- /dev/null +++ b/config/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -e + +sed -i "s|/var/www/html|/var/www/html${SERVE_PATH}|g" /etc/nginx/nginx.conf + +/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/config/fpm-pool.conf b/config/fpm-pool.conf new file mode 100644 index 0000000..4be2061 --- /dev/null +++ b/config/fpm-pool.conf @@ -0,0 +1,56 @@ +[global] +; Log to stderr +error_log = /dev/stderr + +[www] +; The address on which to accept FastCGI requests. +; Valid syntaxes are: +; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on +; a specific port; +; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on +; a specific port; +; 'port' - to listen on a TCP socket to all addresses +; (IPv6 and IPv4-mapped) on a specific port; +; '/path/to/unix/socket' - to listen on a unix socket. +; Note: This value is mandatory. +listen = /run/php-fpm.sock + +; Enable status page +pm.status_path = /fpm-status + +; Ondemand process manager +pm = ondemand + +; The number of child processes to be created when pm is set to 'static' and the +; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'. +; This value sets the limit on the number of simultaneous requests that will be +; served. Equivalent to the ApacheMaxClients directive with mpm_prefork. +; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP +; CGI. The below defaults are based on a server without much resources. Don't +; forget to tweak pm.* to fit your needs. +; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand' +; Note: This value is mandatory. +pm.max_children = 100 + +; The number of seconds after which an idle process will be killed. +; Note: Used only when pm is set to 'ondemand' +; Default Value: 10s +pm.process_idle_timeout = 10s; + +; The number of requests each child process should execute before respawning. +; This can be useful to work around memory leaks in 3rd party libraries. For +; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS. +; Default Value: 0 +pm.max_requests = 1000 + +; Make sure the FPM workers can reach the environment variables for configuration +clear_env = no + +; Catch output from PHP +catch_workers_output = yes + +; Remove the 'child 10 said into stderr' prefix in the log and only show the actual message +decorate_workers_output = no + +; Enable ping page to use in healthcheck +ping.path = /fpm-ping diff --git a/config/nginx.conf b/config/nginx.conf new file mode 100644 index 0000000..fd61217 --- /dev/null +++ b/config/nginx.conf @@ -0,0 +1,101 @@ +worker_processes auto; +error_log stderr warn; +pid /run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + # Define custom log format to include reponse times + log_format main_timed '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + '$request_time $upstream_response_time $pipe $upstream_cache_status'; + + access_log /dev/stdout main_timed; + error_log /dev/stderr notice; + + keepalive_timeout 65; + + # Write temporary files to /tmp so they can be created as a non-privileged user + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + # Default server definition + server { + listen [::]:8080 default_server; + listen 8080 default_server; + server_name _; + + sendfile off; + tcp_nodelay on; + absolute_redirect off; + + root /var/www/html; + index index.php index.html; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to index.php + try_files $uri $uri/ /index.php?q=$uri&$args; + } + + # Redirect server error pages to the static page /50x.html + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /var/lib/nginx/html; + } + + # Pass the PHP scripts to PHP-FPM listening on php-fpm.sock + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:/run/php-fpm.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_index index.php; + include fastcgi_params; + } + + location ~* \.(jpg|jpeg|gif|png|css|js|ico|xml)$ { + expires 5d; + } + + # Deny access to . files, for security + location ~ /\. { + log_not_found off; + deny all; + } + + # Allow fpm ping and status from localhost + location ~ ^/(fpm-status|fpm-ping)$ { + access_log off; + allow 127.0.0.1; + deny all; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_pass unix:/run/php-fpm.sock; + } + } + + # Hardening + proxy_hide_header X-Powered-By; + fastcgi_hide_header X-Powered-By; + server_tokens off; + + gzip on; + gzip_proxied any; + gzip_types text/plain application/xml text/css text/js text/xml application/x-javascript text/javascript application/json application/xml+rss; + gzip_vary on; + gzip_disable "msie6"; + + # Include other server configs + include /etc/nginx/conf.d/*.conf; +} diff --git a/config/php.ini b/config/php.ini new file mode 100644 index 0000000..aacb71d --- /dev/null +++ b/config/php.ini @@ -0,0 +1,4 @@ +[Date] +date.timezone="UTC" +expose_php=Off + diff --git a/config/supervisord.conf b/config/supervisord.conf new file mode 100644 index 0000000..002bda2 --- /dev/null +++ b/config/supervisord.conf @@ -0,0 +1,25 @@ +[supervisord] +nodaemon=true +user=nobody +logfile=/dev/null +logfile_maxbytes=0 +pidfile=/run/supervisord.pid + +[program:php-fpm] +command=php-fpm84 -F +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=false +startretries=0 + +[program:nginx] +command=nginx -g 'daemon off;' +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=false +startretries=0 +