This document explains the architecture of the Cloud Foundry PHP buildpack, with particular focus on why it differs from other Cloud Foundry buildpacks (Go, Ruby, Python, Node.js).
- Overview
- Why PHP is Different
- Buildpack Lifecycle
- Runtime Architecture
- Pre-compiled Binaries
- Template Rewriting System
- Process Management
- Extensions System
- Comparison with Other Buildpacks
The PHP buildpack uses a hybrid architecture that combines:
- Bash wrapper scripts for buildpack lifecycle hooks (detect, supply, finalize, release)
- Go implementations for core logic (compiled at staging time)
- Pre-compiled runtime utility for application startup (start)
This design optimizes for both flexibility during staging and performance at runtime.
Unlike Go, Ruby, Python, or Node.js applications, PHP applications require a multi-process architecture:
┌─────────────────────────────────────────┐
│ PHP Application │
├─────────────────────────────────────────┤
│ ┌────────────┐ ┌──────────────┐ │
│ │ PHP-FPM │◄────►│ Web Server │ │
│ │ (FastCGI) │ TCP │ (httpd/nginx)│ │
│ │ Port 9000 │ │ │ │
│ └────────────┘ └──────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ └────────┬───────────┘ │
│ │ │
│ Process Manager │
│ ($HOME/.bp/bin/start) │
└─────────────────────────────────────────┘
Key differences from other languages:
| Language | Architecture | Startup Command |
|---|---|---|
| Go | Single process | ./my-app |
| Ruby | Single process (Puma/Unicorn) | bundle exec rails s |
| Python | Single process (Gunicorn) | gunicorn app:app |
| Node.js | Single process | node server.js |
| PHP | Two processes | .bp/bin/start (manager) |
PHP requires:
- PHP-FPM - Executes PHP code via FastCGI protocol
- Web Server - Serves static files, proxies PHP requests to PHP-FPM
Bash wrapper that compiles and runs src/php/detect/cli/main.go:
#!/bin/bash
# Compiles Go code at staging time
GOROOT=$GoInstallDir $GoInstallDir/bin/go build -o $output_dir/detect ./src/php/detect/cli
$output_dir/detect "$BUILD_DIR"Why bash wrapper?
- Allows on-the-fly compilation with correct Go version
- No pre-built binaries needed for different platforms
- Simpler maintenance (one codebase for all platforms)
Installs dependencies:
- PHP runtime
- Web server (httpd or nginx)
- PHP extensions
- Composer (if needed)
Location: src/php/supply/supply.go
Configures the application for runtime:
- Processes configuration files to replace build-time placeholders with runtime values
- Generates start scripts with correct paths
- Copies
startbinary to$HOME/.bp/bin/ - Sets up environment variables
Location: src/php/finalize/finalize.go
Key code (finalize.go:160-212):
func (f *Finalizer) CreateStartScript(depsIdx string) error {
// Read WEB_SERVER from options.json
opts, _ := options.LoadOptions(buildDir)
switch opts.WebServer {
case "nginx":
startScript = f.generateNginxStartScript(depsIdx, opts)
case "httpd":
startScript = f.generateHTTPDStartScript(depsIdx, opts)
case "none":
startScript = f.generatePHPFPMStartScript(depsIdx, opts)
}
// Write to $DEPS_DIR/0/start_script.sh
os.WriteFile(startScriptPath, []byte(startScript), 0755)
}Outputs the default process type:
default_process_types:
web: $HOME/.bp/bin/startLocation: src/php/release/cli/main.go
When a PHP application starts, Cloud Foundry runs:
$HOME/.bp/bin/startThis triggers the following sequence:
1. Cloud Foundry
└─► $HOME/.bp/bin/start
│
├─► Load .procs file
│ (defines processes to run)
│
├─► Handle dynamic runtime variables
│ (PORT, TMPDIR via sed replacement)
│
├─► Start PHP-FPM
│ (background, port 9000)
│
├─► Start Web Server
│ (httpd or nginx)
│
└─► Monitor both processes
(multiplex output, handle failures)
The buildpack includes a pre-compiled runtime utility:
Unlike lifecycle hooks (detect, supply, finalize) which run during staging, this utility runs during application startup. Pre-compilation provides:
- Fast startup time - No compilation delay when starting the app
- Reliability - Go toolchain not available in runtime container
- Simplicity - Single binary, no dependencies
Purpose: Multi-process manager
Source: src/php/start/cli/main.go
Why needed: PHP requires coordinated management of two processes (PHP-FPM + Web Server) with:
- Output multiplexing (combined logs)
- Lifecycle management (start both, stop if one fails)
- Signal handling (graceful shutdown)
- Process monitoring
How it works:
// 1. Load process definitions from $HOME/.procs
procs, err := loadProcesses("$HOME/.procs")
// Format: name: command
// php-fpm: $DEPS_DIR/0/start_script.sh
// 2. Create process manager
pm := NewProcessManager()
for name, cmd := range procs {
pm.AddProcess(name, cmd)
}
// 3. Start all processes
pm.Start()
// 4. Multiplex output with timestamps
// 14:23:45 php-fpm | Starting PHP-FPM...
// 14:23:46 httpd | Starting Apache...
// 5. Monitor for failures
// If any process exits, shutdown all and exit
pm.Loop()Process file format ($HOME/.procs):
# Comments start with #
process-name: shell command to run
# Example:
php-fpm: $DEPS_DIR/0/start_script.sh
Signal handling:
SIGTERM,SIGINT→ Graceful shutdown of all processes- Child process exits → Shutdown all and exit with same code
The buildpack uses a sophisticated template system to handle runtime configuration:
Cloud Foundry provides runtime-assigned values:
# Assigned by Cloud Foundry when container starts
export PORT=8080 # HTTP port (random)
export HOME=/home/vcap/app # Application directory
export DEPS_DIR=/home/vcap/deps # Dependencies directoryThese values cannot be known at staging time, so configuration files use templates:
| Pattern | Description | Example |
|---|---|---|
@{VAR} |
Braced @ syntax | @{PORT} → 8080 |
#{VAR} |
Braced # syntax | #{HOME} → /home/vcap/app |
@VAR@ |
@ delimited | @WEBDIR@ → htdocs |
#VAR |
# prefix (word boundary) | #PHPRC → /home/vcap/deps/0/php/etc |
| Variable | Description | Example Value |
|---|---|---|
PORT |
HTTP listen port | 8080 |
HOME |
Application root | /home/vcap/app |
WEBDIR |
Web document root | htdocs |
LIBDIR |
Library directory | lib |
PHP_FPM_LISTEN |
PHP-FPM socket | 127.0.0.1:9000 |
PHPRC |
PHP config dir | /home/vcap/deps/0/php/etc |
┌──────────────────────────────────────────────────────────────┐
│ 1. Staging Time (supply phase) │
│ - Copy template configs with @{PORT}, #{HOME}, etc. │
│ - Placeholders remain in config files │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 2. Finalize Phase (build-time processing) │
│ - Replace build-time placeholders with known values │
│ - Process PHP, PHP-FPM, and web server configs │
│ - Dynamic runtime values (PORT, TMPDIR) handled via sed │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 3. Runtime (start script) │
│ - Export environment variables (PORT, TMPDIR, etc.) │
│ - Use sed to replace remaining dynamic variables │
│ - Configs now have actual values instead of templates │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 4. Start Processes │
│ - PHP-FPM reads php-fpm.conf (with real PORT) │
│ - Web server reads config (with real HOME, WEBDIR) │
└──────────────────────────────────────────────────────────────┘
At staging time (defaults/config/nginx/nginx.conf):
server {
listen @{PORT};
root #{HOME}/@WEBDIR@;
location ~ \.php$ {
fastcgi_pass #{PHP_FPM_LISTEN};
}
}At finalize/runtime (after placeholder replacement with PORT=8080, HOME=/home/vcap/app, WEBDIR=htdocs, PHP_FPM_LISTEN=127.0.0.1:9000):
server {
listen 8080;
root /home/vcap/app/htdocs;
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
}
}The start binary implements a sophisticated process manager:
-
Multi-process coordination
- Start processes in defined order
- Monitor all processes
- Shutdown all if any fails
-
Output multiplexing
- Combine stdout/stderr from all processes
- Add timestamps and process names
- Aligned formatting
-
Signal handling
- Forward signals to all child processes
- Graceful shutdown on SIGTERM/SIGINT
- Exit with appropriate code
-
Failure detection
- Monitor process exit codes
- Immediate shutdown if critical process fails
- Propagate exit code to Cloud Foundry
14:23:45 php-fpm | [08-Jan-2025 14:23:45] NOTICE: fpm is running, pid 42
14:23:45 php-fpm | [08-Jan-2025 14:23:45] NOTICE: ready to handle connections
14:23:46 httpd | [Wed Jan 08 14:23:46.123] [mpm_event:notice] [pid 43] AH00489: Apache/2.4.54 configured
14:23:46 httpd | [Wed Jan 08 14:23:46.456] [core:notice] [pid 43] AH00094: Command line: 'httpd -D FOREGROUND'
Location: src/php/start/cli/main.go
Key components:
type ProcessManager struct {
processes []*Process // Managed processes
mu sync.Mutex // Thread safety
wg sync.WaitGroup // Process coordination
done chan struct{} // Shutdown signal
exitCode int // Final exit code
}
// Main loop
func (pm *ProcessManager) Loop() int {
// Start all processes
pm.Start()
// Setup signal handlers
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
// Wait for signal or process failure
select {
case sig := <-sigChan:
pm.Shutdown(sig)
case <-pm.done:
// A process exited
}
return pm.exitCode
}The buildpack uses an extensions architecture for optional functionality:
Located in src/php/extensions/:
- composer - Manages PHP dependencies via Composer
- dynatrace - Application performance monitoring
- newrelic - Application monitoring and analytics
Extensions hook into buildpack phases:
type Extension interface {
// Called during supply phase
Supply(stager libbuildpack.Stager) error
// Called during finalize phase
Finalize(stager libbuildpack.Stager) error
}Example: Composer Extension (src/php/extensions/composer/composer.go)
func (c *ComposerExtension) Supply(stager libbuildpack.Stager) error {
// 1. Check if composer.json exists
if !fileExists("composer.json") {
return nil
}
// 2. Install composer.phar
if err := c.installComposer(); err != nil {
return err
}
// 3. Run composer install
cmd := exec.Command("php", "composer.phar", "install", "--no-dev")
return cmd.Run()
}# Go is simple: single binary
default_process_types:
web: ./my-go-appNo need for:
- Multi-process management
- Runtime configuration templating
- Pre-compiled utilities
# Ruby uses single application server
default_process_types:
web: bundle exec puma -C config/puma.rbSimilar to Go: Single process, no web server separation
# Python uses WSGI server
default_process_types:
web: gunicorn app:appSimilar to Go/Ruby: Single process model
# PHP requires process manager
default_process_types:
web: $HOME/.bp/bin/startUnique requirements:
- ✅ Multi-process coordination (PHP-FPM + Web Server)
- ✅ Runtime configuration templating (PORT assigned at runtime)
- ✅ Pre-compiled utilities (rewrite, start)
- ✅ Complex lifecycle management
| Feature | Go | Ruby | Python | PHP |
|---|---|---|---|---|
| Process count | 1 | 1 | 1 | 2 |
| Process manager | ❌ | ❌ | ❌ | ✅ |
| Runtime templating | ❌ | ❌ | ❌ | ✅ |
| Pre-compiled utilities | ❌ | ❌ | ❌ | ✅ |
| Web server | Built-in | Built-in | Built-in | Separate |
| FastCGI | ❌ | ❌ | ❌ | ✅ |
# Build Go binaries
./scripts/build.sh
# Package buildpack
./scripts/package.sh --uncached
# Run tests
./scripts/unit.sh
./scripts/integration.sh# Set up test environment
export BUILD_DIR=/tmp/test-build
export CACHE_DIR=/tmp/test-cache
export DEPS_DIR=/tmp/test-deps
export DEPS_IDX=0
mkdir -p $BUILD_DIR $CACHE_DIR $DEPS_DIR/0
# Copy test fixture
cp -r fixtures/default/* $BUILD_DIR/
# Run buildpack phases
./bin/detect $BUILD_DIR
./bin/supply $BUILD_DIR $CACHE_DIR $DEPS_DIR $DEPS_IDX
./bin/finalize $BUILD_DIR $CACHE_DIR $DEPS_DIR $DEPS_IDX
# Check generated files
cat $DEPS_DIR/0/start_script.sh
ls -la $BUILD_DIR/.bp/bin/# Enable debug logging in start script
export BP_DEBUG=true
# Start script will output:
# - set -ex (verbose execution)
# - Binary existence checks
# - Environment variables
# - Process startup logs# Edit source
vim src/php/start/cli/main.go
# Rebuild binary
cd src/php/start/cli
go build -o ../../../../bin/start
# Test changes
./scripts/integration.shThe PHP buildpack's unique architecture is driven by PHP's multi-process nature:
- Multi-process requirement - PHP-FPM + Web Server (unlike Go/Ruby/Python single process)
- Build-time configuration processing - Most placeholders replaced during finalize phase
- Runtime variable handling - Dynamic values (PORT, TMPDIR) handled via sed at startup
- Process coordination - Two processes must start, run, and shutdown together
- Pre-compiled utility - Fast startup, no compilation during app start
This architecture ensures PHP applications run reliably and efficiently in Cloud Foundry while maintaining compatibility with standard PHP deployment patterns.