Can not use app script after upgrade to 1.0

I get This script requires "docker-compose" to be installed error:

umbrel@umbrel:~/umbrel/scripts$ sudo ./app
This script requires "docker-compose" to be installed
umbrel@umbrel:~/umbrel/scripts$ uname -a
Linux umbrel 6.1.0-18-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.76-1 (2024-02-01) x86_64 GNU/Linux

Fixed the script. Here is the diff:

--- app	2022-11-02 14:24:03.000000000 +0000
+++ app-new	2024-03-19 21:18:06.000000000 +0000
@@ -4,7 +4,7 @@
 VERSION="0.0.3"

 UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
-USER_FILE="${UMBREL_ROOT}/db/user.json"
+USER_FILE="${UMBREL_ROOT}/db/user.json.migrated"

 APP_PROXY_SERVICE_NAME="app_proxy"
 REMOTE_TOR_ACCESS="false"
@@ -74,7 +74,7 @@
   # Load in existing Umbrel .env
   # So that apps in their exports.sh can access
   # e.g. $TOR_PROXY_IP, $TOR_PROXY_PORT
-  [[ -f "${UMBREL_ROOT}/.env" ]] && . "${UMBREL_ROOT}/.env"
+  [[ -f "${UMBREL_ROOT}/.env.migrated" ]] && . "${UMBREL_ROOT}/.env.migrated"

   export NETWORK_IP="${NETWORK_IP}"

@@ -140,7 +140,7 @@
 }

 # Check dependencies
-check_dependencies docker-compose jq yq openssl envsubst
+check_dependencies docker jq yq openssl envsubst

 if [ -z ${1+x} ]; then
   command=""
@@ -217,7 +217,7 @@
   local -r common_compose_file="${UMBREL_ROOT}/scripts/support/docker-compose.common.yml"
   local -r app_compose_file="${app_data_dir}/docker-compose.yml"

-  local -r umbrel_env_file="${UMBREL_ROOT}/.env"
+  local -r umbrel_env_file="${UMBREL_ROOT}/.env.migrated"

   # We need to use the proxy compose file first
   # To allow vars. in the app's compose file to override variables
@@ -245,7 +245,7 @@
   # Merge compose files and args. passed into 'compose'
   compose_args=("${compose_files[@]}" "${@}")

-  docker-compose \
+  docker compose \
     --env-file "${umbrel_env_file}" \
     --project-name "${app}" \
     "${compose_args[@]}"

Have the same problem. Can you explain what I need to change exactly in this script?

You can just apply this patch to your umbrel/scripts/app file or replace it with the patched version you find below. (or better to create a new one umbrel/scripts/app-new)

I only replaced docker-compose with docker compose as compose is now a docker’s command, and there is no separate executable. Also, I removed docker-compose from dependency check, and added docker instead.

Also, for some reason .env file was renamed to .env.migrated. It looks like upgrade script didn’t finish successfully, and probably needs to be fixed by umbrel in a later release.

#!/usr/bin/env bash
set -euo pipefail

VERSION="0.0.3"

UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
USER_FILE="${UMBREL_ROOT}/db/user.json.migrated"

APP_PROXY_SERVICE_NAME="app_proxy"
REMOTE_TOR_ACCESS="false"
if [[ -f "${USER_FILE}" ]]; then
  REMOTE_TOR_ACCESS=$(cat "${USER_FILE}" | jq 'has("remoteTorAccess") and .remoteTorAccess')
fi

show_help() {
  cat << EOF
CLI (v${VERSION}) for managing Umbrel apps

Usage: app <command> <app> [<arguments>]

Commands:
    install                    Pulls down images for an app and starts it
    uninstall                  Removes images and destroys all data for an app
    reinstall                  Calls 'uninstall', followed by 'install' for an app
    start                      Starts an installed app
    stop                       Stops an installed app
    restart                    Restarts an installed app
    compose                    Passes all arguments to docker-compose
    ls-installed               Lists installed apps
EOF
}

