Skip to content

Repository files navigation

Image Server

A high-performance image processing server written in Go. Supports on-demand image resizing, format conversion, and cloud storage integration.

Features

  • On-demand image processing - Resize, crop, and convert images via URL parameters
  • Multiple output formats - JPEG, WebP, GIF, PNG, HEIC/HEIF (iPhone images)
  • Cloud storage - Upload processed images to Amazon S3
  • Signed URLs - Secure uploads with HMAC-SHA256 signed URLs (similar to AWS S3 pre-signed URLs)
  • Batch processing - Process multiple image sizes in a single request
  • Prometheus metrics - Built-in metrics endpoint for monitoring
  • Webhooks - Notify external systems when images are uploaded or processed
  • Docker support - Ready-to-use Docker image

Quick Start

Using Docker

docker build -t image-server .
docker run -p 7000:7000 -p 7002:7002 image-server

Building from Source

Requires Go 1.21+ and libvips.

# macOS
brew install vips
# Ubuntu/Debian
apt-get install libvips-dev
# Build
go build -o image-server .# Run
./image-server server --port 7000

Server

Uploading Images

Images are uploaded to a namespace. Namespaces group image types (e.g., avatars vs product images may need different sizes).

Upload from URL:

curl -X POST "http://localhost:7000/products?source=https://example.com/image.jpg"

Upload binary data:

curl --data-binary "@./image.jpg" -X POST http://localhost:7000/products

Response:

{
"hash": "6e0072682e66287b662827da75b244a3",
"height": 600,
"width": 800,
"content_type": "image/jpeg"
}

Upload and process immediately:

curl --data-binary "@./image.jpg" -X POST "http://localhost:7000/products?outputs=x300.jpg,x300.webp"

Retrieving Images

Images are accessed via their hash, partitioned into path segments:

GET http://localhost:7000/{namespace}/{hash[0:3]}/{hash[3:6]}/{hash[6:9]}/{hash[9:]}/{dimensions}.{format}

Examples:

# By width (maintains aspect ratio)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/w200.jpg
# Square crop
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.jpg
# Specific dimensions (width x height)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/300x200.jpg
# With quality adjustment (1-100)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200-q50.jpg
# WebP format
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.webp

Image Information

curl http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/info.json

Batch Processing

Process multiple sizes for an existing image:

curl -X POST "http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/process?outputs=x100.jpg,x200.jpg,x300.webp"

Signed URLs (Authentication)

Secure your image server by requiring signed URLs for uploads (and optionally reads). This works similarly to AWS S3 pre-signed URLs.

Setup

  1. Generate a signing secret:

    ./image-server generate-secret > /etc/image-server/secrets.txt
  2. Start the server with signature validation:

    ./image-server server \
    --require-signature \
    --signing-secrets-file /etc/image-server/secrets.txt \
    --signature-max-ttl 60
  3. Generate signed URLs in your backend application:

    The signature algorithm:

    StringToSign = METHOD + "\n" + PATH + "\n" + EXPIRES_UNIX_TIMESTAMP
    Signature = Base64RawURL(HMAC-SHA256(secret, StringToSign))
    

    URL format:

    POST /namespace?X-Expires=1702156800&X-Path=/namespace&X-Signature=...
    

    Go example:

    import"github.com/image-server/image-server/core/signature"signer:=signature.NewSigner("your-secret", "https://images.example.com")
    url:=signer.SignURL("POST", "/products", 15*time.Minute)
  4. Test with the CLI:

    ./image-server sign-url \
    --secret "your-secret" \
    --base-url "https://images.example.com" \
    --method POST \
    --path /products \
    --ttl 15m

Configuration Options

FlagDescriptionDefault
--require-signatureEnable signature validation for uploadsfalse
--require-signature-for-readsAlso require signatures for GET requestsfalse
--signing-secrets-filePath to file with secrets (one per line)-
--signature-max-ttlMaximum allowed TTL in minutes60

Secret Rotation

The secrets file supports multiple secrets for rotation. Add new secrets to the top of the file:

new-secret-abc123
old-secret-xyz789

The server validates against all secrets, so you can:

  1. Add the new secret
  2. Update your backend apps to use it
  3. Remove the old secret after existing URLs expire

Path-Based Signing

Sign a path prefix to allow uploads to any path under it:

# Sign for entire namespace
./image-server sign-url --secret "..." --path /products --ttl 15m
# Allows: POST /products, POST /products/abc/def/...# Sign for specific path only
./image-server sign-url --secret "..." --path /products/abc/def/ghi/jkl --ttl 15m
# Allows only that exact path

Webhooks

Send HTTP notifications to external systems when images are uploaded or processed. Useful for triggering downstream workflows like OCR, ML pipelines, or cache invalidation.

Setup

./image-server server \
--webhook-url "https://api.example.com/webhooks/images" \
--webhook-secret "$(./image-server generate-secret)" \
--webhook-timeout 10 \
--webhook-events "uploaded,batch_complete"

Events

EventTriggerUse Case
uploadedOriginal image uploaded to storageStart processing pipeline
processedEach image variant processedCache warming
failedProcessing errorAlerting
batch_completeAll variants done (or already existed)Trigger downstream workflow

By default, all events are sent. Use --webhook-events to filter.

Payload Format

{
"event": "image.uploaded",
"timestamp": "2025-12-09T15:30:00Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"width": 1920,
"height": 1080,
"content_type": "image/jpeg",
"remote_url": "https://cdn.example.com/products/6e0/072/.../original"
}
}

For image.processed:

{
"event": "image.processed",
"timestamp": "2025-12-09T15:30:01Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"filename": "x300.webp",
"format": "webp",
"width": 300,
"height": 169,
"quality": 75,
"remote_url": "https://cdn.example.com/products/6e0/072/.../x300.webp"
}
}

Security

Webhooks are signed with HMAC-SHA256. Verify the signature in your handler:

Headers:

X-Webhook-Signature: sha256=<hex-encoded-hmac>
X-Webhook-Timestamp: 1702135800

Verification (Python):

importhmacimporthashlibdefverify_webhook(secret: str, timestamp: str, body: bytes, signature: str) ->bool:
expected="sha256="+hmac.new(
secret.encode(),
f"{timestamp}.{body.decode()}".encode(),
hashlib.sha256
).hexdigest()
returnhmac.compare_digest(expected, signature)
# In your handler:ifnotverify_webhook(SECRET, request.headers["X-Webhook-Timestamp"],
request.body, request.headers["X-Webhook-Signature"]):
return401

Verification (Go):

funcverifyWebhook(secret, timestampstring, body []byte, signaturestring) bool {
h:=hmac.New(sha256.New, []byte(secret))
h.Write([]byte(fmt.Sprintf("%s.%s", timestamp, string(body))))
expected:="sha256="+hex.EncodeToString(h.Sum(nil))
returnhmac.Equal([]byte(expected), []byte(signature))
}

Configuration

FlagDescriptionDefault
--webhook-urlEndpoint URL (enables webhooks)-
--webhook-secretHMAC signing secret-
--webhook-timeoutHTTP timeout in seconds10
--webhook-eventsEvents to send (comma-separated)all

Reliability

  • Webhooks are sent asynchronously (non-blocking)
  • Failed deliveries retry up to 3 times with exponential backoff (1s, 4s)
  • Webhook failures don't affect image processing

Cloud Storage (S3)

./image-server server \
--uploader s3 \
--aws_access_key_id $AWS_ACCESS_KEY_ID \
--aws_secret_key $AWS_SECRET_KEY \
--aws_bucket $AWS_BUCKET \
--aws_region us-west-1 \
--remote_base_path "images/" \
--remote_base_url "https://cdn.example.com"

Server Configuration

FlagDescriptionDefault
--portServer port7000
--listenListen address127.0.0.1
--local_base_pathLocal image storage directorypublic
--extensionsAllowed file extensionsjpg,gif,webp
--maximum_widthMaximum output width1000
--default_qualityDefault JPEG/WebP quality75
--outputsDefault output formats-
--uploaderStorage backend (s3 or noop)auto
--uploader_concurrencyParallel upload workers10
--processor_concurrencyParallel processing workers4
--http_timeoutHTTP request timeout (seconds)5
--max_file_ageLocal file cleanup age (minutes)30

Admin Server

A separate admin server runs on port 7002 with health and metrics endpoints:

EndpointDescription
/probe/readyReadiness check
/probe/liveLiveness check
/metricsPrometheus metrics

CLI Commands

Process images locally

./image-server cli /path/to/images --outputs "x300.jpg,x300.webp"

Generate signing secret

./image-server generate-secret
./image-server generate-secret --length 64 --count 3

Generate signed URL

