BlogTutorialsHost Your Photography Portfolio on S3: A Complete Guide for Photographers (2025)

Host Your Photography Portfolio on S3: A Complete Guide for Photographers (2025)

Adrian Silaghi
Adrian Silaghi
January 17, 2026
20 min read
6 views
#photography #portfolio #s3 #hosting #client galleries #hugo #static site #smugmug alternative #pixieset alternative #web hosting
Host Your Photography Portfolio on S3: A Complete Guide for Photographers (2025)

SmugMug costs €14/month. Squarespace costs €16/month. Pixieset charges per client gallery. What if you could host unlimited portfolio images and client galleries for €3.99/month?

This guide shows you how to use S3-compatible storage to host your photography portfolio, client proofing galleries, and even sell prints—all while keeping full ownership of your work.

Why Photographers Should Consider S3 Hosting

Feature SmugMug Squarespace S3 + Static Site
Monthly Cost €14-42 €16-42 €3.99
Storage Limit Unlimited Limited video Pay for what you use
Data Ownership Platform-dependent Platform-dependent 100% yours
Custom Domain Yes Yes Yes
Performance Good Good Excellent (CDN optional)
SEO Control Limited Good Full control
Print Sales Built-in Via integrations Via integrations
Client Galleries Built-in Limited Custom solutions

S3 Hosting Options for Photographers

Option 1: Direct Image Hosting (Simplest)

Use S3 purely for image storage, embed in any website builder:

  • Host images on S3 with public read access
  • Embed image URLs in WordPress, Webflow, Wix, etc.
  • Keep your existing website, just save on image hosting costs

Option 2: Static Portfolio Site (Best Value)

Host your entire portfolio as a static website on S3:

  • Use a static site generator (Hugo, Jekyll, Astro)
  • Lightning-fast performance (no server processing)
  • Complete control over design and SEO
  • Cost: Just storage fees (typically under €5/month)

Option 3: Client Gallery Platform (Professional)

Build password-protected client galleries with download functionality:

  • Custom gallery generator or SaaS tool
  • Password protection per client
  • Download tracking and favoriting
  • Optional print integration

Part 1: Direct Image Hosting on S3

The quickest way to use S3: host your images and embed them anywhere.

Step 1: Create Your Bucket

  1. Sign up for DanubeData
  2. Create a storage bucket named portfolio-images
  3. Note your bucket endpoint: https://s3.danubedata.com/portfolio-images

Step 2: Upload and Organize Images

# Using rclone
rclone sync ~/Portfolio/web-images danubedata:portfolio-images/gallery 
    --progress

# Folder structure
portfolio-images/
├── gallery/
│   ├── weddings/
│   │   ├── johnson-2024-hero.jpg
│   │   └── smith-2024-ceremony.jpg
│   ├── portraits/
│   │   └── corporate-headshots-2024.jpg
│   └── landscapes/
│       └── iceland-aurora.jpg
└── clients/              # Private galleries (see Part 3)
    └── johnson-wedding/

Step 3: Generate Web-Optimized Images

Before uploading, optimize your images for web:

#!/bin/bash
# optimize-for-web.sh - Creates web-optimized versions of your portfolio images

SOURCE="$HOME/Portfolio/high-res"
DEST="$HOME/Portfolio/web-images"

mkdir -p "$DEST"

# Process each image
find "$SOURCE" -type f ( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" ) | while read img; do
    filename=$(basename "$img")
    relative_path=$(dirname "${img#$SOURCE/}")

    mkdir -p "$DEST/$relative_path"

    # Create web-optimized version (2048px long edge, 85% quality)
    convert "$img" 
        -resize "2048x2048>" 
        -quality 85 
        -strip 
        "$DEST/$relative_path/$filename"

    echo "Processed: $filename"
done

echo "Web optimization complete!"
echo "Now upload with: rclone sync ~/Portfolio/web-images danubedata:portfolio-images/gallery"

Step 4: Make Images Public

Set a bucket policy to allow public read access:

# bucket-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicRead",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::portfolio-images/gallery/*"
    }
  ]
}

