The Problem
I was tired of PHP's type system. Even with PHPStan and Psalm, there's no substitute for real compile-time guarantees. But I'm also practical - I don't want to pay for a VPS, maintain a server, manage security updates, configure databases, set up backups, and babysit infrastructure when shared hosting costs < $10/month and handles all of that for me.
The constraint: How do you get the benefits of compiled, statically-typed languages (Go, Rust) on restrictive shared hosting that only officially supports PHP?
The Solution: PHP as a thin wrapper
The approach is relatively simple:
- PHP receives the HTTP request
- PHP serializes request context (headers, body, query params) to JSON
- PHP spawns your compiled binary as a subprocess
- Binary reads JSON from stdin, does its work, outputs response
- PHP captures stdout and returns it to the client
It's essentially CGI-style execution, but bypassing the hosting provider's CGI restrictions by going through PHP.
Implementation
PHP Side (simplified)
<?php
$context = [
'method' => $_SERVER['REQUEST_METHOD'],
'uri' => $_SERVER['REQUEST_URI'],
'query' => $_GET,
'headers' => getallheaders(),
'body' => file_get_contents('php://input') // may want to limit this
];
// may want to set a timeout
$process = proc_open(
'./myapp', // Your compiled binary
[
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'] // stderr
],
$pipes
);
if (is_resource($process)) {
// Send context to binary
fwrite($pipes[0], json_encode($context));
fclose($pipes[0]);
// Get response
$output = stream_get_contents($pipes[1]);
$errors = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($process);
if ($exitCode === 0) {
echo $output;
} else {
http_response_code(500);
echo json_encode(['error' => 'Process failed', 'stderr' => $errors]);
}
}
Go Side (simplified)
package main
import (
"database/sql"
"encoding/json"
"fmt"
"io"
"os"
_ "github.com/go-sql-driver/mysql"
)
type Context struct {
Method string `json:"method"`
URI string `json:"uri"`
Query map[string][]string `json:"query"`
Headers map[string]string `json:"headers"`
Body string `json:"body"`
}
type Response struct {
Data interface{} `json:"data"`
}
func main() {
// Read context from stdin
input, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read input: %v\n", err)
os.Exit(1)
}
var ctx Context
if err := json.Unmarshal(input, &ctx); err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse context: %v\n", err)
os.Exit(1)
}
// Your business logic here
result, err := handleRequest(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Request failed: %v\n", err)
os.Exit(1)
}
// Output response
fmt.Println("Content-Type: application/json\n")
json.NewEncoder(os.Stdout).Encode(result)
}
func handleRequest(ctx Context) (*Response, error) {
// Connect to database (use Unix socket for shared hosting)
db, err := sql.Open("mysql", "user:pass@unix(/var/run/mysqld/mysqld.sock)/dbname")
if err != nil {
return nil, err
}
defer db.Close()
// Your query logic
rows, err := db.Query("SELECT * FROM my_table")
if err != nil {
return nil, err
}
defer rows.Close()
// Process results...
var data []interface{}
// ... populate data ...
return &Response{Data: data}, nil
}
Building for shared hosting
The critical part: statically link your binary so it doesn't depend on the host's glibc version.
# For Go (pure Go, no cgo)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp \
-a -ldflags '-extldflags "-static"' .
# Verify it's static
ldd myapp # Should say "not a dynamic executable"
Upload via SFTP/SSH and set executable permissions:
chmod +x myapp
Database connection gotcha
Shared hosting typically blocks direct TCP connections to MySQL. Use Unix sockets instead:
// Unlikely to work on shared hosting:
db, err := sql.Open("mysql", "user:pass@tcp(localhost:3306)/dbname")
// Should work on shared hosting:
db, err := sql.Open("mysql", "user:pass@unix(/var/run/mysqld/mysqld.sock)/dbname")
You will need to find your socket path; you may use phpinfo()
for this.
Performance results (YMMV)
Single row query:
- Total time: 40ms
- Memory: ~2.4 MB
700 rows (406 KB JSON):
- Total time: 493ms
- Memory: ~2.4 MB
- Breakdown:
- PHP + spawn overhead: ~38ms
- MySQL query: ~350ms
- JSON encoding: ~50ms
- Pipe I/O: ~55ms
Memory usage is pretty good - Node.js would use >40 MB for the same workload.
Security Considerations
Important: Processes spawned on shared hosting are typically not sandboxed. Your binary runs with the same permissions as your PHP scripts - it can read/write any files your account can access and make any network connections allowed by your hosting provider.
This means:
- Validate all input rigorously - Never trust data passed from PHP to your binary
- Don't expose this pattern to untrusted users - This approach is for your own applications, not multi-tenant systems
- Sanitize file paths - Be careful with any file operations
- Limit what your binary can do - Follow principle of least privilege
- Monitor resource usage - Shared hosts will kill processes that consume too much CPU/memory
The security model is essentially the same as running PHP scripts - your code runs in your account's context with no additional isolation. Design accordingly.
Why This Works
Advantages:
- Real type safety - Compile-time guarantees, no runtime surprises
- Performance - Compiled binaries are fast
- Low memory - 2-10 MB vs 50-200 MB for typical app servers
- No server maintenance - Let your host handle updates, security patches, database management, backups
-
Cheap hosting - Works on < $10/month shared hosting (though some providers disable
proc_open
and such) - Language choice - Use Go, Rust, or any compiled language that can generate a fully static binary
- Familiar deployment - Just upload files via FTP/SFTP
Trade-offs:
- Process spawn overhead - ~30-40ms per request (mitigated with caching)
- No persistent state - Each request is isolated (use external storage/cache)
- Complexity - Two codebases (PHP wrapper + binary)
- Build step - Must compile for Linux x86_64
- Limited isolation - Binaries run with your account's full permissions
Conclusion
This approach bridges two worlds: the accessibility of shared hosting and the robustness of compiled languages. It's far from perfect - you sacrifice some performance for compatibility - but for side projects and small applications, it's a sweet spot of cost, safety, and simplicity.
You get:
- Solid type-safe business logic (if you use Rust)
- Reasonable performance (sub-second responses)
- Minimal memory footprint
- < $10/month hosting with zero infrastructure management
- Managed database, backups, and security updates already included in your hosting plan
Do I ultimately recommend this? For low traffic websites, educational purposes, why not. For production grade systems you may want to run your VPS or go for a containerized approach.
Top comments (0)