본문 바로가기
WIKI 기술 지식 베이스

php_fastcgi 지시문

원문 보기 위키 갱신

php_fastcgi 지시문 (PHP FastCGI 프록시)

php_fastcgi 지시문은 요청을 php-fpm 같은 PHP FastCGI 서버로 프록시하는 의견이 반영된(opinionated) 지시문이에요. 보통 root 지시문과 file_server 지시문과 함께 사용해요.

출처: Caddy 공식 문서

본문

요청을 php-fpm 같은 PHP FastCGI 서버로 프록시하는 의견이 반영된 지시문이에요.

Caddy의 reverse_proxy는 모든 FastCGI 애플리케이션을 서빙할 수 있지만, 이 지시문은 PHP 앱에 특화되어 있어요. 이 지시문은 더 긴 설정을 대체하는 편리한 단축이에요.

사이트 루트의 index.php가 라우터 역할을 한다고 기대해요. 원하지 않으면 try_files 하위 지시문을 재구성해 기본 재작성 동작을 바꾸거나, 확장 형태를 기반으로 필요에 맞게 커스터마이즈할 수 있어요.

아래 나열된 하위 지시문에 더해, 이 지시문은 reverse_proxy의 모든 하위 지시문도 지원해요. 예를 들어 부하 분산과 헬스체크를 활성화할 수 있어요.

대부분의 현대 PHP 앱은 추가 하위 지시문이나 커스터마이즈 없이 잘 동작해요. 하위 지시문은 보통 특정 엣지 케이스나 레거시 PHP 앱에서만 사용돼요.

문법 (Syntax)

php_fastcgi [<matcher>] <php-fpm_gateways...> {
	root <path>
	split <substrings...>
	index <filename>|off
	try_files <files...>
	env [<key> <value>]
	resolve_root_symlink
	capture_stderr
	dial_timeout  <duration>
	read_timeout  <duration>
	write_timeout <duration>

	<any other reverse_proxy subdirectives...>
}
  • **<php-fpm_gateways...>**는 FastCGI 서버의 주소예요. 보통 TCP 소켓 또는 유닉스 소켓 파일이에요.

  • root는 사이트의 루트 폴더를 설정해요. 항상 root 지시문을 php_fastcgi와 함께 쓰는 것을 권장하지만, PHP-FPM 업스트림이 Caddy와 다른 루트를 사용할 때 이 값을 재정의하면 유용해요(예시). root 지시문을 쓰면 그 값을 기본으로 하고, 그 외엔 Caddy의 현재 작업 디렉터리를 기본으로 해요.

  • split은 URI를 두 부분으로 나누는 부분 문자열을 설정해요. 첫 번째로 일치하는 부분 문자열이 "path info"를 경로에서 분리하는 데 사용돼요. 첫 번째 조각은 일치하는 부분 문자열로 접미사가 붙고 실제 리소스(CGI 스크립트) 이름으로 간주돼요. 두 번째 조각은 CGI 스크립트가 사용할 PATH_INFO로 설정돼요. 기본값: .php

  • index는 디렉터리 인덱스 파일로 취급할 파일 이름을 지정해요. 확장 형태의 파일 matcher에 영향을 줘요. 기본값: index.php. 일치하는 파일이 없을 때 index.php로의 재작성 폴백을 비활성화하려면 off로 설정할 수 있어요.

  • try_files는 기본 try-files 재작성에 대한 재정의를 지정해요. 자세한 내용은 try_files 지시문을 참고해요. 기본값: {path} {path}/index.php index.php.

  • env는 주어진 값으로 추가 환경 변수를 설정해요. 여러 환경 변수에 대해 여러 번 지정할 수 있어요. 기본적으로 모든 관련 FastCGI 환경 변수가 이미 설정돼 있지만(SERVER_ADDR와 HTTP_* 변수로 된 HTTP 헤더 포함), 필요에 따라 변수를 추가하거나 재정의할 수 있어요. httpoxy를 완화하기 위해 클라이언트의 Proxy 요청 헤더는 HTTP_PROXY로 절대 전달되지 않아요.

  • resolve_root_symlink는 root 디렉터리가 심볼릭 링크일 때 이를 실제 값으로 해석하도록 해요. 배포 전략으로, 다른 디렉터리의 새 버전을 가리키도록 심볼릭 링크만 바꿔치기하는 방식으로 때때로 사용돼요. 반복된 시스템 호출을 피하기 위해 기본적으로 비활성화돼요.

  • capture_stderr는 업스트림 fastcgi 서버가 stderr로 보낸 모든 메시지를 캡처하고 기록하도록 해요. 기본적으로 WARN 레벨로 기록돼요. 응답이 4xx 또는 5xx 상태면 ERROR 레벨이 대신 사용돼요. 기본적으로 stderr는 무시돼요.

  • dial_timeout은 업스트림 소켓에 연결할 때 기다리는 시간을 설정하는 기간 값이에요. 기본값: 3s.

  • read_timeout은 FastCGI 업스트림에서 읽을 때 기다리는 시간을 설정하는 기간 값이에요. 기본값: 타임아웃 없음.

  • write_timeout은 FastCGI 업스트림에 보낼 때 기다리는 시간을 설정하는 기간 값이에요. 기본값: 타임아웃 없음.

