Last updated: April 3, 2025

<span aria-hidden="true" id="build-a-fast-image-processing-server-with-go-and-libvips"></span>

# Build a fast image processing server with Go and Libvips

![Tim Koschützki](/assets/images/teammates/avatar-tim-kos-1.jpg?dpl=dpl_3fBRD5jmFtSDABLXGJJyXU1nVXf8)

**Tim Koschützki**

Co-founder · Berlin, Germany · Show bio

[](https://x.com/tim%5Fkos)[](https://github.com/tim-kos)

Efficient image processing is crucial for modern web applications, especially when handling large volumes of images. In this DevTip, we'll explore how to build a high-performance image processing server using Go and libvips, a powerful image processing library known for its speed and low memory usage.

<span aria-hidden="true" id="why-libvips"></span>

## Why Libvips?

libvips is an open-source image processing library designed for speed and efficiency. Unlike traditional libraries such as ImageMagick, libvips processes images using a streaming approach, significantly reducing memory usage and improving performance, especially with large images. This makes libvips particularly suitable for demanding image processing tasks.

Key advantages of libvips include:

* Low memory footprint compared to alternatives.
* High-speed processing through efficient operations and parallel execution.
* Support for numerous image formats (JPEG, PNG, WebP, TIFF, AVIF, HEIC, etc.).
* Efficient handling of very large images without loading the entire image into memory.

<span aria-hidden="true" id="setting-up-your-go-environment"></span>

## Setting up your Go environment

To integrate libvips with Go, we'll use the popular Go binding[govips⁠](https://github.com/davidbyttow/govips).

<span aria-hidden="true" id="prerequisites"></span>

### Prerequisites

Ensure you have the following installed:

* Go 1.16+
* libvips 8.10+
* `pkg-config` (used by Go to find libvips)

<span aria-hidden="true" id="installation"></span>

### Installation

First, install libvips and `pkg-config`:

```bash
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y libvips-dev pkg-config

# macOS (using homebrew)
brew install vips pkg-config

```

Next, initialize your Go project and install the `govips` v2 package:

```bash
mkdir image-server && cd image-server
go mod init image-server
go get github.com/davidbyttow/govips/v2/vips@latest
go mod tidy

```

<span aria-hidden="true" id="building-a-basic-image-processing-server"></span>

## Building a basic image processing server

Let's create a simple HTTP server in Go that accepts an image upload and resizes it based on query parameters.

```go
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"strconv"
	"strings"

	"github.com/davidbyttow/govips/v2/vips"
)

func resizeHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
		return
	}

	// Parse width and height from query parameters
	widthStr := r.URL.Query().Get("width")
	heightStr := r.URL.Query().Get("height")

	width, err := strconv.Atoi(widthStr)
	if err != nil || width <= 0 {
		http.Error(w, "Invalid or missing 'width' parameter", http.StatusBadRequest)
		return
	}

	height, err := strconv.Atoi(heightStr)
	if err != nil || height <= 0 {
		http.Error(w, "Invalid or missing 'height' parameter", http.StatusBadRequest)
		return
	}

	// Validate content type - expecting multipart form data
	contentType := r.Header.Get("Content-Type")
	if !strings.HasPrefix(contentType, "multipart/form-data") {
		http.Error(w, "Invalid content type, expected 'multipart/form-data'", http.StatusBadRequest)
		return
	}

	// Parse the multipart form (limit memory usage)
	err = r.ParseMultipartForm(32 << 20) // 32MB max memory
	if err != nil {
		log.Printf("Failed to parse multipart form: %v", err)
		http.Error(w, "Failed to parse multipart form", http.StatusBadRequest)
		return
	}

	// Retrieve the file from form data
	file, _, err := r.FormFile("image")
	if err != nil {
		log.Printf("Failed to get 'image' file from form: %v", err)
		http.Error(w, "Missing 'image' file in form", http.StatusBadRequest)
		return
	}
	defer file.Close()

	// Read the file content into a buffer
	buf, err := io.ReadAll(file)
	if err != nil {
		log.Printf("Failed to read image file content: %v", err)
		http.Error(w, "Failed to read image file content", http.StatusInternalServerError)
		return
	}

	// Create a govips image from the buffer
	img, err := vips.NewImageFromBuffer(buf)
	if err != nil {
		log.Printf("Failed to create image from buffer: %v", err)
		http.Error(w, "Failed to process image", http.StatusInternalServerError)
		return
	}
	defer img.Close() // Ensure image resources are released

	// Resize the image using ResizeWithVScale for better quality control
	// vips.KernelAuto selects an appropriate interpolation kernel
	err = img.ResizeWithVScale(float64(width), float64(height), vips.KernelAuto)
	if err != nil {
		log.Printf("Failed to resize image: %v", err)
		http.Error(w, "Failed to resize image", http.StatusInternalServerError)
		return
	}

	// Export the resized image to a buffer (defaulting to JPEG)
	// For other formats, use specific export parameters (e.g., vips.NewDefaultPNGExportParams())
	ep := vips.NewDefaultJPEGExportParams()
	outBuf, _, err := img.Export(ep)
	if err != nil {
		log.Printf("Failed to export image: %v", err)
		http.Error(w, "Failed to export image", http.StatusInternalServerError)
		return
	}

	// Set the content type and write the image buffer to the response
	w.Header().Set("Content-Type", "image/jpeg")
	_, err = w.Write(outBuf)
	if err != nil {
		log.Printf("Failed to write response: %v", err)
	}
}

func main() {
	// Initialize libvips. It's recommended to do this once at startup.
	// Pass nil for default configuration.
	vips.Startup(nil)
	defer vips.Shutdown() // Ensure libvips cleans up resources on exit

	http.HandleFunc("/resize", resizeHandler)
	// Register other handlers here (e.g., http.HandleFunc("/crop", cropHandler))

	port := ":8080"
	fmt.Printf("Server starting on port %s\n", port)
	log.Fatal(http.ListenAndServe(port, nil))
}


```

To test this server, run `go run main.go` and send a POST request with an image file:

```bash
curl -X POST "http://localhost:8080/resize?width=300&height=200" \
     -F "image=@/path/to/your/image.jpg" \
     -o resized_image.jpg

```

<span aria-hidden="true" id="implementing-common-image-operations"></span>

## Implementing common image operations

Beyond resizing, `govips` allows easy implementation of other common operations. Here are examples integrated into HTTP handlers.

**Note**

The following handlers would need to be registered in `main()` similar to `resizeHandler`. They reuse the image loading logic from `resizeHandler` for brevity, which you might refactor into a helper function in a real application.

<span aria-hidden="true" id="cropping-an-image"></span>

### Cropping an image

This handler crops an image based on `x`, `y`, `width`, and `height` query parameters.

```go
func cropHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
		return
	}

	// --- Parameter Parsing (x, y, width, height) ---
	xStr := r.URL.Query().Get("x")
	yStr := r.URL.Query().Get("y")
	cropWidthStr := r.URL.Query().Get("width")
	cropHeightStr := r.URL.Query().Get("height")

	x, err := strconv.Atoi(xStr)
	if err != nil || x < 0 {
		http.Error(w, "Invalid or missing 'x' parameter (must be >= 0)", http.StatusBadRequest)
		return
	}
	y, err := strconv.Atoi(yStr)
	if err != nil || y < 0 {
		http.Error(w, "Invalid or missing 'y' parameter (must be >= 0)", http.StatusBadRequest)
		return
	}
	cropWidth, err := strconv.Atoi(cropWidthStr)
	if err != nil || cropWidth <= 0 {
		http.Error(w, "Invalid or missing 'width' parameter (must be > 0)", http.StatusBadRequest)
		return
	}
	cropHeight, err := strconv.Atoi(cropHeightStr)
	if err != nil || cropHeight <= 0 {
		http.Error(w, "Invalid or missing 'height' parameter (must be > 0)", http.StatusBadRequest)
		return
	}

	// --- Image Loading (Similar to resizeHandler, assumes 'buf' is loaded) ---
	contentType := r.Header.Get("Content-Type")
	if !strings.HasPrefix(contentType, "multipart/form-data") {
		http.Error(w, "Invalid content type, expected 'multipart/form-data'", http.StatusBadRequest)
		return
	}
	err = r.ParseMultipartForm(32 << 20)
	if err != nil {
		log.Printf("Failed to parse multipart form: %v", err)
		http.Error(w, "Failed to parse multipart form", http.StatusBadRequest)
		return
	}
	file, _, err := r.FormFile("image")
	if err != nil {
		log.Printf("Failed to get 'image' file from form: %v", err)
		http.Error(w, "Missing 'image' file in form", http.StatusBadRequest)
		return
	}
	defer file.Close()
	buf, err := io.ReadAll(file)
	if err != nil {
		log.Printf("Failed to read image file content: %v", err)
		http.Error(w, "Failed to read image file content", http.StatusInternalServerError)
		return
	}
	// --- (End Image Loading) ---

	img, err := vips.NewImageFromBuffer(buf)
	if err != nil {
		log.Printf("Failed to create image from buffer: %v", err)
		http.Error(w, "Failed to process image", http.StatusInternalServerError)
		return
	}
	defer img.Close()

	// Validate crop dimensions against image size
	if x+cropWidth > img.Width() || y+cropHeight > img.Height() {
		msg := fmt.Sprintf("Crop area (%dx%d at %d,%d) exceeds image boundaries (%dx%d)",
			cropWidth, cropHeight, x, y, img.Width(), img.Height())
		http.Error(w, msg, http.StatusBadRequest)
		return
	}

	// Perform the crop operation
	err = img.ExtractArea(x, y, cropWidth, cropHeight)
	if err != nil {
		log.Printf("Failed to crop image: %v", err)
		http.Error(w, "Failed to crop image", http.StatusInternalServerError)
		return
	}

	// --- Image Export (Similar to resizeHandler) ---
	ep := vips.NewDefaultJPEGExportParams()
	outBuf, _, err := img.Export(ep)
	if err != nil {
		log.Printf("Failed to export cropped image: %v", err)
		http.Error(w, "Failed to export image", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "image/jpeg")
	_, err = w.Write(outBuf)
	if err != nil {
		log.Printf("Failed to write response: %v", err)
	}
	// --- (End Image Export) ---
}

```

<span aria-hidden="true" id="adding-a-watermark"></span>

### Adding a watermark

This handler composites a watermark image (e.g., `watermark.png` located in the server's directory) onto the uploaded image at specified `x`, `y` coordinates.

```go
func watermarkHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
		return
	}

	// --- Parameter Parsing (x, y) ---
	xStr := r.URL.Query().Get("x")
	yStr := r.URL.Query().Get("y")
	x, err := strconv.Atoi(xStr)
	if err != nil { // Allow x=0
		http.Error(w, "Invalid or missing 'x' parameter", http.StatusBadRequest)
		return
	}
	y, err := strconv.Atoi(yStr)
	if err != nil { // Allow y=0
		http.Error(w, "Invalid or missing 'y' parameter", http.StatusBadRequest)
		return
	}

	// --- Image Loading (Similar to resizeHandler, assumes 'buf' is loaded) ---
	contentType := r.Header.Get("Content-Type")
	if !strings.HasPrefix(contentType, "multipart/form-data") {
		http.Error(w, "Invalid content type, expected 'multipart/form-data'", http.StatusBadRequest)
		return
	}
	err = r.ParseMultipartForm(32 << 20)
	if err != nil {
		log.Printf("Failed to parse multipart form: %v", err)
		http.Error(w, "Failed to parse multipart form", http.StatusBadRequest)
		return
	}
	file, _, err := r.FormFile("image")
	if err != nil {
		log.Printf("Failed to get 'image' file from form: %v", err)
		http.Error(w, "Missing 'image' file in form", http.StatusBadRequest)
		return
	}
	defer file.Close()
	buf, err := io.ReadAll(file)
	if err != nil {
		log.Printf("Failed to read image file content: %v", err)
		http.Error(w, "Failed to read image file content", http.StatusInternalServerError)
		return
	}
	// --- (End Image Loading) ---

	img, err := vips.NewImageFromBuffer(buf)
	if err != nil {
		log.Printf("Failed to create image from buffer: %v", err)
		http.Error(w, "Failed to process image", http.StatusInternalServerError)
		return
	}
	defer img.Close()

	// Load the watermark image from a file
	watermarkPath := "./watermark.png" // Ensure this file exists
	watermark, err := vips.NewImageFromFile(watermarkPath)
	if err != nil {
		log.Printf("Failed to load watermark image '%s': %v", watermarkPath, err)
		http.Error(w, "Failed to load watermark", http.StatusInternalServerError)
		return
	}
	defer watermark.Close()

	// Apply the watermark using composite
	// vips.BlendModeOver places the watermark on top
	// Ensure watermark fits within the base image dimensions if needed
	if x < 0 || y < 0 || x+watermark.Width() > img.Width() || y+watermark.Height() > img.Height() {
		msg := fmt.Sprintf("Watermark position (%d,%d) or size (%dx%d) is out of bounds for image (%dx%d)",
			x, y, watermark.Width(), watermark.Height(), img.Width(), img.Height())
		http.Error(w, msg, http.StatusBadRequest)
		return
	}

	err = img.Composite(watermark, vips.BlendModeOver, x, y)
	if err != nil {
		log.Printf("Failed to apply watermark: %v", err)
		http.Error(w, "Failed to apply watermark", http.StatusInternalServerError)
		return
	}

	// --- Image Export (Similar to resizeHandler) ---
	// Export as PNG if watermark or original image has transparency
	var ep vips.ExportParams = vips.NewDefaultJPEGExportParams()
	contentTypeOut := "image/jpeg"
	if img.HasAlpha() || watermark.HasAlpha() {
		ep = vips.NewDefaultPNGExportParams()
		contentTypeOut = "image/png"
	}

	outBuf, _, err := img.Export(ep)
	if err != nil {
		log.Printf("Failed to export watermarked image: %v", err)
		http.Error(w, "Failed to export image", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", contentTypeOut)
	_, err = w.Write(outBuf)
	if err != nil {
		log.Printf("Failed to write response: %v", err)
	}
	// --- (End Image Export) ---
}

```

<span aria-hidden="true" id="converting-image-formats"></span>

### Converting image formats

This handler converts the uploaded image to a format specified by the `format` query parameter (e.g., `png`, `webp`).

```go
func convertHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
		return
	}

	// --- Parameter Parsing (format) ---
	format := strings.ToLower(r.URL.Query().Get("format"))
	if format == "" {
		http.Error(w, "Missing 'format' parameter (e.g., jpeg, png, webp)", http.StatusBadRequest)
		return
	}

	// --- Image Loading (Similar to resizeHandler, assumes 'buf' is loaded) ---
	contentType := r.Header.Get("Content-Type")
	if !strings.HasPrefix(contentType, "multipart/form-data") {
		http.Error(w, "Invalid content type, expected 'multipart/form-data'", http.StatusBadRequest)
		return
	}
	err := r.ParseMultipartForm(32 << 20)
	if err != nil {
		log.Printf("Failed to parse multipart form: %v", err)
		http.Error(w, "Failed to parse multipart form", http.StatusBadRequest)
		return
	}
	file, _, err := r.FormFile("image")
	if err != nil {
		log.Printf("Failed to get 'image' file from form: %v", err)
		http.Error(w, "Missing 'image' file in form", http.StatusBadRequest)
		return
	}
	defer file.Close()
	buf, err := io.ReadAll(file)
	if err != nil {
		log.Printf("Failed to read image file content: %v", err)
		http.Error(w, "Failed to read image file content", http.StatusInternalServerError)
		return
	}
	// --- (End Image Loading) ---

	img, err := vips.NewImageFromBuffer(buf)
	if err != nil {
		log.Printf("Failed to create image from buffer: %v", err)
		http.Error(w, "Failed to process image", http.StatusInternalServerError)
		return
	}
	defer img.Close()

	var exportParams vips.ExportParams
	var contentTypeOut string

	// Set export parameters based on the requested format
	switch format {
	case "jpeg", "jpg":
		exportParams = vips.NewDefaultJPEGExportParams()
		contentTypeOut = "image/jpeg"
	case "png":
		exportParams = vips.NewDefaultPNGExportParams()
		contentTypeOut = "image/png"
	case "webp":
		exportParams = vips.NewDefaultWEBPExportParams()
		contentTypeOut = "image/webp"
	// Add cases for other formats like AVIF, TIFF if needed
	// case "avif":
	// 	exportParams = vips.NewDefaultAVIFExportParams()
	// 	contentTypeOut = "image/avif"
	default:
		http.Error(w, fmt.Sprintf("Unsupported format: %s. Supported: jpeg, png, webp", format), http.StatusBadRequest)
		return
	}

	// Export the image in the requested format
	outBuf, _, err := img.Export(exportParams)
	if err != nil {
		log.Printf("Failed to convert image to %s: %v", format, err)
		http.Error(w, "Failed to convert image", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", contentTypeOut)
	_, err = w.Write(outBuf)
	if err != nil {
		log.Printf("Failed to write response: %v", err)
	}
}

```

<span aria-hidden="true" id="optimizing-performance-and-memory-usage"></span>

## Optimizing performance and memory usage

To get the most out of libvips and Go, especially under load, consider these strategies:

<span aria-hidden="true" id="tuning-libvips-configuration"></span>

### Tuning Libvips configuration

You can customize libvips' behavior at startup, for example, to control concurrency and caching:

```go
// Example configuration in main() before http.ListenAndServe
func main() {
    config := &vips.Config{
        ConcurrencyLevel: 4, // Limit concurrent threads used by vips operations
        MaxCacheFiles:    100, // Max number of intermediate files vips can use
        MaxCacheMem:      500 * 1024 * 1024, // 500MB max memory for vips cache
        MaxCacheSize:     1000, // Max number of operations to keep in cache
        // ReportInputBufferLeaks: true, // Useful for debugging memory leaks
    }
    vips.Startup(config)
    defer vips.Shutdown()

    // ... register handlers and start server ...
    http.HandleFunc("/resize", resizeHandler)
    http.HandleFunc("/crop", cropHandler)
    http.HandleFunc("/watermark", watermarkHandler)
    http.HandleFunc("/convert", convertHandler)

    port := ":8080"
    fmt.Printf("Server starting on port %s\n", port)
    log.Fatal(http.ListenAndServe(port, nil))
}

```

Experiment with these values based on your server's resources and workload.

<span aria-hidden="true" id="implementing-a-worker-pool"></span>

### Implementing a worker pool

For high-traffic servers, processing each request synchronously in its own goroutine can lead to excessive resource consumption. A worker pool limits the number of concurrent image processing tasks.

```go
// Define a job structure
type ImageJob struct {
	Input      []byte
	Operation  string // e.g., "resize", "crop"
	Params     map[string]interface{} // Operation-specific parameters
	ResultChan chan []byte // Channel to send result back
	ErrChan    chan error  // Channel to send error back
}

// Worker function that processes jobs from a channel
func imageWorker(id int, jobs <-chan ImageJob) {
	log.Printf("Worker %d started", id)
	for job := range jobs {
		log.Printf("Worker %d processing job (%s)", id, job.Operation)
		img, err := vips.NewImageFromBuffer(job.Input)
		if err != nil {
			job.ErrChan <- fmt.Errorf("worker %d failed to load image: %w", id, err)
			continue
		}

		// --- Perform operation based on job.Operation and job.Params ---
		// Example for resize:
		if job.Operation == "resize" {
			width := job.Params["width"].(int)
			height := job.Params["height"].(int)
			err = img.ResizeWithVScale(float64(width), float64(height), vips.KernelAuto)
		}
		// Add cases for other operations (crop, watermark, etc.)
		// ...

		if err != nil {
			img.Close()
			job.ErrChan <- fmt.Errorf("worker %d failed during %s: %w", id, job.Operation, err)
			continue
		}

		// --- Export image (determine format based on operation or params) ---
		ep := vips.NewDefaultJPEGExportParams() // Default or determine from params
		outBuf, _, err := img.Export(ep)
		img.Close() // Close image after processing

		if err != nil {
			job.ErrChan <- fmt.Errorf("worker %d failed to export image: %w", id, err)
		} else {
			job.ResultChan <- outBuf
		}
		log.Printf("Worker %d finished job (%s)", id, job.Operation)
	}
	log.Printf("Worker %d stopped", id)
}

// Global job queue channel (initialize in main)
var jobQueue chan ImageJob

// In your main function:
func main() {
    // ... vips.Startup ...

    numWorkers := 4 // Adjust based on CPU cores and workload
    jobs := make(chan ImageJob, 100) // Buffered channel
    jobQueue = jobs

    for w := 1; w <= numWorkers; w++ {
        go imageWorker(w, jobs)
    }

    // ... register handlers (modify them to use the jobQueue) ...
    http.HandleFunc("/resize", resizeHandlerWithWorkerPool) // Example modified handler

    // ... start server ...
    // ... vips.Shutdown ...
}

// Example modified handler using the worker pool:
func resizeHandlerWithWorkerPool(w http.ResponseWriter, r *http.Request) {
    // ... (parse parameters: width, height) ...
    // ... (load image into buf) ...

    resultChan := make(chan []byte)
    errChan := make(chan error)

    params := map[string]interface{}{
        "width":  width, // Assume width is parsed
        "height": height, // Assume height is parsed
    }

    job := ImageJob{
        Input:      buf, // Assume buf is loaded
        Operation:  "resize",
        Params:     params,
        ResultChan: resultChan,
        ErrChan:    errChan,
    }

    // Send job to the worker pool
    // Consider adding a timeout for sending to the queue
    jobQueue <- job

    // Wait for the result or an error (add timeout)
    select {
    case resultBuf := <-resultChan:
        w.Header().Set("Content-Type", "image/jpeg")
        w.Write(resultBuf)
    case err := <-errChan:
        log.Printf("Error processing job via worker: %v", err)
        http.Error(w, "Failed to process image", http.StatusInternalServerError)
    // case <-time.After(30 * time.Second):
    //     http.Error(w, "Processing timeout", http.StatusServiceUnavailable)
    }
}

```

This requires modifying your handlers to submit jobs to the `jobQueue` and wait for results, adding complexity but improving concurrency control.

<span aria-hidden="true" id="monitoring-resource-usage"></span>

### Monitoring resource usage

`govips` provides functions to inspect libvips' internal memory usage, which can be helpful for tuning.

```go
import "runtime/debug"

func logVipsStats() {
	stats := vips.MemoryStats()
	log.Printf("VIPS Memory - Mem: %d, MemHigh: %d, Allocs: %d",
		stats.Mem, stats.MemHighwater, stats.Allocs)

	// Force Go GC and re-check VIPS memory; useful for leak detection analysis
	debug.FreeOSMemory()
	stats = vips.MemoryStats()
	log.Printf("VIPS Memory after GC - Mem: %d", stats.Mem)
}

// You could call logVipsStats periodically using time.Ticker
// or expose it via a debug HTTP endpoint.

```

<span aria-hidden="true" id="deploying-with-docker"></span>

## Deploying with docker

Docker simplifies packaging your Go application along with the libvips dependency. Here's an improved multi-stage Dockerfile for a smaller final image:

```dockerfile
# ---- builder stage ----
FROM golang:1.22-alpine AS builder

# Install build dependencies including cgo tools and Libvips dev files
# Build-base includes gcc, make, etc.
RUN apk add --no-cache build-base gcc pkgconfig vips-dev

WORKDIR /app

# Copy go module files and download dependencies first for caching
COPY go.mod go.sum ./
RUN go mod download

# Copy the rest of the application source code
COPY . .

# Copy watermark file if needed during build or runtime
# Copy watermark.png .

# Build the go application
# Cgo_enabled=1 is required for govips
# Use -ldflags "-s -w" to strip debug symbols and reduce binary size
RUN CGO_ENABLED=1 GOOS=linux go build -ldflags="-s -w" -o image-server .

# ---- final stage ----
FROM alpine:latest

# Install only runtime dependencies (Libvips library)
# Vips package contains the necessary shared libraries
RUN apk add --no-cache vips

WORKDIR /app

# Copy the built binary from the builder stage
COPY --from=builder /app/image-server .

# Copy runtime assets like the watermark image from the builder stage
# Ensure the path matches where the go app expects it (e.g., ./watermark.png)
COPY --from=builder /app/watermark.png .

# Expose the port the server listens on
EXPOSE 8080

# Set the entrypoint command
CMD ["./image-server"]

```

Build and run your Docker container:

```bash
docker build -t go-vips-server .
docker run -p 8080:8080 -d --name my-image-server go-vips-server

```

This setup provides a robust foundation for building a high-performance image processing service using the speed of Go and the efficiency of libvips.

At Transloadit, we leverage libvips extensively in our[🤖 /image/resize](/docs/robots/image-resize.md) Robot for efficient image processing.

\#go#libvips#image-processing#docker#performance-optimization#govips#image-processing-service

### 👩‍💻 Join 20k+ developers

Sign up for our [monthly newsletter](/newsletters.md) to receive direct links to 3 exclusive tech — and 2 product updates. No less, no more.

Your email:

Get access

## File uploading and encoding. Made simple.

Transloadit streamlines file handling for developers, trusted by brands like Coursera and The New York Times. We’re known for a reliable API, top-notch support, and a strong commitment to open source, with projects like [Uppy⁠](https://uppy.io) and [Tus⁠](https://tus.io) setting standards in file processing.

[Sign up](/c/)[Book a Demo](https://survey.typeform.com/to/kRg47Xi5)

No credit card needed · 5 GB included in the free plan

Cancel anytime
