1. Overview
Ente is an end-to-end encrypted alternative to Google Photos. While YunoHost does not have a native Ente app, it can be hosted efficiently using Docker Compose.
Key Challenges Solved in this Guide:
- Configuring SMTP for OTP-based login (Hostinger).
- Resolving "Incorrect Code" errors due to server time drift.
- Overcoming the default 10GB storage quota.
- Managing MinIO (S3) bucket conflicts during reinstallations.
2. Preparation
Directory Setup
Create a dedicated directory for the project to ensure data persistence outside the containers.
mkdir -p /home/user/ente/data
cd /home/user/ente
Generating Security Keys
Ente requires three specific cryptographic keys. Run these commands using openssl and save the outputs. You will need them for the configuration file.
# 1. JWT Secret (for authentication)
openssl rand -base64 32
# 2. Key Encryption Key (for admin keys)
openssl rand -base64 32
# 3. Hash Key (for passwords)
openssl rand -base64 32
3. Configuration
A. The Server Config (museum.yaml)
Create museum.yaml. This file defines the database connection, storage backend, and email settings.
- Critical Note: We use Port 465 (SSL) for Hostinger SMTP to ensure reliable email delivery.
- Replace: The
jwtandkeysections with the strings you generated above.
http:
port: 8080
use_https: false # Nginx (YunoHost) handles SSL
db:
host: postgres
port: 5432
name: ente_db
user: pguser
password: pgpass
sslmode: disable
# SMTP Settings (Hostinger Example)
smtp:
host: "smtp.hostinger.com"
port: 465
username: "your@email.here"
password: "YOUR_EMAIL_PASSWORD"
encryption: "ssl"
email: "your@email.here"
sender_name: "Ente Photos"
# Internal MinIO (S3) configuration
s3:
are_local_buckets: true
b2-eu-cen:
key: minioadmin
secret: minioadmin
endpoint: http://minio:9000
region: eu-central-1
bucket: ente-photos
wasabi-eu-central-2-v3:
key: minioadmin
secret: minioadmin
endpoint: http://minio:9000
region: eu-central-1
bucket: ente-videos
# Security Keys (Replace with your OpenSSL outputs)
jwt:
secret: "REPLACE_WITH_YOUR_JWT_SECRET"
key:
encryption: "REPLACE_WITH_YOUR_ENCRYPTION_KEY"
hash: "REPLACE_WITH_YOUR_HASH_KEY"
internal:
admins: []
B. The Docker Map (compose.yaml)
Create compose.yaml.
- Critical Fix: We mount
/etc/localtimeand setTZ=Asia/Kuala_Lumpur. Without this, the server defaults to UTC or an incorrect timezone, causing OTP codes to fail immediately.
services:
# The Ente Server (API)
museum:
image: ghcr.io/ente-io/server:latest
container_name: ente_museum
restart: unless-stopped
ports:
- "54752:8080" # Mapped to specific host port
volumes:
- ./museum.yaml:/museum.yaml:ro
- ./data/logs:/var/logs
- ./data/museum-data:/var/lib/museum
# SYNC HOST TIME TO CONTAINER
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
depends_on:
postgres:
condition: service_healthy
minio:
condition: service_started
environment:
- ENTE_DB_PASSWORD=pgpass
- TZ=Asia/Kuala_Lumpur # Forces correct OTP generation time
# Database
postgres:
image: postgres:16
container_name: ente_postgres
restart: unless-stopped
environment:
POSTGRES_USER: pguser
POSTGRES_PASSWORD: pgpass
POSTGRES_DB: ente_db
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pguser -d ente_db"]
interval: 5s
timeout: 5s
retries: 5
# Object Storage (MinIO)
minio:
image: minio/minio
container_name: ente_minio
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- ./data/minio:/data
# Automatic Bucket Provisioner
minio-provision:
image: minio/mc
depends_on:
- minio
entrypoint: >
/bin/sh -c "
until mc alias set local http://minio:9000 minioadmin minioadmin; do echo 'Waiting for MinIO...'; sleep 1; done;
mc mb local/ente-photos;
mc mb local/ente-videos;
exit 0;
"
4. Deployment & Network
-
Launch the stack:
bash docker compose up -d -
YunoHost Configuration:
- Go to YunoHost Admin > Applications > Install Custom Webapp.
- Domain:
ente.tcp-ip.my - Path:
/ - Destination:
http://127.0.0.1:54752
5. The "Incorrect Code" Fix (Crucial)
If you receive an "Incorrect Code" error during signup, it is likely a time synchronisation issue.
- Check Host Time: Ensure the physical server has NTP enabled and the correct timezone.
bash sudo timedatectl set-timezone Asia/Kuala_Lumpur sudo timedatectl set-ntp true - The "One Click" Rule: When signing up on the mobile app, tap "Send" once. Do not tap "Resend" immediately. Wait 2–3 minutes for the email. Clicking twice invalidates the first code.
6. Post-Install: Unlocking Storage Quota
By default, self-hosted users are limited to 10GB. To increase this (e.g., to 32GB + 10GB Base), you must modify the database.
-
Enter the Database Container:
bash docker exec -it ente_postgres psql -U pguser -d ente_db -
Insert Storage Bonus:
Run this SQL command to add 32GB (in bytes) and set the expiration to the year 2100 (to fix the "1970" display bug).sql INSERT INTO storage_bonus (bonus_id, user_id, storage, type, valid_till) VALUES ( 'manual-32gb', (SELECT user_id FROM users LIMIT 1), 34359738368, 'ADD_ON_SUPPORT', 4102444800000000 );(Note:
34359738368bytes = 32GB.4102444800000000microseconds = Year 2100). -
Verify: Refresh the mobile app. You should see "42 GB Total".
7. Troubleshooting: The "Nuclear Wipe"
If you need to restart the installation (e.g., after changing keys or fixing a corrupted database), you must wipe both the database and the MinIO storage. If you only wipe the DB, the new installation will crash (Error 500) because the storage buckets already exist.
The Clean Reset Command:
docker compose down
sudo rm -rf ./data # Deletes DB, MinIO, and Logs
docker compose up -d