./image-server sign-url --secret "..." --path /namespace --ttl 15m

Version

./image-server version

Monitoring

Prometheus Metrics

Available at http://localhost:7002/metrics

Statsd

./image-server server --enable_statsd --statsd_host 127.0.0.1 --statsd_port 8125

Events:

  • image_server.image_request - Image processed and uploaded
  • image_server.image_request.{format} - By format (jpg, webp, etc.)
  • image_server.image_request_fail - Processing failed
  • image_server.original_downloaded - Original fetched from source
  • image_server.original_unavailable - Original not found (404)

Profiling

./image-server server --profile
# pprof available at http://localhost:6060

Development

Running locally

# Without S3
make dev-server
# With S3export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_KEY=...
export AWS_BUCKET=...
export AWS_REGION=...
export IMG_REMOTE_BASE_PATH=...
export IMG_REMOTE_BASE_URL=...
make dev-server-s3

Tests

make test# or
go test ./...

Building

make build
# Creates binaries in bin/ for multiple platforms

Error Handling

StatusDescription
401Invalid or missing signature (when signatures required)
404Image not found
400Invalid request parameters

License

MIT

About

No description, website, or topics provided.

Resources

Stars

23 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - image-server/image-server · GitHub
Skip to content

Repository files navigation

Image Server

A high-performance image processing server written in Go. Supports on-demand image resizing, format conversion, and cloud storage integration.

Features

  • On-demand image processing - Resize, crop, and convert images via URL parameters
  • Multiple output formats - JPEG, WebP, GIF, PNG, HEIC/HEIF (iPhone images)
  • Cloud storage - Upload processed images to Amazon S3
  • Signed URLs - Secure uploads with HMAC-SHA256 signed URLs (similar to AWS S3 pre-signed URLs)
  • Batch processing - Process multiple image sizes in a single request
  • Prometheus metrics - Built-in metrics endpoint for monitoring
  • Webhooks - Notify external systems when images are uploaded or processed
  • Docker support - Ready-to-use Docker image

Quick Start

Using Docker

docker build -t image-server .
docker run -p 7000:7000 -p 7002:7002 image-server

Building from Source

Requires Go 1.21+ and libvips.

# macOS
brew install vips
# Ubuntu/Debian
apt-get install libvips-dev
# Build
go build -o image-server .# Run
./image-server server --port 7000

Server

Uploading Images

Images are uploaded to a namespace. Namespaces group image types (e.g., avatars vs product images may need different sizes).

Upload from URL:

curl -X POST "http://localhost:7000/products?source=https://example.com/image.jpg"

Upload binary data:

curl --data-binary "@./image.jpg" -X POST http://localhost:7000/products

Response:

{
"hash": "6e0072682e66287b662827da75b244a3",
"height": 600,
"width": 800,
"content_type": "image/jpeg"
}

Upload and process immediately:

curl --data-binary "@./image.jpg" -X POST "http://localhost:7000/products?outputs=x300.jpg,x300.webp"

Retrieving Images

Images are accessed via their hash, partitioned into path segments:

GET http://localhost:7000/{namespace}/{hash[0:3]}/{hash[3:6]}/{hash[6:9]}/{hash[9:]}/{dimensions}.{format}

Examples:

# By width (maintains aspect ratio)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/w200.jpg
# Square crop
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.jpg
# Specific dimensions (width x height)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/300x200.jpg
# With quality adjustment (1-100)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200-q50.jpg
# WebP format
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.webp

Image Information

curl http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/info.json

Batch Processing

Process multiple sizes for an existing image:

curl -X POST "http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/process?outputs=x100.jpg,x200.jpg,x300.webp"

Signed URLs (Authentication)

Secure your image server by requiring signed URLs for uploads (and optionally reads). This works similarly to AWS S3 pre-signed URLs.

Setup

  1. Generate a signing secret:

    ./image-server generate-secret > /etc/image-server/secrets.txt
  2. Start the server with signature validation:

    ./image-server server \
    --require-signature \
    --signing-secrets-file /etc/image-server/secrets.txt \
    --signature-max-ttl 60
  3. Generate signed URLs in your backend application:

    The signature algorithm:

    StringToSign = METHOD + "\n" + PATH + "\n" + EXPIRES_UNIX_TIMESTAMP
    Signature = Base64RawURL(HMAC-SHA256(secret, StringToSign))
    

    URL format:

    POST /namespace?X-Expires=1702156800&X-Path=/namespace&X-Signature=...
    

    Go example:

    import"github.com/image-server/image-server/core/signature"signer:=signature.NewSigner("your-secret", "https://images.example.com")
    url:=signer.SignURL("POST", "/products", 15*time.Minute)
  4. Test with the CLI:

    ./image-server sign-url \
    --secret "your-secret" \
    --base-url "https://images.example.com" \
    --method POST \
    --path /products \
    --ttl 15m

Configuration Options

FlagDescriptionDefault
--require-signatureEnable signature validation for uploadsfalse
--require-signature-for-readsAlso require signatures for GET requestsfalse
--signing-secrets-filePath to file with secrets (one per line)-
--signature-max-ttlMaximum allowed TTL in minutes60

Secret Rotation

The secrets file supports multiple secrets for rotation. Add new secrets to the top of the file:

new-secret-abc123
old-secret-xyz789

The server validates against all secrets, so you can:

  1. Add the new secret
  2. Update your backend apps to use it
  3. Remove the old secret after existing URLs expire

Path-Based Signing

Sign a path prefix to allow uploads to any path under it:

# Sign for entire namespace
./image-server sign-url --secret "..." --path /products --ttl 15m
# Allows: POST /products, POST /products/abc/def/...# Sign for specific path only
./image-server sign-url --secret "..." --path /products/abc/def/ghi/jkl --ttl 15m
# Allows only that exact path

Webhooks

Send HTTP notifications to external systems when images are uploaded or processed. Useful for triggering downstream workflows like OCR, ML pipelines, or cache invalidation.

Setup

./image-server server \
--webhook-url "https://api.example.com/webhooks/images" \
--webhook-secret "$(./image-server generate-secret)" \
--webhook-timeout 10 \
--webhook-events "uploaded,batch_complete"

Events

EventTriggerUse Case
uploadedOriginal image uploaded to storageStart processing pipeline
processedEach image variant processedCache warming
failedProcessing errorAlerting
batch_completeAll variants done (or already existed)Trigger downstream workflow

By default, all events are sent. Use --webhook-events to filter.

Payload Format

{
"event": "image.uploaded",
"timestamp": "2025-12-09T15:30:00Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"width": 1920,
"height": 1080,
"content_type": "image/jpeg",
"remote_url": "https://cdn.example.com/products/6e0/072/.../original"
}
}

For image.processed:

{
"event": "image.processed",
"timestamp": "2025-12-09T15:30:01Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"filename": "x300.webp",
"format": "webp",
"width": 300,
"height": 169,
"quality": 75,
"remote_url": "https://cdn.example.com/products/6e0/072/.../x300.webp"
}
}

Security

Webhooks are signed with HMAC-SHA256. Verify the signature in your handler:

Headers:

X-Webhook-Signature: sha256=<hex-encoded-hmac>
X-Webhook-Timestamp: 1702135800

Verification (Python):

importhmacimporthashlibdefverify_webhook(secret: str, timestamp: str, body: bytes, signature: str) ->bool:
expected="sha256="+hmac.new(
secret.encode(),
f"{timestamp}.{body.decode()}".encode(),
hashlib.sha256
).hexdigest()
returnhmac.compare_digest(expected, signature)
# In your handler:ifnotverify_webhook(SECRET, request.headers["X-Webhook-Timestamp"],
request.body, request.headers["X-Webhook-Signature"]):
return401

Verification (Go):

funcverifyWebhook(secret, timestampstring, body []byte, signaturestring) bool {
h:=hmac.New(sha256.New, []byte(secret))
h.Write([]byte(fmt.Sprintf("%s.%s", timestamp, string(body))))
expected:="sha256="+hex.EncodeToString(h.Sum(nil))
returnhmac.Equal([]byte(expected), []byte(signature))
}

Configuration

FlagDescriptionDefault
--webhook-urlEndpoint URL (enables webhooks)-
--webhook-secretHMAC signing secret-
--webhook-timeoutHTTP timeout in seconds10
--webhook-eventsEvents to send (comma-separated)all

Reliability

  • Webhooks are sent asynchronously (non-blocking)
  • Failed deliveries retry up to 3 times with exponential backoff (1s, 4s)
  • Webhook failures don't affect image processing

Cloud Storage (S3)

