Skip to content

Commit 4a67d5e

Browse files
committed
initial commit
0 parents  commit 4a67d5e

File tree

1 file changed

+245
-0
lines changed

1 file changed

+245
-0
lines changed

README.md

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
# ImgProxy cache
2+
3+
A transparent caching reverse proxy for [imgproxy](https://imgproxy.net/) that automatically uploads processed images to S3-compatible storage (Tigris, AWS S3, MinIO, etc.).
4+
5+
## What It Does
6+
7+
This application acts as a transparent layer in front of imgproxy:
8+
9+
1. **Receives** image processing requests
10+
2. **Proxies** them to imgproxy for processing
11+
3. **Returns** the processed image to the client immediately
12+
4. **Uploads** the processed image to Tigris or S3 asynchronously for future use
13+
14+
The upload happens in the background, so client responses are not delayed. This creates a "cache-on-write" pattern where every successfully processed image is automatically stored in the target bucket.
15+
16+
## Architecture
17+
18+
```
19+
Client Request
20+
21+
[imgproxy-cache :8080] ← This application
22+
23+
Response → Client (immediate)
24+
25+
S3 Upload (background)
26+
```
27+
28+
The proxy:
29+
- Buffers the complete response in memory
30+
- Sends it immediately to the client
31+
- Spawns a goroutine to upload to S3
32+
- Logs upload success/failure without blocking
33+
34+
## Use Cases
35+
36+
- **Persistent Cache**: Ensure processed images are stored durably, even if imgproxy's local cache is cleared
37+
- **Multi-Region**: Process images once, store in Tigris or S3, serve from multiple regions
38+
- **Cost Optimization**: Reduce repeated processing of the same images
39+
40+
## Installation
41+
42+
### Using Docker (Recommended)
43+
44+
The Docker image includes both imgproxy (v3.30) and the Go proxy in a single container:
45+
46+
```bash
47+
docker build -t imgproxy-cache .
48+
docker run -p 8080:8080 \
49+
-e S3_BUCKET="your-bucket" \
50+
-e AWS_ACCESS_KEY_ID="your-key" \
51+
-e AWS_SECRET_ACCESS_KEY="your-secret" \
52+
imgproxy-cache
53+
```
54+
55+
When the container starts:
56+
1. imgproxy starts on `127.0.0.1:8081` (internal)
57+
2. The Go proxy starts on `:8080` (exposed)
58+
3. Both processes run under supervision - if either exits, the container stops
59+
60+
You can pass imgproxy-specific configuration via environment variables prefixed with `IMGPROXY_`:
61+
62+
```bash
63+
docker run -p 8080:8080 \
64+
-e S3_BUCKET="your-bucket" \
65+
-e AWS_ACCESS_KEY_ID="your-key" \
66+
-e AWS_SECRET_ACCESS_KEY="your-secret" \
67+
-e IMGPROXY_MAX_SRC_RESOLUTION=16384 \
68+
-e IMGPROXY_QUALITY=90 \
69+
imgproxy-cache
70+
```
71+
72+
See [imgproxy documentation](https://docs.imgproxy.net/configuration) for all available options.
73+
74+
## Example client code
75+
### HTML
76+
```html
77+
<img
78+
src="https://process-image-url"
79+
onerror="this.onerror=null; this.src='https://proxy-url/image-processing-params/original-image-url';"
80+
/>
81+
```
82+
83+
### Elixir (with imgproxy signing)
84+
85+
```elixir
86+
@doc """
87+
Renders an image with proxy URL transformation.
88+
The image URL will be transformed to: <image_proxy_url>/<dimensions>/<image_url>
89+
"""
90+
attr :src, :string, required: true
91+
attr :dimensions, :string, required: true
92+
attr :resize_mode, :string, default: "fit", values: ["fit", "fill"]
93+
attr :class, :string, default: nil
94+
attr :alt, :string, default: nil
95+
96+
def image(assigns) do
97+
img_path =
98+
"/rs:#{assigns.resize_mode}:#{assigns.dimensions}:1/dpr:2/g:ce/" <>
99+
Base.encode64(assigns.src) <> ".webp"
100+
101+
signature =
102+
:crypto.mac(
103+
:hmac,
104+
:sha256,
105+
Base.decode16!(
106+
System.get_env("IMGPROXY_KEY"),
107+
case: :lower
108+
),
109+
Base.decode16!(
110+
System.get_env("IMGPROXY_SALT"),
111+
case: :lower
112+
) <> img_path
113+
)
114+
|> Base.url_encode64(padding: false)
115+
116+
full_path = "/" <> signature <> img_path
117+
118+
assigns =
119+
assigns
120+
|> assign(:proxy_src, "#{Application.get_env(:manage, :image_proxy_url)}#{full_path}")
121+
|> assign(
122+
:cached_src,
123+
Application.get_env(:manage, :image_cache_url) <>
124+
"/" <>
125+
(:crypto.hash(:md5, full_path)
126+
|> Base.encode16(case: :lower))
127+
)
128+
129+
~H"""
130+
<img
131+
src={@cached_src}
132+
class={@class}
133+
alt={@alt}
134+
onerror={"this.onerror=null; this.src='#{@proxy_src}';"}
135+
/>
136+
"""
137+
end
138+
```
139+
140+
## Configuration
141+
142+
### Environment Variables
143+
144+
| Variable | Required | Default | Description |
145+
|----------|----------|---------|-------------|
146+
| `S3_BUCKET` | **Yes** | - | S3 bucket name where images will be stored |
147+
| `S3_FOLDER` | No | `""` | Prefix/folder path within the bucket |
148+
| `S3_ENDPOINT` | No | `https://fly.storage.tigris.dev` | S3-compatible endpoint URL |
149+
| `IMGPROXY_BIND` | No | `:8080` | Address and port for the proxy to bind to |
150+
| `HEALTH_CHECK_TIMEOUT_IN_SEC` | No | `30` | Seconds to wait for imgproxy to become healthy |
151+
152+
### AWS Credentials
153+
154+
The application uses the AWS SDK v2, which automatically loads credentials from:
155+
- Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
156+
- Shared credentials file (`~/.aws/credentials`)
157+
- IAM roles (when running on EC2/ECS/Lambda)
158+
- Web identity tokens (when running on EKS)
159+
160+
See [AWS SDK documentation](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/) for full details.
161+
162+
## How Caching Works
163+
164+
### Key Generation
165+
166+
S3 keys are generated by MD5 hashing the imgproxy URL path:
167+
168+
```
169+
Request: /resize:fill:300:300/plain/https://example.com/image.jpg
170+
S3 Key: a3f8c9d2e1b4f7a6c8d9e2f1b3a4c5d6
171+
```
172+
173+
This ensures:
174+
- **Consistent**: Same URL always maps to the same S3 key
175+
- **Compact**: Keys are fixed-length 32 characters
176+
- **Safe**: No special characters or path traversal issues
177+
178+
### Upload Behavior
179+
180+
- **Only successful responses** (HTTP 200) are uploaded
181+
- **Uploads are asynchronous** - client doesn't wait for S3 confirmation
182+
- **Failed uploads are logged** but don't affect the client response
183+
- **No deduplication** - same request will re-upload (consider implementing checks)
184+
185+
### Storage Structure
186+
187+
```
188+
s3://your-bucket/
189+
└── processed/ (if S3_FOLDER is set)
190+
├── a3f8c9d2e1b4f7a6... (image 1)
191+
├── b2e7d8c1f0a9e3b7... (image 2)
192+
└── c9f1a2b3e4d5c6a7... (image 3)
193+
```
194+
195+
## Usage Example
196+
197+
### Start the Service
198+
199+
```bash
200+
export S3_BUCKET="my-images"
201+
export AWS_ACCESS_KEY_ID="your-key"
202+
export AWS_SECRET_ACCESS_KEY="your-secret"
203+
204+
./imgproxy-cache
205+
```
206+
207+
### Process an Image
208+
209+
```bash
210+
# Request an image through the proxy
211+
curl http://localhost:8080/resize:fill:300:300/plain/https://example.com/cat.jpg > output.jpg
212+
213+
# The processed image is:
214+
# 1. Returned immediately to your curl command
215+
# 2. Uploaded to S3 in the background
216+
```
217+
218+
### Check Logs
219+
220+
```
221+
2025/10/20 10:30:00 INFO Waiting for imgproxy to be ready...
222+
2025/10/20 10:30:01 INFO imgproxy is ready
223+
2025/10/20 10:30:15 INFO Uploaded to S3 path=/resize:fill:300:300/plain/https://example.com/cat.jpg bucket=my-images key=a3f8c9d2e1b4f7a6c8d9e2f1b3a4c5d6
224+
```
225+
226+
227+
## Development
228+
229+
### Testing
230+
231+
```bash
232+
go test -v ./...
233+
```
234+
235+
## Limitations & Considerations
236+
237+
- **Memory Usage**: Entire response is buffered in memory before upload
238+
- **No Retry Logic**: Failed S3 uploads are not retried
239+
- **No Deduplication**: Same image can be uploaded multiple times
240+
- **No Cleanup**: Old/unused images are never deleted from S3
241+
- **No Validation**: Uploads happen even if the same key already exists in S3
242+
243+
## License
244+
245+
[MIT LICENSE](./LICENSE.md)

0 commit comments

Comments
 (0)