check_dependencies () {
  for cmd in "$@"; do
    if ! command -v $cmd >/dev/null 2>&1; then
      >&2 echo "This script requires \"${cmd}\" to be installed"
      exit 1
    fi
  done
}

list_installed_apps() {
  cat "${USER_FILE}" 2> /dev/null | jq -r 'if has("installedApps") then .installedApps else [] end | join("\n")' || true
}

# Deterministically derives 128 bits of cryptographically secure entropy
derive_entropy () {
  # Make sure we use the seed from the real Umbrel installation if this is
  # an OTA update.
  SEED_FILE="${UMBREL_ROOT}/db/umbrel-seed/seed"
  if [[ ! -f "${SEED_FILE}" ]] && [[ -f "${UMBREL_ROOT}/../.umbrel" ]]; then
    SEED_FILE="${UMBREL_ROOT}/../db/umbrel-seed/seed"
  fi

  identifier="${1}"
  umbrel_seed=$(cat "${SEED_FILE}") || true

  if [[ -z "$umbrel_seed" ]] || [[ -z "$identifier" ]]; then
    >&2 echo "Missing derivation parameter, this is unsafe, exiting."
    exit 1
  fi

  # We need `sed 's/^.* //'` to trim the "(stdin)= " prefix from some versions of openssl
  printf "%s" "${identifier}" | openssl dgst -sha256 -hmac "${umbrel_seed}" | sed 's/^.* //'
}

# Setup env. for this context for a given app
source_app() {
  local -r app="${1}"

  local -r app_domain="$(hostname -s 2>/dev/null || echo "umbrel").local"
  local -r app_entropy_identifier="app-${app}-seed"

  # Load in existing Umbrel .env
  # So that apps in their exports.sh can access
  # e.g. $TOR_PROXY_IP, $TOR_PROXY_PORT
  [[ -f "${UMBREL_ROOT}/.env.migrated" ]] && . "${UMBREL_ROOT}/.env.migrated"

  export NETWORK_IP="${NETWORK_IP}"

  # Set other useful vars. used in exports
  export DEVICE_HOSTNAME="$(cat /proc/sys/kernel/hostname 2>/dev/null || echo "umbrel")"
  export DEVICE_DOMAIN_NAME="${DEVICE_HOSTNAME}.local"

  # Set env using all installed apps exports.sh
  # Do this first so that no app exports can
  # Override any app specific exports defined below
  EXPORTS_TOR_DATA_DIR="${UMBREL_ROOT}/tor/data"

  APPS_TO_SOURCE="$(list_installed_apps)"

  # $app might not be in the 'installed apps list' yet
  # i.e. If it is currently being installed
  # So we'll add it to the list of apps that will be 'sourced'
  if ! echo "${APPS_TO_SOURCE}" | grep --quiet "^${app}$"; then
    APPS_TO_SOURCE="${APPS_TO_SOURCE}"$'\n'"${app}"
  fi

  for EXPORTS_APP_ID in $APPS_TO_SOURCE; do
    EXPORTS_APP_DIR="${UMBREL_ROOT}/app-data/${EXPORTS_APP_ID}"
    EXPORTS_APP_FILE="${EXPORTS_APP_DIR}/exports.sh"
    EXPORTS_APP_DATA_DIR="${EXPORTS_APP_DIR}/data"

    [[ -f "${EXPORTS_APP_FILE}" ]] && . "${EXPORTS_APP_FILE}"
  done

  # App specific exports
  export APP_ID="${app}"
  export APP_MANIFEST_FILE="${app_data_dir}/umbrel-app.yml"
  export APP_VERSION=$(cat "${APP_MANIFEST_FILE}" | yq '.version')

  # This provides the app proxy with context of the app
  export APP_PROXY_HOSTNAME="app_proxy_${app}"
  export APP_PROXY_PORT=$(cat "${APP_MANIFEST_FILE}" | yq '.port')

  export APP_DATA_DIR="${app_data_dir}"
  export APP_DOMAIN="${app_domain}"
  export APP_HIDDEN_SERVICE="not-enabled.onion"
  if [[ "${REMOTE_TOR_ACCESS}" == "true" ]]; then
    export APP_HIDDEN_SERVICE="$(cat "${app_hidden_service_file}" 2>/dev/null || echo "notyetset.onion")"
  fi
  export APP_SEED=$(derive_entropy "${app_entropy_identifier}")
  export APP_PASSWORD=$(derive_entropy "${app_entropy_identifier}-APP_PASSWORD")

  # Tor specific exports
  export TOR_DATA_DIR="${UMBREL_ROOT}/tor/data"
  export TOR_ENTRYPOINT_SCRIPT="${UMBREL_ROOT}/scripts/support/tor-entrypoint.sh"
  export TOR_HS_APP_DIR="/data/app-${app}"
  export TOR_HS_PORTS="80:${APP_PROXY_HOSTNAME}:${APP_PROXY_PORT}"

  tor_extra_hs_varname=$(echo "APP_${APP_ID^^}_TOR_HS_EXTRA_PORTS" | tr '-' '_')
  tor_hs_extra_ports="${!tor_extra_hs_varname:-}"

  if [[ ! -z "${tor_hs_extra_ports}" ]]; then
    export TOR_HS_PORTS="${TOR_HS_PORTS} ${tor_hs_extra_ports}"
  fi

  # Other
  export UMBREL_ROOT
}

