Skip to content

🖼️ Media and Image Storage (Garage S3 + Caddy)

Audience: Backend Engineers / DevOps / Web and Mobile Engineers
Purpose: Canonical guide for how Apollyon stores, serves, and manages public media assets.

At Apollyon, public media is served through a self-hosted object storage stack built with Garage S3 and Caddy. This page defines architecture, usage boundaries, and operational runbooks.

Storage Boundary

Use this platform for application media delivery and S3-style object operations. For team collaboration files and document sharing, use Nextcloud instead.


1. When to Use Garage vs Nextcloud

  • Garage S3 + Caddy: Public application assets (avatars, event media, image files used by web/mobile apps).
  • Nextcloud: Internal team file synchronization, shared documents, and operational artifacts.

2. Architecture Overview

2.1 Core Components

  • Storage Engine: Garage S3
  • Reverse Proxy: Caddy
  • Public Domain: images.apollyon.lat
  • Primary Bucket: public-media

2.2 Public Read Flow

[ Client App ]
      |
      | HTTPS GET https://images.apollyon.lat/<object-key>
      v
[ Caddy Proxy ] -- cache hit --> response
      |
      | cache miss
      v
[ Garage S3 Web API :3902 ] --> object read from storage

3. Port and Trust Boundaries

Garage exposes separate interfaces with distinct security expectations:

  • 3900 (S3 API): Authenticated. Backend-only object operations (upload/delete/manage).
  • 3901 (RPC): Internal Garage communication. Never exposed publicly.
  • 3902 (Web API): Read path for public assets, proxied via Caddy.

Do Not Collapse Port Roles

Keep authenticated write paths and public read paths separated to reduce blast radius and simplify policy enforcement.


4. Storage Layout and Data Safety

To balance performance and capacity:

  • Metadata: /opt/garage/meta
  • Data blobs: /mnt/newhdd/garage/data

No Manual File Operations

Never move, delete, or copy object data directly in Garage data directories. All object mutations must go through the S3 API.


5. Developer Usage

5.1 Frontend and Mobile Reads

Use normal web URLs with no client-side credentials:

<img src="https://images.apollyon.lat/avatars/user_101.jpg" alt="User profile" />

5.2 Backend Uploads

Backend services must use an S3-compatible SDK against port 3900.

import boto3
from botocore.config import Config

s3_client = boto3.client(
    "s3",
    endpoint_url="http://garage:3900",
    aws_access_key_id="YOUR_GARAGE_KEY_ID",
    aws_secret_access_key="YOUR_GARAGE_SECRET",
    region_name="us-east-1",
    config=Config(signature_version="s3v4"),
)

def upload_media(file_path: str, destination_key: str) -> None:
    s3_client.upload_file(
        Filename=file_path,
        Bucket="public-media",
        Key=destination_key,
        ExtraArgs={"ContentType": "image/jpeg"},
    )

6. Operations Runbook

Run Garage admin commands from the active container context.

docker exec -it garage /bin/sh

6.1 Key Management

  • Create key: garage key create <key-name>
  • List keys: garage key list
  • Revoke key: garage key rm <key-id>

6.2 Bucket Provisioning

# 1) Create bucket
garage bucket create project-assets

# 2) Grant backend read/write access
garage bucket allow project-assets --key <backend-key-id> --read --write

# 3) Optional: allow anonymous web reads via Web API
garage bucket website --allow project-assets

6.3 Caddy Routing Pattern

assets.apollyon.lat {
    reverse_proxy 127.0.0.1:3902 {
        header_up Host "project-assets.web.yourdomain.com"
    }

    header Cache-Control "public, max-age=31536000, immutable"
    header Access-Control-Allow-Origin "*"
}

7. Troubleshooting Quick Checks

  • Validate bucket host header mapping in Caddy and Garage website configuration.
  • Verify key permissions when uploads fail (--read and --write grants).
  • Confirm expected cache behavior before assuming stale object data is a backend bug.

8. Future Improvement Path

For dynamic image optimization (resizing, crop, format conversion), place an image processing layer between Caddy and Garage in a future iteration.