Cara menggunakan php-fpm docker slow

When deploying a Laravel application, the goal is to make sure that the deployment process is as fast and secure as possible. A big part of achieving this goal is choosing the right base Linux image to compose the container image where the application will be running and later deployed.

Alpine Linux has shown that there is no faster distro when working with a container for any language. Since Docker's first release, the popularity of the Alpine distro has grown and keeps growing because it is a tiny, container, and security-focused distro.

To be able to run an application just PHP and Composer isn't enough NGINX and Supervisor it's also required, and here is where a little of complexity comes in. But don't worry, a Dockerfile it's going to be dissected, and you will get to understand why things are the way they are.

Content

Dockerfile

Down below, there is an entire Dockerfile used locally and in production to serve a Laravel application. Notice that it's not optimized to have a minimal number of layers, and that is on purpose, since we will grab small pieces of the file and understand what each part does.

FROM alpine:latest

WORKDIR /var/www/html/

# Essentials
RUN echo "UTC" > /etc/timezone
RUN apk add --no-cache zip unzip curl sqlite nginx supervisor

# Installing bash
RUN apk add bash
RUN sed -i 's/bin\/ash/bin\/bash/g' /etc/passwd

# Installing PHP
RUN apk add --no-cache php8 \
    php8-common \
    php8-fpm \
    php8-pdo \
    php8-opcache \
    php8-zip \
    php8-phar \
    php8-iconv \
    php8-cli \
    php8-curl \
    php8-openssl \
    php8-mbstring \
    php8-tokenizer \
    php8-fileinfo \
    php8-json \
    php8-xml \
    php8-xmlwriter \
    php8-simplexml \
    php8-dom \
    php8-pdo_mysql \
    php8-pdo_sqlite \
    php8-tokenizer \
    php8-pecl-redis

# Installing composer
RUN curl -sS //getcomposer.org/installer -o composer-setup.php
RUN php composer-setup.php --install-dir=/usr/local/bin --filename=composer
RUN rm -rf composer-setup.php

# Configure supervisor
RUN mkdir -p /etc/supervisor.d/
COPY .docker/supervisord.ini /etc/supervisor.d/supervisord.ini

# Configure PHP
RUN mkdir -p /run/php/
RUN touch /run/php/php8.0-fpm.pid

COPY .docker/php-fpm.conf /etc/php8/php-fpm.conf
COPY .docker/php.ini-production /etc/php8/php.ini

# Configure nginx
COPY .docker/nginx.conf /etc/nginx/
COPY .docker/nginx-laravel.conf /etc/nginx/modules/

RUN mkdir -p /run/nginx/
RUN touch /run/nginx/nginx.pid

RUN ln -sf /dev/stdout /var/log/nginx/access.log
RUN ln -sf /dev/stderr /var/log/nginx/error.log

# Building process
COPY . .
RUN composer install --no-dev
RUN chown -R nobody:nobody /var/www/html/storage

EXPOSE 80
CMD ["supervisord", "-c", "/etc/supervisor.d/supervisord.ini"]

Enter fullscreen mode Exit fullscreen mode

Defining image bases

The first step towards the construction of a Dockerfile is to create the file itself and define a Linux distribution and its version. Once that is done, you can start composing your Dockerfile with the instructions needed to build your container image.

FROM alpine:latest

WORKDIR /var/www/html/

Enter fullscreen mode Exit fullscreen mode

The FROM instruction sets the Base Image for subsequent instructions. Notice that alpine:latest gets defined, which sets alpine as the base Linux image. After the distro name, there is a

FROM alpine:latest

WORKDIR /var/www/html/
5  used to specify a tag or version, so when the instruction
FROM alpine:latest

WORKDIR /var/www/html/
6 gets interpreted, it will set alpine at the latest version as the base image.

While the WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile. So, when the instruction

FROM alpine:latest

WORKDIR /var/www/html/
7  is interpreted, every command execution in the Dockerfile will take place into /var/www/html/.

Software installation

Now that the container image base got defined, it's time to start looking into the software that we need to install to run the application. As mentioned, PHP, Composer, NGINX, and Supervisor are software programs to install, but that's not all. As this software has dependencies, they also have to be installed. Here is the installation process broken into explained pieces, so you can understand it.

Install essentials