# Check dependencies
check_dependencies docker jq yq openssl envsubst

if [ -z ${1+x} ]; then
  command=""
else
  command="$1"
fi

# Lists installed apps
if [[ "$command" = "ls-installed" ]]; then
  list_installed_apps

  exit
fi

if [ -z ${2+x} ]; then
  show_help
  exit 1
else
  app="$2"

  repo=$(cat "${USER_FILE}" 2> /dev/null | jq -r ".appOrigin.\"${app}\"" || true)
  repo_path=$("${UMBREL_ROOT}/scripts/repo" "path" "${repo}")
  app_repo_dir="${repo_path}/${app}"
  app_data_dir="${UMBREL_ROOT}/app-data/${app}"

  app_hidden_service_file="${UMBREL_ROOT}/tor/data/app-${app}/hostname"

  if [[ "${app}" == "installed" ]]; then
    for app in $(list_installed_apps); do
      if [[ "${app}" != "" ]]; then
        "${0}" "${1}" "${app}" "${@:3}" &
      fi
    done
    wait
    exit
  fi

  if [[ -z "${app}" ]]; then
    >&2 echo "Error: \"${app}\" is not a valid app"
    exit 1
  fi
fi

if [ -z ${3+x} ]; then
  args=""
else
  args="${@:3}"
fi

execute_hook() {
  local -r app="${1}"
  local -r name="${2}"

  local -r app_hooks_dir="${UMBREL_ROOT}/app-data/${app}/hooks"
  local -r hook="${app_hooks_dir}/${name}"

  if [[ -x "${hook}" ]]; then
    echo "Executing hook: ${hook}"
    # Swallow non-zero exit code
    "${hook}" || true
  fi
}

compose() {
  local -r app="${1}"
  shift

  # Source env.
  source_app "${app}"

  # Define support compose files
  local -r app_proxy_compose_file="${UMBREL_ROOT}/scripts/support/docker-compose.app_proxy.yml"
  local -r tor_compose_file="${UMBREL_ROOT}/scripts/support/docker-compose.tor.yml"
  local -r common_compose_file="${UMBREL_ROOT}/scripts/support/docker-compose.common.yml"
  local -r app_compose_file="${app_data_dir}/docker-compose.yml"

  local -r umbrel_env_file="${UMBREL_ROOT}/.env.migrated"

  # We need to use the proxy compose file first
  # To allow vars. in the app's compose file to override variables
  compose_files=()

  # Detect if the 'app_proxy' service has been defined
  # In the app's docker-compose file
  has_app_proxy_service=$(cat "${app_compose_file}" | yq ".services | has(\"${APP_PROXY_SERVICE_NAME}\")")

  if [[ "${has_app_proxy_service}" == "true" ]]; then
    compose_files+=( "--file" "${app_proxy_compose_file}" )
  fi

  # If remote Tor access is enabled
  # Then include a compose file for Tor
  if [[ "${REMOTE_TOR_ACCESS}" == "true" ]]; then
    compose_files+=( "--file" "${tor_compose_file}" )
  fi

  # Add app's compose file last so that it can override
  # Any of the other compose files
  compose_files+=( "--file" "${common_compose_file}" )
  compose_files+=( "--file" "${app_compose_file}" )

  # Merge compose files and args. passed into 'compose'
  compose_args=("${compose_files[@]}" "${@}")

  docker compose \
    --env-file "${umbrel_env_file}" \
    --project-name "${app}" \
    "${compose_args[@]}"
}