이 지시문은 리버스 프록시를 감싼 의견이 반영된 래퍼이므로, reverse_proxy의 어떤 하위 지시문이든 커스터마이즈에 사용할 수 있어요.

FastCGI는 요청 본문의 길이를 미리 알아야 해요. 본문이 있지만 Content-Length가 없는 요청(예: Transfer-Encoding: chunked, 또는 헤더 없이 보내진 HTTP/2·HTTP/3 요청)은 본문 전체가 request_buffers 버퍼에 들어가서 길이를 결정할 수 있는 경우가 아니면 411 Length Required로 거부돼요. 버퍼는 본문보다 커야 해요. 버퍼와 정확히 같은 크기의 본문도 여전히 거부돼요. request_buffers unlimited로 설정하면 어떤 크기의 본문도 버퍼링하지만, request_body 지시문으로 요청 크기를 제한하는 것도 고려해요.

확장 형태 (Expanded form)

php_fastcgi 지시문(하위 지시문 없이)은 다음 설정과 동일해요. 대부분의 현대 PHP 앱은 이 프리셋과 잘 작동해요. 그렇지 않다면 php_fastcgi 단축 대신 이 형태에서 필요한 부분을 가져와 커스터마이즈해도 좋아요.

route {
	# 디렉터리 요청에 뒤 슬래시 추가
	# 이 리다이렉트는 "{http.request.uri.path}/index.php"가
	# try_files 목록에 없으면 자동으로 비활성화됨
	@canonicalPath {
		file {path}/index.php
		not path */
	}
	redir @canonicalPath {http.request.orig_uri.path}/ 308

	# 요청한 파일이 없으면 인덱스 파일을 시도하고 index.php는 항상 존재한다고 가정
	@indexFiles file {
		try_files {path} {path}/index.php index.php
		try_policy first_exist_fallback
		split_path .php
	}
	rewrite @indexFiles {file_match.relative}

	# PHP 파일을 FastCGI 응답기로 프록시
	@phpFiles path *.php
	reverse_proxy @phpFiles <php-fpm_gateway> {
		transport fastcgi {
			split .php
		}
	}
}