RUN echo "UTC" > /etc/timezone
RUN apk add --no-cache zip unzip curl sqlite nginx supervisor

Enter fullscreen mode Exit fullscreen mode

The first RUN instruction will execute any commands in a new layer on top of the current image and commit the results. Hence, when

FROM alpine:latest

WORKDIR /var/www/html/
8 is interpreted, the echo command will print out the UTC string into /etc/timezone file. As a result of the command execution, UTC becomes the standard timezone.

In the second RUN instruction, an apk command appears, apk is Alpine package manager, another well-known package manager is apt from Ubuntu. With that said, when

FROM alpine:latest

WORKDIR /var/www/html/
9 is processed, it installs those software programs in the base image.

Install bash

RUN apk add bash
RUN sed -i 's/bin\/ash/bin\/bash/g' /etc/passwd

Enter fullscreen mode Exit fullscreen mode

The first RUN instruction tells that bash has to be installed. The second instruction sets it as a standard shell by replacing the string /bin/ash by /bin/bash into the /etc/passwd file. This change is because Alpine standard shell, ash, works differently, and these differences can get in your way when you or your team need to execute a shell script in the container.

Install PHP

RUN apk add --no-cache php8 \
    php8-common \
    php8-fpm \
    php8-pdo \
    php8-opcache \
    php8-zip \
    php8-phar \
    php8-iconv \
    php8-cli \
    php8-curl \
    php8-openssl \
    php8-mbstring \
    php8-tokenizer \
    php8-fileinfo \
    php8-json \
    php8-xml \
    php8-xmlwriter \
    php8-simplexml \
    php8-dom \
    php8-pdo_mysql \
    php8-pdo_sqlite \
    php8-tokenizer \
    php8-pecl-redis

Enter fullscreen mode Exit fullscreen mode

The first RUN instruction, says that PHP and all listed extensions have to get installed. As mentioned before, this Dockerfile gets used to serve Laravel applications, so the PHP extensions are arbitrary and may change depending on the framework or application you are trying to run.

Lastly, you can find what the PHP extensions does by checking PHP extensions documentation and the PHP extension community library PECL pages and search for them.

Install Composer

RUN curl -sS //getcomposer.org/installer -o composer-setup.php
RUN php composer-setup.php --install-dir=/usr/local/bin --filename=composer
RUN rm -rf composer-setup.php

Enter fullscreen mode Exit fullscreen mode

In this RUN instruction composer binary, composer-setup.php gets downloaded from the composer's official page. Then in the second instruction, the binary gets used to install composer into /usr/local/bin directory. Lastly, the binary gets removed after composer installation since his binary has no use to the system any longer.

Software configuration

Now that all needed software it's installed, they have to be configured and tight together to make the serving of a Laravel application work as expected.

Configure supervisor

RUN mkdir -p /etc/supervisor.d/
COPY .docker/supervisord.ini /etc/supervisor.d/supervisord.ini

Enter fullscreen mode Exit fullscreen mode

In this RUN instruction, the Dockerfile is specifying that the directory supervisor.d has to be created inside the /etc/ directory. This directory will hold initializer files that specify sets of instructions that the Supervisor will run upon when the OS starts, in this case when the container starts, since these two events cannot happen without each other.

In the second RUN instruction, the supervisord.ini file gets copied from a local .docker folder into /etc/supervisor.d/ container folder. As mentioned above, this file contains the instructions that Supervisor will run upon, and these instructions are:

[supervisord]
nodaemon=true

[program:nginx]
command=nginx
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:php-fpm]
command=php-fpm8
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

Enter fullscreen mode Exit fullscreen mode

Explaining supervisor.ini

  • nodaemon=true

Start Supervisor in the foreground instead of daemonizing.

  • command=nginx

The command that will run when Supervisor starts.

  • stdout_logfile=/dev/stdout

Redirect all output to the Alpine standard output device that is the container itself, allowing us to see Supervisor logs about NGINX execution when running docker logs MY_CONTAINER or docker-compose up to start container stack.

  • stdout_logfile_maxbytes=0

The maximum number of bytes that can get consumed by stdout_logfile before it rotates, since files didn't get written, has to get deactivated by setting maxbytes to 0.

  • stderr_logfile=/dev/stderr

