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
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: