diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..895609d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +docker-compose.yml +laravel +tests/laravel diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ca79ca5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0c671af..7f3486f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,62 +3,96 @@ on: push: branches: - main + workflow_dispatch: + inputs: + legacy: + description: 'Also build legacy images (7.2, 7.3, 7.4, 8.0, 8.1)' + required: false + default: false + type: boolean + +env: + LATEST_PHP: "8.5" + jobs: docker: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ["8.2", "8.3", "8.4", "8.5"] + variant: ["debian", "alpine"] steps: - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to DockerHub - uses: docker/login-action@v2 + - uses: actions/checkout@v7 + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Login to DockerHub + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push 8.1 - uses: docker/build-push-action@v3 + - name: Compute tags + id: meta + run: | + image="${{ vars.BUILDKIT_IMAGE }}" + if [ "${{ matrix.variant }}" = "alpine" ]; then + file=./src/docker/Dockerfile.alpine + tags="${image}:${{ matrix.php }}-alpine" + [ "${{ matrix.php }}" = "${LATEST_PHP}" ] && tags="${tags},${image}:alpine" + else + file=./src/docker/Dockerfile + tags="${image}:${{ matrix.php }}" + [ "${{ matrix.php }}" = "${LATEST_PHP}" ] && tags="${tags},${image}:latest" + fi + echo "file=${file}" >> "$GITHUB_OUTPUT" + echo "tags=${tags}" >> "$GITHUB_OUTPUT" + - name: Build and push ${{ matrix.php }} ${{ matrix.variant }} + uses: docker/build-push-action@v7 with: - file: "./src/docker/Dockerfile" + context: . + file: ${{ steps.meta.outputs.file }} push: true + pull: true platforms: linux/amd64,linux/arm64 build-args: | - phpVersion=8.1 - tags: "${{ vars.BUILDKIT_IMAGE }}:8.1" - - - name: Build and push 8.2 - uses: docker/build-push-action@v3 + phpVersion=${{ matrix.php }} + tags: ${{ steps.meta.outputs.tags }} + cache-from: type=gha,scope=${{ matrix.php }}-${{ matrix.variant }} + cache-to: type=gha,mode=max,scope=${{ matrix.php }}-${{ matrix.variant }} + + legacy: + if: ${{ github.event_name == 'workflow_dispatch' && inputs.legacy }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - { tag: "7.2", phpVersion: "7.2", file: ./src/docker/7.2/Dockerfile, platforms: linux/amd64 } + - { tag: "7.3", phpVersion: "7.3", file: ./src/docker/7.3/Dockerfile, platforms: linux/amd64 } + - { tag: "7.4", phpVersion: "7.4", file: ./src/docker/7.4/Dockerfile, platforms: linux/amd64 } + - { tag: "8.0", phpVersion: "8.0", file: ./src/docker/8.0/Dockerfile, platforms: "linux/amd64,linux/arm64" } + - { tag: "8.1", phpVersion: "8.1", file: ./src/docker/Dockerfile, platforms: "linux/amd64,linux/arm64" } + - { tag: "8.1-alpine", phpVersion: "8.1", file: ./src/docker/Dockerfile.alpine, platforms: "linux/amd64,linux/arm64" } + steps: + - uses: actions/checkout@v7 + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Login to DockerHub + uses: docker/login-action@v4 with: - file: "./src/docker/Dockerfile" - push: true - platforms: linux/amd64,linux/arm64 - build-args: | - phpVersion=8.2 - tags: "${{vars.BUILDKIT_IMAGE}}:8.2" - - - name: Build and push 8.3 - uses: docker/build-push-action@v3 + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push ${{ matrix.tag }} + uses: docker/build-push-action@v7 with: - file: "./src/docker/Dockerfile" + context: . + file: ${{ matrix.file }} push: true - platforms: linux/amd64,linux/arm64 + platforms: ${{ matrix.platforms }} build-args: | - phpVersion=8.3 - tags: | - "${{vars.BUILDKIT_IMAGE}}:8.3" - - - name: Build and push 8.4 - uses: docker/build-push-action@v3 - with: - file: "./src/docker/Dockerfile" - push: true - platforms: linux/amd64,linux/arm64 - build-args: | - phpVersion=8.4 - tags: | - "${{vars.BUILDKIT_IMAGE}}:8.4" - "${{vars.BUILDKIT_IMAGE}}:latest" + phpVersion=${{ matrix.phpVersion }} + tags: ${{ vars.BUILDKIT_IMAGE }}:${{ matrix.tag }} diff --git a/.github/workflows/manual-build.yml b/.github/workflows/manual-build.yml deleted file mode 100644 index 0610d60..0000000 --- a/.github/workflows/manual-build.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: Manual-build -on: - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - inputs: - docker_tag: - description: 'Docker tag' - required: true - default: 'latest' - type: string -env: - BUILDKIT_IMAGE: jkaninda/nginx-php-fpm -jobs: - docker: - runs-on: ubuntu-latest - steps: - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push 7.2 - uses: docker/build-push-action@v3 - with: - file: "./src/docker/7.2/Dockerfile" - push: true - tags: "${{env.BUILDKIT_IMAGE}}:7.2" - - - name: Build and push 7.3 - uses: docker/build-push-action@v3 - with: - file: "./src/docker/7.3/Dockerfile" - push: true - tags: "${{env.BUILDKIT_IMAGE}}:7.3" - - - name: Build and push 7.4 - uses: docker/build-push-action@v3 - with: - file: "./src/docker/7.4/Dockerfile" - push: true - tags: "${{env.BUILDKIT_IMAGE}}:7.4" - - - name: Build and push 8.0 - uses: docker/build-push-action@v3 - with: - file: "./src/docker/8.0/Dockerfile" - push: true - platforms: linux/amd64,linux/arm64 - tags: "${{env.BUILDKIT_IMAGE}}:8.0" - - - name: Build and push 8.1 - uses: docker/build-push-action@v3 - with: - file: "./src/docker/Dockerfile" - push: true - platforms: linux/amd64,linux/arm64 - build-args: | - phpVersion=8.1 - tags: "${{ vars.BUILDKIT_IMAGE }}:8.1" - - - name: Build and push 8.2 - uses: docker/build-push-action@v3 - with: - file: "./src/docker/Dockerfile" - push: true - platforms: linux/amd64,linux/arm64 - build-args: | - phpVersion=8.2 - tags: "${{vars.BUILDKIT_IMAGE}}:8.2" - - - name: Build and push 8.3 - uses: docker/build-push-action@v3 - with: - file: "./src/docker/Dockerfile" - push: true - platforms: linux/amd64,linux/arm64 - build-args: | - phpVersion=8.3 - tags: | - "${{vars.BUILDKIT_IMAGE}}:8.3" - "${{vars.BUILDKIT_IMAGE}}:latest" - - - name: Build and push 8.4 RC - uses: docker/build-push-action@v3 - with: - file: "./src/docker/Dockerfile" - push: true - platforms: linux/amd64,linux/arm64 - build-args: | - phpVersion=8.4.0RC4 - tags: | - "${{vars.BUILDKIT_IMAGE}}:8.4.0RC4" \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d909d37..00a1136 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,53 +1,31 @@ -name: Test +name: Tests on: push: + pull_request: + jobs: integration: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ["8.2", "8.3", "8.4", "8.5"] + variant: ["debian", "alpine"] steps: - - uses: actions/checkout@v4 - # Build the Docker image - - name: Build Docker Image - run: | - docker buildx build -f src/docker/Dockerfile --build-arg phpVersion=8.4 -t ${{ vars.BUILDKIT_IMAGE }}:latest --load . - - name: Verify Docker images - run: | - docker images - - name: Create Laravel project - run: | - composer create-project laravel/laravel laravel - - name: Fix Permission - run: chmod -R 777 ./laravel/storage - - name: Run docker-compose - run: - cp ./tests/compose.yaml compose.yaml && - docker compose -f "compose.yaml" up -d - - name: Create script.js for K6 test - run: | - touch script.js && cat > script.js < /dev/null ; do - echo "Waiting for database connection..." - sleep 5 - done - - name: Laravel database migration test - run: | - docker exec nginx-php-fpm php artisan migrate \ No newline at end of file + - uses: actions/checkout@v7 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Build Docker image + uses: docker/build-push-action@v7 + with: + context: . + file: ${{ matrix.variant == 'alpine' && './src/docker/Dockerfile.alpine' || './src/docker/Dockerfile' }} + load: true + build-args: | + phpVersion=${{ matrix.php }} + tags: nginx-php-fpm:test + cache-from: type=gha,scope=${{ matrix.php }}-${{ matrix.variant }} + - name: Run integration tests + env: + IMAGE: nginx-php-fpm:test + run: ./tests/integration.sh diff --git a/.gitignore b/.gitignore index 94d3c9c..1f72e37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_Store /.history laravel -docker-compose.yml \ No newline at end of file +docker-compose.yml +tests/modules.txt diff --git a/Makefile b/Makefile index 478ed80..a9ebb53 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,41 @@ IMAGE_NAME?=jkaninda/nginx-php-fpm +PHP_VERSIONS?=8.2 8.3 8.4 8.5 +LATEST_PHP?=8.5 + .PHONY: all all: build + ##@ Build .PHONY: build -build: build-81 build-82 build-83 build-84 -.PHONY: build-80 -build-80: - docker build --build-arg phpVersion=8.0 -f src/docker/8.0/Dockerfile -t ${IMAGE_NAME}:8.0 . -.PHONY: build-81 -build-81: - docker build --build-arg phpVersion=8.1 -f src/docker/Dockerfile -t ${IMAGE_NAME}:8.1 . -.PHONY: build-82 -build-82: - docker build --build-arg phpVersion=8.2 -f src/docker/Dockerfile -t ${IMAGE_NAME}:8.2 . -.PHONY: build-83 -build-83: - docker build --build-arg phpVersion=8.3 -f src/docker/Dockerfile -t ${IMAGE_NAME}:8.3 . -.PHONY: build-84 -build-84: - docker build --build-arg phpVersion=8.4 -f src/docker/Dockerfile -t ${IMAGE_NAME}:8.4 . +build: $(addprefix build-,$(PHP_VERSIONS)) + +.PHONY: build-alpine +build-alpine: $(addsuffix -alpine,$(addprefix build-,$(PHP_VERSIONS))) + +.PHONY: build-latest +build-latest: build-$(LATEST_PHP) build-$(LATEST_PHP)-alpine + docker tag ${IMAGE_NAME}:$(LATEST_PHP) ${IMAGE_NAME}:latest + docker tag ${IMAGE_NAME}:$(LATEST_PHP)-alpine ${IMAGE_NAME}:alpine + +# Legacy images (7.2, 7.3, 7.4, 8.0). PHP 8.1 is legacy too but uses the +# shared Dockerfiles: make build-8.1 / make build-8.1-alpine +.PHONY: build-7.2 build-7.3 build-7.4 build-8.0 +build-7.2 build-7.3 build-7.4 build-8.0: build-%: + docker build -f src/docker/$*/Dockerfile -t ${IMAGE_NAME}:$* . + +.PHONY: build-%-alpine +build-%-alpine: + docker build --build-arg phpVersion=$* -f src/docker/Dockerfile.alpine -t ${IMAGE_NAME}:$*-alpine . + +.PHONY: build-% +build-%: + docker build --build-arg phpVersion=$* -f src/docker/Dockerfile -t ${IMAGE_NAME}:$* . + +##@ Test +.PHONY: test +test: build-$(LATEST_PHP) + IMAGE=${IMAGE_NAME}:$(LATEST_PHP) ./tests/integration.sh + +.PHONY: test-alpine +test-alpine: build-$(LATEST_PHP)-alpine + IMAGE=${IMAGE_NAME}:$(LATEST_PHP)-alpine ./tests/integration.sh diff --git a/README.md b/README.md index d647524..b759839 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,15 @@ A **ready-to-use container** designed for running PHP-based applications, includ #### **Features** - **PHP Application Support**: Optimized to run Laravel or any PHP-based applications. +- **Nginx and PHP-FPM in one container**, managed by Supervisord. - **Integrated Extensions**: - **Database**: MySQL and PostgreSQL. - **Caching**: Redis and Memcached. - **Messaging**: Kafka for event-driven architecture. - - **Task Scheduling**: Laravel Scheduler and Cron jobs support. -- **Custom Configuration**: Pre-configured with sensible defaults, allowing seamless customization. -- **Event Handling**: Support for advanced event-driven processes. + - **Task Scheduling**: Laravel Scheduler and Queue Worker started automatically. +- **Custom Configuration**: Pre-configured with sensible defaults, tunable through environment variables. +- **Debian and Alpine variants**, multi-arch (`linux/amd64`, `linux/arm64`). +- **Production-ready**: healthcheck, graceful shutdown, OPcache, logs on stdout, `supervisorctl` support. - **Optimized for Microservices**: Built with modern PHP microservices in mind. This image is ideal for developers looking for a streamlined, high-performance solution to deploy PHP applications with essential tools already integrated. @@ -26,35 +28,47 @@ This image is ideal for developers looking for a streamlined, high-performance s - [Docker Hub](https://hub.docker.com/r/jkaninda/nginx-php-fpm) - [GitHub Repository](https://github.com/jkaninda/nginx-php-fpm) -## **Supported PHP Versions** -- 8.4 -- 8.3 -- 8.2 -- 8.1 -- 8.0 -- 7.4 -- 7.2 +## **Supported PHP Versions & Tags** -## **Specifications** -This Docker image comes pre-installed with the following: -- **PHP Extensions**: - - Composer - - OpenSSL - - XML - - PDO - - Rdkafka - - Redis - - Mbstring - - PCNTL - - ZIP - - GD - - BCMath - - Memcached +| PHP | Debian tag | Alpine tag | +|-----|------------|------------| +| 8.5 | `8.5`, `latest` | `8.5-alpine`, `alpine` | +| 8.4 | `8.4` | `8.4-alpine` | +| 8.3 | `8.3` | `8.3-alpine` | +| 8.2 | `8.2` | `8.2-alpine` | -- **Additional Features**: - - Laravel Cron Jobs & Scheduler - - Supervisord - - Node.js & NPM +Legacy images (PHP is end-of-life, no longer rebuilt automatically or tested): `8.1`, `8.1-alpine`, `8.0`, `7.4`, `7.3`, `7.2`. + +--- + +## **Specifications** + +### **PHP Extensions** +- Composer 2 +- OpenSSL +- XML +- PDO (MySQL, PostgreSQL and SQLite) +- Rdkafka +- Redis +- Mbstring +- Intl +- Exif +- PCNTL +- ZIP +- GD (FreeType, JPEG, PNG, WebP) +- BCMath +- Memcached +- Opcache + +### **Additional Features** +- Nginx +- Laravel Scheduler +- Laravel Queue Worker +- Supervisord +- Node.js 24 LTS and NPM +- Git, Zip/Unzip, SQLite CLI + +--- ## **Basic Usage with Docker Compose** @@ -62,17 +76,14 @@ This Docker image comes pre-installed with the following: ```yaml services: app: - image: jkaninda/nginx-php-fpm:8.3 + image: jkaninda/nginx-php-fpm:8.5 container_name: app restart: unless-stopped - user: www-data # Optional for production volumes: # Project root - ./src:/var/www/html ports: - "80:80" - networks: - - default ``` ### **Commands** @@ -84,27 +95,27 @@ docker compose up -d #### Create a new Laravel project: ```sh -docker compose exec app composer create-project --prefer-dist laravel/laravel . +docker compose exec -u www-data app composer create-project --prefer-dist laravel/laravel . ``` #### Generate application key: ```sh -docker compose exec app php artisan key:generate +docker compose exec -u www-data app php artisan key:generate ``` #### Create a storage symlink: ```sh -docker compose exec app php artisan storage:link +docker compose exec -u www-data app php artisan storage:link ``` #### Fix storage and cache permissions: ```sh -docker compose exec app chmod -R 777 storage bootstrap/cache +docker compose exec app chown -R www-data:www-data storage bootstrap/cache ``` #### Run Laravel migrations: ```sh -docker compose exec app php artisan migrate +docker compose exec -u www-data app php artisan migrate ``` #### Access the container shell: @@ -112,37 +123,91 @@ docker compose exec app php artisan migrate docker exec -it app bash ``` +#### Run a one-off command (Nginx and PHP-FPM are not started): +```sh +docker run --rm -v "$PWD:/var/www/html" jkaninda/nginx-php-fpm composer install +``` + +#### Restart the queue workers after a deployment: +```sh +docker compose exec app supervisorctl restart Laravel-worker:* +``` + --- -## **Advanced Nginx PHP-FPM Setup** +## **Environment Variables** + +### Nginx + +| Variable | Default | Description | +|----------|---------|-------------| +| `DOCUMENT_ROOT` | `$APP_DIR/public` if it exists, otherwise `$APP_DIR` | Nginx web root | +| `DOMAIN` | `_` | Nginx `server_name` | +| `CLIENT_MAX_BODY_SIZE` | `15M` | Nginx `client_max_body_size` | + +### Laravel + +| Variable | Default | Description | +|----------|---------|-------------| +| `LARAVEL_SCHEDULER_ENABLED` | `true` | Run `php artisan schedule:run` every minute | +| `LARAVEL_QUEUE_WORKER_ENABLED` | `true` | Run `php artisan queue:work` | +| `LARAVEL_QUEUE_OPTIONS` | `--sleep=3 --tries=3` | Options passed to `queue:work` (e.g. `redis --queue=high,default`) | +| `LARAVEL_PROCS_NUMBER` | `1` | Number of queue worker processes | +| `APP_DIR` | `/var/www/html` | Application directory (where `artisan` is looked up) | + +The scheduler and queue worker are only started when an `artisan` file is found in `APP_DIR`. + +### PHP + +| Variable | Default | +|----------|---------| +| `PHP_MEMORY_LIMIT` | `256M` | +| `PHP_UPLOAD_MAX_FILESIZE` | `80M` | +| `PHP_POST_MAX_SIZE` | `80M` | +| `PHP_MAX_EXECUTION_TIME` | `30` | +| `PHP_DATE_TIMEZONE` | `UTC` | +| `PHP_DISPLAY_ERRORS` | `Off` | + +### OPcache + +| Variable | Default | Description | +|----------|---------|-------------| +| `PHP_OPCACHE_ENABLE` | `1` | | +| `PHP_OPCACHE_ENABLE_CLI` | `0` | | +| `PHP_OPCACHE_MEMORY_CONSUMPTION` | `512` | MB | +| `PHP_OPCACHE_MAX_ACCELERATED_FILES` | `50000` | | +| `PHP_OPCACHE_VALIDATE_TIMESTAMPS` | `0` | Set to `1` in development so code changes are picked up | +| `PHP_OPCACHE_REVALIDATE_FREQ` | `0` | | + +### PHP-FPM + +| Variable | Default | +|----------|---------| +| `PHP_FPM_PM` | `dynamic` | +| `PHP_FPM_PM_MAX_CHILDREN` | `20` | +| `PHP_FPM_PM_START_SERVERS` | `2` | +| `PHP_FPM_PM_MIN_SPARE_SERVERS` | `1` | +| `PHP_FPM_PM_MAX_SPARE_SERVERS` | `3` | +| `PHP_FPM_PM_MAX_REQUESTS` | `500` | + +Container environment variables (e.g. `DB_HOST`, `APP_KEY`) are passed to PHP-FPM workers (`clear_env = no`). + +### Development example -### **Extended `docker-compose.yml` Example** ```yaml -version: '3' services: app: - image: jkaninda/nginx-php-fpm - container_name: app - restart: unless-stopped + image: jkaninda/nginx-php-fpm:8.5 ports: - "80:80" - volumes: - # Project root - - ./:/var/www/html - - ~/.ssh:/root/.ssh # Use private CVS if needed - # Optional custom PHP config - # - ./php.ini:/usr/local/etc/php/conf.d/php.ini environment: - - APP_ENV=development # or production - - LARAVEL_PROCS_NUMBER=2 # Optional: Queue worker processes - # - CLIENT_MAX_BODY_SIZE=20M # Optional - # - DOMAIN=example.com # Optional - - DOCUMENT_ROOT=/var/www/html # Optional -``` - -### **Default Web Root** -``` -/var/www/html + PHP_OPCACHE_VALIDATE_TIMESTAMPS: 1 + PHP_DISPLAY_ERRORS: "On" + CLIENT_MAX_BODY_SIZE: 20M + LARAVEL_QUEUE_OPTIONS: "redis --sleep=3 --tries=3" + LARAVEL_PROCS_NUMBER: 2 + volumes: + - ./:/var/www/html ``` --- @@ -151,7 +216,7 @@ services: ### **Dockerfile** ```Dockerfile -FROM jkaninda/nginx-php-fpm:8.3 +FROM jkaninda/nginx-php-fpm:8.5 # Copy Laravel project files COPY . /var/www/html # Storage Volume @@ -161,23 +226,32 @@ WORKDIR /var/www/html # Fix permissions RUN chown -R www-data:www-data /var/www/html - -USER www-data ``` --- ## **Custom Nginx Configuration** -To enable custom Nginx configurations, use the following files: -- `/var/www/html/conf/nginx/nginx.conf` -- `/var/www/html/conf/nginx/nginx-site.conf` +To use custom Nginx configurations, add the following files to your project: +- `conf/nginx/nginx.conf`: replaces `/etc/nginx/nginx.conf` +- `conf/nginx/nginx-site.conf`: replaces the generated server block. PHP-FPM listens on `127.0.0.1:9000`. + +--- + +## **Custom PHP Configuration** + +Most common settings can be changed with [environment variables](#environment-variables). +For anything else, either: + +- mount or copy your own `.ini` file into `/usr/local/etc/php/conf.d/`, or +- place a `php.ini` file at `conf/php/php.ini` in your project; it is copied into `conf.d` at startup. --- ## **Supervisord Integration** Supervisord can be used to manage tasks or processes within the container. +Place your programs in `conf/worker/supervisor.conf` in your project, or mount them into `/etc/supervisor/conf.d/`. ### **Example Configuration for Kafka Consumer** ```conf @@ -196,16 +270,23 @@ stdout_logfile=/var/www/html/storage/logs/kafka.log ## **Fixing Storage Permissions** -If you encounter storage permission issues, run the following commands: +`www-data` has UID/GID `1000` in both Debian and Alpine variants. If you encounter permission issues, run: ```sh -docker compose exec php-fpm /bin/bash +docker compose exec app chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache +docker compose exec app chmod -R 775 /var/www/html/storage ``` -Then inside the container: +To match your host user, rebuild with `--build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g)`. + +--- + +## **Building & Testing Locally** + ```sh -chown -R www-data:www-data /var/www/html/ -chmod -R 775 /var/www/html/storage +make build-8.5 # Debian image +make build-8.5-alpine # Alpine image +make test # Integration test (Laravel + MySQL) ``` --- @@ -236,5 +317,3 @@ Whether you're managing internal APIs or exposing public endpoints, **Goma Gatew ## ⭐️ **Support the Project** If this project helped you, do not skip on giving it a star. Thanks! - - diff --git a/build.sh b/build.sh index 4ae1943..b3464df 100755 --- a/build.sh +++ b/build.sh @@ -1,17 +1,17 @@ #!/bin/bash -if [ $# -eq 0 ] - then - tag='latest' - else - tag=$1 -fi -if [ $tag != 'latest' ] -then - echo 'Build from tag' - docker build -f src/docker/${tag}/Dockerfile -t jkaninda/nginx-php-fpm:$tag . +# Usage: ./build.sh [php-version] (default: 8.5) +# ./build.sh 8.4-alpine +set -e +tag=${1:-8.5} +version=${tag%-alpine} + +if [ -f "src/docker/${version}/Dockerfile" ]; then + echo "Build legacy image ${tag}" + docker build -f "src/docker/${version}/Dockerfile" -t "jkaninda/nginx-php-fpm:${tag}" . +elif [ "${tag}" != "${version}" ]; then + echo "Build ${tag}" + docker build --build-arg phpVersion="${version}" -f src/docker/Dockerfile.alpine -t "jkaninda/nginx-php-fpm:${tag}" . else - echo 'Build latest' - docker build -f src/docker/8.3/Dockerfile -t jkaninda/nginx-php-fpm:$tag . - + echo "Build ${tag}" + docker build --build-arg phpVersion="${version}" -f src/docker/Dockerfile -t "jkaninda/nginx-php-fpm:${tag}" . fi -docker compose up -d --force-recreate diff --git a/src/default.conf b/src/default.conf deleted file mode 100644 index aedbfc0..0000000 --- a/src/default.conf +++ /dev/null @@ -1,41 +0,0 @@ - -server { - listen 80 default_server; - listen [::]:80 default_server; - server_name _; - # Add index.php to setup Nginx, PHP & PHP-FPM config - index index.php index.html index.htm index.nginx-debian.html; error_log /var/log/nginx/error.log; - access_log /var/log/nginx/access.log; - root /var/www/html; - # pass PHP scripts on Nginx to FastCGI (PHP-FPM) server - location ~ \.php$ { - try_files $uri =404; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - # Nginx php-fpm config: - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PATH_INFO $fastcgi_path_info; - - } - client_max_body_size 15M; - server_tokens off; - - # Hide PHP headers - fastcgi_hide_header X-Powered-By; - fastcgi_hide_header X-CF-Powered-By; - fastcgi_hide_header X-Runtime; - - location / { - try_files $uri $uri/ /index.php?$query_string; - gzip_static on; - } - # deny access to Apache .htaccess on Nginx with PHP, - # if Apache and Nginx document roots concur - location ~ /\.ht {deny all;} - location ~ /\.svn/ {deny all;} - location ~ /\.git/ {deny all;} - location ~ /\.hg/ {deny all;} - location ~ /\.bzr/ {deny all;} -} \ No newline at end of file diff --git a/src/docker/7.2/Dockerfile b/src/docker/7.2/Dockerfile index 8c1516c..daf6ba0 100644 --- a/src/docker/7.2/Dockerfile +++ b/src/docker/7.2/Dockerfile @@ -1,7 +1,19 @@ FROM php:7.2-fpm ARG WORKDIR=/var/www/html -ENV DOCUMENT_ROOT=${WORKDIR} ENV LARAVEL_PROCS_NUMBER=1 +ENV APP_DIR=/var/www/html \ + PHP_DATE_TIMEZONE=UTC \ + PHP_DISPLAY_ERRORS=Off \ + PHP_MEMORY_LIMIT=256M \ + PHP_UPLOAD_MAX_FILESIZE=80M \ + PHP_POST_MAX_SIZE=80M \ + PHP_MAX_EXECUTION_TIME=30 \ + PHP_OPCACHE_ENABLE=1 \ + PHP_OPCACHE_ENABLE_CLI=0 \ + PHP_OPCACHE_MEMORY_CONSUMPTION=512 \ + PHP_OPCACHE_MAX_ACCELERATED_FILES=50000 \ + PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 \ + PHP_OPCACHE_REVALIDATE_FREQ=0 ENV DOMAIN=_ ENV CLIENT_MAX_BODY_SIZE=15M ENV NODE_VERSION=17.x @@ -84,7 +96,6 @@ RUN rm -rf /etc/nginx/sites-available/default RUN rm -rf /etc/nginx/nginx.conf COPY src/nginx.conf /etc/nginx/nginx.conf -COPY src/default.conf /etc/nginx/conf.d/ RUN usermod -u ${USER_ID} ${USER_NAME} RUN groupmod -g ${USER_ID} ${GROUP_NAME} diff --git a/src/docker/7.3/Dockerfile b/src/docker/7.3/Dockerfile index 2a4f567..158cf8b 100644 --- a/src/docker/7.3/Dockerfile +++ b/src/docker/7.3/Dockerfile @@ -1,7 +1,19 @@ FROM php:7.3-fpm ARG WORKDIR=/var/www/html -ENV DOCUMENT_ROOT=${WORKDIR} ENV LARAVEL_PROCS_NUMBER=1 +ENV APP_DIR=/var/www/html \ + PHP_DATE_TIMEZONE=UTC \ + PHP_DISPLAY_ERRORS=Off \ + PHP_MEMORY_LIMIT=256M \ + PHP_UPLOAD_MAX_FILESIZE=80M \ + PHP_POST_MAX_SIZE=80M \ + PHP_MAX_EXECUTION_TIME=30 \ + PHP_OPCACHE_ENABLE=1 \ + PHP_OPCACHE_ENABLE_CLI=0 \ + PHP_OPCACHE_MEMORY_CONSUMPTION=512 \ + PHP_OPCACHE_MAX_ACCELERATED_FILES=50000 \ + PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 \ + PHP_OPCACHE_REVALIDATE_FREQ=0 ENV DOMAIN=_ ENV CLIENT_MAX_BODY_SIZE=15M ENV NODE_VERSION=17.x @@ -85,7 +97,6 @@ RUN rm -rf /etc/nginx/sites-available/default RUN rm -rf /etc/nginx/nginx.conf COPY src/nginx.conf /etc/nginx/nginx.conf -COPY src/default.conf /etc/nginx/conf.d/ RUN usermod -u ${USER_ID} ${USER_NAME} RUN groupmod -g ${USER_ID} ${GROUP_NAME} diff --git a/src/docker/7.4/Dockerfile b/src/docker/7.4/Dockerfile index 4e0adef..62c8707 100644 --- a/src/docker/7.4/Dockerfile +++ b/src/docker/7.4/Dockerfile @@ -1,7 +1,19 @@ FROM php:7.4-fpm ARG WORKDIR=/var/www/html -ENV DOCUMENT_ROOT=${WORKDIR} ENV LARAVEL_PROCS_NUMBER=1 +ENV APP_DIR=/var/www/html \ + PHP_DATE_TIMEZONE=UTC \ + PHP_DISPLAY_ERRORS=Off \ + PHP_MEMORY_LIMIT=256M \ + PHP_UPLOAD_MAX_FILESIZE=80M \ + PHP_POST_MAX_SIZE=80M \ + PHP_MAX_EXECUTION_TIME=30 \ + PHP_OPCACHE_ENABLE=1 \ + PHP_OPCACHE_ENABLE_CLI=0 \ + PHP_OPCACHE_MEMORY_CONSUMPTION=512 \ + PHP_OPCACHE_MAX_ACCELERATED_FILES=50000 \ + PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 \ + PHP_OPCACHE_REVALIDATE_FREQ=0 ENV DOMAIN=_ ENV CLIENT_MAX_BODY_SIZE=15M ENV NODE_VERSION=17.x @@ -84,7 +96,6 @@ RUN rm -rf /etc/nginx/sites-available/default RUN rm -rf /etc/nginx/nginx.conf COPY src/nginx.conf /etc/nginx/nginx.conf -COPY src/default.conf /etc/nginx/conf.d/ RUN usermod -u ${USER_ID} ${USER_NAME} RUN groupmod -g ${USER_ID} ${GROUP_NAME} diff --git a/src/docker/8.0/Dockerfile b/src/docker/8.0/Dockerfile index ec0095f..9014642 100644 --- a/src/docker/8.0/Dockerfile +++ b/src/docker/8.0/Dockerfile @@ -1,7 +1,19 @@ FROM php:8.0-fpm ARG WORKDIR=/var/www/html -ENV DOCUMENT_ROOT=${WORKDIR} ENV LARAVEL_PROCS_NUMBER=1 +ENV APP_DIR=/var/www/html \ + PHP_DATE_TIMEZONE=UTC \ + PHP_DISPLAY_ERRORS=Off \ + PHP_MEMORY_LIMIT=256M \ + PHP_UPLOAD_MAX_FILESIZE=80M \ + PHP_POST_MAX_SIZE=80M \ + PHP_MAX_EXECUTION_TIME=30 \ + PHP_OPCACHE_ENABLE=1 \ + PHP_OPCACHE_ENABLE_CLI=0 \ + PHP_OPCACHE_MEMORY_CONSUMPTION=512 \ + PHP_OPCACHE_MAX_ACCELERATED_FILES=50000 \ + PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 \ + PHP_OPCACHE_REVALIDATE_FREQ=0 ENV DOMAIN=_ ENV CLIENT_MAX_BODY_SIZE=15M ENV NODE_VERSION=17.x @@ -96,7 +108,6 @@ RUN rm -rf /etc/nginx/sites-available/default RUN rm -rf /etc/nginx/nginx.conf COPY src/nginx.conf /etc/nginx/nginx.conf -COPY src/default.conf /etc/nginx/conf.d/ RUN usermod -u ${USER_ID} ${USER_NAME} RUN groupmod -g ${USER_ID} ${GROUP_NAME} diff --git a/src/docker/Dockerfile b/src/docker/Dockerfile index dba39bd..97b01cf 100644 --- a/src/docker/Dockerfile +++ b/src/docker/Dockerfile @@ -1,123 +1,109 @@ -ARG phpVersion=8.3-dev +# syntax=docker/dockerfile:1 +ARG phpVersion=8.5 +FROM composer:2 AS composer + FROM php:${phpVersion}-fpm ARG WORKDIR=/var/www/html -ENV DOCUMENT_ROOT=${WORKDIR} -ENV LARAVEL_PROCS_NUMBER=1 -ENV DOMAIN=_ -ENV CLIENT_MAX_BODY_SIZE=15M -ENV NODE_VERSION=20.x ARG GROUP_ID=1000 ARG USER_ID=1000 -ENV USER_NAME=www-data ARG GROUP_NAME=www-data -# Install system dependencies -RUN apt-get update && apt-get install -y \ - git \ - curl \ - libfreetype6-dev \ - libjpeg62-turbo-dev \ - libmemcached-dev \ - libzip-dev \ - libpng-dev \ - libonig-dev \ - libxml2-dev \ - librdkafka-dev \ - libpq-dev \ - openssh-server \ - zip \ - unzip \ - supervisor \ - sqlite3 \ - nano \ - cron +ARG NODE_MAJOR=24 -RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION} | bash - - # Install Node -RUN apt-get install -y nodejs -# Install nginx -RUN apt-get update && apt-get install -y nginx +LABEL org.opencontainers.image.title="nginx-php-fpm" \ + org.opencontainers.image.description="Nginx and PHP-FPM image for Laravel and PHP applications" \ + org.opencontainers.image.source="https://github.com/jkaninda/nginx-php-fpm" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.authors="Jonas Kaninda" -# Clear cache -RUN apt-get clean && rm -rf /var/lib/apt/lists/* -# Install Kafka -RUN git clone https://github.com/arnaud-lb/php-rdkafka.git\ - && cd php-rdkafka \ - && phpize \ - && ./configure \ - && make all -j 5 \ - && make install +ENV APP_DIR=${WORKDIR} \ + USER_NAME=www-data \ + NODE_MAJOR=${NODE_MAJOR} \ + DOMAIN=_ \ + CLIENT_MAX_BODY_SIZE=15M \ + LARAVEL_PROCS_NUMBER=1 \ + LARAVEL_QUEUE_WORKER_ENABLED=true \ + LARAVEL_QUEUE_OPTIONS="--sleep=3 --tries=3" \ + LARAVEL_SCHEDULER_ENABLED=true \ + PHP_DATE_TIMEZONE=UTC \ + PHP_DISPLAY_ERRORS=Off \ + PHP_MEMORY_LIMIT=256M \ + PHP_UPLOAD_MAX_FILESIZE=80M \ + PHP_POST_MAX_SIZE=80M \ + PHP_MAX_EXECUTION_TIME=30 \ + PHP_OPCACHE_ENABLE=1 \ + PHP_OPCACHE_ENABLE_CLI=0 \ + PHP_OPCACHE_MEMORY_CONSUMPTION=512 \ + PHP_OPCACHE_MAX_ACCELERATED_FILES=50000 \ + PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 \ + PHP_OPCACHE_REVALIDATE_FREQ=0 \ + PHP_FPM_PM=dynamic \ + PHP_FPM_PM_MAX_CHILDREN=20 \ + PHP_FPM_PM_START_SERVERS=2 \ + PHP_FPM_PM_MIN_SPARE_SERVERS=1 \ + PHP_FPM_PM_MAX_SPARE_SERVERS=3 \ + PHP_FPM_PM_MAX_REQUESTS=500 -# Install Rdkafka and enable it -RUN docker-php-ext-enable rdkafka \ - && cd .. \ - && rm -rf /php-rdkafka +# Runtime tools, Nginx and Node.js +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + ca-certificates curl gnupg git openssh-client unzip zip \ + nginx supervisor cron sqlite3 nano; \ + mkdir -p /etc/apt/keyrings; \ + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \ + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" > /etc/apt/sources.list.d/nodesource.list; \ + apt-get update; \ + apt-get install -y --no-install-recommends nodejs; \ + rm -rf /var/lib/apt/lists/* -# Install PHP extensions zip, mbstring, exif, bcmath, intl -RUN docker-php-ext-configure gd --with-freetype --with-jpeg -RUN docker-php-ext-install zip mbstring exif pcntl bcmath -j$(nproc) gd intl +# PHP extensions (build dependencies are purged, runtime libraries are kept) +RUN set -eux; \ + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + libfreetype6-dev libjpeg62-turbo-dev libpng-dev libwebp-dev \ + libicu-dev libmemcached-dev libonig-dev libpq-dev librdkafka-dev \ + libxml2-dev libzip-dev zlib1g-dev; \ + docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp; \ + docker-php-ext-install -j"$(nproc)" bcmath exif gd intl mbstring pcntl pdo_mysql pdo_pgsql zip; \ + pecl install redis memcached rdkafka; \ + docker-php-ext-enable redis memcached rdkafka; \ + # OPcache is always compiled in since PHP 8.5 + if ! php -m | grep -qi '^zend opcache$'; then docker-php-ext-install opcache; fi; \ + apt-mark auto '.*' > /dev/null; \ + apt-mark manual $savedAptMark > /dev/null; \ + find /usr/local -type f \( -executable -o -name '*.so' \) -exec ldd '{}' ';' 2>/dev/null \ + | awk '/=>/ { so = $(NF-1); if (index(so, "/usr/local/") == 1) { next }; gsub("^/(usr/)?", "", so); printf "*%s\n", so }' \ + | sort -u | xargs -r dpkg-query --search | awk -F: '!/^diversion/ { print $1 }' | sort -u | xargs -r apt-mark manual; \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + rm -rf /var/lib/apt/lists/* /tmp/pear ~/.pearrc; \ + ! php -m 2>&1 | grep -iE 'warning|unable to load' -# Install Redis and enable it -RUN pecl install redis && docker-php-ext-enable redis +COPY --from=composer /usr/bin/composer /usr/local/bin/composer - - -# Install the php memcached extension -RUN pecl install memcached && docker-php-ext-enable memcached - -# Install the PHP pdo_mysql extention -RUN docker-php-ext-install pdo_mysql - -# Install the PHP pdo_pgsql extention -RUN docker-php-ext-install pdo_pgsql - -# Install PHP Opcache extention -RUN docker-php-ext-install opcache - -# Install Composer -RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer - -# Set working directory WORKDIR $WORKDIR -RUN rm -Rf /var/www/* && \ -mkdir -p /var/www/html - -ADD src/index.php $WORKDIR/index.php -ADD src/php.ini $PHP_INI_DIR/conf.d/ -ADD src/opcache.ini $PHP_INI_DIR/conf.d/ -ADD src/supervisor/supervisord.conf /etc/supervisor/supervisord.conf - -COPY src/entrypoint.sh /usr/local/bin/ -RUN chmod +x /usr/local/bin/entrypoint.sh && \ - ln -s /usr/local/bin/entrypoint.sh / - -RUN rm -rf /etc/nginx/conf.d/default.conf && \ -rm -rf /etc/nginx/sites-enabled/default && \ - rm -rf /etc/nginx/sites-available/default && \ - rm -rf /etc/nginx/nginx.conf +RUN rm -Rf /var/www/* /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default /etc/nginx/conf.d/default.conf && \ + mkdir -p ${WORKDIR} /etc/supervisor/conf.d /var/log/supervisor /var/log/nginx /var/cache/nginx /etc/nginx/conf.d +COPY src/index.php $WORKDIR/index.php +COPY src/php.ini src/opcache.ini $PHP_INI_DIR/conf.d/ +COPY src/php-fpm.d/zz-nginx-php-fpm.conf /usr/local/etc/php-fpm.d/zz-nginx-php-fpm.conf COPY src/nginx.conf /etc/nginx/nginx.conf -COPY src/default.conf /etc/nginx/conf.d/ +COPY src/supervisor/supervisord.conf /etc/supervisor/supervisord.conf +COPY --chmod=755 src/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN ln -s /usr/local/bin/entrypoint.sh /entrypoint.sh RUN usermod -u ${USER_ID} ${USER_NAME} && \ - groupmod -g ${USER_ID} ${GROUP_NAME} + groupmod -g ${GROUP_ID} ${GROUP_NAME} && \ + touch /var/run/nginx.pid && \ + chown -R ${USER_NAME}:${GROUP_NAME} /var/www /var/log /tmp $PHP_INI_DIR/conf.d \ + /etc/supervisor/conf.d /etc/nginx/nginx.conf /etc/nginx/conf.d \ + /var/cache/nginx /var/lib/nginx /var/run/nginx.pid -RUN mkdir -p /var/log/supervisor /var/log/nginx /var/cache/nginx +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --start-interval=2s --retries=3 \ + CMD php -r 'exit(@fsockopen("127.0.0.1", 9000) && @fsockopen("127.0.0.1", 80) ? 0 : 1);' -RUN chown -R ${USER_NAME}:${GROUP_NAME} /var/www && \ - chown -R ${USER_NAME}:${GROUP_NAME} /var/log/ && \ - chown -R ${USER_NAME}:${GROUP_NAME} /etc/supervisor/conf.d/ && \ - chown -R ${USER_NAME}:${GROUP_NAME} $PHP_INI_DIR/conf.d/ && \ - touch /var/run/nginx.pid && \ - chown -R $USER_NAME:$USER_NAME /var/cache/nginx && \ - chown -R $USER_NAME:$USER_NAME /var/lib/nginx/ && \ - chown -R $USER_NAME:$USER_NAME /etc/nginx/nginx.conf && \ - chown -R $USER_NAME:$USER_NAME /var/run/nginx.pid && \ - chown -R $USER_NAME:$USER_NAME /var/log/supervisor && \ - chown -R $USER_NAME:$USER_NAME /etc/nginx/conf.d/ && \ - chown -R ${USER_NAME}:${GROUP_NAME} /tmp - - -#USER ${USER_NAME} EXPOSE 80 -ENTRYPOINT ["entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["entrypoint.sh"] +CMD ["entrypoint"] diff --git a/src/docker/Dockerfile.alpine b/src/docker/Dockerfile.alpine new file mode 100644 index 0000000..7181ab8 --- /dev/null +++ b/src/docker/Dockerfile.alpine @@ -0,0 +1,99 @@ +# syntax=docker/dockerfile:1 +ARG phpVersion=8.5 +FROM composer:2 AS composer + +FROM php:${phpVersion}-fpm-alpine +ARG WORKDIR=/var/www/html +ARG GROUP_ID=1000 +ARG USER_ID=1000 +ARG GROUP_NAME=www-data + +LABEL org.opencontainers.image.title="nginx-php-fpm" \ + org.opencontainers.image.description="Nginx and PHP-FPM image for Laravel and PHP applications (Alpine)" \ + org.opencontainers.image.source="https://github.com/jkaninda/nginx-php-fpm" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.authors="Jonas Kaninda" + +ENV APP_DIR=${WORKDIR} \ + USER_NAME=www-data \ + DOMAIN=_ \ + CLIENT_MAX_BODY_SIZE=15M \ + LARAVEL_PROCS_NUMBER=1 \ + LARAVEL_QUEUE_WORKER_ENABLED=true \ + LARAVEL_QUEUE_OPTIONS="--sleep=3 --tries=3" \ + LARAVEL_SCHEDULER_ENABLED=true \ + PHP_DATE_TIMEZONE=UTC \ + PHP_DISPLAY_ERRORS=Off \ + PHP_MEMORY_LIMIT=256M \ + PHP_UPLOAD_MAX_FILESIZE=80M \ + PHP_POST_MAX_SIZE=80M \ + PHP_MAX_EXECUTION_TIME=30 \ + PHP_OPCACHE_ENABLE=1 \ + PHP_OPCACHE_ENABLE_CLI=0 \ + PHP_OPCACHE_MEMORY_CONSUMPTION=512 \ + PHP_OPCACHE_MAX_ACCELERATED_FILES=50000 \ + PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 \ + PHP_OPCACHE_REVALIDATE_FREQ=0 \ + PHP_FPM_PM=dynamic \ + PHP_FPM_PM_MAX_CHILDREN=20 \ + PHP_FPM_PM_START_SERVERS=2 \ + PHP_FPM_PM_MIN_SPARE_SERVERS=1 \ + PHP_FPM_PM_MAX_SPARE_SERVERS=3 \ + PHP_FPM_PM_MAX_REQUESTS=500 + +# Runtime tools, Nginx and Node.js +RUN apk add --no-cache bash curl git openssh-client unzip zip nginx supervisor sqlite nano nodejs npm + +# PHP extensions (build dependencies are removed, runtime libraries are kept) +RUN set -eux; \ + apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \ + freetype-dev libjpeg-turbo-dev libpng-dev libwebp-dev \ + icu-dev libmemcached-dev oniguruma-dev libpq-dev librdkafka-dev \ + libxml2-dev libzip-dev zlib-dev cyrus-sasl-dev linux-headers; \ + docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp; \ + docker-php-ext-install -j"$(nproc)" bcmath exif gd intl mbstring pcntl pdo_mysql pdo_pgsql zip; \ + pecl install redis memcached rdkafka; \ + docker-php-ext-enable redis memcached rdkafka; \ + # OPcache is always compiled in since PHP 8.5 + if ! php -m | grep -qi '^zend opcache$'; then docker-php-ext-install opcache; fi; \ + runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ + | tr ',' '\n' | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )"; \ + apk add --no-cache --virtual .php-ext-rundeps $runDeps; \ + apk del --no-network .build-deps; \ + rm -rf /tmp/pear ~/.pearrc; \ + ! php -m 2>&1 | grep -iE 'warning|unable to load' + +COPY --from=composer /usr/bin/composer /usr/local/bin/composer + +WORKDIR $WORKDIR + +RUN rm -Rf /var/www/* /etc/nginx/http.d/default.conf && \ + mkdir -p ${WORKDIR} /etc/supervisor/conf.d /var/log/supervisor /var/log/nginx /var/cache/nginx /etc/nginx/conf.d + +COPY src/index.php $WORKDIR/index.php +COPY src/php.ini src/opcache.ini $PHP_INI_DIR/conf.d/ +COPY src/php-fpm.d/zz-nginx-php-fpm.conf /usr/local/etc/php-fpm.d/zz-nginx-php-fpm.conf +COPY src/nginx.conf /etc/nginx/nginx.conf +COPY src/supervisor/supervisord.conf /etc/supervisor/supervisord.conf +COPY --chmod=755 src/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN ln -s /usr/local/bin/entrypoint.sh /entrypoint.sh && \ + ln -sf /etc/supervisor/supervisord.conf /etc/supervisord.conf + +RUN apk add --no-cache --virtual .user-deps shadow && \ + usermod -u ${USER_ID} ${USER_NAME} && \ + groupmod -g ${GROUP_ID} ${GROUP_NAME} && \ + apk del --no-network .user-deps && \ + touch /var/run/nginx.pid && \ + chown -R ${USER_NAME}:${GROUP_NAME} /var/www /var/log /tmp $PHP_INI_DIR/conf.d \ + /etc/supervisor/conf.d /etc/nginx/nginx.conf /etc/nginx/conf.d \ + /var/cache/nginx /var/lib/nginx /var/run/nginx.pid + +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --start-interval=2s --retries=3 \ + CMD php -r 'exit(@fsockopen("127.0.0.1", 9000) && @fsockopen("127.0.0.1", 80) ? 0 : 1);' + +EXPOSE 80 +ENTRYPOINT ["entrypoint.sh"] +CMD ["entrypoint"] diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 4e97686..5172fd5 100644 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -2,70 +2,84 @@ set -e -echo "" -echo "***********************************************************" -echo " Starting NGINX PHP-FPM Docker Container " -echo "***********************************************************" - # Logging functions -info() { echo "[INFO] $*"; } -warning() { echo "[WARNING] $*"; } -fatal() { echo "[ERROR] $*" >&2; exit 1; } - -ARTISAN_FILE="/var/www/html/artisan" -SUPERVISOR_CONF_DIR="/etc/supervisor/conf.d" -NGINX_CONF_DIR="/etc/nginx/conf.d" -CUSTOM_NGINX_CONF="/var/www/html/conf/nginx" -CUSTOM_SUPERVISOR_CONF="/var/www/html/conf/worker/supervisor.conf" -DOCUMENT_ROOT="/var/www/html/public" - -# Laravel Supervisor Setup -setup_laravel_supervisor() { - info "Artisan file found, creating Laravel supervisor config" - export DOCUMENT_ROOT - - cat > "$SUPERVISOR_CONF_DIR/laravel-worker.conf" <&2 + exit 1 +} + +is_true() { + case "$1" in + 1|true|TRUE|True|yes|YES|on|ON) return 0 ;; + *) return 1 ;; + esac +} + +APP_DIR="${APP_DIR:-/var/www/html}" +USER_NAME="${USER_NAME:-www-data}" +DOMAIN="${DOMAIN:-_}" +CLIENT_MAX_BODY_SIZE="${CLIENT_MAX_BODY_SIZE:-15M}" +LARAVEL_PROCS_NUMBER="${LARAVEL_PROCS_NUMBER:-1}" +LARAVEL_QUEUE_OPTIONS="${LARAVEL_QUEUE_OPTIONS:---sleep=3 --tries=3}" + +# Default to the Laravel public directory when it exists +if [ -z "$DOCUMENT_ROOT" ]; then + if [ -d "$APP_DIR/public" ]; then + DOCUMENT_ROOT="$APP_DIR/public" + else + DOCUMENT_ROOT="$APP_DIR" + fi +fi + +NGINX_CONF_DIR="/etc/nginx/conf.d" +CUSTOM_NGINX_CONF="$APP_DIR/conf/nginx" +CUSTOM_SUPERVISOR_CONF="$APP_DIR/conf/worker/supervisor.conf" +SUPERVISOR_CONF_DIR="/etc/supervisor/conf.d" + +# Check if custom php.ini file exists +PHP_INI_SOURCE="$APP_DIR/conf/php/php.ini" +PHP_INI_TARGET="$PHP_INI_DIR/conf.d/php.ini" + +if [ -f "$PHP_INI_SOURCE" ]; then + cp "$PHP_INI_SOURCE" "$PHP_INI_TARGET" + info "Custom php.ini file found and copied to $PHP_INI_TARGET" +fi + +# Run any other command (e.g. "php artisan migrate", "composer install", "bash") +if [ $# -gt 0 ] && [ "$1" != "entrypoint" ] && [ "$1" != "supervisord" ]; then + exec "$@" +fi + +# Banner +echo "" +echo "***********************************************************" +echo " Starting NGINX PHP-FPM Container " +echo "***********************************************************" + +# Nginx configuration +if [ -f "$CUSTOM_NGINX_CONF/nginx.conf" ]; then cp "$CUSTOM_NGINX_CONF/nginx.conf" /etc/nginx/nginx.conf info "Using custom nginx.conf" - fi +fi - if [ -f "$CUSTOM_NGINX_CONF/nginx-site.conf" ]; then - info "Custom nginx site config found" - rm -f "$NGINX_CONF_DIR/default.conf" +if [ -f "$CUSTOM_NGINX_CONF/nginx-site.conf" ]; then cp "$CUSTOM_NGINX_CONF/nginx-site.conf" "$NGINX_CONF_DIR/default.conf" - info "Start nginx with custom server config..." - else - info "nginx-site.conf not found" - info "If you want to use custom configs, create it at $CUSTOM_NGINX_CONF/nginx-site.conf" - info "Start nginx with default config..." - + info "Using custom nginx site config $CUSTOM_NGINX_CONF/nginx-site.conf" +else cat > "$NGINX_CONF_DIR/default.conf" < "$SUPERVISOR_TASK" + + if is_true "${LARAVEL_SCHEDULER_ENABLED:-true}"; then + cat >> "$SUPERVISOR_TASK" <> "$SUPERVISOR_TASK" </dev/null || true + info "Artisan file not found at $ARTISAN_PATH" +fi + +# Custom supervisor programs +if [ -f "$CUSTOM_SUPERVISOR_CONF" ]; then cp "$CUSTOM_SUPERVISOR_CONF" "$SUPERVISOR_CONF_DIR/supervisor.conf" - fi -} + info "Custom supervisor config found and copied" +fi -# Main Execution -[ -f "$ARTISAN_FILE" ] && setup_laravel_supervisor || info "artisan file not found" - -setup_nginx_config -load_custom_supervisor_config - -# Start Supervisor -supervisord -c /etc/supervisor/supervisord.conf +# Start Supervisor (programs drop privileges to $USER_NAME themselves) +if [ "$(id -u)" = "0" ]; then + exec supervisord -c /etc/supervisor/supervisord.conf -u root +fi +exec supervisord -c /etc/supervisor/supervisord.conf diff --git a/src/nginx.conf b/src/nginx.conf index ef61525..7cfcd5a 100644 --- a/src/nginx.conf +++ b/src/nginx.conf @@ -1,8 +1,10 @@ user www-data; worker_processes auto; -error_log /var/log/nginx/error.log crit; +error_log /dev/stderr warn; pid /var/run/nginx.pid; +# Dynamic modules (Debian / Alpine) include /etc/nginx/modules-enabled/*.conf; +include /etc/nginx/modules/*.conf; events { worker_connections 1024; @@ -43,7 +45,7 @@ http { gzip_min_length 512; gzip_comp_level 6; gzip_buffers 8 64k; - gzip_types text/plain text/xml text/css text/js application/x-javascript application/xml image/png image/x-icon image/gif image/jpeg image/svg+xml application/xml+rss text/javascript application/atom+xml application/javascript application/json application/x-font-ttf font/opentype; + gzip_types text/plain text/xml text/css text/js application/x-javascript application/xml image/x-icon image/svg+xml application/xml+rss text/javascript application/atom+xml application/javascript application/json application/x-font-ttf font/opentype; # Proxy settings proxy_redirect off; diff --git a/src/opcache.ini b/src/opcache.ini index 9710908..00877bf 100644 --- a/src/opcache.ini +++ b/src/opcache.ini @@ -1,12 +1,12 @@ [opcache] ; Determines if Zend OPCache is enabled -opcache.enable=1 +opcache.enable=${PHP_OPCACHE_ENABLE} ; Determines if Zend OPCache is enabled for the CLI version of PHP -;opcache.enable_cli=1 +opcache.enable_cli=${PHP_OPCACHE_ENABLE_CLI} ; The OPcache shared memory storage size. -opcache.memory_consumption=512 +opcache.memory_consumption=${PHP_OPCACHE_MEMORY_CONSUMPTION} ; The amount of memory for interned strings in Mbytes. opcache.interned_strings_buffer=64 @@ -15,7 +15,7 @@ opcache.interned_strings_buffer=64 ; Only numbers between 200 and 1000000 are allowed. ;If you have multiple PHP sites on the server then consider the value 130986 ; for magento 2, keep 65406 -opcache.max_accelerated_files=50000 +opcache.max_accelerated_files=${PHP_OPCACHE_MAX_ACCELERATED_FILES} ; The maximum percentage of "wasted" memory until a restart is scheduled. opcache.max_wasted_percentage=15 @@ -30,7 +30,7 @@ opcache.max_wasted_percentage=15 ; webserver for changes to the filesystem to take effect. ; For Development / testing, keep 1 ; For performance / production, keep 0 -opcache.validate_timestamps=0 +opcache.validate_timestamps=${PHP_OPCACHE_VALIDATE_TIMESTAMPS} ;opcache.revalidate_freq How often in seconds should the code ;cache expire and check if your code has changed. 0 means it @@ -39,7 +39,7 @@ opcache.validate_timestamps=0 ;should not be enabled by default, as long as it's disabled then any value for opcache. ;revalidate_freq will basically be ignored. You should really only ever enable ;this during development, you don't really want to enable this setting for a production application. -opcache.revalidate_freq=0 +opcache.revalidate_freq=${PHP_OPCACHE_REVALIDATE_FREQ} ; Enables or disables file search in include_path optimization ;opcache.revalidate_path=0 @@ -48,10 +48,6 @@ opcache.revalidate_freq=0 ; size of the optimized code. opcache.save_comments=1 -; If enabled, a fast shutdown sequence is used for the accelerated code -; Depending on the used Memory Manager this may cause some incompatibilities. -opcache.fast_shutdown=1 - ; Allow file existence override (file_exists, etc.) performance feature. ;opcache.enable_file_override=0 @@ -106,7 +102,7 @@ opcache.fast_shutdown=1 ; processes have to map shared memory into the same address space. This ; directive allows to manually fix the "Unable to reattach to base address" ; errors. -opcache.mmap_base=0x20000000 +;opcache.mmap_base= ; Enables and sets the second level cache directory. ; It should improve performance when SHM memory is full, at server restart or @@ -122,7 +118,7 @@ opcache.mmap_base=0x20000000 ; Implies opcache.file_cache_only=1 for a certain process that failed to ; reattach to the shared memory (for Windows only). Explicitly enabled file ; cache is required. -opcache.file_cache_fallback=1 +;opcache.file_cache_fallback=1 ; Enables or disables copying of PHP code (text segment) into HUGE PAGES. ; This should improve performance, but requires appropriate OS configuration. diff --git a/src/php-fpm.d/zz-nginx-php-fpm.conf b/src/php-fpm.d/zz-nginx-php-fpm.conf new file mode 100644 index 0000000..ccce079 --- /dev/null +++ b/src/php-fpm.d/zz-nginx-php-fpm.conf @@ -0,0 +1,15 @@ +[www] +; Expose container environment variables (DB_HOST, APP_KEY, ...) to PHP workers +clear_env = no + +; Process manager, tunable through PHP_FPM_* environment variables +pm = ${PHP_FPM_PM} +pm.max_children = ${PHP_FPM_PM_MAX_CHILDREN} +pm.start_servers = ${PHP_FPM_PM_START_SERVERS} +pm.min_spare_servers = ${PHP_FPM_PM_MIN_SPARE_SERVERS} +pm.max_spare_servers = ${PHP_FPM_PM_MAX_SPARE_SERVERS} +pm.max_requests = ${PHP_FPM_PM_MAX_REQUESTS} + +; Health endpoint for FastCGI probes +ping.path = /ping +ping.response = pong diff --git a/src/php.ini b/src/php.ini index 21ff610..67e281a 100644 --- a/src/php.ini +++ b/src/php.ini @@ -1,6 +1,9 @@ -date.timezone=UTC -display_errors=Off +; Values can be overridden with environment variables (see README) +date.timezone=${PHP_DATE_TIMEZONE} +display_errors=${PHP_DISPLAY_ERRORS} log_errors=On -upload_max_filesize= 80M -post_max_size= 80M -memory_limit = 256M +expose_php=Off +upload_max_filesize=${PHP_UPLOAD_MAX_FILESIZE} +post_max_size=${PHP_POST_MAX_SIZE} +memory_limit=${PHP_MEMORY_LIMIT} +max_execution_time=${PHP_MAX_EXECUTION_TIME} diff --git a/src/supervisor/supervisord.conf b/src/supervisor/supervisord.conf index 3dacdde..3cea4e7 100644 --- a/src/supervisor/supervisord.conf +++ b/src/supervisor/supervisord.conf @@ -1,32 +1,41 @@ [supervisord] nodaemon=true -user=www-data logfile=/var/log/supervisor/supervisord.log logfile_maxbytes = 50MB pidfile=/tmp/supervisord.pid directory = /tmp +[unix_http_server] +file=/tmp/supervisor.sock +chmod=0700 + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///tmp/supervisor.sock [program:php-fpm] -command=/usr/local/sbin/php-fpm +command=/usr/local/sbin/php-fpm --nodaemonize numprocs=1 autostart=true autorestart=true -stderr_logfile=/var/log/supervisor/php-fpm.err.log -stdout_logfile=/var/log/supervisor/php-fpm.out.log -user=www-data +redirect_stderr=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 priority=1 +stopsignal=QUIT [program:nginx] command=/usr/sbin/nginx -g "daemon off;" numprocs=1 autostart=true autorestart=true -stderr_logfile=/var/log/nginx/nginx.err.log -stdout_logfile=/var/log/nginx/nginx.out.log -logfile_maxbytes = 50MB -user=www-data +redirect_stderr=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 priority=2 +stopsignal=QUIT [include] -files = /etc/supervisor/conf.d/*.conf \ No newline at end of file +files = /etc/supervisor/conf.d/*.conf diff --git a/tests/compose.yaml b/tests/compose.yaml index f6c4e2c..98e2cee 100644 --- a/tests/compose.yaml +++ b/tests/compose.yaml @@ -1,30 +1,34 @@ -version: '3' services: - mysql: - image: mysql:8.0 - container_name: mysql - restart: unless-stopped - environment: - MYSQL_DATABASE: php - MYSQL_ROOT_PASSWORD: password - MYSQL_PASSWORD: password - MYSQL_USER: php - ports: - - 3306:3306 - nginx-php-fpm: - image: jkaninda/nginx-php-fpm:latest - container_name: nginx-php-fpm - restart: unless-stopped - depends_on: - - mysql - environment: - DB_HOST: mysql - DB_PORT: 3306 - DB_DATABASE: php - DB_USERNAME: php - DB_PASSWORD: password - volumes: - #Project root - - ./laravel:/var/www/html - ports: - - 80:80 + mysql: + image: mysql:8.4 + container_name: mysql + restart: unless-stopped + environment: + MYSQL_DATABASE: php + MYSQL_ROOT_PASSWORD: password + MYSQL_PASSWORD: password + MYSQL_USER: php + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-ppassword"] + interval: 5s + timeout: 5s + retries: 30 + nginx-php-fpm: + image: ${IMAGE:-jkaninda/nginx-php-fpm:latest} + container_name: nginx-php-fpm + restart: unless-stopped + depends_on: + mysql: + condition: service_healthy + environment: + DB_CONNECTION: mysql + DB_HOST: mysql + DB_PORT: 3306 + DB_DATABASE: php + DB_USERNAME: php + DB_PASSWORD: password + CLIENT_MAX_BODY_SIZE: 20M + volumes: + - ./laravel:/var/www/html + ports: + - "${HTTP_PORT:-80}:80" diff --git a/tests/integration.sh b/tests/integration.sh new file mode 100755 index 0000000..afd3d17 --- /dev/null +++ b/tests/integration.sh @@ -0,0 +1,69 @@ +#!/bin/sh +# Integration test: create a Laravel project with the image, serve it through +# the bundled Nginx, run migrations and check that Nginx, PHP-FPM, the queue +# worker and the scheduler are running. +# Usage: IMAGE=jkaninda/nginx-php-fpm:8.5 [HTTP_PORT=8080] ./tests/integration.sh +set -eu + +IMAGE="${IMAGE:-jkaninda/nginx-php-fpm:latest}" +HTTP_PORT="${HTTP_PORT:-80}" +export IMAGE HTTP_PORT +cd "$(dirname "$0")" + +cleanup() { + docker compose logs nginx-php-fpm || true + docker compose down -v --remove-orphans || true +} +trap cleanup EXIT + +echo "==> Checking PHP extensions" +docker run --rm "$IMAGE" php -m > modules.txt +for ext in bcmath exif gd intl mbstring memcached pcntl pdo_mysql pdo_pgsql rdkafka redis zip "Zend OPcache"; do + grep -qx "$ext" modules.txt || { echo "Missing extension: $ext"; exit 1; } +done +if docker run --rm "$IMAGE" php -m 2>&1 | grep -iE 'warning|unable to load'; then + echo "PHP startup warnings detected"; exit 1 +fi +rm -f modules.txt +docker run --rm "$IMAGE" sh -c 'php -v && composer -V && node -v && npm -v && nginx -t' + +echo "==> Creating Laravel project" +rm -rf laravel +docker run --rm -v "$PWD:/app" -w /app "$IMAGE" \ + composer create-project --prefer-dist --no-interaction --no-progress laravel/laravel laravel +docker run --rm -v "$PWD/laravel:/var/www/html" "$IMAGE" \ + chown -R www-data:www-data storage bootstrap/cache database + +echo "==> Starting stack" +docker compose up -d + +echo "==> Waiting for the container to be healthy" +i=0 +until [ "$(docker inspect -f '{{.State.Health.Status}}' nginx-php-fpm)" = "healthy" ]; do + i=$((i + 1)); [ "$i" -gt 60 ] && { echo "Container not healthy"; exit 1; } + sleep 2 +done + +echo "==> Checking generated Nginx config" +docker exec nginx-php-fpm grep -q "root /var/www/html/public;" /etc/nginx/conf.d/default.conf +docker exec nginx-php-fpm grep -q "client_max_body_size 20M;" /etc/nginx/conf.d/default.conf + +echo "==> Running migrations" +docker exec --user www-data nginx-php-fpm php artisan migrate --force + +echo "==> Checking HTTP response" +code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$HTTP_PORT") +echo "HTTP $code" +[ "$code" = "200" ] + +echo "==> Checking supervisor programs" +for program in php-fpm nginx Laravel-worker Laravel-scheduler; do + i=0 + until docker exec nginx-php-fpm supervisorctl status | grep "^$program" | grep -q RUNNING; do + i=$((i + 1)); [ "$i" -gt 30 ] && { echo "$program is not running"; docker exec nginx-php-fpm supervisorctl status; exit 1; } + sleep 2 + done +done +docker exec nginx-php-fpm supervisorctl status + +echo "==> All tests passed"