Updating a container
The update command inside a container, how /usr/bin/update is written, and the checks that run before an application is updated.
Every container created by a script has an update command. Run it in the
container's console as root:
updateIt downloads the current version of the container script the container was built
with and runs its update_script function. Nothing is stored locally except a
small entry file, so the update logic is always the latest one from the
repository.
To update many containers from the host, see update-apps.
What happens when you type update
/usr/bin/updatedownloads the helpermisc/update.shfrom the engine repository and runs it.- The helper asks the website whether the script still exists.
- The helper downloads
ct/<name>.shand runs it. - The container script loads the engine.
startsees that it is inside a container and asks how to update:YES (Silent Mode),YES (Verbose Mode)orNO (Cancel Update). - The engine prepares the shell, runs the guards described below, and calls the
script's
update_script. - When the script exits with code 0, the engine cleans package and language
caches (
cleanup_lxc) and refreshes the IP address in the login banner.
The question in step 4 is skipped in three cases: PHS_SILENT=1 is set, there is
no terminal (No interactive terminal detected – defaulting to silent update mode),
or whiptail is not installed, in which case the same three choices are asked as
plain text with a 60-second timeout.
/usr/bin/update
Content
The file is generated by write_update_entrypoint in
core/core.func.
For a Gotify container built from the main repository it looks like this:
#!/usr/bin/env bash
# Community-Scripts update entrypoint (generated - do not edit by hand).
# Regenerated on install and on every successful update.
export SCRIPT_SLUG="gotify"
export UPDATE_SCRIPT_NAME="gotify"
export COMMUNITY_SCRIPTS_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
export COMMUNITY_SCRIPTS_CORE_URL="https://raw.githubusercontent.com/community-scripts/core/main"
export COMMUNITY_SCRIPTS_WEBSITE_URL="https://community-scripts.org"
# Pull the shared helper; it decides whether this app can still be updated. If it
# cannot be fetched, fall back to the legacy direct pull so `update` still works.
_cs_helper="$(curl -fsSL --connect-timeout 10 "${COMMUNITY_SCRIPTS_CORE_URL}/misc/update.sh" 2>/dev/null)"
if [[ -n "$_cs_helper" ]]; then
bash -c "$_cs_helper"
else
echo "Update helper unavailable - falling back to direct update."
bash -c "$(curl -fsSL "${COMMUNITY_SCRIPTS_URL}/ct/${UPDATE_SCRIPT_NAME}.sh")"
fi| Line | Meaning |
|---|---|
SCRIPT_SLUG | The name used to look the script up on the website |
UPDATE_SCRIPT_NAME | The ct/<name>.sh to download |
COMMUNITY_SCRIPTS_URL | The script repository the container was built from. A container built from a fork or from DevScripts keeps updating from there |
COMMUNITY_SCRIPTS_CORE_URL | The engine repository |
COMMUNITY_SCRIPTS_WEBSITE_URL | Where the status is asked |
When it is written
- At install.
customize, the second to last call of every install script, writes the file. - During an update, if it is outdated. Before the guards run,
migrate_update_entrypointlooks at the existing file. It leaves the file alone when it already usesmisc/update.sh, does not point at the retiredgit.community-scripts.orgmirror, and records a valid script name. Otherwise it rewrites the file, taking the repository URL and the script name from the old one, so the container keeps updating from the source it was built from. - By a script, when an application moves. A container script can replace the
file itself.
ct/dockge.shdoes this when Dockge is migrated to its add-on script: afterwardsupdaterunstools/addon/dockge.sh.
The file is always written to a temporary name in /usr/bin and then moved into
place. During an update /usr/bin/update is the script that is currently running,
and overwriting a running shell script in place makes the shell continue reading
at its old position in new content.
Do not edit the file by hand. If it is damaged or missing, running the container script once inside the container repairs it:
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/gotify.sh)"The helper: is the script still there?
misc/update.sh requests /api/update-info?slug=<slug> from the website and acts
on the state in the answer.
| State | Meaning | What the helper does |
|---|---|---|
active | Normal | Runs the container script |
disabled | Temporarily switched off by the maintainers | Runs the container script; the status guard below then explains and stops |
unknown | No record for this name. This includes the plain OS containers | Runs the container script |
deleted | The script was removed from the project | Prints <name>: this script has been removed from community-scripts. and the reason if one was recorded, does not download anything, and exits with 0. Installed add-ons are still offered for update |
If the website does not answer, the helper prints
Update service unreachable — continuing with a normal update. and carries on. An
outage of the website never blocks an update.
Renamed scripts
A container keeps the name it was built with, while scripts are sometimes renamed.
Before downloading, the helper checks that ct/<name>.sh exists. If it does not,
it tries the name without a leading alpine-, then without a trailing -v<number>,
and for pbs the name proxmox-backup-server. A match is announced as
Script was renamed: <old> -> <new>. If nothing matches:
No update script found for '<name>' (<repository>/ct/<name>.sh)Preparation
Before the guards and the script's own code, the engine fixes two things that commonly broke updates:
- PATH.
updateruns in a non-login shell.ensure_profile_loadedsources/etc/profile.d/*.shand root's.profileand adds/usr/local/bin, so tools installed there or under~/.cargo/binare found. - Missing toolchains. Every
setup_*helper records what it installed under/var/cache/app-versions. If a recorded tool (cargo,go,node,uv,composer,yq,ruby) is no longer there, it is installed again with<tool> is recorded for this container but missing - reinstalling.
It also tells dpkg to keep existing configuration files during package upgrades
instead of asking, which would hang without a terminal. If upstream shipped a new
version of a file you kept, the end of the update lists it:
Kept your version of these config files - upstream shipped changes to them too:Guards
Three checks run after the script is downloaded and before update_script is
called. They are part of the engine
(ui/menu.func),
so they also apply when someone runs the ct/ one-liner by hand inside a
container. A guard that stops the update ends the run with exit code 0: nothing
failed, the update was not attempted.
Disabled and deleted scripts
runtime_script_status_guard reads the script's record from the project
database.
| Record says | Output | Result |
|---|---|---|
| deleted | This script is no longer available in community-scripts. and the recorded reason, or This script was removed and cannot be installed or updated. | stops |
| disabled | This script is currently disabled in community-scripts. and the recorded reason, or Updates and installs are temporarily disabled for this script. | stops, unless var_ignore_disable is set |
| a pinned version that is the installed one | You are already on the pinned version (<version>). and the reason | continues; the script's version check then finds nothing to do |
The same guard runs on the host before an install, so a disabled script cannot be
installed either. If the database cannot be reached within three seconds the guard
prints Script status check is unavailable. Continuing without status verification.
and lets the update through.
Breaking changes
check_breaking_change_guard asks the website for active breaking-change
advisories for this script. These are the same advisories shown as a banner on the
script's page. Each one names the pull request, a summary, the merge date and, if
the author provided them, steps to take before updating.
| Situation | Interactive | Without a terminal or with PHS_SILENT=1 |
|---|---|---|
| Advisory | Dialog BREAKING CHANGE with the buttons Continue and Cancel & review. The default is to cancel | Prints Breaking change advisory for <app> and the details as warnings, then continues |
| Advisory marked as blocking | Dialog BREAKING CHANGE BLOCKS UPDATE, no way to continue | Prints Update blocked by a breaking change. and stops |
- An advisory you continued past is recorded by pull request number in
/usr/local/community-scripts/breaking-changes.seeninside the container and is not shown again. var_force_breaking_update=1lets a blocked update proceed. The advisory is still shown.var_ignore_breaking_changes=1skips the check completely.- If the website cannot be reached the check is skipped silently.
OS mismatch
check_container_os_guard compares var_os and var_version with the
container's /etc/os-release. An exact match, or a more specific version of the
same release (Alpine 3.22.1 for 3.22), passes. Otherwise there are three cases:
| Case | Dialog | Advice |
|---|---|---|
| Different distribution | WRONG OS | Create a new container on the right distribution; there is no upgrade path |
| Container older than the target | OS VERSION MISMATCH | Upgrade the container OS first, then run the update again |
| Container newer than the target | OS VERSION MISMATCH | The target was probably lowered on purpose; check the issues for the app |
The dialog offers No (cancel update), Yes (continue this time) and
Yes (continue and don't ask again). The last choice writes the accepted target,
for example debian 13, to /usr/local/community-scripts/ignore-os-mismatch.
Without a terminal the update is skipped with
Container OS <os> <version> does not match the recommended <os> <version> — skipping update.
and the command that creates that file.
One thing to know about the current engine: inside a container,
core/build.func sets an empty var_os and var_version from
/etc/os-release before the script's own var_version="${var_version:-13}" line
is read. With that usual form of the line both sides of the comparison are the
container's own values and the guard passes. It compares against a different
target only when the variables are given explicitly, for example
var_version=13 update.
After update_script
- Exit code 0: the engine's exit handler runs
cleanup_lxcand updates the IP address in the login banner. - Any other exit code: the error handler prints the failing line, the exit code with an explanation, and the output of the failing step. See Troubleshooting.
- Add-ons: add-on scripts place an
update_<name>script in/usr/local/bin. Ifupdate_scriptreturns instead of exiting, each one is offered withDo you also want to update addon "<name>"? (y/N) [60s]:. WithPHS_SILENT=1they are skipped. An add-on can always be updated on its own withbash /usr/local/bin/update_<name>.
Switches
All of these are set in front of the command, for example
var_ignore_disable=1 update. Saved defaults files are not read during an update,
and the var_ignore_* and var_force_* switches are never accepted from a file:
each one has to be asked for on the run that needs it.
| Variable | Effect |
|---|---|
PHS_SILENT=1 | No questions: silent mode, menus take their default, add-on updates are skipped |
var_ignore_disable=1 | Update although the script is disabled |
var_ignore_breaking_changes=1 | Skip the breaking-change check |
var_force_breaking_update=1 | Continue past a blocking advisory |
var_ignore_os_mismatch=1 | Continue although the OS does not match, with a warning |
var_os, var_version | The target the OS guard compares against |
var_cpu, var_ram | The requirement check_container_resources compares against. var_ram=1024 update lowers it for one run |
var_github_token or GITHUB_TOKEN | A GitHub token for the release check, see Troubleshooting |
The values 1, yes, true and on are accepted for the var_ignore_* and
var_force_* switches.
In an unattended update the two questions an update function usually asks are answered with "no": a container with less memory or fewer cores than required ends with exit code 113, and one whose root disk is more than 80 % full with 114.