Runtime on host and in the container
What runs on the host and what runs inside the container during an install and an update, in order.
One command on the host produces a finished container, but the work is split between two places. The ct script runs on the host and creates the container. The install script runs inside the container and sets up the application. This page lists both halves in the order they happen, for a Proxmox VE host. Incus follows the same outline with its own backend; see Platforms.
The examples use
ct/vaultwarden.sh
and
install/vaultwarden-install.sh.
The two scripts
The ct script ends with three calls. Everything before them only sets
variables and defines update_script:
start
build_container
descriptionThe install script is framed by engine calls, with the application's own steps in between:
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
# application steps
motd_ssh
customize
cleanup_lxcPhase 1: host, before the container exists
| Step | Code | What happens |
|---|---|---|
| 1 | loader lines | core/build.func is sourced; it starts host probes, prefetches the engine, detects the platform and loads the UI and the Proxmox VE backend. See Loading |
| 2 | script header | APP, var_tags, var_cpu, var_ram, var_disk, var_os, var_version, var_unprivileged and others get their defaults |
| 3 | header_info | prints the banner |
| 4 | variables | derives NSAPP and var_install, creates the session ID, sets BUILD_LOG=/tmp/create-lxc-<SESSION_ID>.log, parses dev_mode, remembers the script's CPU, RAM and disk values |
| 5 | color, catch_errors | colours and the error traps |
| 6 | start | loads lib/tools.func. On a Proxmox VE host it asks db.community-scripts.org whether the script is disabled or deleted and then calls install_script |
| 7 | install_script checks | shell_check, root_check, pve_check, ensure_whiptail, arch_check, arm64_notice, ssh_check, maxkeys_check, storage_content_check, diagnostics_check |
| 8 | install_script menu | reads the next free container ID and the host timezone, shows the dev-mode picker if requested, then the menu: Default Install, Advanced Install, User Defaults, App Defaults for the app (only if such a file exists) and Settings. mode= on the command line selects an entry without showing the menu |
Nothing has been created at this point. Cancelling a dialog exits without leaving anything behind. The menu and its settings are described in Wizard.
Phase 2: host, creating the container
All of this is build_container in
pve/backend.func.
| Step | What happens |
|---|---|
| 1 | lxc_os_support_check: can this host's LXC stack run the chosen OS release |
| 2 | The network string is built from bridge, MAC, IP, gateway, VLAN, MTU and IPv6 settings. A bridge that does not exist on the node ends the run with exit 208 |
| 3 | The feature list is built: nesting=1 unless disabled, keyctl=1 for unprivileged containers, fuse=1, mknod=1 and mount= when requested |
| 4 | lxc/install.func is read into FUNCTIONS_FILE_PATH, and the settings the container needs are exported: app, APPLICATION, PASSWORD, VERBOSE, SSH_ROOT, CTID, CTTYPE, PCT_OSTYPE, PCT_OSVERSION, PCT_ARCH, tz, SESSION_ID, INSTALL_LOG, both source URLs, MODE and the DEV_MODE_* flags |
| 5 | validate_ct_settings, then the pct create options are assembled. The root password is passed as a separate argument |
| 6 | create_lxc_container: telemetry reports the start of the install, storages and (on clusters) quorum are validated, the template is found locally or downloaded, and pct create runs |
| 7 | GPU and USB passthrough entries are written to the container's config if requested |
| 8 | pct start. If the container does not start: exit 227; if it does not reach running: exit 117 |
| 9 | The host waits for an IP address on eth0 (exit 118 if none appears) and tests connectivity from inside the container |
| 10 | "Customizing container": proxy settings and host CA certificates are applied, then base packages are installed with pct exec. Alpine gets bash newt curl openssh nano mc ncurses jq; Debian, Ubuntu and Devuan get sudo curl mc gnupg2 jq after locale and timezone are set; other distributions get curl if it is missing |
| 11 | SSH keys chosen in the wizard are copied into the container |
| 12 | install/<app>-install.sh is read from the scripts root. If it cannot be fetched: exit 115 |
| 13 | The install script is started with lxc-attach -n <CTID> -- bash -c "<script>" |
Steps 9 to 11 are host commands that act on the container through
pct exec. The install script in step 13 is the first code of the scripts
repository that runs inside the container.
lxc-attach passes the host's exported environment into the container. That
is how the install script receives FUNCTIONS_FILE_PATH and its settings, and
why any var_* variable you exported on the command line is visible in the
install script too.
Phase 3: inside the container
| Step | Code | What happens |
|---|---|---|
| 1 | source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" | lxc/install.func runs its bootstrap: makes sure curl exists, marks the context as container, loads core/core.func, api/exitcodes.func, api/errorlog.func and core/error_handler.func, calls load_functions, catch_errors and get_lxc_ip, then detect_os |
| 2 | color | colours again, for the script's own use |
| 3 | verb_ip6 | applies verbose mode; disables IPv6 through sysctl when that was chosen |
| 4 | catch_errors | error traps for the install script |
| 5 | setting_up_container | fixes ownership and mode of / and /etc where needed, writes /etc/timezone if missing, waits for an IP address (10 tries, 3 seconds apart, then exit 1), removes Python's EXTERNALLY-MANAGED marker, sets a C.UTF-8 locale on apt systems |
| 6 | network_check | tests IPv4 and IPv6 connectivity and that github.com, raw.githubusercontent.com and api.github.com resolve. If the apt mirrors do not resolve it writes 8.8.8.8 and 1.1.1.1 to /etc/resolv.conf |
| 7 | update_os | applies proxy and APT cacher settings, updates and upgrades the OS packages, then loads lib/tools.func (lib/alpine.func on Alpine) |
| 8 | application steps | the script's own commands, usually msg_info / $STD … / msg_ok blocks and setup_* helpers. Scripts that support several OS families define setup_deb_based, setup_alpine and so on and call run_os_setup |
| 9 | motd_ssh | login banner; SSH server and root login when selected |
| 10 | customize | console autologin when no password was set, /usr/bin/update, authorized SSH key |
| 11 | cleanup_lxc | package and tool caches are removed |
Output of commands run through $STD goes to
/root/.install-<SESSION_ID>.log inside the container. Messages appear on your
terminal because lxc-attach is connected to it.
The container sends telemetry progress pings during these steps
(post_progress_to_api in setting_up_container, update_os, motd_ssh
and customize), but only when diagnostics are enabled. It never
sends the final status. The reference for the functions is in
core.func and Containers.
What the install script cannot do
- Ask questions. Its stdin is not a terminal and
MODEmarks the run as unattended, soprompt_*functions return their defaults. Take input fromvar_*variables. - Reach the host. There is no
pctand no access to host files. - See the host's engine directory. It downloads the engine files it needs
from
COMMUNITY_SCRIPTS_CORE_URL.
Phase 4: host, after the install
| Step | What happens |
|---|---|
| 1 | lxc-attach returns. The host removes the .pid and .done markers and looks for /root/.install-<SESSION_ID>.failed in the container |
| 2 | On failure: combined log, telemetry report and the recovery menu |
| 3 | On success build_container returns and the ct script calls description |
| 4 | description reads the container's IP address, writes the notes shown in the Proxmox VE interface with pct set <CTID> -description, runs the host hook from var_post_install if one is configured, and sends the final done status to telemetry |
| 5 | The last lines of the ct script print the success message and the URL of the application |
Closing the console
During phase 3 the install does not depend on your terminal. If you close the
console, the host and the container both move their output to their log files
and the install finishes; the host waits for the container's .done or
.failed marker. Ctrl-C aborts and stops the container. Details are in
Error handling.
The update run
Running the same ct script inside the container, which is what update does,
takes a different path through the same files. The loader lines detect the
platform container, and start goes to the update branch instead of
install_script:
| Step | What happens |
|---|---|
| 1 | The update mode is chosen: a whiptail menu with YES (Silent Mode), YES (Verbose Mode) and NO (Cancel Update). Without a terminal, or with PHS_SILENT=1, it runs silently without asking |
| 2 | ensure_profile_loaded, ensure_recorded_toolchains, get_lxc_ip, migrate_update_entrypoint |
| 3 | Three guards that can stop the update: whether the script is disabled or deleted, whether the container still runs the OS release the script expects, and breaking-change advisories |
| 4 | apt_conffile_guard |
| 5 | Your update_script |
| 6 | Add-on update scripts found as /usr/local/bin/update_* are offered (skipped with PHS_SILENT=1) |
| 7 | On exit with status 0: cleanup_lxc and a refreshed MOTD address |
build_container and description are never reached in a container, because
start exits when the update is done. See Updates.
Where each engine file runs
| File | Host, create | Container, install | Container, update |
|---|---|---|---|
core/build.func | yes | no | yes |
ui/*.func | yes | no | yes |
pve/backend.func | yes | no | loaded, unused |
api/api.func (telemetry) | yes | no | yes |
core/core.func | yes | yes | yes |
core/error_handler.func | yes | yes | yes |
api/exitcodes.func, api/errorlog.func | yes | yes | yes |
lxc/install.func | as text only | yes | no |
lib/tools.func | yes (loaded by start) | yes | yes |