A high-performance image processing service built with Go, designed for seamless integration with Cloudflare R2 (S3-compatible storage). This server automates image conversion to WebP and provides on-the-fly resizing via HTTP API or scheduled cron jobs.
- Automated WebP Conversion: Automatically converts various image formats (JPEG, PNG, GIF, BMP, TIFF) to WebP to reduce storage and bandwidth costs.
- Cloudflare R2 Integration: Specifically optimized for Cloudflare R2, providing a cost-effective alternative to AWS S3.
- On-the-fly Resizing: Resize images dynamically using HTTP query parameters or predefined presets (e.g., thumbnail, medium, large).
- Scheduled Cron Jobs: Periodically scans your R2 bucket for new images and converts them automatically to keep your storage optimized.
- State Management: Tracks processing status to ensure efficient incremental updates without reprocessing existing images.
- Webhooks: Notifies your endpoint when a cron batch completes, with optional retry for failed deliveries.
- Go 1.21 or higher
go mod downloadgo run main.goThe server will start on http://localhost:4000
-
Environment Setup: Copy
.env.exampleto.envand fill in your R2 credentials.cp .env.example .env
-
Run with Docker Compose:
docker-compose up -d
The server will start on
http://localhost:4000(default). -
Port Customization: To use a different port, set
SERVER_PORTin your.envfile or environment:SERVER_PORT=9000 docker-compose up -d
Main settings live in config/config.yaml:
- Conversion: formats, quality, max image size
- Resize presets: thumbnail, medium, large (used as
?preset=thumbnailin API) - Cron: scheduled WebP conversion job
Cron uses standard cron expression (minute hour day month weekday) in server local time. Default "0 12 * * *" runs at UTC 12:00 (noon UTC). On a UTC server that equals 21:00 KST. Adjust the hour if your server uses a different timezone. See docs/CRON.md for details.
When a cron batch run finishes, the server can send a POST request to a URL you configure. This is useful for logging, monitoring, or triggering downstream workflows (e.g. cache invalidation, CDN purge).
- Only after the scheduled cron job completes (not on one-off API conversions).
- Sent only if
WEBHOOK_URLis set. If the URL is empty, no request is made.
| Item | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| Timeout | 10 seconds |
| Field | Type | Description |
|---|---|---|
event |
string | Always "batch.completed" for cron batch completion. |
processed_count |
number | Number of images successfully converted in this run. |
failed_count |
number | Number of images that failed (download, convert, or upload). |
images |
array | List of converted images in this run. |
images[].source |
string | Original object key (e.g. path/to/image.jpg). |
images[].destination |
string | New object key after conversion (e.g. path/to/image.webp). |
Example body:
{
"event": "batch.completed",
"processed_count": 3,
"failed_count": 0,
"images": [
{ "source": "uploads/photo.jpg", "destination": "uploads/photo.webp" },
{ "source": "assets/logo.png", "destination": "assets/logo.webp" },
{ "source": "gallery/1.gif", "destination": "gallery/1.webp" }
]
}If no images were converted in the run, images is an empty array and counts may be zero.
Set the webhook URL (and optional retry settings) via environment variables (or config/config.yaml under webhook):
| Variable | Description | Default |
|---|---|---|
WEBHOOK_URL |
Full URL to receive the POST (e.g. https://your-server.com/webhook/conversion-batch). |
(none) |
WEBHOOK_RETRY_ENABLED |
If true, failed deliveries are stored and retried. |
false |
WEBHOOK_RETRY_INTERVAL_MINUTES |
Minutes between retry passes. | 5 |
WEBHOOK_MAX_RETRIES |
Max retries per failed payload; then the pending file is removed (or moved to dead letter if configured). | 5 |
WEBHOOK_PENDING_DIR |
Directory for storing failed webhook payloads for retry. | data/webhook_pending |
Receiver expectations: Your endpoint should respond with an HTTP status in the 2xx range. Any other status (or timeout/connection error) is treated as failure. If retry is enabled, the payload is written under WEBHOOK_PENDING_DIR and retried periodically until success or WEBHOOK_MAX_RETRIES is reached.
Admins can trigger the same webhook on demand (e.g. for testing or to notify about specific images) without waiting for a cron batch.
-
Method:
POST -
URL:
/api/webhook/send -
Request body (JSON):
Field Type Description imagesarray List of image entries (source and destination keys). Must not be empty. images[].sourcestring Original object key (e.g. uploads/a.jpg).images[].destinationstring Converted object key (e.g. uploads/a.webp).Example:
{ "images": [ { "source": "uploads/a.jpg", "destination": "uploads/a.webp" } ] } -
Response: Same webhook payload shape is POSTed to
WEBHOOK_URLwithevent: "manual.triggered"(so receivers can distinguish frombatch.completed). -
Status codes:
200success;400invalid/empty body;502webhook delivery failed;503webhook URL not configured.
GET /- Main endpointGET /health- Health check endpointPOST /api/webhook/send- Trigger webhook manually (see Webhook section above)
MIT License - see LICENSE file for details