설명 (Explanation)

  • 첫 번째 섹션은 요청 경로를 정규화하는 작업이에요. 목표는 디스크의 디렉터리를 대상으로 하는 요청에 실제로 뒤 슬래시 /가 추가되도록 보장해서, 그 디렉터리에 대한 요청에 단일 URL만 유효하게 만드는 거예요.

    이 정규화는 try_files 하위 지시문이 {path}/index.php(기본값)를 포함할 때만 일어나요.

    슬래시로 끝나지 않고, index.php 파일을 포함하는 디스크 디렉터리에 매핑되는 요청만 매칭하는 요청 matcher로 수행하며, 매칭되면 뒤 슬래시를 붙인 HTTP 308 리다이렉트를 수행해요. 예를 들어 /foo/index.php가 디스크에 있으면 경로 /foo를 /foo/로 리다이렉트해요(슬래시 /를 붙여 경로를 디렉터리로 정규화).

  • 다음 섹션은 일치하는 파일이 디스크에 존재하는지에 따라 경로 재작성을 수행해요. 이것은 .php 뒤의 경로 부분을 기억하는 부수 효과도 있어요(요청 경로에 .php가 있었다면). 이는 Caddy가 FastCGI 환경 변수를 올바르게 설정하는 데 중요해요.

  • 먼저 {path}가 디스크에 존재하는 파일인지 확인해요. 그렇다면 그 경로로 재작성해요. 이는 나머지를 사실상 단락시키고, 디스크에 존재하는 파일에 대한 요청이 다른 방식으로 재작성되지 않게 보장해요(아래 다음 단계 참고). 예를 들어 /js/app.js 파일이 디스크에 있으면 그 경로에 대한 요청은 동일하게 유지돼요.

  • 둘째, {path}/index.php가 디스크에 존재하는 파일인지 확인해요. 그렇다면 그 경로로 재작성해요. /foo/ 같은 디렉터리 요청에 대해서는 /foo//index.php(/foo/index.php로 정규화됨)를 찾고, 존재하면 요청을 그 경로로 재작성해요. 웹루트의 하위 디렉터리에서 다른 PHP 앱을 실행 중이라면 이 동작이 때때로 유용해요.

  • 마지막으로 항상 index.php로 재작성해요(현대 PHP 앱에는 거의 항상 존재해요). 이를 통해 PHP 앱이 디스크의 파일에 매핑되지 않는 경로에 대한 모든 요청을 index.php 스크립트를 진입점으로 처리할 수 있어요.

  • 그리고 마지막 섹션이 실제로 요청을 PHP FastCGI(또는 PHP-FPM) 서비스로 프록시해 PHP 코드를 실행해요. 요청 matcher는 .php로 끝나는 요청만 매칭하므로, PHP 스크립트 가 아니고 디스크에 존재하는 파일은 이 지시문이 처리하지 않고 흘려보내요.

php_fastcgi 지시문만으로는 보통 충분하지 않아요. 거의 항상 root 지시문과 짝을 이뤄 디스크의 파일 위치를 설정하고(현대 PHP 앱에서는 /var/www/html/public일 수 있으며, public 디렉터리에 index.php가 있음), file_server 지시문으로 이 지시문이 처리하지 않고 흘러내려온 정적 파일(JS, CSS, 이미지 등)을 서빙해야 해요.

예시 (Examples)

모든 PHP 요청을 127.0.0.1:9000에서 수신하는 FastCGI 응답기로 프록시해요:

php_fastcgi 127.0.0.1:9000

같은 동작이지만 /blog/ 아래 요청에만:

php_fastcgi /blog/* localhost:9000

유닉스 소켓으로 수신하는 PHP-FPM을 사용할 때:

php_fastcgi unix//run/php/php8.2-fpm.sock

root 지시문은 거의 항상 PHP 스크립트가 있는 디렉터리를 지정하고, file_server 지시문은 정적 파일을 서빙하는 데 쓰여요:

example.com {
	root /var/www/html/public
	php_fastcgi 127.0.0.1:9000
	file_server
}

Caddy로 여러 PHP 앱을 서빙할 때, 각 앱의 웹루트가 달라야 Caddy가 정적 파일을 별도로 읽고 서빙하며 PHP 파일 존재를 감지할 수 있어요.

Docker를 사용한다면 PHP-FPM 컨테이너에 파일이 종종 같은 루트에 마운트돼 있어요. 그 경우 해결책은 파일을 Caddy 컨테이너의 다른 디렉터리에 마운트한 다음 root 하위 지시문으로 각 컨테이너의 루트를 설정하는 거예요:

app1.example.com {
	root /srv/app1/public
	php_fastcgi app1:9000 {
		root /var/www/html/public
	}
	file_server
}

app2.example.com {
	root /srv/app2/public
	php_fastcgi app2:9000 {
		root /var/www/html/public
	}
	file_server
}

index.php를 진입점으로 사용하지 않는 PHP 사이트에서는 대신 404 에러를 발생시키도록 폴백할 수 있어요. 그 에러는 handle_errors 지시문으로 잡아 처리할 수 있어요:

example.com {
	php_fastcgi localhost:9000 {
		try_files {path} {path}/index.php =404
	}

	handle_errors {
		respond "{err.status_code} {err.status_text}"
	}
}

더 알아보기 (Learn more)