./image-server server \
--uploader s3 \
--aws_access_key_id $AWS_ACCESS_KEY_ID \
--aws_secret_key $AWS_SECRET_KEY \
--aws_bucket $AWS_BUCKET \
--aws_region us-west-1 \
--remote_base_path "images/" \
--remote_base_url "https://cdn.example.com"

Server Configuration

FlagDescriptionDefault
--portServer port7000
--listenListen address127.0.0.1
--local_base_pathLocal image storage directorypublic
--extensionsAllowed file extensionsjpg,gif,webp
--maximum_widthMaximum output width1000
--default_qualityDefault JPEG/WebP quality75
--outputsDefault output formats-
--uploaderStorage backend (s3 or noop)auto
--uploader_concurrencyParallel upload workers10
--processor_concurrencyParallel processing workers4
--http_timeoutHTTP request timeout (seconds)5
--max_file_ageLocal file cleanup age (minutes)30

Admin Server

A separate admin server runs on port 7002 with health and metrics endpoints:

EndpointDescription
/probe/readyReadiness check
/probe/liveLiveness check
/metricsPrometheus metrics

CLI Commands

Process images locally

./image-server cli /path/to/images --outputs "x300.jpg,x300.webp"

Generate signing secret

./image-server generate-secret
./image-server generate-secret --length 64 --count 3

Generate signed URL

./image-server sign-url --secret "..." --path /namespace --ttl 15m

Version

./image-server version

Monitoring

Prometheus Metrics

Available at http://localhost:7002/metrics

Statsd

./image-server server --enable_statsd --statsd_host 127.0.0.1 --statsd_port 8125

Events:

  • image_server.image_request - Image processed and uploaded
  • image_server.image_request.{format} - By format (jpg, webp, etc.)
  • image_server.image_request_fail - Processing failed
  • image_server.original_downloaded - Original fetched from source
  • image_server.original_unavailable - Original not found (404)

Profiling

./image-server server --profile
# pprof available at http://localhost:6060

Development

Running locally

# Without S3
make dev-server
# With S3export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_KEY=...
export AWS_BUCKET=...
export AWS_REGION=...
export IMG_REMOTE_BASE_PATH=...
export IMG_REMOTE_BASE_URL=...
make dev-server-s3

Tests

make test# or
go test ./...

Building

make build
# Creates binaries in bin/ for multiple platforms

Error Handling

StatusDescription
401Invalid or missing signature (when signatures required)
404Image not found
400Invalid request parameters

License

MIT

About

No description, website, or topics provided.

Resources

Stars

23 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - image-server/image-server · GitHub
Skip to content

Repository files navigation

Image Server

A high-performance image processing server written in Go. Supports on-demand image resizing, format conversion, and cloud storage integration.

Features

  • On-demand image processing - Resize, crop, and convert images via URL parameters
  • Multiple output formats - JPEG, WebP, GIF, PNG, HEIC/HEIF (iPhone images)
  • Cloud storage - Upload processed images to Amazon S3
  • Signed URLs - Secure uploads with HMAC-SHA256 signed URLs (similar to AWS S3 pre-signed URLs)
  • Batch processing - Process multiple image sizes in a single request
  • Prometheus metrics - Built-in metrics endpoint for monitoring
  • Webhooks - Notify external systems when images are uploaded or processed
  • Docker support - Ready-to-use Docker image

Quick Start

Using Docker

docker build -t image-server .
docker run -p 7000:7000 -p 7002:7002 image-server

Building from Source

Requires Go 1.21+ and libvips.

# macOS
brew install vips
# Ubuntu/Debian
apt-get install libvips-dev
# Build
go build -o image-server .# Run
./image-server server --port 7000

Server

Uploading Images

Images are uploaded to a namespace. Namespaces group image types (e.g., avatars vs product images may need different sizes).

Upload from URL:

curl -X POST "http://localhost:7000/products?source=https://example.com/image.jpg"

Upload binary data:

curl --data-binary "@./image.jpg" -X POST http://localhost:7000/products

Response:

{
"hash": "6e0072682e66287b662827da75b244a3",
"height": 600,
"width": 800,
"content_type": "image/jpeg"
}

Upload and process immediately:

curl --data-binary "@./image.jpg" -X POST "http://localhost:7000/products?outputs=x300.jpg,x300.webp"

Retrieving Images

Images are accessed via their hash, partitioned into path segments:

GET http://localhost:7000/{namespace}/{hash[0:3]}/{hash[3:6]}/{hash[6:9]}/{hash[9:]}/{dimensions}.{format}

Examples:

# By width (maintains aspect ratio)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/w200.jpg
# Square crop
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.jpg
# Specific dimensions (width x height)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/300x200.jpg
# With quality adjustment (1-100)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200-q50.jpg
# WebP format
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.webp

Image Information

curl http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/info.json

Batch Processing

Process multiple sizes for an existing image:

curl -X POST "http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/process?outputs=x100.jpg,x200.jpg,x300.webp"

Signed URLs (Authentication)

Secure your image server by requiring signed URLs for uploads (and optionally reads). This works similarly to AWS S3 pre-signed URLs.

Setup

  1. Generate a signing secret:

    ./image-server generate-secret > /etc/image-server/secrets.txt
  2. Start the server with signature validation:

    ./image-server server \
    --require-signature \
    --signing-secrets-file /etc/image-server/secrets.txt \
    --signature-max-ttl 60
  3. Generate signed URLs in your backend application:

    The signature algorithm:

    StringToSign = METHOD + "\n" + PATH + "\n" + EXPIRES_UNIX_TIMESTAMP
    Signature = Base64RawURL(HMAC-SHA256(secret, StringToSign))
    

    URL format:

    POST /namespace?X-Expires=1702156800&X-Path=/namespace&X-Signature=...
    

    Go example:

    import"github.com/image-server/image-server/core/signature"signer:=signature.NewSigner("your-secret", "https://images.example.com")
    url:=signer.SignURL("POST", "/products", 15*time.Minute)
  4. Test with the CLI:

    ./image-server sign-url \
    --secret "your-secret" \
    --base-url "https://images.example.com" \
    --method POST \
    --path /products \
    --ttl 15m

Configuration Options

FlagDescriptionDefault
--require-signatureEnable signature validation for uploadsfalse
--require-signature-for-readsAlso require signatures for GET requestsfalse
--signing-secrets-filePath to file with secrets (one per line)-
--signature-max-ttlMaximum allowed TTL in minutes60

Secret Rotation

The secrets file supports multiple secrets for rotation. Add new secrets to the top of the file:

new-secret-abc123
old-secret-xyz789

The server validates against all secrets, so you can:

  1. Add the new secret
  2. Update your backend apps to use it
  3. Remove the old secret after existing URLs expire

Path-Based Signing

Sign a path prefix to allow uploads to any path under it:

# Sign for entire namespace
./image-server sign-url --secret "..." --path /products --ttl 15m
# Allows: POST /products, POST /products/abc/def/...# Sign for specific path only
./image-server sign-url --secret "..." --path /products/abc/def/ghi/jkl --ttl 15m
# Allows only that exact path

Webhooks

Send HTTP notifications to external systems when images are uploaded or processed. Useful for triggering downstream workflows like OCR, ML pipelines, or cache invalidation.

Setup

./image-server server \
--webhook-url "https://api.example.com/webhooks/images" \
--webhook-secret "$(./image-server generate-secret)" \
--webhook-timeout 10 \
--webhook-events "uploaded,batch_complete"

Events

EventTriggerUse Case
uploadedOriginal image uploaded to storageStart processing pipeline
processedEach image variant processedCache warming
failedProcessing errorAlerting
batch_completeAll variants done (or already existed)Trigger downstream workflow

By default, all events are sent. Use --webhook-events to filter.

Payload Format

{
"event": "image.uploaded",
"timestamp": "2025-12-09T15:30:00Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"width": 1920,
"height": 1080,
"content_type": "image/jpeg",
"remote_url": "https://cdn.example.com/products/6e0/072/.../original"
}
}

For image.processed:

{
"event": "image.processed",
"timestamp": "2025-12-09T15:30:01Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"filename": "x300.webp",
"format": "webp",
"width": 300,
"height": 169,
"quality": 75,
"remote_url": "https://cdn.example.com/products/6e0/072/.../x300.webp"
}
}

Security

Webhooks are signed with HMAC-SHA256. Verify the signature in your handler:

Headers:

X-Webhook-Signature: sha256=<hex-encoded-hmac>
X-Webhook-Timestamp: 1702135800

Verification (Python):

importhmacimporthashlibdefverify_webhook(secret: str, timestamp: str, body: bytes, signature: str) ->bool:
expected="sha256="+hmac.new(
secret.encode(),
f"{timestamp}.{body.decode()}".encode(),
hashlib.sha256
).hexdigest()
returnhmac.compare_digest(expected, signature)
# In your handler:ifnotverify_webhook(SECRET, request.headers["X-Webhook-Timestamp"],
request.body, request.headers["X-Webhook-Signature"]):
return401

Verification (Go):

funcverifyWebhook(secret, timestampstring, body []byte, signaturestring) bool {
h:=hmac.New(sha256.New, []byte(secret))
h.Write([]byte(fmt.Sprintf("%s.%s", timestamp, string(body))))
expected:="sha256="+hex.EncodeToString(h.Sum(nil))
returnhmac.Equal([]byte(expected), []byte(signature))
}

Configuration

FlagDescriptionDefault
--webhook-urlEndpoint URL (enables webhooks)-
--webhook-secretHMAC signing secret-
--webhook-timeoutHTTP timeout in seconds10
--webhook-eventsEvents to send (comma-separated)all

Reliability

  • Webhooks are sent asynchronously (non-blocking)
  • Failed deliveries retry up to 3 times with exponential backoff (1s, 4s)
  • Webhook failures don't affect image processing

Cloud Storage (S3)

./image-server server \
--uploader s3 \
--aws_access_key_id $AWS_ACCESS_KEY_ID \
--aws_secret_key $AWS_SECRET_KEY \
--aws_bucket $AWS_BUCKET \
--aws_region us-west-1 \
--remote_base_path "images/" \
--remote_base_url "https://cdn.example.com"

Server Configuration

FlagDescriptionDefault
--portServer port7000
--listenListen address127.0.0.1
--local_base_pathLocal image storage directorypublic
--extensionsAllowed file extensionsjpg,gif,webp
--maximum_widthMaximum output width1000
--default_qualityDefault JPEG/WebP quality75
--outputsDefault output formats-
--uploaderStorage backend (s3 or noop)auto
--uploader_concurrencyParallel upload workers10
--processor_concurrencyParallel processing workers4
--http_timeoutHTTP request timeout (seconds)5
--max_file_ageLocal file cleanup age (minutes)30

Admin Server

A separate admin server runs on port 7002 with health and metrics endpoints:

EndpointDescription
/probe/readyReadiness check
/probe/liveLiveness check
/metricsPrometheus metrics

CLI Commands

Process images locally

./image-server cli /path/to/images --outputs "x300.jpg,x300.webp"

Generate signing secret

./image-server generate-secret
./image-server generate-secret --length 64 --count 3

Generate signed URL

./image-server sign-url --secret "..." --path /namespace --ttl 15m

Version

./image-server version

Monitoring

Prometheus Metrics

Available at http://localhost:7002/metrics

Statsd

./image-server server --enable_statsd --statsd_host 127.0.0.1 --statsd_port 8125

Events:

  • image_server.image_request - Image processed and uploaded
  • image_server.image_request.{format} - By format (jpg, webp, etc.)
  • image_server.image_request_fail - Processing failed
  • image_server.original_downloaded - Original fetched from source
  • image_server.original_unavailable - Original not found (404)

Profiling

./image-server server --profile
# pprof available at http://localhost:6060

Development

Running locally

# Without S3
make dev-server
# With S3export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_KEY=...
export AWS_BUCKET=...
export AWS_REGION=...
export IMG_REMOTE_BASE_PATH=...
export IMG_REMOTE_BASE_URL=...
make dev-server-s3

Tests

make test# or
go test ./...

Building

make build
# Creates binaries in bin/ for multiple platforms

Error Handling

StatusDescription
401Invalid or missing signature (when signatures required)
404Image not found
400Invalid request parameters

License

MIT

About

No description, website, or topics provided.

Resources

Stars

23 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - image-server/image-server · GitHub
Skip to content

Repository files navigation

Image Server

A high-performance image processing server written in Go. Supports on-demand image resizing, format conversion, and cloud storage integration.

Features

  • On-demand image processing - Resize, crop, and convert images via URL parameters
  • Multiple output formats - JPEG, WebP, GIF, PNG, HEIC/HEIF (iPhone images)
  • Cloud storage - Upload processed images to Amazon S3
  • Signed URLs - Secure uploads with HMAC-SHA256 signed URLs (similar to AWS S3 pre-signed URLs)
  • Batch processing - Process multiple image sizes in a single request
  • Prometheus metrics - Built-in metrics endpoint for monitoring
  • Webhooks - Notify external systems when images are uploaded or processed
  • Docker support - Ready-to-use Docker image

Quick Start

Using Docker

docker build -t image-server .
docker run -p 7000:7000 -p 7002:7002 image-server

Building from Source

Requires Go 1.21+ and libvips.

# macOS
brew install vips
# Ubuntu/Debian
apt-get install libvips-dev
# Build
go build -o image-server .# Run
./image-server server --port 7000

Server

Uploading Images

Images are uploaded to a namespace. Namespaces group image types (e.g., avatars vs product images may need different sizes).

Upload from URL:

curl -X POST "http://localhost:7000/products?source=https://example.com/image.jpg"

Upload binary data:

curl --data-binary "@./image.jpg" -X POST http://localhost:7000/products

Response:

{
"hash": "6e0072682e66287b662827da75b244a3",
"height": 600,
"width": 800,
"content_type": "image/jpeg"
}

Upload and process immediately:

curl --data-binary "@./image.jpg" -X POST "http://localhost:7000/products?outputs=x300.jpg,x300.webp"

Retrieving Images

Images are accessed via their hash, partitioned into path segments:

GET http://localhost:7000/{namespace}/{hash[0:3]}/{hash[3:6]}/{hash[6:9]}/{hash[9:]}/{dimensions}.{format}

Examples:

# By width (maintains aspect ratio)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/w200.jpg
# Square crop
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.jpg
# Specific dimensions (width x height)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/300x200.jpg
# With quality adjustment (1-100)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200-q50.jpg
# WebP format
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.webp

Image Information

curl http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/info.json

Batch Processing

Process multiple sizes for an existing image:

curl -X POST "http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/process?outputs=x100.jpg,x200.jpg,x300.webp"

Signed URLs (Authentication)

Secure your image server by requiring signed URLs for uploads (and optionally reads). This works similarly to AWS S3 pre-signed URLs.

Setup

  1. Generate a signing secret:

    ./image-server generate-secret > /etc/image-server/secrets.txt
  2. Start the server with signature validation:

    ./image-server server \
    --require-signature \
    --signing-secrets-file /etc/image-server/secrets.txt \
    --signature-max-ttl 60
  3. Generate signed URLs in your backend application:

    The signature algorithm:

    StringToSign = METHOD + "\n" + PATH + "\n" + EXPIRES_UNIX_TIMESTAMP
    Signature = Base64RawURL(HMAC-SHA256(secret, StringToSign))
    

    URL format:

    POST /namespace?X-Expires=1702156800&X-Path=/namespace&X-Signature=...
    

    Go example:

    import"github.com/image-server/image-server/core/signature"signer:=signature.NewSigner("your-secret", "https://images.example.com")
    url:=signer.SignURL("POST", "/products", 15*time.Minute)
  4. Test with the CLI:

    ./image-server sign-url \
    --secret "your-secret" \
    --base-url "https://images.example.com" \
    --method POST \
    --path /products \
    --ttl 15m

Configuration Options

FlagDescriptionDefault
--require-signatureEnable signature validation for uploadsfalse
--require-signature-for-readsAlso require signatures for GET requestsfalse
--signing-secrets-filePath to file with secrets (one per line)-
--signature-max-ttlMaximum allowed TTL in minutes60

Secret Rotation

The secrets file supports multiple secrets for rotation. Add new secrets to the top of the file:

new-secret-abc123
old-secret-xyz789

The server validates against all secrets, so you can:

  1. Add the new secret
  2. Update your backend apps to use it
  3. Remove the old secret after existing URLs expire

Path-Based Signing

Sign a path prefix to allow uploads to any path under it:

# Sign for entire namespace
./image-server sign-url --secret "..." --path /products --ttl 15m
# Allows: POST /products, POST /products/abc/def/...# Sign for specific path only
./image-server sign-url --secret "..." --path /products/abc/def/ghi/jkl --ttl 15m
# Allows only that exact path

Webhooks

Send HTTP notifications to external systems when images are uploaded or processed. Useful for triggering downstream workflows like OCR, ML pipelines, or cache invalidation.

Setup

./image-server server \
--webhook-url "https://api.example.com/webhooks/images" \
--webhook-secret "$(./image-server generate-secret)" \
--webhook-timeout 10 \
--webhook-events "uploaded,batch_complete"

Events

EventTriggerUse Case
uploadedOriginal image uploaded to storageStart processing pipeline
processedEach image variant processedCache warming
failedProcessing errorAlerting
batch_completeAll variants done (or already existed)Trigger downstream workflow

By default, all events are sent. Use --webhook-events to filter.

Payload Format

{
"event": "image.uploaded",
"timestamp": "2025-12-09T15:30:00Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"width": 1920,
"height": 1080,
"content_type": "image/jpeg",
"remote_url": "https://cdn.example.com/products/6e0/072/.../original"
}
}

For image.processed:

{
"event": "image.processed",
"timestamp": "2025-12-09T15:30:01Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"filename": "x300.webp",
"format": "webp",
"width": 300,
"height": 169,
"quality": 75,
"remote_url": "https://cdn.example.com/products/6e0/072/.../x300.webp"
}
}

Security

Webhooks are signed with HMAC-SHA256. Verify the signature in your handler:

Headers:

X-Webhook-Signature: sha256=<hex-encoded-hmac>
X-Webhook-Timestamp: 1702135800

Verification (Python):

importhmacimporthashlibdefverify_webhook(secret: str, timestamp: str, body: bytes, signature: str) ->bool:
expected="sha256="+hmac.new(
secret.encode(),
f"{timestamp}.{body.decode()}".encode(),
hashlib.sha256
).hexdigest()
returnhmac.compare_digest(expected, signature)
# In your handler:ifnotverify_webhook(SECRET, request.headers["X-Webhook-Timestamp"],
request.body, request.headers["X-Webhook-Signature"]):
return401

Verification (Go):

funcverifyWebhook(secret, timestampstring, body []byte, signaturestring) bool {
h:=hmac.New(sha256.New, []byte(secret))
h.Write([]byte(fmt.Sprintf("%s.%s", timestamp, string(body))))
expected:="sha256="+hex.EncodeToString(h.Sum(nil))
returnhmac.Equal([]byte(expected), []byte(signature))
}

Configuration

FlagDescriptionDefault
--webhook-urlEndpoint URL (enables webhooks)-
--webhook-secretHMAC signing secret-
--webhook-timeoutHTTP timeout in seconds10
--webhook-eventsEvents to send (comma-separated)all

Reliability

  • Webhooks are sent asynchronously (non-blocking)
  • Failed deliveries retry up to 3 times with exponential backoff (1s, 4s)
  • Webhook failures don't affect image processing

Cloud Storage (S3)

./image-server server \
--uploader s3 \
--aws_access_key_id $AWS_ACCESS_KEY_ID \
--aws_secret_key $AWS_SECRET_KEY \
--aws_bucket $AWS_BUCKET \
--aws_region us-west-1 \
--remote_base_path "images/" \
--remote_base_url "https://cdn.example.com"

Server Configuration

FlagDescriptionDefault
--portServer port7000
--listenListen address127.0.0.1
--local_base_pathLocal image storage directorypublic
--extensionsAllowed file extensionsjpg,gif,webp
--maximum_widthMaximum output width1000
--default_qualityDefault JPEG/WebP quality75
--outputsDefault output formats-
--uploaderStorage backend (s3 or noop)auto
--uploader_concurrencyParallel upload workers10
--processor_concurrencyParallel processing workers4
--http_timeoutHTTP request timeout (seconds)5
--max_file_ageLocal file cleanup age (minutes)30

Admin Server

A separate admin server runs on port 7002 with health and metrics endpoints:

EndpointDescription
/probe/readyReadiness check
/probe/liveLiveness check
/metricsPrometheus metrics

CLI Commands

Process images locally

./image-server cli /path/to/images --outputs "x300.jpg,x300.webp"

Generate signing secret

./image-server generate-secret
./image-server generate-secret --length 64 --count 3

Generate signed URL

./image-server sign-url --secret "..." --path /namespace --ttl 15m

Version

./image-server version

Monitoring

Prometheus Metrics

Available at http://localhost:7002/metrics

Statsd

./image-server server --enable_statsd --statsd_host 127.0.0.1 --statsd_port 8125

Events:

  • image_server.image_request - Image processed and uploaded
  • image_server.image_request.{format} - By format (jpg, webp, etc.)
  • image_server.image_request_fail - Processing failed
  • image_server.original_downloaded - Original fetched from source
  • image_server.original_unavailable - Original not found (404)

Profiling

./image-server server --profile
# pprof available at http://localhost:6060

Development

Running locally

# Without S3
make dev-server
# With S3export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_KEY=...
export AWS_BUCKET=...
export AWS_REGION=...
export IMG_REMOTE_BASE_PATH=...
export IMG_REMOTE_BASE_URL=...
make dev-server-s3

Tests

make test# or
go test ./...

Building

make build
# Creates binaries in bin/ for multiple platforms

Error Handling

StatusDescription
401Invalid or missing signature (when signatures required)
404Image not found
400Invalid request parameters

License

MIT

About

No description, website, or topics provided.

Resources

Stars

23 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - image-server/image-server · GitHub
Skip to content

Repository files navigation

Image Server

A high-performance image processing server written in Go. Supports on-demand image resizing, format conversion, and cloud storage integration.

Features

  • On-demand image processing - Resize, crop, and convert images via URL parameters
  • Multiple output formats - JPEG, WebP, GIF, PNG, HEIC/HEIF (iPhone images)
  • Cloud storage - Upload processed images to Amazon S3
  • Signed URLs - Secure uploads with HMAC-SHA256 signed URLs (similar to AWS S3 pre-signed URLs)
  • Batch processing - Process multiple image sizes in a single request
  • Prometheus metrics - Built-in metrics endpoint for monitoring
  • Webhooks - Notify external systems when images are uploaded or processed
  • Docker support - Ready-to-use Docker image

Quick Start

Using Docker

docker build -t image-server .
docker run -p 7000:7000 -p 7002:7002 image-server

Building from Source

Requires Go 1.21+ and libvips.

# macOS
brew install vips
# Ubuntu/Debian
apt-get install libvips-dev
# Build
go build -o image-server .# Run
./image-server server --port 7000

Server

Uploading Images

Images are uploaded to a namespace. Namespaces group image types (e.g., avatars vs product images may need different sizes).

Upload from URL:

curl -X POST "http://localhost:7000/products?source=https://example.com/image.jpg"

Upload binary data:

curl --data-binary "@./image.jpg" -X POST http://localhost:7000/products

Response:

{
"hash": "6e0072682e66287b662827da75b244a3",
"height": 600,
"width": 800,
"content_type": "image/jpeg"
}

Upload and process immediately:

curl --data-binary "@./image.jpg" -X POST "http://localhost:7000/products?outputs=x300.jpg,x300.webp"

Retrieving Images

Images are accessed via their hash, partitioned into path segments:

GET http://localhost:7000/{namespace}/{hash[0:3]}/{hash[3:6]}/{hash[6:9]}/{hash[9:]}/{dimensions}.{format}

Examples:

# By width (maintains aspect ratio)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/w200.jpg
# Square crop
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.jpg
# Specific dimensions (width x height)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/300x200.jpg
# With quality adjustment (1-100)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200-q50.jpg
# WebP format
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.webp

Image Information

curl http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/info.json

Batch Processing

Process multiple sizes for an existing image:

curl -X POST "http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/process?outputs=x100.jpg,x200.jpg,x300.webp"

Signed URLs (Authentication)

Secure your image server by requiring signed URLs for uploads (and optionally reads). This works similarly to AWS S3 pre-signed URLs.

Setup

  1. Generate a signing secret:

    ./image-server generate-secret > /etc/image-server/secrets.txt
  2. Start the server with signature validation:

    ./image-server server \
    --require-signature \
    --signing-secrets-file /etc/image-server/secrets.txt \
    --signature-max-ttl 60
  3. Generate signed URLs in your backend application:

    The signature algorithm:

    StringToSign = METHOD + "\n" + PATH + "\n" + EXPIRES_UNIX_TIMESTAMP
    Signature = Base64RawURL(HMAC-SHA256(secret, StringToSign))
    

    URL format:

    POST /namespace?X-Expires=1702156800&X-Path=/namespace&X-Signature=...
    

    Go example:

    import"github.com/image-server/image-server/core/signature"signer:=signature.NewSigner("your-secret", "https://images.example.com")
    url:=signer.SignURL("POST", "/products", 15*time.Minute)
  4. Test with the CLI:

    ./image-server sign-url \
    --secret "your-secret" \
    --base-url "https://images.example.com" \
    --method POST \
    --path /products \
    --ttl 15m

Configuration Options

FlagDescriptionDefault
--require-signatureEnable signature validation for uploadsfalse
--require-signature-for-readsAlso require signatures for GET requestsfalse
--signing-secrets-filePath to file with secrets (one per line)-
--signature-max-ttlMaximum allowed TTL in minutes60

