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_openand 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 (2)
Great read! 👏 I love how you’ve shown that you can have your cake and eat it too by leveraging shared hosting while enjoying the benefits of a compiled language like Rust.
A few thoughts:
One question: how do you handle caching or persistent connections when every request spins up a fresh process? Would love to hear about any patterns you've found.
Thanks for sharing — this is a very practical approach for budget‐conscious devs wanting type safety!
Thanks! Yeah, in theory it sounds neat, but in practice the setup isn't nearly as rosy. My hosting provider doesn't support CGI or FastCGI (or at least not in any way that’s actually configurable through .htaccess; or it is possible I am too thick to figure it out). So every request does indeed spawn a new process.
The only way to keep any form of state is through MySQL (which is included) or memcached on the higher-tier plans. It's not elegant, but it works well enough for low-traffic or background tasks. Once you start needing real throughput, the shared hosting model shows its limits pretty quickly.