Haru Utils

애플리케이션 · 구성 레시피

PHP-FPM·NGINX 애플리케이션 구성

Ubuntu에서 전용 PHP-FPM 풀과 Unix 소켓을 만들고, 실제 PHP 파일 존재를 확인하는 NGINX FastCGI 설정을 문법 검사 후 적용하는 안전한 배포 절차입니다.
PHP-FPM 설치NGINX PHPFastCGI socketPHP pool502 Bad Gateway
지원 환경Ubuntu Server 24.04 LTS의 지원 PHP-FPM 패키지
예상 시간50
난이도중급
검토일2026-08-28

BEFORE YOU START

시작 전에 준비하세요

01

애플리케이션이 지원하는 PHP 주 버전·필수 extension 목록·healthz.php 경로

02

sudo 권한, 기존 SSH 세션, 콘솔 또는 프록시 우회 경로

03

읽기 전용 웹 루트와 별도 업로드·캐시·세션 쓰기 경로

04

기존 FPM 풀·NGINX 설정·애플리케이션 데이터의 검증된 백업

권장 대상 PHP 애플리케이션을 기본 www 풀과 분리하고 NGINX 뒤에서 최소 권한으로 운영하려는 개발자·운영자

FOLLOW THE RECIPE

8단계 구성·점검 레시피

1
확인시스템을 변경하지 않는 점검 단계

PHP 지원 범위와 실행 환경 확인

배포판 후보 PHP 버전과 애플리케이션 지원 범위를 맞춥니다. 예시의 <PHP_VERSION>·<PHP_FPM_SERVICE>·<PHP_FPM_BINARY>를 실제 값으로 고정합니다.

운영체제
cat /etc/os-release
PHP 후보
apt-cache policy php-fpm php-cli
기존 PHP
php --version
php --ini
FPM 서비스 후보
systemctl list-unit-files 'php*-fpm.service'
  • 프레임워크가 배포판 PHP 주 버전을 지원한다.
  • 필수 extension과 메모리·업로드 요구량을 확인했다.
  • 자리표시자에 사용할 실제 서비스와 소켓 경로를 기록했다.
결과 읽기

애플리케이션이 배포판 PHP를 지원하지 않으면 비공식 저장소를 즉시 추가하지 말고 업그레이드 또는 격리 배포를 별도 설계합니다.

다음 판단

기존 80·443 포트, FPM 풀, 소켓과 웹 루트 권한을 확인합니다.

2
확인시스템을 변경하지 않는 점검 단계

기존 풀·소켓·웹 루트 점검

기본 풀을 덮어쓰지 않고 myapp 전용 풀을 추가합니다. 동일 이름 소켓과 NGINX server_name 충돌을 먼저 찾습니다.

리스닝 포트와 소켓
sudo ss -ltnp
sudo ss -lxnp | grep php
기존 FPM 풀
sudo find /etc/php -path '*/fpm/pool.d/*.conf' -type f -print
기존 NGINX 호스트
sudo nginx -T | grep -E 'server_name|fastcgi_pass'
출력에 내부 도메인과 경로가 포함될 수 있으므로 외부에 그대로 공유하지 않습니다.
웹 루트 권한
sudo stat -c '%U:%G %a %n' /srv/phpapp/current/public
  • myapp 풀·소켓·server_name이 기존 구성과 겹치지 않는다.
  • 웹 루트는 PHP 실행 계정이 수정할 수 없다.
  • 업로드·캐시·세션 쓰기 경로를 웹 루트 밖 또는 제한된 하위 경로로 분리했다.
결과 읽기

기존 소켓을 다른 사이트가 사용한다면 공유하지 말고 전용 풀과 소켓 이름을 선택합니다.

다음 판단

배포판 패키지의 변경 목록을 검토하고 설치합니다.

3
변경패키지·설정·서비스 상태가 달라지는 단계

PHP-FPM과 NGINX 설치

Ubuntu 지원 저장소에서 FPM·CLI·OPcache와 NGINX를 설치합니다. 애플리케이션 extension은 필요한 것만 명시적으로 추가합니다.

변경 단계입니다. 대상 서버, 백업 파일, 서비스 중단 영향과 바로 이전 상태로 돌아가는 방법을 다시 확인하세요.
APT 인덱스
sudo apt update
변경 미리 보기
apt-get --simulate install php-fpm php-cli php-opcache nginx
신규 NGINX 자동 시작 차단
dpkg-query -W nginx >/dev/null 2>&1 || sudo systemctl mask nginx.service
NGINX 패키지가 아직 없는 신규 호스트에서만 mask합니다. 기존 NGINX가 있으면 서비스·기본 사이트를 보존하고 변경 배포 분기로 진행합니다.
패키지 설치
sudo apt-get install php-fpm php-cli php-opcache nginx
신규 설치 비노출 확인
systemctl 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 구성을 작성합니다.

4
주의권한·연결·중단 영향을 확인할 단계

전용 FPM 풀과 FastCGI 구성

PHP 실행 계정과 NGINX 소켓 권한을 분리합니다. NGINX는 실제 파일이 존재할 때만 PHP-FPM으로 전달해 임의 경로 실행을 막습니다.