update_installed_apps() {
  local -r action="${1}"
  local -r app="${2}"
  local -r repo="${3:-null}"

  while ! (set -o noclobber; echo "$$" > "${USER_FILE}.lock") 2> /dev/null; do
    echo "Waiting for JSON lock to be released for ${app} update..."
    sleep 1
  done
  # This will cause the lock-file to be deleted in case of a
  # premature exit.
  trap "rm -f "${USER_FILE}.lock"; exit $?" INT TERM EXIT

  [[ "${action}" == "add" ]] && operator="+" || operator="-"
  updated_json=$(cat "${USER_FILE}" | jq ".installedApps |= (. ${operator} [\"${app}\"] | unique)")
  echo "${updated_json}" > "${USER_FILE}"

  if [[ "${action}" == "add" ]]; then
    updated_json=$(cat "${USER_FILE}" | jq ".appOrigin |= (. ${operator} {\"${app}\":\"${repo}\"})")
  else
    updated_json=$(cat "${USER_FILE}" | jq "del(.appOrigin.\"${app}\")")
  fi
  echo "${updated_json}" > "${USER_FILE}"

  rm -f "${USER_FILE}.lock"
}

template_app() {
  local -r app="${1}"

  # Loop over all templates within app and populate them
  APP_TEMPLATE_FILES="${app_data_dir}/*.template"

  shopt -s nullglob
  for APP_TEMPLATE_INPUT_FILE in $APP_TEMPLATE_FILES; do
    # Output filename is the same as input with .template stripped off
    APP_TEMPLATE_OUTPUT_FILE="${APP_TEMPLATE_INPUT_FILE%.*}"

    # First we'll copy the file so we ensure the output
    # has the same fs permissions as the input
    cp --archive "${APP_TEMPLATE_INPUT_FILE}" "${APP_TEMPLATE_OUTPUT_FILE}"
    cat "${APP_TEMPLATE_INPUT_FILE}" | envsubst > "${APP_TEMPLATE_OUTPUT_FILE}"
  done
}

copy_app_files() {
  local -r files_to_copy="${1}"

  for filename in $files_to_copy; do
    APP_FILES="${app_repo_dir}/${filename}"

    for app_file in $APP_FILES; do
      if [[ -f "${app_file}" ]] || [[ -d "${app_file}" ]]; then
        cp --archive "${app_file}" "${app_data_dir}"
      fi
    done
  done
}

wait_for_tor_hs() {
  local -r app="${1}"

  # Check if the app's hidden service hostname
  # Has been already generated and exit early
  if [[ -f "${app_hidden_service_file}" ]]; then
    return
  fi

  # Check that the app has the App Proxy service defined
  local -r app_compose_file="${app_data_dir}/docker-compose.yml"
  has_app_proxy_service=$(cat "${app_compose_file}" | yq ".services | has(\"${APP_PROXY_SERVICE_NAME}\")")

  if [[ "${has_app_proxy_service}" == "false" ]]; then
    echo
    >&2 echo "Warning: \"${app}\" has no '${APP_PROXY_SERVICE_NAME}' defined"
    >&2 echo "         \"${app}\" needs this to generate Tor HS"
    echo
    return
  fi

  # If a tor service will start
  # and there is no existing tor hs hostname
  # Let's allow 10 seconds to generate it and then start the app
  if [[ "${REMOTE_TOR_ACCESS}" == "true" ]]; then
    echo "Generating hidden services for ${app}..."
    # We must first start the App Proxy
    # So that it's hostname is resolvable by Tor
    # More details here: https://github.com/torproject/tor/blob/01bda6c23f58947ad1e20ea6367a5c260f53dfab/src/feature/hs/hs_common.c#L743
    # And here: https://github.com/torproject/tor/blob/22552ad88e1e95ef9d2c6655c7602b7b25836075/src/lib/net/resolve.c#L297
    # Otherwise Tor will throw this error:
    # Unparseable address in hidden service port configuration.
    compose "${app}" up --detach app_proxy
    compose "${app}" up --detach tor_server

    for attempt in $(seq 1 100); do
      if [[ -f "${app_hidden_service_file}" ]]; then
        echo "Hidden service file created successfully!"
        break
      fi
      sleep 0.1
    done

    if [[ ! -f "${app_hidden_service_file}" ]]; then
      echo "Hidden service file wasn't created"
    fi
  fi
}