Redirect all errors to the Alpine standard error device that is the container itself, allowing us to see Supervisor logs about NGINX execution when running docker logs MY_CONTAINER or docker-compose up to start container stack.

  • stderr_logfile_maxbytes=0

The maximum number of bytes that can get consumed by stderr_logfile before it rotates, since files didn't get written, has to get deactivated by setting maxbytes to 0.

Configure PHP

RUN mkdir -p /run/php/
RUN touch /run/php/php8.0-fpm.pid

COPY .docker/php.ini-production /etc/php8/php.ini
COPY .docker/php-fpm.conf /etc/php8/php-fpm.conf

Enter fullscreen mode Exit fullscreen mode

In the first RUN statement, the Dockerfile is specifying that the directory php has to get created inside the /run/ directory. This directory will hold .pid files that contain the process ID specific to the software.

The second statement, create the file php8.0-fpm.pid inside /run/php/ directory. Now the Alpine distro has a file to store the process ID that will get created when PHP-FPM starts.

The third statement copies a php.ini-production file from a local .docker folder into /etc/php8/ container folder. This file contains all the configurations that PHP will run upon, the content of this file got copied from PHP's official repository on GitHub.

The fourth statement copies a php-fpm.conf file from a local .docker folder into /etc/php8/ container folder. This file contains all the configurations that PHP-FPM will run upon, and here are the configurations:

;;;;;;;;;;;;;;;;;;;;
; FPM Configuration ;
;;;;;;;;;;;;;;;;;;;;;

; All relative paths in this configuration file are relative to PHP's install
; prefix [/usr]. This prefix can be dynamically changed by using the
; '-p' argument from the command line.

;;;;;;;;;;;;;;;;;;
; Global Options ;
;;;;;;;;;;;;;;;;;;

[global]
; Pid file
; Note: the default prefix is /var
; Default Value: none
pid = /run/php/php8.0-fpm.pid

; Error log file
; If it's set to "syslog", log is sent to syslogd instead of being written
; in a local file.
; Note: the default prefix is /var
; Default Value: log/php-fpm.log
error_log = /proc/self/fd/2

; syslog_facility is used to specify what type of program is logging the
; message. This lets syslogd specify that messages from different facilities
; will be handled differently.
; See syslog[3] for possible values [ex daemon equiv LOG_DAEMON]
; Default Value: daemon
;syslog.facility = daemon

; syslog_ident is prepended to every message. If you have multiple FPM
; instances running on the same server, you can change the default value
; which must suit common needs.
; Default Value: php-fpm
;syslog.ident = php-fpm

; Log level
; Possible Values: alert, error, warning, notice, debug
; Default Value: notice
;log_level = notice

; If this number of child processes exit with SIGSEGV or SIGBUS within the time
; interval set by emergency_restart_interval then FPM will restart. A value
; of '0' means 'Off'.
; Default Value: 0
;emergency_restart_threshold = 0

; Interval of time used by emergency_restart_interval to determine when
; a graceful restart will be initiated.  This can be useful to work around
; accidental corruptions in an accelerator's shared memory.
; Available Units: s[econds], m[inutes], h[ours], or d[ays]
; Default Unit: seconds
; Default Value: 0
;emergency_restart_interval = 0

; Time limit for child processes to wait for a reaction on signals from master.
; Available units: s[econds], m[inutes], h[ours], or d[ays]
; Default Unit: seconds
; Default Value: 0
;process_control_timeout = 0

; The maximum number of processes FPM will fork. This has been design to control
; the global number of processes when using dynamic PM within a lot of pools.
; Use it with caution.
; Note: A value of 0 indicates no limit
; Default Value: 0
; process.max = 128

; Specify the nice[2] priority to apply to the master process [only if set]
; The value can vary from -19 [highest priority] to 20 [lower priority]
; Note: - It will only work if the FPM master process is launched as root
;       - The pool process will inherit the master process priority
;         unless it specified otherwise
; Default Value: no set
; process.priority = -19

; Send FPM to background. Set to 'no' to keep FPM in foreground for debugging.
; Default Value: yes
daemonize = no

; Set open file descriptor rlimit for the master process.
; Default Value: system defined value
;rlimit_files = 1024

; Set max core size rlimit for the master process.
; Possible Values: 'unlimited' or an integer greater or equal to 0
; Default Value: system defined value
;rlimit_core = 0

