Last active
May 7, 2026 17:25
-
-
Save via-justa/8c7cecb8b9a92d821e36bdb92fe7903c to your computer and use it in GitHub Desktop.
Ente on TrueNAS. download the install script, run it from the NAS shell, and follow the instructions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| x-portals: | |
| - host: api-ente.${DOMAIN} | |
| name: Ente API | |
| path: / | |
| port: 30037 | |
| scheme: https | |
| - host: albums.${DOMAIN} | |
| name: Ente Albums | |
| path: / | |
| port: 30031 | |
| scheme: https | |
| services: | |
| museum: | |
| container_name: ente-museum | |
| image: ghcr.io/ente-io/server | |
| depends_on: | |
| postgres: | |
| condition: service_healthy | |
| environment: [] | |
| ports: | |
| - mode: ingress | |
| protocol: tcp | |
| published: 30037 | |
| target: 8080 | |
| deploy: | |
| resources: | |
| limits: | |
| cpus: '2' | |
| memory: 4096M | |
| group_add: | |
| - "568" | |
| privileged: false | |
| security_opt: | |
| - no-new-privileges:true | |
| user: "568:568" | |
| volumes: | |
| - bind: | |
| create_host_path: false | |
| propagation: rprivate | |
| read_only: true | |
| source: ${BASE_PATH}/museum.yaml | |
| target: /museum.yaml | |
| type: bind | |
| - bind: | |
| create_host_path: false | |
| propagation: rprivate | |
| read_only: false | |
| source: ${BASE_PATH}/data | |
| target: /data | |
| type: bind | |
| socat: | |
| image: alpine/socat | |
| network_mode: service:museum | |
| depends_on: [museum] | |
| command: "TCP-LISTEN:3200,fork,reuseaddr TCP:minio:3200" | |
| postgres: | |
| container_name: ente-postgres | |
| image: postgres:15 | |
| environment: | |
| POSTGRES_DB: ente_db | |
| POSTGRES_PASSWORD: ${DB_PASSWORD} | |
| POSTGRES_USER: pguser | |
| healthcheck: | |
| test: pg_isready -q -d ${POSTGRES_DB} -U ${POSTGRES_USER} | |
| start_period: 30s | |
| start_interval: 1s | |
| deploy: | |
| resources: | |
| limits: | |
| cpus: '2' | |
| memory: 4096M | |
| volumes: | |
| - bind: | |
| create_host_path: false | |
| propagation: rprivate | |
| read_only: false | |
| source: ${BASE_PATH}/pg-data | |
| target: /var/lib/postgresql/data | |
| type: bind | |
| web: | |
| container_name: ente-web | |
| image: ghcr.io/ente-io/web | |
| environment: | |
| ENTE_API_ORIGIN: https://api-ente.${DOMAIN} | |
| ENTE_ALBUMS_ORIGIN: https://albums.${DOMAIN} | |
| ENTE_PHOTOS_ORIGIN: https://accounts-ente.${DOMAIN} | |
| ports: | |
| # Photos | |
| - mode: ingress | |
| protocol: tcp | |
| published: 30030 | |
| target: 3000 | |
| # Accounts | |
| - mode: ingress | |
| protocol: tcp | |
| published: 30031 | |
| target: 3001 | |
| # Albums | |
| - mode: ingress | |
| protocol: tcp | |
| published: 30032 | |
| target: 3002 | |
| # # Auth | |
| # - mode: ingress | |
| # protocol: tcp | |
| # published: 30033 | |
| # target: 3003 | |
| # # Cast | |
| # - mode: ingress | |
| # protocol: tcp | |
| # published: 30034 | |
| # target: 3004 | |
| # Locker | |
| - mode: ingress | |
| protocol: tcp | |
| published: 30035 | |
| target: 3005 | |
| # Embed | |
| - mode: ingress | |
| protocol: tcp | |
| published: 30036 | |
| target: 3006 | |
| # Paste | |
| - mode: ingress | |
| protocol: tcp | |
| published: 30038 | |
| target: 3008 | |
| # Memories | |
| - mode: ingress | |
| protocol: tcp | |
| published: 30040 | |
| target: 3010 | |
| deploy: | |
| resources: | |
| limits: | |
| cpus: '2' | |
| memory: 4096M | |
| privileged: false | |
| security_opt: | |
| - no-new-privileges:true | |
| minio: | |
| container_name: ente-minio | |
| image: quay.io/minio/aistor/minio:RELEASE.2026-05-04T23-02-27Z | |
| configs: | |
| - source: license | |
| target: /minio.license | |
| environment: | |
| GID: '568' | |
| GROUP_ID: '568' | |
| MINIO_LICENSE: /minio.license | |
| MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} | |
| MINIO_ROOT_USER: ente-user | |
| NVIDIA_VISIBLE_DEVICES: void | |
| PGID: '568' | |
| PUID: '568' | |
| TZ: Etc/Berlin | |
| UID: '568' | |
| UMASK: '002' | |
| UMASK_SET: '002' | |
| USER_ID: '568' | |
| ports: | |
| - mode: ingress | |
| protocol: tcp | |
| published: 30043 | |
| target: 3200 | |
| - mode: ingress | |
| protocol: tcp | |
| published: 30042 | |
| target: 3201 # MinIO Console (uncomment to access externally) | |
| cap_drop: | |
| - ALL | |
| command: | |
| - minio | |
| - server | |
| - /data | |
| - '--address=:3200' | |
| - '--console-address=:3201' | |
| - '--certs-dir=/tmp/ix-certs' | |
| deploy: | |
| resources: | |
| limits: | |
| cpus: '2' | |
| memory: 4096M | |
| depends_on: | |
| permissions: | |
| condition: service_completed_successfully | |
| group_add: | |
| - "568" | |
| healthcheck: | |
| interval: 30s | |
| retries: 5 | |
| start_interval: 2s | |
| start_period: 15s | |
| test: | |
| - CMD | |
| - curl | |
| - '--request' | |
| - GET | |
| - '--silent' | |
| - '--output' | |
| - /dev/null | |
| - '--show-error' | |
| - '--fail' | |
| - http://127.0.0.1:3200/minio/health/live | |
| privileged: False | |
| restart: unless-stopped | |
| security_opt: | |
| - no-new-privileges=true | |
| stdin_open: False | |
| tty: False | |
| user: '568:568' | |
| volumes: | |
| - bind: | |
| create_host_path: false | |
| propagation: rprivate | |
| read_only: false | |
| source: ${BASE_PATH}/minio | |
| target: /data | |
| type: bind | |
| post_start: | |
| - command: | | |
| sh -c ' | |
| #!/bin/sh | |
| while ! mc alias set h0 http://minio:3200 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD} 2>/dev/null | |
| do | |
| echo "Waiting for minio..." | |
| sleep 0.5 | |
| done | |
| cd /data | |
| mc mb -p ente | |
| ' | |
| permissions: | |
| cap_add: | |
| - CHOWN | |
| - DAC_OVERRIDE | |
| - FOWNER | |
| cap_drop: | |
| - ALL | |
| configs: | |
| - mode: 320 | |
| source: permissions_actions_data | |
| target: /script/actions.json | |
| deploy: | |
| resources: | |
| limits: | |
| cpus: '2' | |
| memory: 1024M | |
| entrypoint: | |
| - python3 | |
| - /script/permissions.py | |
| environment: | |
| GID: '568' | |
| GROUP_ID: '568' | |
| NVIDIA_VISIBLE_DEVICES: void | |
| PGID: '568' | |
| PUID: '568' | |
| TZ: Etc/Berlin | |
| UID: '568' | |
| UMASK: '002' | |
| UMASK_SET: '002' | |
| USER_ID: '568' | |
| group_add: | |
| - 568 | |
| healthcheck: | |
| disable: True | |
| image: ixsystems/container-utils:1.0.2 | |
| network_mode: none | |
| platform: linux/amd64 | |
| privileged: False | |
| restart: on-failure:1 | |
| security_opt: | |
| - no-new-privileges=true | |
| stdin_open: False | |
| tty: False | |
| user: '0:0' | |
| volumes: | |
| - bind: | |
| create_host_path: False | |
| propagation: rprivate | |
| read_only: False | |
| source: ${BASE_PATH}/minio | |
| target: /mnt/permission/data | |
| type: bind | |
| configs: | |
| permissions_actions_data: | |
| content: >- | |
| [{"read_only": false, "mount_path": "/mnt/permission/data", | |
| "is_temporary": false, "identifier": "data", "recursive": false, "mode": | |
| "check", "uid": 568, "gid": 568, "chmod": null}] | |
| license: | |
| content: ${AISTOR_LICENSE} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Interactive setup script for Ente configuration files. | |
| # | |
| # What this script does: | |
| # 1) Prompts for: | |
| # - DOMAIN (top-level domain, e.g. example.com) | |
| # - BASE_PATH (format: /mnt/<pool>/<dataset>(/<folder>)) | |
| # - AISTOR_LICENSE (MinIO free account license string) | |
| # - EMAIL (admin email / hardcoded OTT email) | |
| # 2) Verifies required tools are installed: | |
| # - sed | |
| # - curl | |
| # 3) Generates random secrets used by the templates. | |
| # 4) Creates these directories under BASE_PATH: | |
| # - data | |
| # - pg-data | |
| # - minio | |
| # 5) Pulls fresh template files from the configured gist URLs: | |
| # - compose.yaml | |
| # - museum.yaml | |
| # 6) Substitutes placeholders using hard values (direct replacements, | |
| # not environment lookups). | |
| # 7) Writes rendered files to: | |
| # - ./compose.yaml | |
| # - BASE_PATH/museum.yaml | |
| # | |
| # Compatibility: | |
| # - Works on Linux and macOS with Bash, curl, and sed available. | |
| # | |
| # Usage: | |
| # chmod +x replace.sh | |
| # ./replace.sh | |
| set -euo pipefail | |
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| COMPOSE_TEMPLATE_URL="https://gist.githubusercontent.com/via-justa/8c7cecb8b9a92d821e36bdb92fe7903c/raw/9009a984e3a7be9e599c27408713de9991c9be93/compose.yaml" | |
| MUSEUM_TEMPLATE_URL="https://gist.githubusercontent.com/via-justa/8c7cecb8b9a92d821e36bdb92fe7903c/raw/9009a984e3a7be9e599c27408713de9991c9be93/museum.yaml" | |
| prompt_non_empty() { | |
| local prompt="$1" | |
| local value="" | |
| while [[ -z "$value" ]]; do | |
| read -r -p "$prompt" value | |
| value="${value//[$'\t\r\n']/}" | |
| if [[ -z "$value" ]]; then | |
| echo "Value cannot be empty." | |
| fi | |
| done | |
| printf '%s\n' "$value" | |
| } | |
| DOMAIN="$(prompt_non_empty 'DOMAIN (top-level domain, e.g. example.com): ')" | |
| while true; do | |
| BASE_PATH="$(prompt_non_empty 'BASE_PATH (format: /mnt/<pool>/<dataset>(/<folder>)): ')" | |
| if [[ "$BASE_PATH" =~ ^/mnt/[^/]+/[^/]+(/[^[:space:]]+)?$ ]]; then | |
| break | |
| fi | |
| echo "Invalid BASE_PATH. Expected format: /mnt/<pool>/<dataset>(/<folder>)" | |
| done | |
| AISTOR_LICENSE="$(prompt_non_empty 'AISTOR_LICENSE (free account license from min.io): ')" | |
| EMAIL="$(prompt_non_empty 'EMAIL (admin email, e.g. you@example.com): ')" | |
| if ! command -v sed >/dev/null 2>&1; then | |
| echo "Error: sed is required but not found." | |
| exit 1 | |
| fi | |
| if ! command -v curl >/dev/null 2>&1; then | |
| echo "Error: curl is required but not found." | |
| exit 1 | |
| fi | |
| DB_PASSWORD="$(head -c 21 /dev/urandom | base64 | tr -d '\n')" | |
| MINIO_ROOT_PASSWORD="$(head -c 32 /dev/urandom | base64 | tr -d '\n')" | |
| ENCRYPTION_KEY="$(head -c 32 /dev/urandom | base64 | tr -d '\n')" | |
| HASH_KEY="$(head -c 64 /dev/urandom | base64 | tr -d '\n')" | |
| JWT_SECRET="$(head -c 32 /dev/urandom | base64 | tr -d '\n' | tr '+/' '-_')" | |
| mkdir -p "$BASE_PATH/data" "$BASE_PATH/pg-data" "$BASE_PATH/minio" | |
| escape_sed_replacement() { | |
| printf '%s' "$1" | sed -e 's/[\\/&|]/\\&/g' | |
| } | |
| substitute_file() { | |
| local input_file="$1" | |
| local output_file="$2" | |
| local tmp_file | |
| local domain_esc | |
| local base_path_esc | |
| local aistor_license_esc | |
| local email_esc | |
| local db_password_esc | |
| local minio_root_password_esc | |
| local encryption_key_esc | |
| local hash_key_esc | |
| local jwt_secret_esc | |
| tmp_file="$(mktemp)" | |
| domain_esc="$(escape_sed_replacement "$DOMAIN")" | |
| base_path_esc="$(escape_sed_replacement "$BASE_PATH")" | |
| aistor_license_esc="$(escape_sed_replacement "$AISTOR_LICENSE")" | |
| email_esc="$(escape_sed_replacement "$EMAIL")" | |
| db_password_esc="$(escape_sed_replacement "$DB_PASSWORD")" | |
| minio_root_password_esc="$(escape_sed_replacement "$MINIO_ROOT_PASSWORD")" | |
| encryption_key_esc="$(escape_sed_replacement "$ENCRYPTION_KEY")" | |
| hash_key_esc="$(escape_sed_replacement "$HASH_KEY")" | |
| jwt_secret_esc="$(escape_sed_replacement "$JWT_SECRET")" | |
| sed \ | |
| -e "s|\${DOMAIN}|$domain_esc|g" \ | |
| -e "s|\${BASE_PATH}|$base_path_esc|g" \ | |
| -e "s|\${AISTOR_LICENSE}|$aistor_license_esc|g" \ | |
| -e "s|\${EMAIL}|$email_esc|g" \ | |
| -e "s|\${DB_PASSWORD}|$db_password_esc|g" \ | |
| -e "s|\${MINIO_ROOT_PASSWORD}|$minio_root_password_esc|g" \ | |
| -e "s|\${ENCRYPTION_KEY}|$encryption_key_esc|g" \ | |
| -e "s|\${HASH_KEY}|$hash_key_esc|g" \ | |
| -e "s|\${JWT_SECRET}|$jwt_secret_esc|g" \ | |
| "$input_file" > "$tmp_file" | |
| mv "$tmp_file" "$output_file" | |
| } | |
| compose_template_file="$(mktemp)" | |
| museum_template_file="$(mktemp)" | |
| cleanup() { | |
| rm -f "$compose_template_file" "$museum_template_file" | |
| } | |
| trap cleanup EXIT | |
| curl -fsSL "$COMPOSE_TEMPLATE_URL" -o "$compose_template_file" | |
| curl -fsSL "$MUSEUM_TEMPLATE_URL" -o "$museum_template_file" | |
| TARGETS=( | |
| "$compose_template_file:$SCRIPT_DIR/compose.yaml" | |
| "$museum_template_file:$BASE_PATH/museum.yaml" | |
| ) | |
| for item in "${TARGETS[@]}"; do | |
| input_file="${item%%:*}" | |
| output_file="${item#*:}" | |
| substitute_file "$input_file" "$output_file" | |
| echo "Updated: $output_file" | |
| done | |
| cat <<EOF | |
| Created directories under $BASE_PATH: | |
| - $BASE_PATH/data | |
| - $BASE_PATH/pg-data | |
| - $BASE_PATH/minio | |
| Updated files: | |
| - $SCRIPT_DIR/compose.yaml | |
| - $BASE_PATH/museum.yaml | |
| Substitution complete. | |
| Create a new custom app in TrueNAS and paste the contents of compose.yaml into the compose editor. | |
| Then deploy. If you get an error, likely one of the ports is already in use. | |
| Check the logs for details and adjust the port mappings in compose.yaml and museum.yaml as needed, then redeploy. | |
| Next you need to create DNS records for the following subdomains pointing to your TrueNAS IP: | |
| * https://albums.your-domain.com (http://nas_ip:30030) | |
| * https://accounts-ente.your-domain.com (http://nas_ip:30031) | |
| * https://albums-ente.your-domain.com (http://nas_ip:30032) | |
| * https://auth-ente.your-domain.com (http://nas_ip:30033) | |
| * https://cast-ente.your-domain.com (http://nas_ip:30034; disabled by default) | |
| * https://share-ente.your-domain.com (http://nas_ip:30035) | |
| * https://embed-ente.your-domain.com (http://nas_ip:30036) | |
| * https://api-ente.your-domain.com (http://nas_ip:30037) | |
| * https://paste-ente.your-domain.com (http://nas_ip:30038) | |
| * https://memories-ente.your-domain.com (http://nas_ip:30040) | |
| * https://minio-ente.your-domain.com (http://nas_ip:30043) | |
| EOF |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| apps: | |
| accounts: https://accounts-ente.${DOMAIN} | |
| public-albums: https://albums-ente.${DOMAIN} | |
| public-locker: https://share-ente.${DOMAIN} | |
| public-paste: https://paste-ente.${DOMAIN} | |
| public-memories: https://memories-ente.${DOMAIN} | |
| # cast: https://cast-ente.${DOMAIN} | |
| embed-albums: https://embed-ente.${DOMAIN} | |
| key: | |
| encryption: ${ENCRYPTION_KEY} | |
| hash: ${HASH_KEY} | |
| jwt: | |
| secret: ${JWT_SECRET} | |
| db: | |
| host: postgres | |
| port: 5432 | |
| name: ente_db | |
| user: pguser | |
| password: ${DB_PASSWORD} | |
| s3: | |
| are_local_buckets: true | |
| use_path_style_urls: true | |
| b2-eu-cen: | |
| key: ente-user | |
| secret: ${MINIO_ROOT_PASSWORD} | |
| endpoint: https://minio-ente.${DOMAIN} | |
| region: eu-central-2 | |
| bucket: ente | |
| internal: | |
| # disable-registration: true | |
| silent: true | |
| trusted-client-ip-header: X-Forwarded-For, X-Real-IP, CF-Connecting-IP | |
| admin: 1580559962386438 | |
| hardcoded-ott: | |
| emails: | |
| - "${EMAIL},123456" | |
| local-domain-suffix: "@${DOMAIN}" | |
| local-domain-value: 012345 | |
| webauthn: | |
| rpid: accounts-ente.${DOMAIN} | |
| rporigins: | |
| - "https://accounts-ente.${DOMAIN}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment