The S3 storage server that runs on shared hosting.
Amazon S3 API — SigV4, multipart, presigned URLs, aws-chunked streaming — as a plain PHP app you deploy like WordPress: upload, open the installer, point your AWS SDK at it.
No Go binary. No Docker. No root. No daemon. No Composer dependencies at runtime. Just PHP 8.1+ and MySQL/MariaDB on any Apache, LiteSpeed or cPanel shared host.
Why not just use MinIO (or anything else)?
Because MinIO needs a server you control — a Go binary, a long-running process, a port to bind — and your $5/month cPanel plan can't give you any of that. And because the other PHP S3 servers each fail a different way:
| You want… | The catch elsewhere | php-s3 |
|---|---|---|
| S3 on shared hosting (cPanel, LiteSpeed, no root) | MinIO/others need a server you administer | ✅ Plain public/index.php + .htaccess, install via browser |
| Signatures that are actually verified | lite-s3 parses SigV4 but the wired path never checks it (see research) | ✅ Strict SigV4 — header + presigned + per-chunk chain, hash_equals, no bypass flags |
| Uploads of big files in constant memory | simple-php-s3-server buffers whole PUT bodies in RAM | ✅ Everything is a 64 KiB streaming loop, hash-while-writing |
| Listings that don’t degrade | buckie/simple-php re-scan the directory per list page — O(n) | ✅ DB is the index; sharded object layout, pagination stays fast |
| Real S3 semantics (XML API, AWS error codes, composite ETags) | buckie-php isn’t S3 at all; lite-s3’s ETags aren’t AWS-format | ✅ Spec-shaped: md5(...)-N multipart ETags, AWS error taxonomy |
| A project you can read and audit | heavyweight options hide behind frameworks | ✅ Framework-free layered core, research + architecture docs in-repo |
And unlike opsfour/s3-server — the best-in-class PHP S3 server, which we openly credit as our quality bar — php-s3 doesn't require PHP 8.4, Amp fibers, and long-running workers. Same correctness bar, deployable where shared hosting exists.
How php-s3 compares.
Fair summary of the four open-source PHP projects we studied in full source before writing a line of code (audit: docs/research/):
| Capability | php-s3 | opsfour/s3-server | simple-php-s3-server | lite-s3 | buckie-php |
|---|---|---|---|---|---|
| Real S3 XML API + SigV4 | ✅ | ✅ | ✅ | ⚠️ | ❌ |
| Signature verified on every request | ✅ | ✅ | ⚠️ | ❌ | n/a |
| Runs on shared hosting (PHP 8.1, no daemon) | ✅ | ❌ | ✅ | ✅ | ✅ |
| Streaming PUT/GET (constant memory) | ✅ | ✅ | ❌ | ⚠️ | ✅ |
| DB-indexed listings (no full scans) | ✅ | ✅ | ❌ | ✅ | ❌ |
| Multipart with AWS-format ETag | ✅ | ✅ | ✅ | ⚠️ | ❌ |
| Presigned URLs (GET/PUT, expiry) | ✅ | ✅ | ⚠️ | ⚠️ | ❌ |
| aws-chunked + per-chunk signatures | ✅ | ✅ | ❌ | ❌ | ❌ |
| Automated tests in the repo | ✅ 191 | ✅ 745 | ⚠️ | ❌ | ✅ 56 |
| Web installer + admin panel | ✅ | ❌ | ❌ | ✅ | ❌ |
| Runtime dependencies | 0 | Amp + Symfony | 2 packages | 0 | 0 |
✅ supported & proven · ⚠️ partial or broken · ❌ not supported
Features.
S3-Compatible API
Buckets, objects, ListObjectsV1/V2 (prefix, delimiter, continuation tokens), DeleteObjects batch, Range/206 for video seeking, and CopyObject.
AWS Signature Version 4
Header mode and presigned URLs (1–604800 s expiry), ±900 s skew, host required in SignedHeaders, constant-time comparison, payload hash verified against the streamed body.
aws-chunked Streaming Uploads
Signed chunk chains and unsigned-trailer framing, verified per chunk; tampered chunks rejected immediately with SignatureDoesNotMatch.
Multipart Uploads
Create, uploadPart, complete, abort, listUploads, listParts, AWS-format composite ETags, and 5 MiB minimum-part enforcement (EntityTooSmall).
Streaming Everything
Bodies are pumped in 64 KiB chunks while MD5 and SHA-256 are computed on the fly; nothing ever loads a whole object into memory.
Safe Storage Layout
sha256(key)-sharded paths + UUID names: object keys never touch the filesystem path (traversal structurally impossible), data root lives outside the web root.
Shared-Hosting Plumbing
public/ docroot, .htaccess with Authorization header passthrough SigV4 needs on Apache/LiteSpeed, dotfiles blocked, and self-locking web installer.
Minimal Admin
Session login with throttling (10 attempts/hr/IP), access-key management with bucket allow-lists, bucket overview, usage stats, and audit log.
Maintenance CLI
CLI commands for maintenance: migrate (schema migrations), gc (purge expired multipart uploads and orphan temp files), key:create, and doctor.
Honest AWS Errors
AWS error taxonomy with correct HTTP statuses and RequestId, so boto3, rclone, and official AWS SDKs fail gracefully instead of mysteriously.
Zero Runtime Dependencies
PHP ≥ 8.1 with pdo_mysql, openssl, fileinfo, and mbstring. No Composer dependencies in production; PHPUnit and AWS SDK are dev-only.
S3 API coverage.
A feature counts as supported only when a test proves it through a real client:
| Area | Operations |
|---|---|
| Service | ListBuckets, GET /_health |
| Buckets | CreateBucket, DeleteBucket (empty only), HeadBucket |
| Objects | PutObject, GetObject (+ Range/206), HeadObject, DeleteObject, CopyObject* |
| Listing | ListObjectsV1, ListObjectsV2 (prefix, delimiter/CommonPrefixes, markers, tokens) |
| Batch | DeleteObjects (≤1000 keys, Quiet mode) |
| Multipart | CreateMultipartUpload, UploadPart, CompleteMultipartUpload, AbortMultipartUpload, ListMultipartUploads, ListParts |
| Auth | SigV4 header, presigned query URLs, aws-chunked streaming |
| Next | checksum headers, conditional requests, boto3 suite, rclone docs |
* routed and implemented; end-to-end proof pending. Full matrix with evidence in docs/s3-compatibility/.
Requirements.
pdo_mysql, openssl, fileinfo, mbstring
Any DB PDO can reach
.htaccess included
No Composer required on server
Quick start.
Shared Hosting (cPanel / LiteSpeed / Any Host)
git clone https://github.com/Luckyyaduvanshiofficial/php-s3.git
cd php-s3
composer install --no-dev # or copy vendor/ from your dev machine- Point your domain (or subdomain) document root at
public/ - Visit
https://your-domain.example/_admin/installand follow the wizard (writesconfig.phpoutside docroot and runs migrations) - Log in, create an access key, create a bucket
- Point any S3 client at your domain — path-style, custom endpoint
Local Development
git clone https://github.com/Luckyyaduvanshiofficial/php-s3.git
cd php-s3
composer install
php -S 127.0.0.1:8099 -t public public/index.php
# open http://127.0.0.1:8099/_admin/installRun the automated suite to verify compatibility on your local machine:
composer test # 191 tests, 334 assertions (PHPUnit 11)Connect your S3 clients.
Configure any standard S3 client in path-style mode with your custom endpoint:
use Aws\S3\S3Client;
$s3 = new S3Client([
'version' => 'latest',
'region' => 'us-east-1',
'endpoint' => 'https://your-domain.example',
'use_path_style_endpoint' => true, // required (path-style)
'credentials' => [
'key' => 'AKIA...',
'secret' => '...',
],
]);
$s3->createBucket(['Bucket' => 'media']);
$s3->putObject([
'Bucket' => 'media',
'Key' => 'video.mp4',
'SourceFile' => __DIR__ . '/video.mp4',
]);
// Multipart, ranged downloads and presigned URLs work unchanged:
$cmd = $s3->getCommand('GetObject', ['Bucket' => 'media', 'Key' => 'video.mp4']);
$url = (string) $s3->createPresignedRequest($cmd, '+1 hour')->getUri();import boto3
from botocore.config import Config
s3 = boto3.client(
"s3",
endpoint_url="https://your-domain.example",
aws_access_key_id="AKIA...",
aws_secret_access_key="...",
region_name="us-east-1",
config=Config(s3={"addressing_style": "path"}), # required
)
s3.create_bucket(Bucket="media")
s3.upload_file("video.mp4", "media", "video.mp4")
# Presigned download link
url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": "media", "Key": "video.mp4"},
ExpiresIn=3600,
)export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_DEFAULT_REGION="us-east-1"
ENDPOINT="https://your-domain.example"
# List buckets and objects
aws s3 ls --endpoint-url $ENDPOINT
aws s3 ls s3://media/ --endpoint-url $ENDPOINT
# Upload and sync directories
aws s3 cp ./video.mp4 s3://media/ --endpoint-url $ENDPOINT
aws s3 sync ./assets/ s3://media/assets/ --endpoint-url $ENDPOINT# ~/.config/rclone/rclone.conf
[php-s3]
type = s3
provider = AWS
access_key_id = AKIA...
secret_access_key = ...
endpoint = https://your-domain.example
region = us-east-1
# Commands
rclone lsd php-s3:
rclone copy ./photos php-s3:media/photos
rclone sync /var/www/uploads php-s3:backups/uploadsArchitecture.
Clean layered design engineered from the ground up for strict correctness and shared hosting safety:
┌────────────────────────────────────────────┐
S3 client ──HTTP──► │ public/index.php (only web-reachable file)│
(SDK, rclone, …) └──────────────┬─────────────────────────────┘
▼
┌──────────────┬──────────────┼──────────────┬───────────────┐
▼ ▼ ▼ ▼ ▼
Http/Request Auth/SigV4 S3/Operation Admin panel XML/Error
(only layer (header + resolver ──► (session auth, (AWS taxonomy,
touching presigned + dispatches keys, usage) XXE-safe)
$_SERVER) aws-chunked) to handlers
▼
S3/Handlers ──► Storage/ (streaming put/get,
sharded FS + MySQL index)One Stack, One Entrypoint
No framework, no middleware maze, one single predictable request pipeline from incoming HTTP to response.
Transport-Agnostic Core
Nothing outside Http/ reads $_SERVER; auth, XML, routing, and errors are pure functions over strings and arrays.
Staging + Atomic Rename
Objects are staged outside the active namespace and become visible only when fully written and verified.
Security.
- Strict SigV4: No bypass flags and no "simple auth" mode — every S3 request is cryptographically signed.
- Anti-enumeration: Permission checked before existence (403 ≠ 404), preventing probing for bucket or object existence.
- Constant-time comparisons:
hash_equalsused for every signature, secret, and ETag comparison. - Defensive grammar: Bucket-name grammar + segment-based key canonicalization +
^[a-f0-9]{32}$uploadId whitelist. Traversal defense by validation, not sanitizing. - XXE-safe XML:
DOCTYPEandENTITYrejected before parse,LIBXML_NONETenabled, with an 8 MiB size cap. - Isolated storage: Objects stored outside the web root; engine-off rules for data dirs; dotfiles strictly denied.
- Admin protection: Login throttling + security audit log in admin panel; secrets displayed only once, at creation.
CLI Commands
Headless management scripts for cron jobs, CI pipelines, and terminal administration:
php cli/php-s3.php migratephp cli/php-s3.php doctorphp cli/php-s3.php gcphp cli/php-s3.php key:create --owner=1 --buckets='*' --description=ciProject status & roadmap.
Open-source development progress tracked phase by phase:
| Phase | Scope | Status |
|---|---|---|
| 1 · Research | Full source audit of 4 reference projects | ✅ docs/RESEARCH.md |
| 2 · Architecture | Design decisions recorded before code | ✅ docs/ARCHITECTURE.md |
| 3 · MVP | SigV4, buckets, objects, ranges, listing, installer, CLI | ✅ shipped |
| 4 · S3 compatibility | Presigned, DeleteObjects, multipart, aws-chunked | ✅ matrix |
| 5 · Hardening | S3 rate limits, quotas, CORS, public-read policy, recovery | 🚧 in progress |
| 6 · Documentation | Deployment guides, boto3/rclone suites, SDK matrix CI | ⏳ next |
Contributions welcome — especially: boto3 compatibility suite, rclone/AWS CLI smoke docs, x-amz-checksum-*, conditional requests, CI workflow.
Frequently asked questions.
Why run an S3 server in PHP instead of MinIO or Rust/Go?
MinIO and Go/Rust servers require a dedicated server, root access, background systemd daemons, and custom open ports. Shared hosting gives you none of that. Most developers already have cPanel or LiteSpeed hosting with unused gigabytes. php-s3 turns that existing hosting into an S3-compatible object store for ₹0 extra.
Does php-s3 strictly validate AWS SigV4 signatures?
Yes. Unlike earlier hobbyist PHP scripts that bypassed signature verification or only parsed headers, php-s3 enforces strict AWS SigV4 on every request. It verifies Authorization header mode and presigned URLs (1 s–7 days expiry, ±900 s clock skew), uses constant-time hash_equals comparison, and validates SHA-256 payload hashes against the streamed body.
Can it upload and download multi-gigabyte files without running out of memory?
Yes. Every body is a 64 KiB streaming pump with hash-while-writing. Requests and responses are never buffered entirely in RAM. It comfortably handles large file uploads within shared hosting limits (even standard 128 MB memory_limit environments).
Does it support multipart uploads?
Yes. It implements the full S3 multipart upload lifecycle: CreateMultipartUpload, UploadPart, CompleteMultipartUpload, AbortMultipartUpload, ListMultipartUploads, and ListParts with AWS-compliant composite ETags (e.g. md5-N) and enforcement of the 5 MiB minimum part size.
Where is data stored and is it protected from directory traversal?
The data directory is configured outside the web document root, making direct HTTP access structurally impossible. Object keys on disk are SHA-256 sharded (/data_root/objects/bu/ck/bucket_id/sha256(key)), preventing any path traversal exploits.
What are the server requirements?
Plain PHP ≥ 8.1 with standard extensions: pdo_mysql, openssl, fileinfo, and mbstring, plus any MySQL 5.7+ or MariaDB 10.3+ database. Apache or LiteSpeed (.htaccess included) or nginx. No Composer is needed at runtime.
Which S3 clients and tools can connect to it?
Any S3-compatible client that supports custom endpoints and path-style addressing: official AWS SDKs (PHP, Python/boto3, Node.js, Go, Java), AWS CLI, rclone, Cyberduck, Nextcloud external storage, and backup tools like Duplicati or Borg.
How does installation work?
It installs like WordPress: point your document root at public/, create an empty MySQL database, open https://your-domain.example/_admin/install in your browser, and submit the wizard. The installer automatically writes config.php outside the web root, runs database migrations, creates the admin user, and locks itself permanently.
Ready to host your own S3 storage?
Free, open-source under the MIT license, with zero telemetry and zero runtime dependencies.