컨테이너 하나에 프로세스 하나가 원칙이다. httpd 와 PHP-FPM 을 따로 띄우고 같은 문서 루트를 공유하는 쪽이 재시작과 상태 판정이 단순하다.
services:
web:
image: httpd:2.4
ports:
- "8080:80"
volumes:
- ./html:/var/www/html
- ./conf/httpd.conf:/usr/local/apache2/conf/httpd.conf
depends_on:
- php
php:
image: php:8.3-fpm
volumes:
- ./html:/var/www/html
이 구성에서는 Apache 쪽 핸들러가 proxy:fcgi://php:9000 을 가리킨다. 두 컨테이너가 같은 경로에 같은 문서 루트를 마운트해야 한다.
합쳐야 하는 사정이 있을 때 — 배포 단위를 하나로 유지해야 하거나, 폐쇄망에 이미지 하나만 반입할 수 있을 때 — 아래처럼 만든다.
RHEL 9 계열에는 mod_php 가 없으므로 한 컨테이너 안에서도 PHP-FPM 을 띄워야 한다. dnf install mod_php 는 No match for argument: mod_php 로 끝난다.
FROM rockylinux:9
RUN dnf install -y \
httpd php php-cli php-common php-fpm \
php-mbstring php-pdo php-mysqlnd php-gd php-xml php-opcache \
&& dnf clean all \
&& rm -rf /var/cache/dnf
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod 755 /usr/local/bin/entrypoint.sh
EXPOSE 80
CMD ["/usr/local/bin/entrypoint.sh"]
설정을 이미지에 굽지 않고 밖에서 마운트한다. echo ... >> httpd.conf 를 여러 줄 쌓는 방식은 다시 빌드하지 않으면 값 하나도 못 바꾸고, 같은 줄이 두 번 들어가도 알아채기 어렵다.
.
├── Dockerfile
├── entrypoint.sh
├── html/
└── conf/
├── httpd.conf
├── php.ini
├── php-fpm.conf
└── php-fpm.d/www.conf
services:
web:
build: .
container_name: web
ports:
- "8080:80"
volumes:
- ./html:/var/www/html
- ./conf/httpd.conf:/etc/httpd/conf/httpd.conf
- ./conf/php.ini:/etc/php.ini
- ./conf/php-fpm.conf:/etc/php-fpm.conf
- ./conf/php-fpm.d/www.conf:/etc/php-fpm.d/www.conf
restart: unless-stopped
유닉스 소켓을 쓰면 /run/php-fpm 디렉터리가 있어야 하는데, 이 경로는 컨테이너 재생성이나 tmpfs 마운트로 쉽게 비워진다. 루프백 TCP 로 두는 편이 덜 깨진다.
; conf/php-fpm.d/www.conf
[www]
user = apache
group = apache
listen = 127.0.0.1:9000
listen.allowed_clients = 127.0.0.1
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 4
# conf/httpd.conf 에서
<FilesMatch \.php$>
SetHandler "proxy:fcgi://127.0.0.1:9000"
</FilesMatch>
DirectoryIndex index.php index.html
AddDefaultCharset UTF-8
ErrorLog /dev/stderr
CustomLog /dev/stdout combined
CMD /usr/sbin/php-fpm --nodaemonize & exec /usr/sbin/httpd -D FOREGROUND 처럼 & 로 뒤에 붙이는 형태가 흔히 돌아다닌다. 동작하는 것처럼 보이지만 PHP-FPM 이 실패해도 아무도 모른다. PID 1 은 httpd 이고 컨테이너는 계속 Running 인데 .php 요청만 503 으로 떨어진다.
최소한의 형태는 entrypoint 스크립트다. PHP-FPM 이 붙을 때까지 기다린 뒤 httpd 를 exec 로 띄워 PID 1 을 넘긴다.
#!/bin/sh
set -e
/usr/sbin/php-fpm --nodaemonize &
FPM_PID=$!
for _ in $(seq 1 20); do
if kill -0 "$FPM_PID" 2>/dev/null; then
break
fi
sleep 0.5
done
kill -0 "$FPM_PID" 2>/dev/null || { echo "php-fpm failed to start" >&2; exit 1; }
rm -f /tmp/httpd.pid
exec /usr/sbin/httpd -D FOREGROUND
이래도 기동 이후에 PHP-FPM 이 죽는 것은 잡지 못한다. 운영에 올릴 것이면 프로세스 관리자를 PID 1 로 둔다.
; /etc/supervisord.conf
[supervisord]
nodaemon=true
[program:php-fpm]
command=/usr/sbin/php-fpm --nodaemonize
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:httpd]
command=/usr/sbin/httpd -D FOREGROUND
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
RUN dnf install -y epel-release && dnf install -y supervisor && dnf clean all
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
docker compose up -d --build
docker exec -it web ps -ef | grep -E 'httpd|php-fpm'
docker exec -it web httpd -M | grep proxy_fcgi
docker logs --tail 50 web
echo '<?php phpinfo();' > html/info.php
curl -sI http://localhost:8080/info.php | head -3
확인이 끝나면 info.php 는 지운다.
listen 짝 맞추기, 503 과 500 의 갈래.exec "$@".