Operations and Security
This page covers post-bootstrap operational tasks. Read the sections relevant to your current task (user policy, replay, hardening, updates, monitoring).
User and group policy
env/bootstrap.env.example now defines user policy by role:
DEVOPS_USER(defaultdevops)COOLIFY_SUDO_NOPASSWD_USER(defaultcoolify)ADDITIONAL_SUDO_USERS(optional list; separators: space, comma, or semicolon)
Effective managed users are: DEVOPS_USER + COOLIFY_SUDO_NOPASSWD_USER + ADDITIONAL_SUDO_USERS.
bootstrap-host.sh enforces this at runtime:
- each managed user is created if missing
- each managed user is added to
sudo,docker, andcoolify - usernames must match
^[a-z_][a-z0-9_-]*[$]?$ rootis forbidden forDEVOPS_USER,COOLIFY_SUDO_NOPASSWD_USER, and inADDITIONAL_SUDO_USERSDEVOPS_USERandCOOLIFY_SUDO_NOPASSWD_USERmust be differentADDITIONAL_SUDO_USERSmust not containCOOLIFY_SUDO_NOPASSWD_USER
More than two sudo users
Example:
DEVOPS_USER=devops
COOLIFY_SUDO_NOPASSWD_USER=coolify
ADDITIONAL_SUDO_USERS=admin ops;dev
Apply path depends on server lifecycle:
- for a new VPS not yet provisioned: re-render VPS-Coolify init and provision with the new file
- for an existing VPS already running: update server
/etc/vps-coolify-bootstrap/bootstrap.envand replay bootstrap Other required variables are omitted for brevity; keep requiredCHANGE_MEvalues (domain, credentials, encryption password, SSH key) fully configured.
Important: users listed in ADDITIONAL_SUDO_USERS can SSH only after bootstrap-host.sh completes successfully and re-syncs AllowUsers from the effective managed user set.
First-login password hardening
On the first SSH login as DEVOPS_USER, set a local account password:
sudo passwd <DEVOPS_USER>
Replace <DEVOPS_USER> with the value from /etc/vps-coolify-bootstrap/bootstrap.env.
DEVOPS_USER has passwordless sudo for operations, but setting a local password is still required for emergency/recovery flows (for example provider console access when SSH key auth is unavailable).
Password vault access
Generated user passwords are stored encrypted at /etc/vps-coolify-bootstrap/user-passwords.enc. Decrypting requires sudo.
DEVOPS_USER and COOLIFY_SUDO_NOPASSWD_USER have passwordless sudo (NOPASSWD:ALL). Other sudo users need their password to run sudo — but their password is inside the vault.
To retrieve passwords for other users, log in as DEVOPS_USER or COOLIFY_SUDO_NOPASSWD_USER and run this recommended sequence (reads the exact password from server bootstrap.env and preserves it through sudo):
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
Alternative: use the provider web console as root.
Using coolify as the managed SSH user
Set COOLIFY_SUDO_NOPASSWD_USER=coolify (default) in server-side /etc/vps-coolify-bootstrap/bootstrap.env, then run replay:
sudo bash /opt/vps-coolify-bootstrap/scripts/bootstrap-host.sh /etc/vps-coolify-bootstrap/bootstrap.env
Bootstrap now syncs Coolify localhost server connection settings automatically:
- server host ->
host.docker.internal - server user ->
COOLIFY_SUDO_NOPASSWD_USER - server port ->
SSH_PORT - localhost private key ->
/data/coolify/ssh/keys/id.<COOLIFY_SUDO_NOPASSWD_USER>@host.docker.internal authorized_keysentry for that key is restricted withfrom="..."to localhost/private ranges- operator key (
SSH_PUBLIC_KEY) is not kept for this user
If UI still shows drift, run replay again and verify with:
sudo bash /opt/vps-coolify-bootstrap/scripts/verify-bootstrap-state.sh /etc/vps-coolify-bootstrap/bootstrap.env
Validation command:
sudo -u coolify -H bash -lc 'sudo -n true && echo OK_NOPASSWD'
Temporary/manual override (when replay is not yet possible):
sudo tee /etc/sudoers.d/zz-coolify-nopasswd >/dev/null <<'EOF'
coolify ALL=(ALL:ALL) NOPASSWD:ALL
EOF
sudo chmod 440 /etc/sudoers.d/zz-coolify-nopasswd
sudo visudo -c
Why zz- prefix: bootstrap writes /etc/sudoers.d/99-bootstrap-sudo-policy. Files loaded later can override prior sudo tag behavior (PASSWD/NOPASSWD).
Bootstrap env reference
Use this section when you need detailed runtime behavior for bootstrap.env variables beyond the quick reference in Getting Started.
A) Auto-resolved on host
DEVOPS_USER- When: bootstrap/replay runtime, before sudo policy is written
- How: defaults to
devopswhen unset - Required: NO
COOLIFY_SUDO_NOPASSWD_USER- When: bootstrap/replay runtime
- How: defaults to
coolify; auto-added to managed user/group lists and passwordless sudo policy - Required: NO
SSH_KEY_ROTATE- When: runtime during SSH key synchronization
- How: applies to
DEVOPS_USER+ADDITIONAL_SUDO_USERS; default0appends key,1replaces theirauthorized_keys; does not apply toCOOLIFY_SUDO_NOPASSWD_USER - Required: NO
- Example:
SSH_KEY_ROTATE=0: keep existing keys and ensure currentSSH_PUBLIC_KEYis present forDEVOPS_USER+ADDITIONAL_SUDO_USERSSSH_KEY_ROTATE=1: replace keys forDEVOPS_USER+ADDITIONAL_SUDO_USERSwith currentSSH_PUBLIC_KEYduring replay/boot
CLOSE_COOLIFY_REALTIME_PORTS- When: runtime during
DOCKER-USERguard sync - How: default
falsekeeps ports public;trueadds guards to block public ingress to6001/6002 - Required: NO
- When: runtime during
DOCKER_DISABLE_IPV6_FOR_PARSEADDR_FIX- When: runtime after Coolify install check (during bootstrap/replay)
- How: default
true; when vulnerable Docker behavior is detected (ParseAddr(".../64")risk), bootstrap writes Docker daemon"ipv6": false, restarts Docker, and waits forcoolifycontainer recovery - Required: NO
B) Coolify admin variables
COOLIFY_PUBLIC_DOMAIN- When: used by bootstrap output and onboarding flow
- How: validated as hostname
- Required: YES
COOLIFY_ROOT_USERNAME- When: passed to Coolify installer
- How: installer environment variable
- Required: YES
COOLIFY_ROOT_USER_EMAIL- When: passed to Coolify installer and used as login identifier
- How: installer environment variable, email format validated
- Required: YES
COOLIFY_ROOT_USER_PASSWORD- When: local generation before provisioning if placeholder/empty
- How: AUTO-GENERATED by
generate-secrets.*only when value is empty/CHANGE_ME(24 chars with lowercase, uppercase, digit, and symbol) - Required: YES (effective value required for bootstrap; auto-generation satisfies requirement when placeholder/empty)
C) Server user variables
SSH_PUBLIC_KEY/SSH_PUBLIC_KEY_PATH- When: local preparation step and host bootstrap key installation
- How: AUTO-DETECTED if a valid key exists on your machine (
~/.ssh/*.pub);generate-secrets.*fillsSSH_PUBLIC_KEYandSSH_PUBLIC_KEY_PATHwhen placeholders are present - Required: NO for bootstrap execution; YES recommended for direct SSH key-based first access
COOLIFY_REALTIME_DOMAIN- When: runtime when value is set
- How: written as
PUSHER_HOST,PUSHER_PORT=443, andPUSHER_SCHEME=httpsin/data/coolify/source/.envwhenever value is set; whenCLOSE_COOLIFY_REALTIME_PORTS=trueand this value is empty, bootstrap falls back toCOOLIFY_PUBLIC_DOMAIN - Required: NO (fallback exists via
COOLIFY_PUBLIC_DOMAINin closed mode)
SSH_PORT- When: bootstrap/replay SSH hardening
- How: applied via
sshd_config.dand service restart - Required: NO
ADDITIONAL_SUDO_USERS- When: runtime user/group reconciliation
- How: optional list separated by space/comma/semicolon; each user is validated and merged into effective managed users
- Required: NO unless team model differs
TIMEZONE- When: early VPS init phase
- How: applied as system timezone
- Required: NO
D) Generated passwords and secrets
USER_PASSWORDS_ENCRYPTION_PASSWORD- When: local generation before provisioning if placeholder/empty
- How: AUTO-GENERATED by
generate-secrets.*only when value is empty/CHANGE_ME(openssl rand -hex 16in Bash) - Required: YES (effective value required for bootstrap; auto-generation satisfies requirement when placeholder/empty)
- account passwords for managed users (
DEVOPS_USER,COOLIFY_SUDO_NOPASSWD_USER,ADDITIONAL_SUDO_USERS)- When: during bootstrap/replay runtime on the VPS host in
ensure-user-passwords.sh - How: not pre-generated locally; password is generated only when account is locked/unset (
/etc/shadowempty hash or prefixed!/*) or when vault has no entry for that user; unlocked accounts already present in vault are not rotated; then vault is encrypted to/etc/vps-coolify-bootstrap/user-passwords.enc - Required: YES for
DEVOPS_USERon first login (sudo passwd "$(whoami)")
- When: during bootstrap/replay runtime on the VPS host in
Replay bootstrap policy (idempotent)
Run:
sudo bash /opt/vps-coolify-bootstrap/scripts/bootstrap-host.sh /etc/vps-coolify-bootstrap/bootstrap.env
Use replay to re-apply baseline policy from server-side bootstrap.env without reprovisioning.
When to run replay:
- after changing policy values (
SSH_PORT,DEVOPS_USER,COOLIFY_SUDO_NOPASSWD_USER,ADDITIONAL_SUDO_USERS) - after partial first-boot execution
- after emergency manual fixes that may have introduced drift
- after updating bootstrap scripts and wanting to apply new safeguards
What replay does not do:
- it does not deploy application workloads
- it does not remove Docker volumes/databases
- it does not replace SSH keys unless
SSH_KEY_ROTATE=1 - it does not rotate already-set (unlocked) account passwords; it only sets passwords for locked/unset accounts
- it does not pre-generate server user account passwords locally before bootstrap
What replay enforces:
- SSH hardening (
sshd_config,AllowUsers, service state) - sudo policy (
DEVOPS_USERandCOOLIFY_SUDO_NOPASSWD_USERpasswordless by default) - user/group memberships (
sudo,docker,coolify) - on-host password generation for locked/unset managed users (during bootstrap/replay) and encrypted vault update
- UFW baseline (
SSH_PORT,80,443) fail2banandunattended-upgrades- Coolify root account seeding/verification (
RootUserSeeder+ DB check) - Coolify localhost connection user/port/private-key synchronization (
COOLIFY_SUDO_NOPASSWD_USER,SSH_PORT) - realtime host env synchronization (
PUSHER_HOST,PUSHER_PORT,PUSHER_SCHEME) from effective realtime domain (COOLIFY_REALTIME_DOMAINorCOOLIFY_PUBLIC_DOMAINfallback) - when
CLOSE_COOLIFY_REALTIME_PORTS=true, enforcesDOCKER-USERpublic-drop guards for6001/6002 - when
DOCKER_DISABLE_IPV6_FOR_PARSEADDR_FIX=true, applies Docker daemon workaround for knownStart ProxyParseAddr(".../64")failures when risk is detected
Operational notes:
- run replay as
DEVOPS_USER,COOLIFY_SUDO_NOPASSWD_USER, or root via provider console - replay resets UFW baseline; re-apply custom rules after replay
- replay restarts SSH service; keep provider console open
- replay enforces single SSH port policy (
SSH_PORTonly): disables legacyPortdirectives and fails if:22still listens
Quick verification after replay:
sudo systemctl is-active ssh.service fail2ban unattended-upgrades
sudo ufw status verbose
sudo ss -lntp | grep -E ':(22|6001|6002|8000)\b' || true
sudo iptables -S DOCKER-USER | grep -E '6001|6002' || true
sudo bash /opt/vps-coolify-bootstrap/scripts/verify-bootstrap-state.sh /etc/vps-coolify-bootstrap/bootstrap.env
UFW SSH policy detail:
- public SSH remains
LIMIT INonSSH_PORT - bootstrap also adds explicit
ALLOW INrules toSSH_PORTfor localhost/private ranges (127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,100.64.0.0/10,::1,fc00::/7,fe80::/10) - this prevents Coolify localhost validation traffic (from Docker bridge networks) from being rejected by SSH rate limiting
Docker ParseAddr workaround
Symptom in Coolify UI (Start Proxy):
ParseAddr(".../64"): unexpected character, want colon
Cause:
- known Docker Engine IPv6 gateway formatting behavior in vulnerable versions can return gateway values with CIDR suffix (
/64) that break proxy startup path parsing.
Bootstrap behavior:
- default
DOCKER_DISABLE_IPV6_FOR_PARSEADDR_FIX=true - if vulnerable behavior is detected, bootstrap updates
/etc/docker/daemon.jsonwith:"ipv6": false
- then restarts Docker and waits for Coolify container recovery
Manual verification:
docker version
sudo docker info --format ''
sudo docker network ls -q | xargs -r -n1 sudo docker network inspect --format '' | grep -E ':[0-9a-fA-F:]+/[0-9]+' || true
If workaround is disabled (DOCKER_DISABLE_IPV6_FOR_PARSEADDR_FIX=false), fix manually:
sudo python3 - <<'PY'
import json, os
p="/etc/docker/daemon.json"
d={}
if os.path.exists(p) and os.path.getsize(p)>0:
with open(p, "r", encoding="utf-8") as f:
d=json.load(f)
d["ipv6"]=False
with open(p, "w", encoding="utf-8") as f:
json.dump(d, f, indent=2)
f.write("\n")
print("updated", p)
PY
sudo systemctl restart docker
Post-onboarding security (required)
Docker-published ports can bypass UFW because Docker writes iptables rules directly. Do not assume UFW alone blocks container -p host:container exposure.
Validate exposed ports:
sudo ss -tulpen
sudo docker ps --format 'table \t'
Pay special attention to Coolify internal ports 8000, 6001, and 6002.
Bootstrap behavior:
- if
CLOSE_COOLIFY_REALTIME_PORTS=true, bootstrap addsDOCKER-USERguard rules to block public ingress to6001/6002 - if
CLOSE_COOLIFY_REALTIME_PORTS=false(default), bootstrap removes only6001/6002guards - if
COOLIFY_REALTIME_DOMAINis set, bootstrap always configures app-level realtime routing viaPUSHER_HOST=<domain>,PUSHER_PORT=443,PUSHER_SCHEME=https(even whenCLOSE_COOLIFY_REALTIME_PORTS=false) - if
COOLIFY_REALTIME_DOMAINis empty andCLOSE_COOLIFY_REALTIME_PORTS=true, bootstrap configures app-level realtime routing usingCOOLIFY_PUBLIC_DOMAIN - bootstrap does not force-close
8000; official Coolify onboarding remains available onhttp://<server-ip>:8000until domain proxy is configured in UI
Operational interpretation:
CLOSE_COOLIFY_REALTIME_PORTS=false+COOLIFY_REALTIME_DOMAINset means “domain routing enabled, direct6001/6002still reachable”.CLOSE_COOLIFY_REALTIME_PORTS=truemeans “domain routing enabled on effective realtime domain (COOLIFY_REALTIME_DOMAINorCOOLIFY_PUBLIC_DOMAINfallback), direct public6001/6002blocked byDOCKER-USERguards”.8000behavior follows official Coolify onboarding flow (initial access on:8000, domain/TLS after UI configuration).- Domain routing on
443is application-level behavior fromPUSHER_*values, not an automatic host-level NAT redirect.
Fast mode-switch helper:
sudo bash /opt/vps-coolify-bootstrap/scripts/update-realtime-mode.sh --mode public --clear-domain
sudo bash /opt/vps-coolify-bootstrap/scripts/update-realtime-mode.sh --mode closed --domain realtime.example.com
Detailed behavior and mode-by-mode guidance: VPS Coolify Realtime Modes
CLOSE_COOLIFY_REALTIME_PORTS=true exact enforcement path:
- Validation stage resolves effective realtime domain:
COOLIFY_REALTIME_DOMAINwhen set- otherwise
COOLIFY_PUBLIC_DOMAIN
- Bootstrap writes realtime app endpoint in Coolify env:
PUSHER_HOST=<effective_realtime_domain>PUSHER_PORT=443PUSHER_SCHEME=https
- Bootstrap restarts
coolifyandcoolify-realtimecontainers ifPUSHER_*values changed. - Bootstrap installs
DOCKER-USERingress guards to block public forwarded traffic to6001/6002. - Bootstrap keeps UFW
80/443ALLOW baseline for domain-facing traffic.
Check effective rules:
sudo iptables -S DOCKER-USER
sudo ip6tables -S DOCKER-USER 2>/dev/null || true
Mitigation options:
- explicit
DOCKER-USERchain rules ufw-dockerpolicy management- Docker
"iptables": falseonly if equivalent firewall rules are fully managed
Traefik security headers
For production apps behind Coolify/Traefik, apply at least:
Strict-Transport-SecurityX-Content-Type-Options: nosniffX-Frame-Options(or CSPframe-ancestors)Referrer-PolicyContent-Security-Policy(app-specific)
Reference:
Coolify update strategy
For production:
- disable auto-updates in Coolify
- schedule upgrades in maintenance windows
- validate backup and rollback plans before upgrade
Coolify update runbook (recommended)
Use this procedure for every Coolify upgrade so hardening policy is re-applied after the upstream installer changes files/containers.
Common drift after upgrade (important):
- domain drift: if
COOLIFY_PUBLIC_DOMAIN(orCOOLIFY_REALTIME_DOMAIN) was changed in Coolify UI but not in/etc/vps-coolify-bootstrap/bootstrap.env, replay can re-apply old domain-based realtime settings from env. - firewall drift: manual Docker/firewall edits can remove or reorder
DOCKER-USERrules for realtime ports (6001/6002), so closed mode may no longer be enforced.
Post-upgrade quick re-sync (recommended every time):
cd /opt/vps-coolify-bootstrap
sudo git pull --ff-only origin main
sudo bash scripts/bootstrap-host.sh /etc/vps-coolify-bootstrap/bootstrap.env
sudo bash scripts/verify-bootstrap-state.sh /etc/vps-coolify-bootstrap/bootstrap.env
-
Keep provider console access open and connect by SSH.
ssh -p <SSH_PORT> <DEVOPS_USER>@<SERVER_IP> -
Create local backups for policy + Coolify env before upgrade.
sudo cp /etc/vps-coolify-bootstrap/bootstrap.env /root/bootstrap.env.bak.$(date +%F-%H%M%S) sudo cp /data/coolify/source/.env /root/coolify.env.bak.$(date +%F-%H%M%S) -
Pull latest bootstrap scripts (contains hardening/recovery fixes).
cd /opt/vps-coolify-bootstrap sudo git pull --ff-only origin main -
Run official Coolify upgrade.
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | sudo bash -
Re-apply bootstrap policy immediately after upgrade.
sudo bash /opt/vps-coolify-bootstrap/scripts/bootstrap-host.sh /etc/vps-coolify-bootstrap/bootstrap.env -
Validate runtime state.
sudo bash /opt/vps-coolify-bootstrap/scripts/verify-bootstrap-state.sh /etc/vps-coolify-bootstrap/bootstrap.env sudo ss -lntp | grep -E ':(22|80|443|8000|6001|6002)\b' || true sudo docker ps --format 'table \t\t' -
If realtime is in closed mode, confirm domain-based routing and port guards.
sudo grep -nE '^PUSHER_HOST=|^PUSHER_PORT=|^PUSHER_SCHEME=' /data/coolify/source/.env sudo iptables -S DOCKER-USER | grep -E '6001|6002' || true sudo ip6tables -S DOCKER-USER 2>/dev/null | grep -E '6001|6002' || true
Operational note:
COOLIFY_REALTIME_DOMAINcan be the same asCOOLIFY_PUBLIC_DOMAIN.- replay after upgrade is mandatory if you rely on bootstrap hardening (
CLOSE_COOLIFY_REALTIME_PORTS=true, SSH single-port policy, sudo policy).
References:
- https://coolify.io/docs/knowledge-base/server/auto-update
- https://coolify.io/docs/knowledge-base/server/upgrade
Monitoring minimum baseline
- disk usage + inode alerts
- memory pressure / OOM events
- container health + restart loops
- TLS expiry checks
- backup success verification
Logging and retention
Configure Docker log retention (max-size, max-file) and verify host logrotate.
Known operational notes
COOLIFY_ROOT_USER_PASSWORD: minimum 16 chars and must include lowercase, uppercase, digit, and symbolbootstrap-artifacts/vps-coolify-init.generated.ymlcontains secrets and must not be committed- bootstrap replay resets UFW baseline each run
SSH_KEY_ROTATE=0appends keys,SSH_KEY_ROTATE=1replaces keys
Input validation rules
See:
Back to Docs Home