Secret Rotation

The secrets file supports multiple secrets for rotation. Add new secrets to the top of the file:

new-secret-abc123
old-secret-xyz789

The server validates against all secrets, so you can:

  1. Add the new secret
  2. Update your backend apps to use it
  3. Remove the old secret after existing URLs expire

Path-Based Signing

Sign a path prefix to allow uploads to any path under it:

# Sign for entire namespace
./image-server sign-url --secret "..." --path /products --ttl 15m
# Allows: POST /products, POST /products/abc/def/...# Sign for specific path only
./image-server sign-url --secret "..." --path /products/abc/def/ghi/jkl --ttl 15m
# Allows only that exact path

Webhooks

Send HTTP notifications to external systems when images are uploaded or processed. Useful for triggering downstream workflows like OCR, ML pipelines, or cache invalidation.

Setup

./image-server server \
--webhook-url "https://api.example.com/webhooks/images" \
--webhook-secret "$(./image-server generate-secret)" \
--webhook-timeout 10 \
--webhook-events "uploaded,batch_complete"

Events

EventTriggerUse Case
uploadedOriginal image uploaded to storageStart processing pipeline
processedEach image variant processedCache warming
failedProcessing errorAlerting
batch_completeAll variants done (or already existed)Trigger downstream workflow

By default, all events are sent. Use --webhook-events to filter.

Payload Format

{
"event": "image.uploaded",
"timestamp": "2025-12-09T15:30:00Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"width": 1920,
"height": 1080,
"content_type": "image/jpeg",
"remote_url": "https://cdn.example.com/products/6e0/072/.../original"
}
}

For image.processed:

{
"event": "image.processed",
"timestamp": "2025-12-09T15:30:01Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"filename": "x300.webp",
"format": "webp",
"width": 300,
"height": 169,
"quality": 75,
"remote_url": "https://cdn.example.com/products/6e0/072/.../x300.webp"
}
}

Security

Webhooks are signed with HMAC-SHA256. Verify the signature in your handler:

Headers:

X-Webhook-Signature: sha256=<hex-encoded-hmac>
X-Webhook-Timestamp: 1702135800

Verification (Python):

importhmacimporthashlibdefverify_webhook(secret: str, timestamp: str, body: bytes, signature: str) ->bool:
expected="sha256="+hmac.new(
secret.encode(),
f"{timestamp}.{body.decode()}".encode(),
hashlib.sha256
).hexdigest()
returnhmac.compare_digest(expected, signature)
# In your handler:ifnotverify_webhook(SECRET, request.headers["X-Webhook-Timestamp"],
request.body, request.headers["X-Webhook-Signature"]):
return401

Verification (Go):

funcverifyWebhook(secret, timestampstring, body []byte, signaturestring) bool {
h:=hmac.New(sha256.New, []byte(secret))
h.Write([]byte(fmt.Sprintf("%s.%s", timestamp, string(body))))
expected:="sha256="+hex.EncodeToString(h.Sum(nil))
returnhmac.Equal([]byte(expected), []byte(signature))
}

Configuration

FlagDescriptionDefault
--webhook-urlEndpoint URL (enables webhooks)-
--webhook-secretHMAC signing secret-
--webhook-timeoutHTTP timeout in seconds10
--webhook-eventsEvents to send (comma-separated)all

Reliability

  • Webhooks are sent asynchronously (non-blocking)
  • Failed deliveries retry up to 3 times with exponential backoff (1s, 4s)
  • Webhook failures don't affect image processing

Cloud Storage (S3)

./image-server server \
--uploader s3 \
--aws_access_key_id $AWS_ACCESS_KEY_ID \
--aws_secret_key $AWS_SECRET_KEY \
--aws_bucket $AWS_BUCKET \
--aws_region us-west-1 \
--remote_base_path "images/" \
--remote_base_url "https://cdn.example.com"

Server Configuration

FlagDescriptionDefault
--portServer port7000
--listenListen address127.0.0.1
--local_base_pathLocal image storage directorypublic
--extensionsAllowed file extensionsjpg,gif,webp
--maximum_widthMaximum output width1000
--default_qualityDefault JPEG/WebP quality75
--outputsDefault output formats-
--uploaderStorage backend (s3 or noop)auto
--uploader_concurrencyParallel upload workers10
--processor_concurrencyParallel processing workers4
--http_timeoutHTTP request timeout (seconds)5
--max_file_ageLocal file cleanup age (minutes)30

Admin Server

A separate admin server runs on port 7002 with health and metrics endpoints:

EndpointDescription
/probe/readyReadiness check
/probe/liveLiveness check
/metricsPrometheus metrics

CLI Commands

Process images locally

./image-server cli /path/to/images --outputs "x300.jpg,x300.webp"

Generate signing secret

./image-server generate-secret
./image-server generate-secret --length 64 --count 3

Generate signed URL

./image-server sign-url --secret "..." --path /namespace --ttl 15m

Version

./image-server version

Monitoring

Prometheus Metrics

Available at http://localhost:7002/metrics

Statsd

./image-server server --enable_statsd --statsd_host 127.0.0.1 --statsd_port 8125

Events:

  • image_server.image_request - Image processed and uploaded
  • image_server.image_request.{format} - By format (jpg, webp, etc.)
  • image_server.image_request_fail - Processing failed
  • image_server.original_downloaded - Original fetched from source
  • image_server.original_unavailable - Original not found (404)

Profiling

./image-server server --profile
# pprof available at http://localhost:6060

Development

Running locally

# Without S3
make dev-server
# With S3export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_KEY=...
export AWS_BUCKET=...
export AWS_REGION=...
export IMG_REMOTE_BASE_PATH=...
export IMG_REMOTE_BASE_URL=...
make dev-server-s3

Tests

make test# or
go test ./...

Building

make build
# Creates binaries in bin/ for multiple platforms

Error Handling

StatusDescription
401Invalid or missing signature (when signatures required)
404Image not found
400Invalid request parameters

License

MIT

About

No description, website, or topics provided.

Resources

Stars

23 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - image-server/image-server · GitHub
Skip to content

Repository files navigation

Image Server

A high-performance image processing server written in Go. Supports on-demand image resizing, format conversion, and cloud storage integration.

Features

  • On-demand image processing - Resize, crop, and convert images via URL parameters
  • Multiple output formats - JPEG, WebP, GIF, PNG, HEIC/HEIF (iPhone images)
  • Cloud storage - Upload processed images to Amazon S3
  • Signed URLs - Secure uploads with HMAC-SHA256 signed URLs (similar to AWS S3 pre-signed URLs)
  • Batch processing - Process multiple image sizes in a single request
  • Prometheus metrics - Built-in metrics endpoint for monitoring
  • Webhooks - Notify external systems when images are uploaded or processed
  • Docker support - Ready-to-use Docker image

Quick Start

Using Docker

docker build -t image-server .
docker run -p 7000:7000 -p 7002:7002 image-server

Building from Source

Requires Go 1.21+ and libvips.

# macOS
brew install vips
# Ubuntu/Debian
apt-get install libvips-dev
# Build
go build -o image-server .# Run
./image-server server --port 7000

Server

Uploading Images

Images are uploaded to a namespace. Namespaces group image types (e.g., avatars vs product images may need different sizes).

Upload from URL:

curl -X POST "http://localhost:7000/products?source=https://example.com/image.jpg"

Upload binary data:

curl --data-binary "@./image.jpg" -X POST http://localhost:7000/products

Response:

{
"hash": "6e0072682e66287b662827da75b244a3",
"height": 600,
"width": 800,
"content_type": "image/jpeg"
}

Upload and process immediately:

curl --data-binary "@./image.jpg" -X POST "http://localhost:7000/products?outputs=x300.jpg,x300.webp"

Retrieving Images

Images are accessed via their hash, partitioned into path segments:

GET http://localhost:7000/{namespace}/{hash[0:3]}/{hash[3:6]}/{hash[6:9]}/{hash[9:]}/{dimensions}.{format}

Examples:

# By width (maintains aspect ratio)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/w200.jpg
# Square crop
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.jpg
# Specific dimensions (width x height)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/300x200.jpg
# With quality adjustment (1-100)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200-q50.jpg
# WebP format
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.webp

Image Information

curl http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/info.json

Batch Processing

Process multiple sizes for an existing image:

curl -X POST "http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/process?outputs=x100.jpg,x200.jpg,x300.webp"

Signed URLs (Authentication)

Secure your image server by requiring signed URLs for uploads (and optionally reads). This works similarly to AWS S3 pre-signed URLs.