start_app() {
  local -r app="${1}"

  # Source env.
  source_app "${app}"

  # Now apply templates
  template_app "${app}"

  # Wait for Tor's HS hostname to exist
  wait_for_tor_hs "${app}"

  execute_hook "${app}" "pre-start"

  # Start all the app's containers
  compose "${app}" up --detach

  execute_hook "${app}" "post-start"
}

# Check that the app is installed
must_be_installed_guard() {
  if ! list_installed_apps | grep --quiet "^${app}$"; then
    >&2 echo "Error: app \"${app}\" is not installed yet"
    exit 1
  fi
}

# Pulls down images for an app and starts it
if [[ "$command" = "install" ]]; then

  repo=$("${UMBREL_ROOT}/scripts/repo" "locate" "${app}")

  if [[ -z "${repo}" ]]; then
    >&2 echo "Error: \"${app}\" not found in any local app repo"
    exit 1
  fi

  app_repo_dir=$("${UMBREL_ROOT}/scripts/repo" "path" "${repo}")
  app_repo_dir="${app_repo_dir}/${app}"

  echo "Installing '${app}' from: ${repo}"

  echo "Setting up data dir for app ${app}..."
  mkdir -p "${app_data_dir}"

  # Copy all app files
  rsync --archive --verbose --exclude ".gitkeep" "${app_repo_dir}/." "${app_data_dir}"

  execute_hook "${app}" "pre-install"

  # Source env.
  source_app "${app}"

  # Now apply templates
  template_app "${app}"

  echo "Pulling images for app ${app}..."
  compose "${app}" pull

  if [[ "$*" != *"--skip-start"* ]]; then
    echo "Starting app ${app}..."
    start_app "${app}"
  fi

  echo "Saving app ${app} in DB..."
  update_installed_apps add "${app}" "${repo}"

  execute_hook "${app}" "post-install"

  echo "Successfully installed app ${app}"
  exit
fi

# Removes images and destroys all data for an app
if [[ "$command" = "uninstall" ]]; then

  must_be_installed_guard

  execute_hook "${app}" "pre-uninstall"

  # If a post uninstal hook exists
  # Then make a copy before it's deleted below
  app_hooks_dir="${UMBREL_ROOT}/app-data/${app}/hooks"
  post_uninstall_app_hook="${app_hooks_dir}/post-uninstall"
  if [[ -x "${post_uninstall_app_hook}" ]]; then
    temp_post_uninstall_app_hook="/tmp/${app}-post-uninstall"

    cp --archive "${post_uninstall_app_hook}" "${temp_post_uninstall_app_hook}"

    post_uninstall_app_hook="${temp_post_uninstall_app_hook}"
  else
    post_uninstall_app_hook=""
  fi

  echo "Removing images for app ${app}..."
  compose "${app}" down --rmi all --remove-orphans

  echo "Deleting app data for app ${app}..."
  if [[ -d "${app_data_dir}" ]]; then
    rm -rf "${app_data_dir}"
  fi

  echo "Removing app ${app} from DB..."
  update_installed_apps remove "${app}"

  if [[ ! -z "${post_uninstall_app_hook}" ]]; then
    "${post_uninstall_app_hook}" || true

    rm -rf "${post_uninstall_app_hook}"
  fi

  echo "Successfully uninstalled app ${app}"
  exit
