Skip to content

Instantly share code, notes, and snippets.

@via-justa
Last active May 7, 2026 17:25
Show Gist options
  • Select an option

  • Save via-justa/8c7cecb8b9a92d821e36bdb92fe7903c to your computer and use it in GitHub Desktop.

Select an option

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
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}
#!/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
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