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 (0)