fi

# Stops an installed app
if [[ "$command" = "stop" ]]; then

  must_be_installed_guard

  execute_hook "${app}" "pre-stop"

  echo "Stopping app ${app}..."
  compose "${app}" rm --force --stop

  execute_hook "${app}" "post-stop"

  exit
fi

if [[ "$command" = "reinstall" ]]; then

  "${0}" "uninstall" "${app}"

  echo
  "${0}" "install" "${app}"

  exit
fi

# Starts an installed app
if [[ "$command" = "start" ]]; then

  must_be_installed_guard

  echo "Starting app ${app}..."
  start_app "${app}"

  exit
fi

# Restarts an installed app
if [[ "$command" = "restart" ]]; then

  "${0}" "stop" "${app}"

  "${0}" "start" "${app}"

  exit
fi

# Update an installed app
if [[ "$command" = "update" ]]; then

  must_be_installed_guard

  # Check that the app folder still exists
  # Within the associated local app repo
  if [[ ! -d "${app_repo_dir}" ]]; then
    >&2 echo "Error: Local app repo no longer exists for ${app}"
    exit 1
  fi

  echo "Updating '${app}' from: ${repo}"
  # Save current images to clean up later
  app_compose_file="${app_data_dir}/docker-compose.yml"
  app_old_images=$(yq e '.services | map(select(.image != null)) | .[].image' "${app_compose_file}")

  if [[ "$*" != *"--skip-stop"* ]]; then
    "${0}" "stop" "${app}"
  fi

  execute_hook "${app}" "pre-update"

  # App updates will only copy files from this whitelist:
  UPDATE_FILES_WHITELIST_PRE="docker-compose.yml *.template exports.sh torrc hooks"

  # We copy umbrel-app.yml after the app has started
  # That way the frontend knows the update has finished
  # And the app is running again
  UPDATE_FILES_WHITELIST_POST="umbrel-app.yml"

  copy_app_files "${UPDATE_FILES_WHITELIST_PRE}"

  # Copy remaining files in the event of error or success
  trap "copy_app_files "${UPDATE_FILES_WHITELIST_POST}"; exit $?" INT TERM EXIT

  # Source env. after new exports.sh is copied (done above via 'copy_app_files')
  source_app "${app}"

  # Now apply templates
  template_app "${app}"

  echo "Pulling images for app ${app}..."
  compose "${app}" pull

  if [[ "$*" != *"--skip-start"* ]]; then
    "${0}" "start" "${app}"
    # Remove any old images we don't need anymore
    docker rmi $app_old_images || true
  fi

  execute_hook "${app}" "post-update"

  exit
fi

# Passes all arguments to docker-compose
if [[ "$command" = "compose" ]]; then

  compose "${app}" ${args}

  exit
fi

# If we get here it means no valid command was supplied
# Show help and exit
show_help
exit 1

Hey @sasha and @umbrel101, sorry for the confusion. scripts/app has been deprecated in umbrelOS 1.0 in favor of a more robust API. To start/stop apps via CLI, you can run the following:

Stop an app:

umbreld client apps.stop.mutate --appId <app-id>

eg.

umbreld client apps.stop.mutate --appId bitcoin

And start it with:

umbreld client apps.restart.mutate --appId <app-id>
1 Like

Thank you, @mayank. Is there a new way for LNCLI, too?
For instance, I used to use this command to see pending channels on my lnd node.

~/umbrel/scripts/app docker compose lightning exec lnd lncli pendingchannels

It no longer works after I updated to 1.0.2. today (Raspberry Pi 4).
It says

This script requires “docker-compose” to be installed

Any solution? Thanks.

Sure thing! On umbrelOS 1.0, lncli can be used with:

sudo docker exec -it lightning_lnd_1 lncli

And bitcoin-cli can be used with:

sudo docker exec -it bitcoin_bitcoind_1 bitcoin-cli