Setup

  1. Generate a signing secret:

    ./image-server generate-secret > /etc/image-server/secrets.txt
  2. Start the server with signature validation:

    ./image-server server \
    --require-signature \
    --signing-secrets-file /etc/image-server/secrets.txt \
    --signature-max-ttl 60
  3. Generate signed URLs in your backend application:

    The signature algorithm:

    StringToSign = METHOD + "\n" + PATH + "\n" + EXPIRES_UNIX_TIMESTAMP
    Signature = Base64RawURL(HMAC-SHA256(secret, StringToSign))
    

    URL format:

    POST /namespace?X-Expires=1702156800&X-Path=/namespace&X-Signature=...
    

    Go example:

    import"github.com/image-server/image-server/core/signature"signer:=signature.NewSigner("your-secret", "https://images.example.com")
    url:=signer.SignURL("POST", "/products", 15*time.Minute)
  4. Test with the CLI:

    ./image-server sign-url \
    --secret "your-secret" \
    --base-url "https://images.example.com" \
    --method POST \
    --path /products \
    --ttl 15m

Configuration Options

FlagDescriptionDefault
--require-signatureEnable signature validation for uploadsfalse
--require-signature-for-readsAlso require signatures for GET requestsfalse
--signing-secrets-filePath to file with secrets (one per line)-
--signature-max-ttlMaximum allowed TTL in minutes60

Secret Rotation

The secrets file supports multiple secrets for rotation. Add new secrets to the top of the file:

new-secret-abc123
old-secret-xyz789

The server validates against all secrets, so you can:

  1. Add the new secret
  2. Update your backend apps to use it
  3. Remove the old secret after existing URLs expire

Path-Based Signing

Sign a path prefix to allow uploads to any path under it:

# Sign for entire namespace
./image-server sign-url --secret "..." --path /products --ttl 15m
# Allows: POST /products, POST /products/abc/def/...# Sign for specific path only
./image-server sign-url --secret "..." --path /products/abc/def/ghi/jkl --ttl 15m
# Allows only that exact path

Webhooks

Send HTTP notifications to external systems when images are uploaded or processed. Useful for triggering downstream workflows like OCR, ML pipelines, or cache invalidation.

Setup

./image-server server \
--webhook-url "https://api.example.com/webhooks/images" \
--webhook-secret "$(./image-server generate-secret)" \
--webhook-timeout 10 \
--webhook-events "uploaded,batch_complete"

Events

EventTriggerUse Case
uploadedOriginal image uploaded to storageStart processing pipeline
processedEach image variant processedCache warming
failedProcessing errorAlerting
batch_completeAll variants done (or already existed)Trigger downstream workflow

By default, all events are sent. Use --webhook-events to filter.

Payload Format

{
"event": "image.uploaded",
"timestamp": "2025-12-09T15:30:00Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"width": 1920,
"height": 1080,
"content_type": "image/jpeg",
"remote_url": "https://cdn.example.com/products/6e0/072/.../original"
}
}

For image.processed:

{
"event": "image.processed",
"timestamp": "2025-12-09T15:30:01Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"filename": "x300.webp",
"format": "webp",
"width": 300,
"height": 169,
"quality": 75,
"remote_url": "https://cdn.example.com/products/6e0/072/.../x300.webp"
}
}

Security

Webhooks are signed with HMAC-SHA256. Verify the signature in your handler:

Headers:

X-Webhook-Signature: sha256=<hex-encoded-hmac>
X-Webhook-Timestamp: 1702135800

Verification (Python):

importhmacimporthashlibdefverify_webhook(secret: str, timestamp: str, body: bytes, signature: str) ->bool:
expected="sha256="+hmac.new(
secret.encode(),
f"{timestamp}.{body.decode()}".encode(),
hashlib.sha256
).hexdigest()
returnhmac.compare_digest(expected, signature)
# In your handler:ifnotverify_webhook(SECRET, request.headers["X-Webhook-Timestamp"],
request.body, request.headers["X-Webhook-Signature"]):
return401

Verification (Go):

funcverifyWebhook(secret, timestampstring, body []byte, signaturestring) bool {
h:=hmac.New(sha256.New, []byte(secret))
h.Write([]byte(fmt.Sprintf("%s.%s", timestamp, string(body))))
expected:="sha256="+hex.EncodeToString(h.Sum(nil))
returnhmac.Equal([]byte(expected), []byte(signature))
}

Configuration

FlagDescriptionDefault
--webhook-urlEndpoint URL (enables webhooks)-
--webhook-secretHMAC signing secret-
--webhook-timeoutHTTP timeout in seconds10
--webhook-eventsEvents to send (comma-separated)all

Reliability

  • Webhooks are sent asynchronously (non-blocking)
  • Failed deliveries retry up to 3 times with exponential backoff (1s, 4s)
  • Webhook failures don't affect image processing

Cloud Storage (S3)

./image-server server \
--uploader s3 \
--aws_access_key_id $AWS_ACCESS_KEY_ID \
--aws_secret_key $AWS_SECRET_KEY \
--aws_bucket $AWS_BUCKET \
--aws_region us-west-1 \
--remote_base_path "images/" \
--remote_base_url "https://cdn.example.com"

Server Configuration

FlagDescriptionDefault
--portServer port7000
--listenListen address127.0.0.1
--local_base_pathLocal image storage directorypublic
--extensionsAllowed file extensionsjpg,gif,webp
--maximum_widthMaximum output width1000
--default_qualityDefault JPEG/WebP quality75
--outputsDefault output formats-
--uploaderStorage backend (s3 or noop)auto
--uploader_concurrencyParallel upload workers10
--processor_concurrencyParallel processing workers4
--http_timeoutHTTP request timeout (seconds)5
--max_file_ageLocal file cleanup age (minutes)30

Admin Server

A separate admin server runs on port 7002 with health and metrics endpoints:

EndpointDescription
/probe/readyReadiness check
/probe/liveLiveness check
/metricsPrometheus metrics

CLI Commands

Process images locally

./image-server cli /path/to/images --outputs "x300.jpg,x300.webp"

Generate signing secret

./image-server generate-secret
./image-server generate-secret --length 64 --count 3

Generate signed URL

./image-server sign-url --secret "..." --path /namespace --ttl 15m

Version

./image-server version

Monitoring

Prometheus Metrics

Available at http://localhost:7002/metrics

Statsd

./image-server server --enable_statsd --statsd_host 127.0.0.1 --statsd_port 8125

Events:

  • image_server.image_request - Image processed and uploaded
  • image_server.image_request.{format} - By format (jpg, webp, etc.)
  • image_server.image_request_fail - Processing failed
  • image_server.original_downloaded - Original fetched from source
  • image_server.original_unavailable - Original not found (404)

Profiling

./image-server server --profile
# pprof available at http://localhost:6060

Development

Running locally

# Without S3
make dev-server
# With S3export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_KEY=...
export AWS_BUCKET=...
export AWS_REGION=...
export IMG_REMOTE_BASE_PATH=...
export IMG_REMOTE_BASE_URL=...
make dev-server-s3

Tests

make test# or
go test ./...

Building

make build
# Creates binaries in bin/ for multiple platforms

Error Handling

StatusDescription
401Invalid or missing signature (when signatures required)
404Image not found
400Invalid request parameters

License

MIT

About

No description, website, or topics provided.

Resources

Stars

23 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - image-server/image-server · GitHub
Skip to content

Repository files navigation

Image Server

A high-performance image processing server written in Go. Supports on-demand image resizing, format conversion, and cloud storage integration.

Features

  • On-demand image processing - Resize, crop, and convert images via URL parameters
  • Multiple output formats - JPEG, WebP, GIF, PNG, HEIC/HEIF (iPhone images)
  • Cloud storage - Upload processed images to Amazon S3
  • Signed URLs - Secure uploads with HMAC-SHA256 signed URLs (similar to AWS S3 pre-signed URLs)
  • Batch processing - Process multiple image sizes in a single request
  • Prometheus metrics - Built-in metrics endpoint for monitoring
  • Webhooks - Notify external systems when images are uploaded or processed
  • Docker support - Ready-to-use Docker image

Quick Start

Using Docker

docker build -t image-server .
docker run -p 7000:7000 -p 7002:7002 image-server

Building from Source

Requires Go 1.21+ and libvips.

# macOS
brew install vips
# Ubuntu/Debian
apt-get install libvips-dev
# Build
go build -o image-server .# Run
./image-server server --port 7000

Server

Uploading Images

Images are uploaded to a namespace. Namespaces group image types (e.g., avatars vs product images may need different sizes).

Upload from URL:

curl -X POST "http://localhost:7000/products?source=https://example.com/image.jpg"

Upload binary data:

curl --data-binary "@./image.jpg" -X POST http://localhost:7000/products