; Specify the event mechanism FPM will use. The following is available:
; - select     [any POSIX os]
; - poll       [any POSIX os]
; - epoll      [linux >= 2.5.44]
; - kqueue     [FreeBSD >= 4.1, OpenBSD >= 2.9, NetBSD >= 2.0]
; - /dev/poll  [Solaris >= 7]
; - port       [Solaris >= 10]
; Default Value: not set [auto detection]
;events.mechanism = epoll

; When FPM is build with systemd integration, specify the interval,
; in second, between health report notification to systemd.
; Set to 0 to disable.
; Available Units: s[econds], m[inutes], h[ours]
; Default Unit: seconds
; Default value: 10
;systemd_interval = 10

;;;;;;;;;;;;;;;;;;;;
; Pool Definitions ;
;;;;;;;;;;;;;;;;;;;;

; Multiple pools of child processes may be started with different listening
; ports and different management options.  The name of the pool will be
; used in logs and stats. There is no limitation on the number of pools which
; FPM can handle. Your system will tell you anyway :]

; Include one or more files. If glob[3] exists, it is used to include a bunch of
; files from a glob[3] pattern. This directive can be used everywhere in the
; file.
; Relative path can also be used. They will be prefixed by:
;  - the global prefix if it's been set [-p argument]
;  - /usr otherwise
include=/etc/php8/php-fpm.d/*.conf

Enter fullscreen mode Exit fullscreen mode

Notice that the php-fpm.conf don't have any custom configuration or optimization, feel free to configure this file according to your needs.

Configure NGINX

FROM alpine:latest

WORKDIR /var/www/html/
0

Enter fullscreen mode Exit fullscreen mode

In this first statement, nginx.conf gets copied from a local .docker folder into  /etc/nginx/ container folder. This file contains all the configurations that NGINX will use to run upon it, and down below you can check the file content:

FROM alpine:latest

WORKDIR /var/www/html/
1

Enter fullscreen mode Exit fullscreen mode

The third statement, copies a nginx-laravel.conf from a local .docker folder into /etc/nginx/modules/ container folder. This file contains all the configurations that NGINX will use to serve Laravel correctly, and down below you can check the file content:

FROM alpine:latest

WORKDIR /var/www/html/
2

Enter fullscreen mode Exit fullscreen mode

The fourth statement specifies that the directory nginx has to get created inside the /run/ directory. As mentioned in the PHP-FPM configuration session, the run directory holds .pid files where the process ID to a specific software gets written.

In the fifth statement, create the file nginx.pid inside /run/nginx/ directory. Now, the Alpine distro has a file to store the process ID that will get created when NGINX starts.

The sixth statement instructs that a symbolic link of the Alpine standard output has to get created at /var/log/nginx/access.log. This configuration, as mentioned in the Supervisor sections, is what allows us to see NGINX logs from containers.

Lastly, the seventh statement instructs that a symbolic link of the Alpine standard error gets created at /var/log/nginx/error.log. This configuration, as mentioned in the Supervisor sections, is what allows us to see NGINX errors from containers.

Build process

The build process is where the application gets copied into the container, and its dependencies get installed, leaving the Laravel application ready to be served by NGINX, PHP-FPM, and Supervisor.

FROM alpine:latest

WORKDIR /var/www/html/
3

Enter fullscreen mode Exit fullscreen mode

At the COPY statement, all Laravel files and folders from the directory where the Dockerfile is, are copied into the working directory specified at the WORKDIR instruction.

At the RUN statement, production dependencies from the Laravel application get installed, making the application ready to be served by Supervisor, NGINX, and PHP-FPM.

Container execution

Now that everything got installed and properly configured, we need to tell how this container image will start serving the application once the container starts and what TCP port to use.

FROM alpine:latest

WORKDIR /var/www/html/
4

Enter fullscreen mode Exit fullscreen mode

The EXPOSE instruction informs that the container listens on the specified network ports at runtime, while the purpose of the CMD instruction is to provide a default command for an executing Docker container.

Now your Dockerfile is finally done, and you can build a container from it by executing

RUN echo "UTC" > /etc/timezone
RUN apk add --no-cache zip unzip curl sqlite nginx supervisor
0 in your terminal.

Bài mới nhất

Chủ Đề