Part 2: Deploying Ente Photos with Backblaze B2
This follows the previous post on setting up Coolify and Vaultwarden. This time, the focus shifts to building a self-hosted photo backup system with end-to-end encryption.
What is Ente Photos?
Ente is a fully open source, end-to-end encrypted alternative to Google Photos and iCloud Photos, with cross-platform support across iOS, Android, web, Mac, Windows, and Linux. A standout feature is on-device machine learning for face recognition — processing happens locally on the phone rather than server-side, preserving privacy while still enabling smart search.
The system is made up of two containerized components: Museum (the backend API) and Web (the frontend). Rather than storing photos directly on the server's disk, this setup uses S3-compatible object storage, separating compute from storage.
What is S3-Compatible Storage?
S3 started as Amazon's object storage service but evolved into an industry standard. Many providers now implement the same API, making them interchangeable. I went with Backblaze B2: $0.006/GB/month storage, one of the cheapest options available, fully S3 compatible, and simple to configure.
Setting Up Backblaze B2
Step 1 — Create an Account
Register at backblaze.com and enable B2 Cloud Storage separately from their backup product.
Step 2 — Create a Bucket
Go to B2 Cloud Storage → Buckets → Create Bucket. Configure it as Private, with Default Encryption enabled.
Step 3 — Generate an Application Key
Create an application key with Read and Write access to the bucket. Save the Key ID and Application Key immediately — Backblaze only shows the Application Key once.
Step 4 — Configure CORS
CORS configuration lets mobile apps upload directly to Backblaze. Install the Backblaze CLI:
pip3 install b2 --break-system-packages
b2 authorize-account [YOUR_KEY_ID] [YOUR_APP_KEY]
Save this CORS configuration as cors.json:
[{
"corsRuleName": "entephotos",
"allowedOrigins": ["*"],
"allowedHeaders": ["*"],
"allowedOperations": [
"b2_download_file_by_id",
"b2_download_file_by_name",
"b2_upload_file",
"b2_upload_part",
"s3_get",
"s3_post",
"s3_put",
"s3_head"
],
"exposeHeaders": ["X-Amz-Request-Id", "X-Amz-Id-2", "ETag"],
"maxAgeSeconds": 3600
}]
Apply it with:
b2 bucket update --cors-rules "$(cat cors.json)" [BUCKET_NAME] allPrivate
Deploying Ente Photos via Coolify
Step 1 — Generate Encryption Keys
Run this on the server:
openssl rand -base64 32 && openssl rand -base64 64 && openssl rand -base64 32
The output maps to three environment variables:
- First line →
ENTE_KEY_ENCRYPTION - Second line →
ENTE_KEY_HASH - Third line →
ENTE_JWT_SECRET
Step 2 — Create the Service
In Coolify, choose New Resource → Docker Compose Empty and use this compose configuration:
services:
museum:
image: ghcr.io/ente-io/server:latest
ports:
- "8080"
dns:
- 8.8.8.8
- 1.1.1.1
environment:
- ENTE_DB_HOST=postgres
- ENTE_DB_PORT=5432
- ENTE_DB_NAME=ente_db
- ENTE_DB_USER=${POSTGRES_USER:-ente}
- ENTE_DB_PASSWORD=${POSTGRES_PASSWORD:-ente_pass}
- ENTE_HTTP_USE_TLS=false
- ENTE_S3_ARE_LOCAL_BUCKETS=false
- ENTE_S3_USE_PATH_STYLE_URLS=true
- ENTE_S3_B2_EU_CEN_KEY=[YOUR_KEY_ID]
- ENTE_S3_B2_EU_CEN_SECRET=[YOUR_APP_KEY]
- ENTE_S3_B2_EU_CEN_ENDPOINT=https://s3.eu-central-003.backblazeb2.com
- ENTE_S3_B2_EU_CEN_REGION=[BUCKET_REGION]
- ENTE_S3_B2_EU_CEN_BUCKET=[BUCKET_NAME]
- ENTE_KEY_ENCRYPTION=[ENTE_KEY_ENCRYPTION]
- ENTE_KEY_HASH=[ENTE_KEY_HASH]
- ENTE_JWT_SECRET=[ENTE_JWT_SECRET]
- ENTE_INTERNAL_DISABLE_REGISTRATION=false
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--spider", "http://127.0.0.1:8080/ping"]
interval: 30s
timeout: 10s
retries: 3
volumes:
- museum-data:/data
web:
image: ghcr.io/ente-io/web:latest
ports:
- "3000"
expose:
- "3000"
environment:
- ENTE_API_ORIGIN=https://museum.yourdomain.com
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_USER=${POSTGRES_USER:-ente}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-ente_pass}
- POSTGRES_DB=ente_db
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ente -d ente_db"]
interval: 10s
timeout: 5s
retries: 5
Step 3 — Deploy
Save and Deploy. Coolify pulls the containers and orchestrates them.
Step 4 — Set Domains
Configure container-specific domains under Settings:
- Museum:
https://museum.yourdomain.com - Web:
https://photos.yourdomain.com
Coolify provisions SSL automatically.
Step 5 — Configure DNS
Set up a wildcard DNS record:
*.yourdomain.com → YOUR_SERVER_IP
Step 6 — Create Accounts
Visit https://photos.yourdomain.com and register. Retrieve OTP verification codes from the Museum container logs:
docker logs $(docker ps | grep ente-io/server | awk '{print $1}') 2>&1 | grep "Verification code"
Step 7 — Set Storage Quota
Connect to the Postgres container:
docker exec -it $(docker ps | grep $(docker ps --format "{{.Names}}" | grep museum | sed 's/museum-//') | grep postgre | awk '{print $1}') psql -U ente -d ente_db
Update the quota (example: 100GB in bytes):
UPDATE subscriptions SET storage = 107374182400;
Step 8 — Disable Registration
Change the compose environment variable:
- ENTE_INTERNAL_DISABLE_REGISTRATION=true
Save and restart to prevent unauthorized account creation.
Connecting Your Phone
Install the official Ente Photos app. Tap the Ente logo seven times on the onboarding screen to reveal a custom server URL field, then enter:
https://museum.yourdomain.com
The Result
- Automatic photo backup from mobile devices to Backblaze B2
- End-to-end encryption preventing server-side access
- Web interface at
https://photos.yourdomain.com - Operating cost of roughly $0.006/GB per month
- Complete data ownership and jurisdictional control
What's Next
The series continues with file storage, followed by disaster recovery — building out a complete private cloud on a single server for minimal monthly cost.
Pricing reflects accuracy at time of publication and is subject to change. Self-hosting requires ongoing responsibility for updates, security, and backups. I have no affiliation with any of the tools mentioned.