애플리케이션이 지원하는 PHP 주 버전·필수 extension 목록·healthz.php 경로
애플리케이션 · 구성 레시피
PHP-FPM·NGINX 애플리케이션 구성
Ubuntu에서 전용 PHP-FPM 풀과 Unix 소켓을 만들고, 실제 PHP 파일 존재를 확인하는 NGINX FastCGI 설정을 문법 검사 후 적용하는 안전한 배포 절차입니다.BEFORE YOU START
시작 전에 준비하세요
sudo 권한, 기존 SSH 세션, 콘솔 또는 프록시 우회 경로
읽기 전용 웹 루트와 별도 업로드·캐시·세션 쓰기 경로
기존 FPM 풀·NGINX 설정·애플리케이션 데이터의 검증된 백업
권장 대상 PHP 애플리케이션을 기본 www 풀과 분리하고 NGINX 뒤에서 최소 권한으로 운영하려는 개발자·운영자
FOLLOW THE RECIPE
8단계 구성·점검 레시피
PHP 지원 범위와 실행 환경 확인
배포판 후보 PHP 버전과 애플리케이션 지원 범위를 맞춥니다. 예시의 <PHP_VERSION>·<PHP_FPM_SERVICE>·<PHP_FPM_BINARY>를 실제 값으로 고정합니다.
cat /etc/os-releaseapt-cache policy php-fpm php-cliphp --version
php --inisystemctl list-unit-files 'php*-fpm.service'- 프레임워크가 배포판 PHP 주 버전을 지원한다.
- 필수 extension과 메모리·업로드 요구량을 확인했다.
- 자리표시자에 사용할 실제 서비스와 소켓 경로를 기록했다.
애플리케이션이 배포판 PHP를 지원하지 않으면 비공식 저장소를 즉시 추가하지 말고 업그레이드 또는 격리 배포를 별도 설계합니다.
기존 80·443 포트, FPM 풀, 소켓과 웹 루트 권한을 확인합니다.
기존 풀·소켓·웹 루트 점검
기본 풀을 덮어쓰지 않고 myapp 전용 풀을 추가합니다. 동일 이름 소켓과 NGINX server_name 충돌을 먼저 찾습니다.
sudo ss -ltnp
sudo ss -lxnp | grep phpsudo find /etc/php -path '*/fpm/pool.d/*.conf' -type f -printsudo nginx -T | grep -E 'server_name|fastcgi_pass'출력에 내부 도메인과 경로가 포함될 수 있으므로 외부에 그대로 공유하지 않습니다.sudo stat -c '%U:%G %a %n' /srv/phpapp/current/public- myapp 풀·소켓·server_name이 기존 구성과 겹치지 않는다.
- 웹 루트는 PHP 실행 계정이 수정할 수 없다.
- 업로드·캐시·세션 쓰기 경로를 웹 루트 밖 또는 제한된 하위 경로로 분리했다.
기존 소켓을 다른 사이트가 사용한다면 공유하지 말고 전용 풀과 소켓 이름을 선택합니다.
배포판 패키지의 변경 목록을 검토하고 설치합니다.
PHP-FPM과 NGINX 설치
Ubuntu 지원 저장소에서 FPM·CLI·OPcache와 NGINX를 설치합니다. 애플리케이션 extension은 필요한 것만 명시적으로 추가합니다.
sudo apt updateapt-get --simulate install php-fpm php-cli php-opcache nginxdpkg-query -W nginx >/dev/null 2>&1 || sudo systemctl mask nginx.serviceNGINX 패키지가 아직 없는 신규 호스트에서만 mask합니다. 기존 NGINX가 있으면 서비스·기본 사이트를 보존하고 변경 배포 분기로 진행합니다.sudo apt-get install php-fpm php-cli php-opcache nginxsystemctl is-enabled nginx.service
sudo ss -ltnp | grep -E ':(80|443)[[:space:]]'신규 설치 분기에서는 nginx가 masked이고 새 리스너가 없어야 합니다. 기존 NGINX 분기에서는 기존 기준값과 비교합니다.sudo adduser --system --group --home /srv/phpapp --no-create-home phpapp계정이 없을 때만 실행합니다.php --version
php -m
systemctl list-unit-files 'php*-fpm.service'- 의도하지 않은 PHP 주 버전 교체나 패키지 제거가 없다.
- 필수 extension만 설치했고 php -m에서 확인된다.
- phpapp는 로그인 불가 시스템 계정이다.
extension이 없다면 무작정 전체 묶음을 설치하지 말고 공식 Ubuntu 패키지 이름과 보안 지원을 확인합니다.
기존 설정을 백업하고 전용 풀·NGINX FastCGI 구성을 작성합니다.
전용 FPM 풀과 FastCGI 구성
PHP 실행 계정과 NGINX 소켓 권한을 분리합니다. NGINX는 실제 파일이 존재할 때만 PHP-FPM으로 전달해 임의 경로 실행을 막습니다.
date -u +%Y%m%dT%H%M%SZsudo cp --preserve=all --no-clobber /etc/php/<PHP_VERSION>/fpm/pool.d/myapp.conf /etc/php/<PHP_VERSION>/fpm/pool.d/myapp.conf.<BACKUP_SUFFIX>.bak기존 파일이 있을 때만 실행합니다.sudo cp --preserve=all --no-clobber /etc/nginx/sites-available/phpapp.conf /etc/nginx/sites-available/phpapp.conf.<BACKUP_SUFFIX>.bak기존 파일이 있을 때만 실행합니다.sudo readlink -f /etc/nginx/sites-enabled/default
sudo nginx -T | grep -E 'listen .*default_server|server_name'신규 설치 분기에서만 패키지 기본 링크인지 확인합니다. 기존 운영 사이트라면 이동하지 않습니다.sudo install -d -m 0755 -o root -g root /etc/nginx/disabled-site-links
sudo mv --no-clobber /etc/nginx/sites-enabled/default /etc/nginx/disabled-site-links/default.<BACKUP_SUFFIX>신규 설치이고 위 확인 결과가 Ubuntu 패키지 기본 링크인 경우에만 실행합니다. sites-enabled 밖으로 이동해야 로드되지 않습니다.sudoedit /etc/php/<PHP_VERSION>/fpm/pool.d/myapp.confsudoedit /etc/nginx/sites-available/phpapp.confsudo ln -s /etc/nginx/sites-available/phpapp.conf /etc/nginx/sites-enabled/phpapp.conf링크가 없을 때만 실행하며 기존 파일을 덮어쓰지 않습니다.[myapp]
user = phpapp
group = phpapp
listen = /run/php/myapp.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = <MAX_CHILDREN>
pm.start_servers = <START_SERVERS>
pm.min_spare_servers = <MIN_SPARE_SERVERS>
pm.max_spare_servers = <MAX_SPARE_SERVERS>
pm.max_requests = 500
clear_env = yes
catch_workers_output = yes
security.limit_extensions = .php
php_admin_flag[display_errors] = off
php_admin_flag[log_errors] = onworker 수는 추측하지 말고 한 프로세스의 실제 메모리와 서버 여유량을 측정해 산정합니다. 비밀값을 pool 파일에 직접 쓰지 않습니다.server {
listen <NGINX_PRIVATE_IP>:<NGINX_PORT>;
server_name <APP_HOST>;
root /srv/phpapp/current/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ .php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/myapp.sock;
fastcgi_read_timeout 60s;
}
location ~ /.(?!well-known) {
deny all;
}
}웹 루트는 public 디렉터리로 제한합니다. 사용자 업로드 위치에서 PHP 실행이 가능하지 않도록 별도 location 정책을 추가합니다.- FPM 풀 실행 계정은 phpapp이고 소켓만 www-data가 접근한다.
- clear_env와 display_errors off를 유지한다.
- NGINX root는 public 디렉터리이며 try_files로 존재를 확인한다.
- 업로드 파일을 PHP로 실행할 수 없다.
max_children을 너무 높이면 OOM, 너무 낮추면 대기가 발생합니다. 실제 RSS와 동시 요청을 측정해 조정합니다.
FPM 문법·서비스·소켓을 먼저 검증하고 NGINX를 반영합니다.
FPM 선검증 후 NGINX 반영
FPM과 NGINX를 독립적으로 검사합니다. 각 reload는 해당 문법 검사가 성공한 경우에만 실행합니다.
sudo <PHP_FPM_BINARY> -tsudo <PHP_FPM_BINARY> -t && sudo systemctl reload <PHP_FPM_SERVICE>sudo ss -lxnp | grep '/run/php/myapp.sock'
sudo journalctl -u <PHP_FPM_SERVICE> -n 80 --no-pagersudo nginx -t && sudo systemctl unmask nginx.service && sudo systemctl enable --now nginx.service신규 설치 분기입니다. 패키지 기본 사이트를 보존 비활성화한 뒤 실행합니다.sudo nginx -t && sudo systemctl reload nginx.service기존 NGINX 운영 분기입니다. 기존 default/site 파일을 삭제하거나 이동하지 않습니다.- FPM 문법 검사가 성공한 뒤에만 FPM을 reload했다.
- myapp.sock가 www-data 접근 가능한 0660으로 생성됐다.
- 신규 또는 기존 NGINX 한 분기만 선택했고 nginx -t 성공 뒤에만 start·reload했다.
- 신규 분기에서는 패키지 default_server가 전 인터페이스에 남지 않았다.
- 기존 SSH 세션과 우회 경로를 유지했다.
소켓이 없으면 NGINX를 반복 reload하지 말고 FPM 풀 이름·문법·로그부터 해결합니다.
민감정보 없는 전용 healthz.php와 대표 기능을 경유 요청으로 검증합니다.
실제 FastCGI 요청 검증
phpinfo 페이지는 만들지 않습니다. 애플리케이션이 제공하는 최소 healthz.php와 대표 읽기 요청으로 FPM까지 도달하는지 확인합니다.
curl --fail --show-error --silent --max-time 5 -H 'Host: <APP_HOST>' http://<NGINX_PRIVATE_IP>:<NGINX_PORT>/healthz.phpsystemctl is-active <PHP_FPM_SERVICE> nginx.service
systemctl is-enabled <PHP_FPM_SERVICE> nginx.servicesudo ss -ltnp
sudo ss -lxnp | grep phpsudo journalctl -u <PHP_FPM_SERVICE> -u nginx.service --since '-10 minutes' --no-pager- healthz.php는 비밀·환경·extension 목록을 노출하지 않고 정상 상태만 반환한다.
- FPM은 TCP 공개 포트가 아니라 Unix 소켓만 사용한다.
- 정적 파일과 PHP 대표 요청이 모두 정상이다.
정적 파일은 되고 PHP만 502면 소켓·FPM을, PHP가 404면 root·try_files·릴리스 경로를 확인합니다.
웹 루트·업로드·오류 표시·소켓 권한을 최종 점검합니다.
실행·쓰기·정보 노출 경계 점검
웹 요청이 수정할 수 있는 경로와 실행 가능한 경로를 분리합니다. FPM 상태 페이지와 phpinfo는 공개하지 않습니다.
sudo stat -c '%U:%G %a %n' /srv/phpapp/current/public /srv/phpapp/sharedsudo <PHP_FPM_BINARY> -tt | grep -E 'clear_env|security.limit_extensions|display_errors|log_errors'CLI php.ini가 아니라 실제 FPM pool의 유효 설정을 확인합니다. 출력에 환경 비밀이 포함되지 않았는지 확인한 뒤에만 공유합니다.sudo nginx -tsudo ss -lntup- phpapp는 애플리케이션 코드와 vendor를 수정할 수 없다.
- 업로드·캐시 경로에는 필요한 최소 쓰기 권한만 있다.
- display_errors가 꺼져 있고 오류는 접근 제한 로그에 기록된다.
- phpinfo·FPM status·ping 페이지를 공개하지 않는다.
- PHP-FPM TCP 포트를 외부에 열지 않는다.
권한 오류를 777로 해결하지 말고 파일 소유권, 실행 계정, 필요한 단일 쓰기 경로를 맞춥니다.
실패 시 FPM 풀과 NGINX를 각각 검증된 백업으로 복원합니다.
FPM 풀과 NGINX 설정 복원
현재 설정은 .failed로 보존하고 실제 존재했던 백업만 복원합니다. 복원 구성도 문법 검사와 실제 health 요청을 통과해야 합니다.
sudo mv /etc/php/<PHP_VERSION>/fpm/pool.d/myapp.conf /etc/php/<PHP_VERSION>/fpm/pool.d/myapp.conf.failed.<ROLLBACK_SUFFIX>sudo cp --preserve=all /etc/php/<PHP_VERSION>/fpm/pool.d/myapp.conf.<BACKUP_SUFFIX>.bak /etc/php/<PHP_VERSION>/fpm/pool.d/myapp.conf && sudo <PHP_FPM_BINARY> -t && sudo systemctl reload <PHP_FPM_SERVICE>sudo mv /etc/nginx/sites-available/phpapp.conf /etc/nginx/sites-available/phpapp.conf.failed.<ROLLBACK_SUFFIX>sudo cp --preserve=all /etc/nginx/sites-available/phpapp.conf.<BACKUP_SUFFIX>.bak /etc/nginx/sites-available/phpapp.conf && sudo nginx -t && sudo systemctl reload nginx.servicecurl --fail --show-error --silent --max-time 5 -H 'Host: <APP_HOST>' http://<NGINX_PRIVATE_IP>:<NGINX_PORT>/healthz.php- 변경 기록과 일치하는 백업을 선택했다.
- FPM과 NGINX를 각각 검사 후 reload했다.
- 현재 실패 설정은 삭제하지 않고 제한된 권한으로 보존했다.
- 복원 후 실제 PHP 요청까지 성공했다.
설정 복원 후에도 실패하면 릴리스·extension·데이터 마이그레이션을 별도 복구합니다.
문제를 풀·소켓·NGINX·코드 계층으로 나눠 재현합니다.
SECURITY CHECK
운영 전 마지막 보안 점검
- PHP-FPM은 전용 비로그인 계정과 Unix 소켓으로 실행한다.
- NGINX는 존재하는 .php 파일만 FPM으로 전달한다.
- 웹 루트와 사용자 쓰기 경로를 분리하고 업로드 실행을 차단한다.
- display_errors·phpinfo·FPM 상태 페이지를 공개하지 않는다.
- FPM과 NGINX는 각 문법 검사가 성공한 뒤에만 reload한다.
COMMON ERRORS
자주 막히는 지점
NGINX가 502 Bad Gateway 반환
- 증상
- 정적 파일은 보이지만 PHP 요청만 502입니다.
- 가능한 원인
- FPM 미기동, 소켓 경로·소유권·AppArmor 정책 불일치일 수 있습니다.
- 확인 순서
- FPM 상태·journal·ss의 Unix 소켓·NGINX error log 순서로 확인합니다.
File not found 또는 Primary script unknown
- 증상
- PHP 요청이 404 또는 FastCGI 파일 경로 오류로 실패합니다.
- 가능한 원인
- NGINX root와 SCRIPT_FILENAME, 실제 릴리스 심볼릭 링크가 맞지 않을 수 있습니다.
- 확인 순서
- 실제 파일 경로와 document_root를 대조하고 try_files 보호는 제거하지 않습니다.
FPM worker 부족 또는 OOM
- 증상
- 요청이 대기하고 max_children 경고 또는 OOM kill이 발생합니다.
- 가능한 원인
- worker당 메모리를 측정하지 않고 max_children을 과도하게 설정했을 수 있습니다.
- 확인 순서
- 실제 RSS·동시 요청·서버 여유 메모리를 측정해 풀 크기를 낮추거나 용량을 확장합니다.
PRIMARY REFERENCES
공식 문서
설치 저장소와 지원 버전은 바뀔 수 있습니다. 검토일 이후에는 링크된 공식 문서와 현재 서버의 패키지 후보 버전을 함께 확인하세요.