Bootstrap Flow
What the VPS-Coolify init YAML does
prepare-vps-coolify-init.sh or prepare-vps-coolify-init.ps1 renders bootstrap-artifacts/vps-coolify-init.generated.yml from:
templates/vps-init.template.yml(VPS init template)bootstrap-artifacts/bootstrap.env
On first boot, the VPS init agent:
- sets the timezone and runs package update/upgrade
- creates initial users (
DEVOPS_USER,COOLIFY_SUDO_NOPASSWD_USER) and sets SSH bootstrap key forDEVOPS_USER(additional users fromADDITIONAL_SUDO_USERSare created later bybootstrap-host.sh) - disables root SSH login and SSH password auth
- installs baseline packages (
curl,git,openssl,ufw,fail2ban,unattended-upgrades, …) - writes hardening and runtime files
- clones bootstrap repo at selected URL/ref
- applies
sysctl --system - runs
scripts/bootstrap-host.sh
First-boot execution order
flowchart TD
A["Prepare env + secrets"] --> B["Render VPS-Coolify init"]
B --> C["Provision Ubuntu 24 VPS"]
C --> D["VPS first boot: run VPS init user-data"]
D --> E["Install packages + write baseline files"]
E --> F["Clone BOOTSTRAP_REPO_URL at BOOTSTRAP_REPO_REF"]
F --> G["Apply kernel/network sysctl profile (sysctl --system)"]
G --> H["Run scripts/bootstrap-host.sh"]
H --> H1["Re-validate runtime inputs on VPS"]
H1 --> I["Ensure users + SSH keys (including COOLIFY_SUDO_NOPASSWD_USER)"]
I --> J["Run ensure-user-passwords.sh"]
J --> K["Set passwords for locked/unset users or users missing from vault"]
K --> L["Write encrypted vault /etc/vps-coolify-bootstrap/user-passwords.enc"]
L --> M["Sync SSH AllowUsers from effective managed users"]
M --> N["Disable ssh.socket, disable legacy Port directives, validate sshd, restart ssh.service, enforce only SSH_PORT (no :22 listener)"]
N --> O["Apply UFW rules and enable fail2ban + unattended-upgrades"]
O --> P["Install Coolify if missing"]
P --> P1["Ensure Coolify root account exists (RootUserSeeder + DB check)"]
P1 --> R["Apply groups + sudo policy"]
R --> Q["Sync localhost-only Coolify SSH user + restricted key + SSH port"]
Q --> Q1["Sync realtime host env from effective realtime domain (COOLIFY_REALTIME_DOMAIN or COOLIFY_PUBLIC_DOMAIN fallback)"]
Q1 --> S["Sync DOCKER-USER guards for 6001/6002 from CLOSE_COOLIFY_REALTIME_PORTS"]
S --> T["SSH login on hardened port"]
T --> U["Finish Coolify onboarding"]
Important: ensure-user-passwords.sh runs on the VPS host during bootstrap/replay. User account passwords are not pre-generated locally during env preparation.
Why validation appears twice
Validation is intentionally done in two stages:
- local render validation (
prepare-vps-coolify-init.*) before generating the YAML - server runtime validation (
bootstrap-host.sh) before applying privileged changes
This is defense-in-depth. Even if a file is edited manually on the VPS after provisioning, runtime checks still block unsafe/invalid values.
Coolify localhost SSH hardening
COOLIFY_SUDO_NOPASSWD_USER is secured for localhost service use:
- the operator SSH key (
SSH_PUBLIC_KEY) is not kept in this user’sauthorized_keys - bootstrap generates a dedicated key pair under
/data/coolify/ssh/keys/ - the dedicated public key is installed with
from="..."restriction, limited to localhost/private ranges used by Docker - Coolify server id
0is synchronized to this user +SSH_PORTonhost.docker.internal
Result: this user is not intended for direct public SSH access from the internet.
Accepted configuration types
Bootstrap accepts multiple configuration types from bootstrap.env. The same values are validated in both local render scripts (prepare-vps-coolify-init.*) and server runtime (bootstrap-host.sh + strict env loader).
1) Env line format types
Accepted key/value line styles:
KEY=value(unquoted, no whitespace in value)KEY='value'(single-quoted literal)KEY="value"(double-quoted; escaped\\,\",\$are supported)export KEY=value(server-side strict loader supports optionalexport)
Rejected by strict loader:
- invalid lines without
= - unquoted whitespace in unquoted values
- shell expansion syntax in unquoted values (for example
$(...),${...}, backticks)
2) Path configuration types
Path-like keys:
SSH_PUBLIC_KEY_PATHTEMPLATE_FILEOUTPUT_FILE
Behavior:
- absolute paths are used as-is
- relative paths are resolved against the env file directory
~and~/...are resolved to the current user home- missing files fail fast during local render
Default paths:
- template:
../templates/vps-init.template.yml - output:
../bootstrap-artifacts/vps-coolify-init.generated.yml
3) Numeric configuration types
SSH_PORTmust be numeric and in range1..65535- non-numeric values are rejected
- value is applied to SSH hardening config, service restart, and Coolify localhost sync
4) Boolean/toggle configuration types
SSH_KEY_ROTATE: accepts0or10: append key toauthorized_keys(default)1: replaceauthorized_keyswith current key
CLOSE_COOLIFY_REALTIME_PORTS: acceptstrue/falseor1/0false: removeDOCKER-USERguards for6001/6002true: addDOCKER-USERguards and require an effective realtime domain (COOLIFY_REALTIME_DOMAINorCOOLIFY_PUBLIC_DOMAINfallback)
DOCKER_DISABLE_IPV6_FOR_PARSEADDR_FIX: acceptstrue/falseor1/0true(default): auto-apply Docker daemon workaround ("ipv6": false) whenStart ProxyParseAddr(".../64")risk is detectedfalse: disable automatic workaround
Legacy compatibility:
- if
CLOSE_COOLIFY_REALTIME_PORTSis unset, legacyALLOW_PUBLIC_COOLIFY_REALTIME_PORTSis mapped0->true1->false
5) UNIX username scalar types
Username keys:
DEVOPS_USERCOOLIFY_SUDO_NOPASSWD_USER
Accepted format:
- regex
^[a-z_][a-z0-9_-]*[$]?$ rootis explicitly forbiddenDEVOPS_USERandCOOLIFY_SUDO_NOPASSWD_USERmust be different
Notes:
DEVOPS_USERdefaults todevopsCOOLIFY_SUDO_NOPASSWD_USERdefaults tocoolify- effective managed users are built as:
DEVOPS_USERCOOLIFY_SUDO_NOPASSWD_USER- users from
ADDITIONAL_SUDO_USERS
6) User-list configuration types
List key:
ADDITIONAL_SUDO_USERS
Accepted format:
- usernames separated by space, comma, or semicolon
- surrounding whitespace is trimmed per item
:is not allowed in usernamesrootis not allowedCOOLIFY_SUDO_NOPASSWD_USERis not allowed in this list
Runtime behavior:
- each effective managed user is ensured to exist
- each effective managed user is added to
sudo,docker, andcoolifygroups
7) SSH public key configuration types
Keys:
SSH_PUBLIC_KEY
Accepted format:
- OpenSSH public key, starting with
ssh-ed25519,ssh-rsa, orssh-ecdsa-*
Resolution rules:
- Use
SSH_PUBLIC_KEYif already set and valid. - Else read first line from
SSH_PUBLIC_KEY_PATH. - If neither is available, generation continues with warnings; no
ssh_authorized_keysis injected forDEVOPS_USER, bootstrap host run does not fail on missingSSH_PUBLIC_KEY, and the operator must use an alternate first-access path.
generate-secrets.* helper behavior:
- auto-detects local
~/.ssh/*.pubkeys - fills
SSH_PUBLIC_KEYandSSH_PUBLIC_KEY_PATHwhen placeholders/empty values are present
8) Domain/email/string scalar types
COOLIFY_PUBLIC_DOMAIN: hostname, no whitespace or/COOLIFY_REALTIME_DOMAIN: hostname, no whitespace or/COOLIFY_ROOT_USER_EMAIL: basic email format validationCOOLIFY_ROOT_USERNAME: regex^[A-Za-z0-9._-]+$BOOTSTRAP_REPO_URL,BOOTSTRAP_REPO_REF: required strings used for first-boot clone
Template safety constraint:
- values injected into template must not contain single quotes (
')
9) Password/secret configuration types
COOLIFY_ROOT_USER_PASSWORD: minimum 16 chars and must include lowercase, uppercase, digit, and symbolUSER_PASSWORDS_ENCRYPTION_PASSWORD: minimum 16 chars
Generation behavior:
- local generation by
generate-secrets.*only when empty/placeholder - root password: strong 24-char value with lowercase, uppercase, digit, and symbol
- encryption password:
openssl rand -hex 16(32 hex chars)
Runtime password vault behavior:
ensure-user-passwords.shruns on VPS host during bootstrap/replay- sets passwords only when needed:
- account is locked/unset in
/etc/shadow(hash empty or starts with!/*) - or account has no stored entry yet in encrypted vault
- account is locked/unset in
- if account already has usable password and has a vault entry, password is kept unchanged
- stores encrypted vault at
/etc/vps-coolify-bootstrap/user-passwords.enc
10) Realtime policy pair (cross-field dependency)
Dependency rule:
- when
CLOSE_COOLIFY_REALTIME_PORTS=true, effective realtime domain is:COOLIFY_REALTIME_DOMAINwhen set- otherwise
COOLIFY_PUBLIC_DOMAIN(fallback)
- when
COOLIFY_REALTIME_DOMAINis set, it must not containCHANGE_ME
Runtime sync behavior:
- if effective realtime domain is available (
COOLIFY_REALTIME_DOMAINor fallbackCOOLIFY_PUBLIC_DOMAIN):- write
PUSHER_HOST=<domain> - write
PUSHER_PORT=443 - write
PUSHER_SCHEME=https
- write
- if both are empty:
- remove those keys from Coolify
.env
- remove those keys from Coolify
- bootstrap syncs host-level
DOCKER-USERguards only for realtime6001/6002 - this
PUSHER_*synchronization is independent fromCLOSE_COOLIFY_REALTIME_PORTSand is applied even whenCLOSE_COOLIFY_REALTIME_PORTS=false - meaning of each mode:
CLOSE_COOLIFY_REALTIME_PORTS=false+ domain set: app points realtime tohttps://<domain>:443, while direct public6001/6002may still be reachableCLOSE_COOLIFY_REALTIME_PORTS=true: app points realtime tohttps://<effective-domain>:443(COOLIFY_REALTIME_DOMAINorCOOLIFY_PUBLIC_DOMAINfallback), and public6001/6002is blocked viaDOCKER-USERguards
11) Placeholder and final render constraints
Local render fails if:
- required values are missing
- required values still contain
CHANGE_ME - unreplaced
_HEREtokens remain in template output - output file exceeds size limit (Hetzner user-data limit: 32768 bytes)
Runtime outputs
- Coolify onboarding URL printed by bootstrap:
http://<your-server-ip>:8000 - Final URL after onboarding/domain setup:
https://<COOLIFY_PUBLIC_DOMAIN> - Encrypted credential vault:
/etc/vps-coolify-bootstrap/user-passwords.enc
To decrypt on the server (must be run as DEVOPS_USER or COOLIFY_SUDO_NOPASSWD_USER, both passwordless sudo by policy), use the recommended sequence below:
export USER_PASSWORDS_ENCRYPTION_PASSWORD="$(
sudo sed -n "s/^USER_PASSWORDS_ENCRYPTION_PASSWORD=//p" /etc/vps-coolify-bootstrap/bootstrap.env | tr -d "'\r"
)"
sudo env USER_PASSWORDS_ENCRYPTION_PASSWORD="$USER_PASSWORDS_ENCRYPTION_PASSWORD" \
openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \
-in /etc/vps-coolify-bootstrap/user-passwords.enc \
-pass env:USER_PASSWORDS_ENCRYPTION_PASSWORD
Note: other sudo users require their password to run sudo, but their password is inside this vault. Only DEVOPS_USER or COOLIFY_SUDO_NOPASSWD_USER (passwordless sudo) or root via provider console can decrypt it.
Back to Docs Home