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
- Sign up for DanubeData
- Create a storage bucket named
portfolio-images - 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 -->

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
- Add a CNAME record pointing to your S3 bucket:
portfolio.yourname.com → portfolio-website.s3.danubedata.com - For apex domains (yourname.com), use DNS provider's ALIAS/ANAME record
- 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:
- Put your S3 bucket behind Cloudflare
- Enable Cloudflare Access
- Create an Access Policy requiring email/password
- 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?
- Create a DanubeData account
- Create a storage bucket for your portfolio
- 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
- Upload your portfolio using rclone
- 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.