When you run a website with screenshots, diagrams, and article images, you need image hosting. I tried all the free options — Imgur, SM.MS, GitHub issues — and every single one had limitations. Either they compress images, limit bandwidth, or disappear without notice.

So I built my own. Here's exactly how.

Why Self-Host?

| Solution | Cost | Limitations | Control |

|----------|------|-------------|---------|

| Imgur | Free | Compresses images, may delete inactive content | None |

| SM.MS | Free/$5/yr | 10MB limit, occasionally unreliable | None |

| GitHub Issues | Free | Public repo, rate limited | Limited |

| Self-hosted (EasyImage) | ~$5/mo VPS | Your bandwidth | Full |

For a content site, full control over your images is worth the setup effort. Once it's running, it costs practically nothing.

What We're Building


User upload → EasyImage (Docker) → Cloudflare Tunnel → Public URL
  • **EasyImage** — Simple, lightweight image hosting PHP app
  • **Docker** — Clean deployment with auto-restart
  • **Cloudflare Tunnel** — Secure public access without opening ports

Step 1: Deploy EasyImage with Docker


docker run -d \
  --name easyimage \
  --restart unless-stopped \
  -p 8080:80 \
  -v /path/to/images:/app/web/i \
  -v /path/to/config:/app/web/config \
  icy2003/easyimage:latest

The application runs on port 8080 locally. `/app/web/i` stores uploaded images, and `/app/web/config` holds the configuration.

Step 2: Configure Cloudflare Tunnel

Instead of exposing port 8080 to the internet (which invites attacks), we use Cloudflare Tunnel:


# config.yaml
tunnel: your-tunnel-id
credentials-file: /path/to/credentials.json

ingress:
  - hostname: images.yourdomain.com
    service: http://localhost:8080
  - service: http_status:404

cloudflared tunnel route dns your-tunnel-id images.yourdomain.com
cloudflared tunnel run your-tunnel-id

Now your image hosting is accessible at `https://images.yourdomain.com` with:

  • ✅ No open firewall ports
  • ✅ DDoS protection
  • ✅ Free SSL/TLS
  • ✅ Global CDN caching

Step 3: Security Hardening

After deploying, I found several security issues that needed fixing:

Disable Anonymous Uploads

EasyImage allows anonymous uploads by default. Any bot that finds your URL can upload arbitrary files:


# In EasyImage config
'allow_anonymous' => false  # Require upload token

Set File Size Limits


client_max_body_size 10M;  # Per-image limit

Enable Image Validation

Restrict uploads to image MIME types only — no PDF, no ZIP, no PHP files disguised as images.

Step 4: Integrate with Your Site

Once your image hosting is running, uploading from scripts is straightforward:


curl -X POST https://images.yourdomain.com/api/upload \
  -F "image=@screenshot.png" \
  -F "token=your-upload-token"

The response includes the public URL, which you can directly embed in your articles:


![alt text](https://images.yourdomain.com/i/2026/06/screenshot.png)

Real-World Performance

After 3 months of production use:

| Metric | Value |

|--------|-------|

| Images stored | 200+ |

| Total size | ~1.2 GB |

| Average load time | ~200ms (with Cloudflare cache) |

| Uptime | 99.9% |

| Cost | $0 additional (shared VPS) |

Pitfalls I Hit

  1. **PHP opcache** — After updating config, changes weren't reflected because opcache cached the old file. Clear opcache after config changes.
  2. 2. Dual-upload vulnerability — The admin panel and API both accept uploads. If you lock one but not the other, it's not secure.

    3. Image compression — EasyImage doesn't compress by default. For large screenshots, add a compression step before upload.

    Verdict

    Self-hosted image hosting is one of those projects that feels like overkill until you've used it for a week. The convenience of having your own image CDN, with no file size limits, no compression, and no third-party dependency, is hard to give up once you have it.

    Total setup time: ~1 hour

    Ongoing maintenance: ~0 hours

    Worth it: Absolutely.