DEV Community

Andrea Mancuso
Andrea Mancuso

Posted on

Running Rust Binaries on Shared Hosting: A Practical Approach to Type Safety on a Budget

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:

  1. PHP receives the HTTP request
  2. PHP serializes request context (headers, body, query params) to JSON
  3. PHP spawns your compiled binary as a subprocess
  4. Binary reads JSON from stdin, does its work, outputs response
  5. 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]);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Upload via SFTP/SSH and set executable permissions:

chmod +x myapp
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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:

  1. Validate all input rigorously - Never trust data passed from PHP to your binary
  2. Don't expose this pattern to untrusted users - This approach is for your own applications, not multi-tenant systems
  3. Sanitize file paths - Be careful with any file operations
  4. Limit what your binary can do - Follow principle of least privilege
  5. 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:

  1. Real type safety - Compile-time guarantees, no runtime surprises
  2. Performance - Compiled binaries are fast
  3. Low memory - 2-10 MB vs 50-200 MB for typical app servers
  4. No server maintenance - Let your host handle updates, security patches, database management, backups
  5. Cheap hosting - Works on < $10/month shared hosting (though some providers disable proc_open and such)
  6. Language choice - Use Go, Rust, or any compiled language that can generate a fully static binary
  7. Familiar deployment - Just upload files via FTP/SFTP

Trade-offs:

  1. Process spawn overhead - ~30-40ms per request (mitigated with caching)
  2. No persistent state - Each request is isolated (use external storage/cache)
  3. Complexity - Two codebases (PHP wrapper + binary)
  4. Build step - Must compile for Linux x86_64
  5. 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)

Collapse
 
cyber8080 profile image
Cyber Safety Zone

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:

  • The PHP‐wrapper + binary spawn trick is clever and makes good use of what many folks already have. (DEV Community)
  • The reminder that binaries run with your account privileges on shared hosting is really important for anyone considering this setup. (DEV Community)
  • For higher traffic or more security‐sensitive apps, you rightly note this may not be the long-term solution. (DEV Community)

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!

Collapse
 
andreamancuso profile image
Andrea Mancuso

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.