백업 접미사
date -u +%Y%m%dT%H%M%SZ
기존 풀 백업
sudo 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
기존 파일이 있을 때만 실행합니다.
기존 NGINX 설정 백업
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 밖으로 이동해야 로드되지 않습니다.
FPM 풀 편집
sudoedit /etc/php/<PHP_VERSION>/fpm/pool.d/myapp.conf
NGINX 호스트 편집
sudoedit /etc/nginx/sites-available/phpapp.conf
사이트 링크 생성
sudo ln -s /etc/nginx/sites-available/phpapp.conf /etc/nginx/sites-enabled/phpapp.conf
링크가 없을 때만 실행하며 기존 파일을 덮어쓰지 않습니다.
설정 예시 · /etc/php/<PHP_VERSION>/fpm/pool.d/myapp.conf
전용 PHP-FPM 풀
[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] = on
worker 수는 추측하지 말고 한 프로세스의 실제 메모리와 서버 여유량을 측정해 산정합니다. 비밀값을 pool 파일에 직접 쓰지 않습니다.
설정 예시 · /etc/nginx/sites-available/phpapp.conf
실제 PHP 파일만 전달하는 NGINX 구성
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를 반영합니다.

5
주의권한·연결·중단 영향을 확인할 단계

FPM 선검증 후 NGINX 반영

FPM과 NGINX를 독립적으로 검사합니다. 각 reload는 해당 문법 검사가 성공한 경우에만 실행합니다.

FPM 문법 검사
sudo <PHP_FPM_BINARY> -t
FPM 검증 후 reload
sudo <PHP_FPM_BINARY> -t && sudo systemctl reload <PHP_FPM_SERVICE>
FPM 소켓과 로그
sudo ss -lxnp | grep '/run/php/myapp.sock'
sudo journalctl -u <PHP_FPM_SERVICE> -n 80 --no-pager
A · 신규 NGINX 검증 후 시작
sudo nginx -t && sudo systemctl unmask nginx.service && sudo systemctl enable --now nginx.service
신규 설치 분기입니다. 패키지 기본 사이트를 보존 비활성화한 뒤 실행합니다.
B · 기존 NGINX 검증 후 reload
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와 대표 기능을 경유 요청으로 검증합니다.

6
확인시스템을 변경하지 않는 점검 단계

실제 FastCGI 요청 검증

phpinfo 페이지는 만들지 않습니다. 애플리케이션이 제공하는 최소 healthz.php와 대표 읽기 요청으로 FPM까지 도달하는지 확인합니다.

NGINX 경유 healthz
curl --fail --show-error --silent --max-time 5 -H 'Host: <APP_HOST>' http://<NGINX_PRIVATE_IP>:<NGINX_PORT>/healthz.php
서비스 상태
systemctl is-active <PHP_FPM_SERVICE> nginx.service
systemctl is-enabled <PHP_FPM_SERVICE> nginx.service
리스닝 범위
sudo ss -ltnp
sudo ss -lxnp | grep php
최근 오류
sudo 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·릴리스 경로를 확인합니다.

다음 판단

웹 루트·업로드·오류 표시·소켓 권한을 최종 점검합니다.

7
주의권한·연결·중단 영향을 확인할 단계

실행·쓰기·정보 노출 경계 점검

웹 요청이 수정할 수 있는 경로와 실행 가능한 경로를 분리합니다. FPM 상태 페이지와 phpinfo는 공개하지 않습니다.

웹 루트와 쓰기 경로 권한
sudo stat -c '%U:%G %a %n' /srv/phpapp/current/public /srv/phpapp/shared
효과적인 PHP 보안 항목
sudo <PHP_FPM_BINARY> -tt | grep -E 'clear_env|security.limit_extensions|display_errors|log_errors'
CLI php.ini가 아니라 실제 FPM pool의 유효 설정을 확인합니다. 출력에 환경 비밀이 포함되지 않았는지 확인한 뒤에만 공유합니다.
NGINX 전체 문법
sudo nginx -t
공개 소켓
sudo ss -lntup
  • phpapp는 애플리케이션 코드와 vendor를 수정할 수 없다.
  • 업로드·캐시 경로에는 필요한 최소 쓰기 권한만 있다.
  • display_errors가 꺼져 있고 오류는 접근 제한 로그에 기록된다.
  • phpinfo·FPM status·ping 페이지를 공개하지 않는다.
  • PHP-FPM TCP 포트를 외부에 열지 않는다.
결과 읽기

권한 오류를 777로 해결하지 말고 파일 소유권, 실행 계정, 필요한 단일 쓰기 경로를 맞춥니다.

다음 판단

실패 시 FPM 풀과 NGINX를 각각 검증된 백업으로 복원합니다.

8
주의권한·연결·중단 영향을 확인할 단계

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>
현재 NGINX 설정 보존
sudo mv /etc/nginx/sites-available/phpapp.conf /etc/nginx/sites-available/phpapp.conf.failed.<ROLLBACK_SUFFIX>
이전 NGINX 복원·검증
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.service
복원 실제 요청
curl --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

공식 문서

설치 저장소와 지원 버전은 바뀔 수 있습니다. 검토일 이후에는 링크된 공식 문서와 현재 서버의 패키지 후보 버전을 함께 확인하세요.

도구 빠른 검색

최근 사용한 도구를 다시 열거나, 이름과 기능으로 검색하세요.

검색어와 도구의 입력·결과는 저장하지 않습니다.

↑↓ 이동 · Enter 열기 · Esc 닫기