Response:

{
"hash": "6e0072682e66287b662827da75b244a3",
"height": 600,
"width": 800,
"content_type": "image/jpeg"
}

Upload and process immediately:

curl --data-binary "@./image.jpg" -X POST "http://localhost:7000/products?outputs=x300.jpg,x300.webp"

Retrieving Images

Images are accessed via their hash, partitioned into path segments:

GET http://localhost:7000/{namespace}/{hash[0:3]}/{hash[3:6]}/{hash[6:9]}/{hash[9:]}/{dimensions}.{format}

Examples:

# By width (maintains aspect ratio)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/w200.jpg
# Square crop
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.jpg
# Specific dimensions (width x height)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/300x200.jpg
# With quality adjustment (1-100)
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200-q50.jpg
# WebP format
GET http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/x200.webp

Image Information

curl http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/info.json

Batch Processing

Process multiple sizes for an existing image:

curl -X POST "http://localhost:7000/products/6e0/072/682/e66287b662827da75b244a3/process?outputs=x100.jpg,x200.jpg,x300.webp"

Signed URLs (Authentication)

Secure your image server by requiring signed URLs for uploads (and optionally reads). This works similarly to AWS S3 pre-signed URLs.

Setup

  1. Generate a signing secret:

    ./image-server generate-secret > /etc/image-server/secrets.txt
  2. Start the server with signature validation:

    ./image-server server \
    --require-signature \
    --signing-secrets-file /etc/image-server/secrets.txt \
    --signature-max-ttl 60
  3. Generate signed URLs in your backend application:

    The signature algorithm:

    StringToSign = METHOD + "\n" + PATH + "\n" + EXPIRES_UNIX_TIMESTAMP
    Signature = Base64RawURL(HMAC-SHA256(secret, StringToSign))
    

    URL format:

    POST /namespace?X-Expires=1702156800&X-Path=/namespace&X-Signature=...
    

    Go example:

    import"github.com/image-server/image-server/core/signature"signer:=signature.NewSigner("your-secret", "https://images.example.com")
    url:=signer.SignURL("POST", "/products", 15*time.Minute)
  4. Test with the CLI:

    ./image-server sign-url \
    --secret "your-secret" \
    --base-url "https://images.example.com" \
    --method POST \
    --path /products \
    --ttl 15m

Configuration Options

FlagDescriptionDefault
--require-signatureEnable signature validation for uploadsfalse
--require-signature-for-readsAlso require signatures for GET requestsfalse
--signing-secrets-filePath to file with secrets (one per line)-
--signature-max-ttlMaximum allowed TTL in minutes60

Secret Rotation

The secrets file supports multiple secrets for rotation. Add new secrets to the top of the file:

new-secret-abc123
old-secret-xyz789

The server validates against all secrets, so you can:

  1. Add the new secret
  2. Update your backend apps to use it
  3. Remove the old secret after existing URLs expire

Path-Based Signing

Sign a path prefix to allow uploads to any path under it:

# Sign for entire namespace
./image-server sign-url --secret "..." --path /products --ttl 15m
# Allows: POST /products, POST /products/abc/def/...# Sign for specific path only
./image-server sign-url --secret "..." --path /products/abc/def/ghi/jkl --ttl 15m
# Allows only that exact path

Webhooks

Send HTTP notifications to external systems when images are uploaded or processed. Useful for triggering downstream workflows like OCR, ML pipelines, or cache invalidation.

Setup

./image-server server \
--webhook-url "https://api.example.com/webhooks/images" \
--webhook-secret "$(./image-server generate-secret)" \
--webhook-timeout 10 \
--webhook-events "uploaded,batch_complete"

Events

EventTriggerUse Case
uploadedOriginal image uploaded to storageStart processing pipeline
processedEach image variant processedCache warming
failedProcessing errorAlerting
batch_completeAll variants done (or already existed)Trigger downstream workflow

By default, all events are sent. Use --webhook-events to filter.

Payload Format

{
"event": "image.uploaded",
"timestamp": "2025-12-09T15:30:00Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"width": 1920,
"height": 1080,
"content_type": "image/jpeg",
"remote_url": "https://cdn.example.com/products/6e0/072/.../original"
}
}

For image.processed:

{
"event": "image.processed",
"timestamp": "2025-12-09T15:30:01Z",
"data": {
"namespace": "products",
"hash": "6e0072682e66287b662827da75b244a3",
"filename": "x300.webp",
"format": "webp",
"width": 300,
"height": 169,
"quality": 75,
"remote_url": "https://cdn.example.com/products/6e0/072/.../x300.webp"
}
}

Security

Webhooks are signed with HMAC-SHA256. Verify the signature in your handler:

Headers:

X-Webhook-Signature: sha256=<hex-encoded-hmac>
X-Webhook-Timestamp: 1702135800

Verification (Python):

importhmacimporthashlibdefverify_webhook(secret: str, timestamp: str, body: bytes, signature: str) ->bool:
expected="sha256="+hmac.new(
secret.encode(),
f"{timestamp}.{body.decode()}".encode(),
hashlib.sha256
).hexdigest()
returnhmac.compare_digest(expected, signature)
# In your handler:ifnotverify_webhook(SECRET, request.headers["X-Webhook-Timestamp"],
request.body, request.headers["X-Webhook-Signature"]):
return401

Verification (Go):

funcverifyWebhook(secret, timestampstring, body []byte, signaturestring) bool {
h:=hmac.New(sha256.New, []byte(secret))
h.Write([]byte(fmt.Sprintf("%s.%s", timestamp, string(body))))
expected:="sha256="+hex.EncodeToString(h.Sum(nil))
returnhmac.Equal([]byte(expected), []byte(signature))
}

Configuration

FlagDescriptionDefault
--webhook-urlEndpoint URL (enables webhooks)-
--webhook-secretHMAC signing secret-
--webhook-timeoutHTTP timeout in seconds10
--webhook-eventsEvents to send (comma-separated)all

Reliability

  • Webhooks are sent asynchronously (non-blocking)
  • Failed deliveries retry up to 3 times with exponential backoff (1s, 4s)
  • Webhook failures don't affect image processing

Cloud Storage (S3)

./image-server server \
--uploader s3 \
--aws_access_key_id $AWS_ACCESS_KEY_ID \
--aws_secret_key $AWS_SECRET_KEY \
--aws_bucket $AWS_BUCKET \
--aws_region us-west-1 \
--remote_base_path "images/" \
--remote_base_url "https://cdn.example.com"

Server Configuration

FlagDescriptionDefault
--portServer port7000
--listenListen address127.0.0.1
--local_base_pathLocal image storage directorypublic
--extensionsAllowed file extensionsjpg,gif,webp
--maximum_widthMaximum output width1000
--default_qualityDefault JPEG/WebP quality75
--outputsDefault output formats-
--uploaderStorage backend (s3 or noop)auto
--uploader_concurrencyParallel upload workers10
--processor_concurrencyParallel processing workers4
--http_timeoutHTTP request timeout (seconds)5
--max_file_ageLocal file cleanup age (minutes)30

Admin Server

A separate admin server runs on port 7002 with health and metrics endpoints:

EndpointDescription
/probe/readyReadiness check
/probe/liveLiveness check
/metricsPrometheus metrics

CLI Commands

Process images locally

./image-server cli /path/to/images --outputs "x300.jpg,x300.webp"

Generate signing secret

./image-server generate-secret
./image-server generate-secret --length 64 --count 3

Generate signed URL

./image-server sign-url --secret "..." --path /namespace --ttl 15m

Version

./image-server version

Monitoring

Prometheus Metrics

Available at http://localhost:7002/metrics

Statsd

./image-server server --enable_statsd --statsd_host 127.0.0.1 --statsd_port 8125

Events:

  • image_server.image_request - Image processed and uploaded
  • image_server.image_request.{format} - By format (jpg, webp, etc.)
  • image_server.image_request_fail - Processing failed
  • image_server.original_downloaded - Original fetched from source
  • image_server.original_unavailable - Original not found (404)

Profiling

./image-server server --profile
# pprof available at http://localhost:6060

Development

Running locally

# Without S3
make dev-server
# With S3export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_KEY=...
export AWS_BUCKET=...
export AWS_REGION=...
export IMG_REMOTE_BASE_PATH=...
export IMG_REMOTE_BASE_URL=...
make dev-server-s3

Tests

make test# or
go test ./...

Building

make build
# Creates binaries in bin/ for multiple platforms

Error Handling

StatusDescription
401Invalid or missing signature (when signatures required)
404Image not found
400Invalid request parameters

License

MIT

About

No description, website, or topics provided.

Resources

Stars

23 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages