Linux things 🐧

un blog sur les technologies des logiciels libres et autres digressions


Ente : une application de stockage de photos

auto-hébergement d'une application libre de gestion et de stockage photos

Tue, 04 Feb 2025 15:15:15 +0200
# ente   # docker   # nomad   # auto-hebergement   # garage  

Introduction

J’ai quittĂ© Google photos il y a un moment et depuis je cherchais une alternative lbre Ă©ventuellement en auto-hĂ©bergement (Ă  savoir qu’aprĂšs un export pour tout supprimer chez eux il vaut mieux passer par l’application Android mais ca ne se fait pas en 1 clic đŸ€Ź)
Ce n’est pas les solutions qui manquent mais j’ai des contraintes de ressources CPU et mĂ©moire (NUC core I3 16Go de RAM). J’auto-hĂ©berge d’autres services sur ce NUC aussi il est essentiel que le service soit lĂ©ger et rapide, je privilĂ©gie donc le langage Go (ou Rust) sauf quand il n’y a pas d’autre choix comme avec Peertube.

J’ai dĂ©couvert il y a quelques temps le service de stockage photos Ente.io que j’ai testĂ© avec l’application Android et desktop pour Linux (un prĂ©requis) et c’est vraiment un excellent service. L’application utilisateur est trĂšs bien conçue sur mobile et sur le bureau. Vous pouvez partager des albums avec vos proches et synchroniser un rĂ©pertoire automatiquement, voir toutes les fonctionnalitĂ©s ici : Features.
L’autre gros avantage est que les photos sont chiffrĂ©es cĂŽtĂ© client avant d’arriver sur leur stockage cloud, voir ici l’architecture technique. De plus elles sont rĂ©pliquĂ©es dans 3 datacenters en Europe (dont un chez Scaleway) sur des serveurs S3 compatible.

Le dernier point est que c’est un logiciel libre (serveur et clients) depuis mars 2024 et dont la sĂ©curitĂ© a Ă©tĂ© auditĂ©. La touche finale qui m’a convaincu de le mettre en place @home est que le serveur est en Go et comme il utilise S3 pour le stockage je vais pouvoir utiliser mes serveurs Garage 🙌

Si vous souhaitez vous auto-hĂ©berger vous pouvez vous aider Ă  choisir grĂące Ă  ce comparatif des diffĂ©rentes solutions opensource en fonction de vos critĂšres : 📾 Free and OpenSource Photo Libraries.

Pour toutes ses qualitĂ©s je conseille fortement ce service SaaS si vous n’avez aucune connaissance technique si vous tenez Ă  vos photos (mĂȘme question que pour vos mots de passe : quelle valeur vous leur donnez ? đŸ€”). Les tarifs ne me paraissent pas dĂ©connant (Plans) 200Go Ă  5€/mois devrait suffir pour la plupart des gens.

Architecture

Avant de plonger dans la configuration regardons ce shĂ©ma de l’architecture du serveur

museum

Lors de mes premiers tests je ne comprenais pas pourquoi l’application Android ne fonctionnait pas en 5G (pas de synchro) mais fonctionnait en WIFI. Or comme on le voit il y a un lien en pointillĂ© des applications vers l’object storage S3. Et oui le serveur S3 doit ĂȘtre exposĂ© sur Internet, les applications s’y connectant directement dessus en plus du serveur API Museum.
Ce n’est pas forcĂ©ment choquant, la plupart des fournisseurs S3 comme Scaleway ou Amazon exposent leur service sur Internet mais pour ma part je ne souhaite pas le faire pour mes serveurs Garage. Voici 3 solutions pour utiliser Ente auto-hĂ©bergĂ© :

  1. Exposer un service S3 auto-hébergé sur Internet via un reverse proxy vers une instance Minio ou Garage.
  2. Utiliser un serveur S3 chez un fournisseur et configurer le serveur Ente auto-hébergé vers ce service.
  3. Exposer son serveur S3 sur une IP Tailscale et installer Tailscale sur le(s) smartphone utisant Ente.

Dans tous les cas vous devez utiliser soit 1 serveur S3 soit 3 si vous souhaitez une réplication, car elle ne fonctionne pas avec 2.

Je pensais utiliser la 3Ăšme solution mais je souhaite partager des albums Ă  des proches et d’autres en public. Évidemment dans ce cas il faut un bucket public.
J’ai choisi la solution 2 mais en ajoutant les 2 serveurs Garage chez moi, le serveur primaire Ă©tant chez Scaleway ce qui me permettra de partager des albums tout en ayant une rĂ©plication locale.

Configuration

Elle va ĂȘtre un peu plus compliquĂ©e que pour d’autres services car il a Ă©tĂ© dĂ©veloppĂ© pour ĂȘtre un SaaS, il n’y a donc pas encore d’interface web d’admin. Si vous ne souhaitez pas utiliser Nomad, Ente propose une documentation pour utiliser uniquement Docker avec docker compose : self-hostring.

Allons y et on commence par S3. La premiĂšre Ă©tape est de configurer un bucket dans Garage.

Garage

Cette étape est à sauter si vous utilisez un seul bucket S3 chez un opérateur.

Sur le NUC voici les quelques commandes Ă  lancer

garage bucket create ente
garage key create ente-app-key
garage bucket allow --read --write --owner ente --key ente-app-key
garage bucket info ente

Il faut bien sûr bien sauvegarder les token key ID et secret key générées.

Dernier point il faut dĂ©finir des rĂšgles CORS sur ce bucket . Pour cela il faut installer la commande aws sur son PC (sudo pacman -S aws-cli sur Manjaro) et exporter des variables d’environnement avec les tokens prĂ©cĂ©demment crĂ©es

AWS_ACCESS_KEY_ID=TOKEN_ID
AWS_SECRET_ACCESS_KEY=TOKEN_KEY
AWS_DEFAULT_REGION=garage
AWS_ENDPOINT_URL=http://IP_LOCAL_NUC:3900

Puis créer un fichier cors.json

{
    "CORSRules": [
        {
            "AllowedOrigins": ["*"],
            "AllowedHeaders": ["*"],
            "AllowedMethods": ["GET", "HEAD", "POST", "PUT", "DELETE"],
            "MaxAgeSeconds": 3000,
            "ExposeHeaders": ["Etag"]
        }
    ]
}

On peut alors lancer la commande aws pour injecter ces rĂšgles

aws s3api put-bucket-cors --bucket ente --cors-configuration file://cors.json

Ces opĂ©rations sont Ă  refaire sur le 2Ăšme serveur garage, chez moi sur un RaspberryPi 4 (voir l’article QNAP : un DAS pour S3).

Nomad

voici le HCL du serveur Ente pour Nomad. Ce serveur qui s’appelle Museum, sert d’API pour les clients desktop et mobile Ente. Il communique avec le ou les serveurs S3 (locaux ou externes) et stocke ses donnĂ©es dans une base PostgreSQL.

ente.hcl

job "ente" {
  datacenters = ["dc1"]
  type        = "service"
  group "home" {
    count = 1

    network {
      mode = "bridge"
      port "http" {
        to           = 8080 # container port the app runs on
        host_network = "tailscale"
      }
      port "postgresql" {
        to = 5432 # container port the app runs on
      }
    }

    task "ente" {
      driver = "docker"

      constraint {
        attribute = "${attr.unique.hostname}"
        value     = "nuc"
      }

      env {
        ENTE_CREDENTIALS_FILE = "/credentials.yaml"
      }

      config {
        image = "ghcr.io/ente-io/server"
        command = "/bin/sh"
        args = [
          "-c",
          "while ! nc -z 127.0.0.1 5432 </dev/null; do echo 'waiting for postgresql...'; sleep 5; done; /museum",
        ]

        volumes = [
          "/data/volumes/ente/logs:/var/logs",
          "/data/volumes/ente/data:/data",
          "/data/volumes/ente/config/museum.yaml:/museum.yaml",
          "/data/volumes/ente/config/credentials.yaml:/credentials.yaml",
          "/data/volumes/ente-replication:/replication"
        ]
        ports = [
          "http"
        ]
      }

      resources {
        cpu    = 300
        memory = 1024
      }

      service {
        name     = "ente"
        provider = "consul"
        port     = "http"
        tags = ["tricot ente.domain.tld"]

        check {
          type     = "http"
          name     = "app_health"
          path     = "/ping"
          interval = "20s"
          timeout  = "10s"
        }
      }
    }

    task "postgresql" {
      driver = "docker"

      constraint {
        attribute = "${attr.unique.hostname}"
        value     = "nuc"
      }

      env {
        POSTGRES_PASSWORD = "PASS"
        POSTGRES_DB       = "ente_db"
        POSTGRES_USER     = "ente"
      }

      config {
        image = "postgres:16-alpine"
        volumes = [
          "/data/volumes/ente/postgresql:/var/lib/postgresql/data"
        ]
        ports = [
          "postgresql"
        ]
      }

      resources {
        cpu    = 500
        memory = 1024
      }

      service {
        name     = "ente-postgresql"
        provider = "consul"
        port     = "postgresql"
      }
    }

  }
}

Ce hcl a besoin de 2 fichiers yaml, museum.yaml et credentials.yaml

/data/volumes/ente/config/museum.yaml

apps:
    # Default is https://albums.ente.io
    #
    # If you're running a self hosted instance and wish to serve public links,
    # set this to the URL where your albums web app is running.
    public-albums: https://ente-albums.domain.tld

key:
    encryption: ENCRYPTION
    hash: HASH


jwt:
    secret: SECRET


#internal:
#    admins:
#        - 1580559962386438

# Replication config
#
# If enabled, replicate each file to 2 other data centers after it gets
# successfully uploaded to the primary hot storage.
replication:
    enabled: true
    # The Cloudflare worker to use to download files from the primary hot
    # bucket. If this isn't specified, files will be downloaded directly.
    worker-url:
    # Number of go routines to spawn for replication
    # This is not related to the worker-url above.
    # Optional, default value is indicated here.
    worker-count: 6
    # Where to store temporary objects during replication v3
    # Optional, default value is indicated here.
    tmp-storage: /replication/data

Dans le fichier museum.yaml on a besoin de générer des clés, pour cela il faut cloner le projet git et lancer un programme

git clone https://github.com/ente-io/ente.git
cd ente/server
go mod init gen
go mod tidy
go run tools/gen-random-keys/main.go
key.encryption: ENCRYPTION
key.hash: HASH
jwt.secret: SECRET

le fichier des credentials

/data/volumes/ente/config/credentials.yaml

db:
    host: localhost
    port: 5432
    name: ente_db
    user: ente
    password: PASS

s3:
    are_local_buckets: false
    use_path_style_urls: true
    b2-eu-cen:
        key: TOKEN
        secret: SECRET
        endpoint: https://s3.fr-par.scw.cloud
        region: fr-par
        bucket: bucket-name
    wasabi-eu-central-2-v3:
        key: TOKEN
        secret: SECRET
        endpoint: http://IP_LOCAL:3900
        region: garage
        bucket: ente
    scw-eu-fr-v3:
        key: TOKEN
        secret: SECRET
        endpoint: http://IP2_LOCAL:3900
        region: garage
        bucket: ente

A savoir que les noms des rĂ©gions ne doivent pas ĂȘtre changĂ© et la primaire est obligatoirement b2-eu-cen
Une fois le service lancĂ©, verifiez que l’API vous rĂ©ponde pong par cette URL : https://ente.domain.tld/ping

Le serveur Museum ne propose pas d’interface web mais uniquement une API pour les diffĂ©rents clients. Il suffira de lancer un des clients mobile ou desktop pour crĂ©er son compte, le premier Ă©tant admin.
A noter que mĂȘme si n’importe qui peut utiliser votre instance pour demander la crĂ©ation d’un compte il n’aura pas accĂšs au code de validation pour finaliser le compte. Par contre il vous sera nĂ©cessaire pour valider la crĂ©ation du votre puis celui d’Ă©ventuels proches.

Par dĂ©faut les clients Ente vont vouloir crĂ©er un compte sur le SaaS Ente.io. Pour pointer sur sa propre instance il faut cliquer ou tapoter 7 fois sur l’Ă©cran de l’app ce qui va afficher un champ de saisie de l’URL. Mettez celle de votre instance dĂ©clarĂ©e dans le tag tricot (dans l’exemple ente.domain.tld) (Connecting to a custom server.)

Ensuite lancer cette commande sur le NUC pour récupérer le code généré

docker logs -f ID_CONTAINER_ENTE 2>&1 |grep Verification

Indiquer son email et son pass dans le client Ente puis coller le code qui apparait dans le log lorsqu’il est demandĂ©. Une fois le compte crĂ©Ă© il n’aura par dĂ©faut que 5Go d’espace, aussi on lance 2 requĂȘtes SQL dans PostgreSQL pour passer Ă  1To

SELECT user_id from users;

puis celle-ci en remplacant USERID par l’ID renvoyĂ© par le SELECT

INSERT INTO storage_bonus (bonus_id, user_id, storage, type, valid_till) VALUES ('self-hosted-myself', USERID, 1099511627776, 'ADD_ON_SUPPORT', 0);

Il suffit de relancer les applications clientes et si tout va bien vous devriez voir 1To d’espace, sinon se dĂ©connecter puis se reconnecter.

Vient enfin la partie la plus longue, l’import des photos (et vidĂ©os). Je l’ai fait depuis mon PC et il a tournĂ© environ plus de 1 jour pour importer 157 Go (54496 fichiers). Je vous conseille si vous avez beaucoup de photos Ă  importer de couper la mise en veille et de ne pas toucher Ă  votre PC durant l’import.

Attention

J’ai eu quelques soucis sur la rĂ©plication des buckets S3 lorsque j’ai activĂ© l’option machine learning (Apprentissage automatique) dans le client. J’ai pu corriger en supprimant les donnĂ©es ML dans le S3 primaire et dans la base de donnĂ©es PostgreSQL.

# pour S3 bucket
aws s3 rm s3://bucket-ente/XXXXXX/file-data/ --recursive

# pour postgresql
delete from file_data where data_type='mldata';

Client web

Le client web permet de partager des albums public et privĂ© par un mot de passe. Dans le fichier museum.yaml l’URL des albums est https://ente-albums.domain.tld. Les applications desktop et mobile utiliseront cette URL pour gĂ©nĂ©rer un lien.

Voici mon album public.

ente-albums.hcl

job "ente-albums" {
  datacenters = ["dc1"]
  type        = "service"
  group "home" {
    count = 1

    network {
      port "http" {
        to           = 3002  # container port the app runs on
        host_network = "tailscale"
      }
    }

    task "ente-albums" {
      driver = "docker"

      constraint {
        attribute = "${attr.unique.hostname}"
        value     = "nuc"
      }

      env {
        NEXT_PUBLIC_ENTE_ENDPOINT = "https://ente.domain.tld"
        NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT = "https://ente-albums.domain.tld"
      }

      config {
        image = "fredix/ente-albums"

        ports = [
          "http"
        ]
      }

      resources {
        cpu    = 200
        memory = 500
      }

      service {
        name     = "ente-albums"
        provider = "consul"
        port     = "http"
        tags = ["tricot ente-albums.domain.tld"]

        check {
          type     = "http"
          name     = "app_health"
          path     = "/"
          interval = "20s"
          timeout  = "10s"
        }
      }
    }


  }
}

A ce jour il n’y a pas encore d’image docker officielle pour l’interface web. J’ai construit la mienne avec ce Dockerfile en Ă©tant positionnĂ© dans le rĂ©pertoire source ente/web

Dockerfile

FROM node:22-alpine AS builder

WORKDIR /app
COPY . .

ARG NEXT_PUBLIC_ENTE_ENDPOINT=https://ente.domain.tld
ENV NEXT_PUBLIC_ENTE_ENDPOINT=${NEXT_PUBLIC_ENTE_ENDPOINT}
ARG NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=https://ente-albums.domain.tld
ENV NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=${NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT}

RUN yarn install && yarn build

FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/apps/photos/out .

RUN npm install -g serve

ENV PORT_ALBUMS=3002
EXPOSE ${PORT_ALBUMS}
CMD serve -s . -l tcp://0.0.0.0:3002

Les URLs étant en dur si vous utilisez mon image fredix/ente-albums ça ne marchera pas chez vous.

Conclusion

Ente est un excellent service de gestion et stockage chiffrĂ© de photos. Cependant la mise en place d’un auto-hĂ©bergement est peut ĂȘtre plus complexe que d’autres services plus simple. C’est le prix Ă  payer pour avoir une rĂ©plication chiffrĂ©e possiblement rĂ©partie sur 3 sites.
A dĂ©faut n’hĂ©sitez pas Ă  tester la version gratuite Ă  5Go sur Ente.io. Compte Ă  crĂ©er par un des clients desktop/mobile ou par le web.

Liens