# Apply policy via AWS CLI or dashboard
aws s3api put-bucket-policy 
    --bucket portfolio-images 
    --policy file://bucket-policy.json 
    --endpoint-url https://s3.danubedata.com

Step 5: Embed in Your Website

Now use the image URLs anywhere:

<!-- In any HTML -->
<img src="https://s3.danubedata.com/portfolio-images/gallery/weddings/johnson-2024-hero.jpg"
     alt="Johnson Wedding - First Dance"
     loading="lazy">

<!-- In WordPress -->
[image src="https://s3.danubedata.com/portfolio-images/gallery/weddings/johnson-2024-hero.jpg"]

<!-- In Markdown -->
![Johnson Wedding](https://s3.danubedata.com/portfolio-images/gallery/weddings/johnson-2024-hero.jpg)

Part 2: Static Portfolio Website with Hugo

Build a blazing-fast portfolio site using Hugo (free, open-source static site generator).

Why Hugo for Photographers?

  • Fast: Generates thousands of pages in seconds
  • Free: No monthly fees for the generator itself
  • Beautiful themes: Photography-specific themes available
  • Image processing: Built-in resizing, optimization

Step 1: Install Hugo

# macOS
brew install hugo

# Windows
choco install hugo-extended

# Linux
snap install hugo --channel=extended

# Verify
hugo version

Step 2: Create Your Portfolio Site

# Create new site
hugo new site my-portfolio
cd my-portfolio

# Add a photography theme (example: Gallery)
git init
git submodule add https://github.com/nicokaiser/hugo-theme-gallery.git themes/gallery

# Or try other photography themes:
# - Starter: github.com/1000ch/hugo-theme-starter
# - Lightbi: github.com/binokochumolvarghese/flavor-starter

Step 3: Configure Your Site

# hugo.toml
baseURL = "https://yourname.photography"
title = "Your Name Photography"
theme = "gallery"

[params]
  description = "Professional Wedding & Portrait Photography"
  author = "Your Name"
  contact = "hello@yourname.photography"
  instagram = "yourname.photo"

  # Image settings
  [params.images]
    thumbnail_width = 400
    full_width = 2048

[menu]
  [[menu.main]]
    name = "Portfolio"
    url = "/portfolio/"
    weight = 1
  [[menu.main]]
    name = "About"
    url = "/about/"
    weight = 2
  [[menu.main]]
    name = "Contact"
    url = "/contact/"
    weight = 3

Step 4: Add Your Portfolio Images

# Create portfolio structure
mkdir -p content/portfolio/weddings
mkdir -p content/portfolio/portraits
mkdir -p static/images

# Create a gallery page
cat > content/portfolio/weddings/_index.md << 'EOF'
---
title: "Wedding Photography"
description: "Capturing your most precious moments"
date: 2025-01-01
---
EOF

# Add individual photo pages (optional) or use bulk image display
# Many themes auto-generate galleries from images in the folder

Step 5: Build and Deploy

# Preview locally
hugo server -D

# Build for production
hugo --minify

# Output is in /public folder
# Upload to S3
rclone sync public/ danubedata:portfolio-website/ --progress

Step 6: Enable Static Website Hosting

Configure your S3 bucket for website hosting:

# website-config.json
{
    "IndexDocument": {
        "Suffix": "index.html"
    },
    "ErrorDocument": {
        "Key": "404.html"
    }
}

# Apply configuration
aws s3api put-bucket-website 
    --bucket portfolio-website 
    --website-configuration file://website-config.json 
    --endpoint-url https://s3.danubedata.com

Step 7: Connect Your Custom Domain

  1. Add a CNAME record pointing to your S3 bucket:
    portfolio.yourname.com → portfolio-website.s3.danubedata.com
  2. For apex domains (yourname.com), use DNS provider's ALIAS/ANAME record
  3. Add SSL certificate (DanubeData provides free certificates)

Part 3: Client Galleries with Password Protection

Create professional client galleries with download functionality.

Option A: Static Gallery Generator

Use a simple script to generate password-protected galleries:

#!/bin/bash
# create-client-gallery.sh

CLIENT_NAME="$1"
PHOTOS_DIR="$2"
PASSWORD="$3"

if [ -z "$CLIENT_NAME" ] || [ -z "$PHOTOS_DIR" ]; then
    echo "Usage: ./create-client-gallery.sh "Johnson Wedding" /path/to/photos [password]"
    exit 1
fi

# Create slug from client name
SLUG=$(echo "$CLIENT_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
OUTPUT_DIR="gallery-output/$SLUG"

mkdir -p "$OUTPUT_DIR/images"

# Process and copy images
echo "Processing images..."
for img in "$PHOTOS_DIR"/*.{jpg,jpeg,JPG,JPEG}; do
    [ -f "$img" ] || continue
    filename=$(basename "$img")

    # Create web version
    convert "$img" -resize "2048x2048>" -quality 85 "$OUTPUT_DIR/images/$filename"

    # Create thumbnail
    convert "$img" -resize "400x400^" -gravity center -extent 400x400 
        "$OUTPUT_DIR/images/thumb_$filename"
done

# Generate HTML gallery
cat > "$OUTPUT_DIR/index.html" << EOF



    
    
    $CLIENT_NAME - Photo Gallery
    
    
    


    

$CLIENT_NAME

Click any image to view full size. Right-click to download.

EOF # Create download zip echo "Creating download archive..." cd "$OUTPUT_DIR/images" zip -r ../download-all.zip *.{jpg,jpeg,JPG,JPEG} 2>/dev/null cd - > /dev/null # Upload to S3 echo "Uploading to S3..." rclone sync "$OUTPUT_DIR" "danubedata:client-galleries/$SLUG" --progress echo "" echo "Gallery created!" echo "URL: https://s3.danubedata.com/client-galleries/$SLUG/index.html" # If password provided, show note about adding authentication if [ -n "$PASSWORD" ]; then echo "" echo "For password protection, consider:" echo "1. Use Cloudflare Access (free for small teams)" echo "2. Add S3 bucket policy with presigned URLs" echo "3. Use a simple serverless function for auth" fi

Option B: Use Existing Gallery Tools

Several tools can generate galleries that work with S3:

  • Thumbsup: npm install -g thumbsup - Node.js gallery generator
  • Sigal: pip install sigal - Python gallery generator
  • PhotoPrism: Self-hosted photo management (more complex)

Example with Thumbsup

# Install
npm install -g thumbsup

# Generate gallery
thumbsup 
    --input /path/to/client-photos 
    --output ./gallery-output 
    --title "Johnson Wedding 2025" 
    --thumb-size 300 
    --large-size 2048

# Upload to S3
rclone sync ./gallery-output danubedata:client-galleries/johnson-2025 --progress

Password Protection Options

Option 1: Presigned URLs (Simple)

Generate time-limited URLs for each client:

#!/bin/bash
# generate-client-link.sh

CLIENT="johnson-2025"
EXPIRY="7d"  # Link valid for 7 days

# Generate presigned URL for entire gallery
rclone link "danubedata:client-galleries/$CLIENT/index.html" --expire $EXPIRY

# Output: https://...presigned-url...
# Share this URL with your client

Option 2: S3 Bucket Policy with Referer Check

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "RestrictToClientPortal",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::client-galleries/*",
            "Condition": {
                "StringLike": {
                    "aws:Referer": [
                        "https://yourname.photography/*",
                        "https://portal.yourname.photography/*"
                    ]
                }
            }
        }
    ]
}

Option 3: Cloudflare Access (Recommended)

Free password protection for up to 50 users:

  1. Put your S3 bucket behind Cloudflare
  2. Enable Cloudflare Access
  3. Create an Access Policy requiring email/password
  4. Send one-time login links to clients

Part 4: Proofing and Selection Workflow

Let clients select their favorite images for albums or prints.

Simple Favoriting with Local Storage

Add to your gallery HTML:

<script>
// Simple favoriting system using localStorage
let favorites = JSON.parse(localStorage.getItem('favorites') || '[]');

function toggleFavorite(imageId) {
    const index = favorites.indexOf(imageId);
    if (index > -1) {
        favorites.splice(index, 1);
    } else {
        favorites.push(imageId);
    }
    localStorage.setItem('favorites', JSON.stringify(favorites));
    updateUI();
}

function exportFavorites() {
    const list = favorites.join('
');
    // Option 1: Copy to clipboard
    navigator.clipboard.writeText(list);
    alert('Favorites copied! Please paste and send to your photographer.');

    // Option 2: Email
    // window.location.href = `mailto:hello@photographer.com?subject=My Selections&body=${encodeURIComponent(list)}`;
}

function updateUI() {
    document.querySelectorAll('.gallery-item').forEach(item => {
        const id = item.dataset.imageId;
        item.classList.toggle('favorited', favorites.includes(id));
    });
    document.getElementById('favorite-count').textContent = favorites.length;
}

// Initialize on page load
document.addEventListener('DOMContentLoaded', updateUI);
</script>

<style>
.gallery-item { position: relative; cursor: pointer; }
.gallery-item.favorited::after {
    content: '❤️';
    position: absolute;
    top: 10px;
    right: 10px;
    font-size: 24px;
}
.favorite-btn { position: absolute; bottom: 10px; right: 10px; }
</style>

Advanced: Selection with Download Limits

For album selections where clients can only choose X images:

<script>
const MAX_SELECTIONS = 50;  // Album includes 50 images
let selections = JSON.parse(localStorage.getItem('selections') || '[]');

function toggleSelection(imageId) {
    const index = selections.indexOf(imageId);

    if (index > -1) {
        // Already selected - remove
        selections.splice(index, 1);
    } else if (selections.length < MAX_SELECTIONS) {
        // Add if under limit
        selections.push(imageId);
    } else {
        alert(`You can only select ${MAX_SELECTIONS} images. Remove one first.`);
        return;
    }

    localStorage.setItem('selections', JSON.stringify(selections));
    updateSelectionUI();
}

function updateSelectionUI() {
    document.getElementById('selection-count').textContent =
        `${selections.length} / ${MAX_SELECTIONS} selected`;

    document.querySelectorAll('.gallery-item').forEach(item => {
        const id = item.dataset.imageId;
        item.classList.toggle('selected', selections.includes(id));
    });
}

function submitSelections() {
    if (selections.length === 0) {
        alert('Please select at least one image.');
        return;
    }

    const data = {
        client: 'Johnson Wedding',
        date: new Date().toISOString(),
        selections: selections
    };

    // Send to photographer via email or form
    const body = `Client selections for Johnson Wedding:

${selections.join('
')}

Total: ${selections.length} images`;
    window.location.href = `mailto:hello@photographer.com?subject=Album Selections - Johnson Wedding&body=${encodeURIComponent(body)}`;
}
</script>

Cost Comparison: Real Numbers

Let's compare actual costs for a wedding photographer with 50 clients/year:

Scenario SmugMug Pro Pixieset S3 Solution
Monthly Base Cost €42 €25 €3.99
Storage Included Unlimited 1TB 1TB
500GB Additional €0 +€15/mo +€2/mo
50 Client Galleries Included Included Included
Custom Domain Included Included ~€10/year
Annual Cost €504 €480 €72
5-Year Savings Baseline €120 €2,160

What You Give Up

To be fair, SmugMug and Pixieset include features you'd need to build or integrate:

  • Print fulfillment: Integrate with WHCC, Printful, or other labs
  • Payment processing: Add Stripe or PayPal
  • Client CRM: Use separate tool or spreadsheet
  • Mobile apps: Clients use responsive web instead

The €2,000+ savings over 5 years can fund these integrations or simply go to your bottom line.

SEO for Photography Portfolios

With S3 hosting, you have complete control over SEO.

Essential SEO Elements

<!-- In your HTML head -->
<title>Wedding Photography Portland | Your Name Photography</title>
<meta name="description" content="Award-winning Portland wedding photographer capturing authentic moments. View portfolio and book your wedding photography.">

<!-- Schema markup for photographers -->
<script type="application/ld+json">
{
    "@context": "https://schema.org",
    "@type": "LocalBusiness",
    "name": "Your Name Photography",
    "@id": "https://yourname.photography",
    "image": "https://yourname.photography/images/logo.jpg",
    "priceRange": "€€",
    "address": {
        "@type": "PostalAddress",
        "addressLocality": "Portland",
        "addressRegion": "OR",
        "addressCountry": "US"
    },
    "geo": {
        "@type": "GeoCoordinates",
        "latitude": 45.5152,
        "longitude": -122.6784
    },
    "telephone": "+1-555-123-4567",
    "sameAs": [
        "https://instagram.com/yourname.photo",
        "https://facebook.com/yournamephotography"
    ]
}
</script>

<!-- Open Graph for social sharing -->
<meta property="og:title" content="Wedding Photography Portfolio | Your Name">
<meta property="og:description" content="View my wedding photography portfolio">
<meta property="og:image" content="https://yourname.photography/images/hero-wedding.jpg">
<meta property="og:url" content="https://yourname.photography/portfolio/weddings">

Image SEO

<!-- Proper image markup -->
<figure>
    <img
        src="https://s3.danubedata.com/portfolio/weddings/portland-wedding-photographer-ceremony.jpg"
        alt="Bride and groom exchanging vows at Portland Japanese Garden wedding ceremony"
        loading="lazy"
        width="2048"
        height="1365">
    <figcaption>Wedding ceremony at Portland Japanese Garden</figcaption>
</figure>

Performance Optimization

Responsive Images

<!-- Serve different sizes based on screen -->
<picture>
    <source
        media="(max-width: 768px)"
        srcset="https://s3.../image-800.jpg">
    <source
        media="(max-width: 1200px)"
        srcset="https://s3.../image-1200.jpg">
    <img
        src="https://s3.../image-2048.jpg"
        alt="Description"
        loading="lazy">
</picture>

Image Processing Script

#!/bin/bash
# generate-responsive-images.sh

SOURCE_DIR="$1"
OUTPUT_DIR="${2:-./responsive}"

SIZES=(400 800 1200 2048)

mkdir -p "$OUTPUT_DIR"

for img in "$SOURCE_DIR"/*.{jpg,jpeg}; do
    [ -f "$img" ] || continue
    base=$(basename "$img" | sed 's/.[^.]*$//')
    ext="${img##*.}"

    for size in "${SIZES[@]}"; do
        convert "$img" 
            -resize "${size}x${size}>" 
            -quality 85 
            -strip 
            "$OUTPUT_DIR/${base}-${size}.${ext}"
    done

    # Create WebP versions for modern browsers
    for size in "${SIZES[@]}"; do
        convert "$img" 
            -resize "${size}x${size}>" 
            -quality 80 
            "$OUTPUT_DIR/${base}-${size}.webp"
    done

    echo "Processed: $base"
done

Get Started Today

Ready to take control of your photography portfolio and save thousands?

  1. Create a DanubeData account
  2. Create a storage bucket for your portfolio
  3. Choose your approach:
    • Quick start: Direct image hosting in your existing site
    • Full control: Static site with Hugo/Jekyll
    • Professional: Client galleries with download functionality
  4. Upload your portfolio using rclone
  5. Share your new, blazing-fast portfolio with the world

DanubeData S3 Storage for Portfolios:

  • €3.99/month includes 1TB storage + 1TB traffic
  • More than enough for most portfolio + client gallery needs
  • No egress fees for normal traffic
  • GDPR compliant (German data centers)
  • 99.9% uptime SLA

👉 Create Your Portfolio Bucket

Need help building your photography portfolio or client gallery system? Contact our team—we love helping photographers showcase their work.

Share this article

Ready to Get Started?

Deploy your infrastructure in minutes with DanubeData's managed services.