Script Workflow
This page documents the detailed local workflow for:
scripts/generate-secrets.sh/scripts/generate-secrets.ps1scripts/prepare-vps-coolify-init.sh/scripts/prepare-vps-coolify-init.ps1scripts/prepare-existing-server.shscripts/generate-infra-secrets.sh/scripts/generate-infra-secrets.ps1scripts/prepare-infra-compose.sh/scripts/prepare-infra-compose.ps1scripts/setup-infra.shscripts/verify-infra-state.shscripts/setup-backup-infra.shscripts/pg-backup-infra.shscripts/pg-basebackup-infra.shscripts/offsite-backup-sync.example.shscripts/generate-docmost-secrets.sh/scripts/generate-docmost-secrets.ps1scripts/prepare-docmost-compose.sh/scripts/prepare-docmost-compose.ps1scripts/generate-plane-secrets.sh/scripts/generate-plane-secrets.ps1scripts/prepare-plane-compose.sh/scripts/prepare-plane-compose.ps1
PowerShell note (Windows)
Recommended:
winget install --id Microsoft.PowerShell --source winget
Fallback if pwsh is not available:
powershell -ExecutionPolicy Bypass -File .\scripts\generate-secrets.ps1
Use the same pwsh -> powershell -ExecutionPolicy Bypass -File replacement for other PowerShell examples.
What generate-secrets.* does
- creates
bootstrap-artifacts/bootstrap.envfromenv/bootstrap.env.examplewhen missing - appends missing key-value lines from
env/bootstrap.env.examplewhen local env is older/stale - fills placeholder/empty secrets (non-destructive by default)
- auto-detects local SSH public key and fills both:
SSH_PUBLIC_KEYSSH_PUBLIC_KEY_PATH
- Bash version sets strict file mode (
chmod 600)
What it does not do:
- does not provision VPS
- does not render VPS init YAML
- does not rotate already valid values unless force flags are used
Detailed local workflow
- Initialize env for fresh clone:
bash scripts/generate-secrets.sh
- Edit required business values in
bootstrap-artifacts/bootstrap.env:COOLIFY_PUBLIC_DOMAINCOOLIFY_ROOT_USERNAMECOOLIFY_ROOT_USER_EMAIL- all remaining
CHANGE_MEplaceholders
- Optional: rotate only Coolify root password:
bash scripts/generate-secrets.sh --force-password
PowerShell:
pwsh -File scripts/generate-secrets.ps1 -ForcePassword
- Optional: rotate only encryption password:
bash scripts/generate-secrets.sh --force-encryption-password
PowerShell:
pwsh -File scripts/generate-secrets.ps1 -ForceEncryptionPassword
- Optional: force SSH key re-detection:
bash scripts/generate-secrets.sh --force-ssh-key
PowerShell:
pwsh -File scripts/generate-secrets.ps1 -ForceSshKey
- Optional: custom env path (parent folders auto-created, env example copied if missing):
bash scripts/generate-secrets.sh --env-file envs/prod/bootstrap.env
PowerShell:
pwsh -File scripts/generate-secrets.ps1 -EnvFile envs/prod/bootstrap.env
- Render VPS init YAML:
bash scripts/prepare-vps-coolify-init.sh --overwrite
PowerShell:
pwsh -File scripts/prepare-vps-coolify-init.ps1 -Overwrite
- Re-render after env changes:
bash scripts/prepare-vps-coolify-init.sh --env-file bootstrap-artifacts/bootstrap.env --overwrite
PowerShell:
pwsh -File scripts/prepare-vps-coolify-init.ps1 -EnvFile bootstrap-artifacts/bootstrap.env -Overwrite
Path resolution note for external env files:
- when
--env-file/-EnvFilepoints outside repo, defaultTEMPLATE_FILEandOUTPUT_FILEare resolved with repo fallback - practical result: you can keep defaults from
env/bootstrap.env.example; output still lands in repobootstrap-artifacts/by default - if you want custom output location, set absolute
OUTPUT_FILE
Validation behavior in prepare-vps-coolify-init.*
- rejects unresolved required placeholders (
CHANGE_ME) - validates formats (SSH port, usernames, email, domain, booleans)
- enforces realtime cross-field rule:
- when
CLOSE_COOLIFY_REALTIME_PORTS=true, effective realtime domain isCOOLIFY_REALTIME_DOMAINor fallbackCOOLIFY_PUBLIC_DOMAIN
- when
- fails if output exists and overwrite is missing
- fails if generated file exceeds provider user-data size limits
SSH_KEY_ROTATE practical behavior
Applies only to:
DEVOPS_USER- users from
ADDITIONAL_SUDO_USERS
Does not apply to:
COOLIFY_SUDO_NOPASSWD_USER(dedicated localhost key flow)root
Modes:
SSH_KEY_ROTATE=0(default): keep existing keys, append currentSSH_PUBLIC_KEYonly if missingSSH_KEY_ROTATE=1: replaceauthorized_keyswith currentSSH_PUBLIC_KEY
Example controlled key replacement:
SSH_PUBLIC_KEY='ssh-ed25519 AAAA...new_key'
SSH_KEY_ROTATE=1
Replay on host:
sudo bash /opt/vps-coolify-bootstrap/scripts/bootstrap-host.sh /etc/vps-coolify-bootstrap/bootstrap.env
Then return to default append mode:
SSH_KEY_ROTATE=0
Existing server preparation (prepare-existing-server.sh)
Use this script on servers that were NOT provisioned via cloud-init/user-data. It installs the packages and applies the kernel/fail2ban config that cloud-init would normally handle at first boot.
Run once before bootstrap-host.sh:
sudo bash /opt/vps-coolify-bootstrap/scripts/prepare-existing-server.sh /etc/vps-coolify-bootstrap/bootstrap.env
What it does:
- waits for apt lock release (max 60s)
- detects Ubuntu version and warns if not 24.04 LTS (noble)
- installs:
ca-certificates,curl,git,openssl,python3,ufw,fail2ban,unattended-upgrades - writes
/etc/sysctl.d/99-hardening.conf(rp_filter, no redirects, syncookies, ip_forward) - applies sysctl settings
- writes
/etc/fail2ban/jail.d/10-bootstrap-sshd.localwithSSH_PORTfrom env
What it does not do:
- does not configure SSH hardening (that is
bootstrap-host.sh) - does not install Docker or Coolify (that is
bootstrap-host.sh) - does not create users (that is
bootstrap-host.sh) - does not configure UFW rules (that is
bootstrap-host.sh)
Arguments:
$1(optional): path tobootstrap.env(default:/etc/vps-coolify-bootstrap/bootstrap.env)- used only to read
SSH_PORTfor the fail2ban jail config - if file is missing, defaults to
SSH_PORT=2222
- used only to read
Typical flow on an existing server:
# 1. Clone repo
sudo git clone https://github.com/rigu/vps-coolify-bootstrap.git /opt/vps-coolify-bootstrap
# 2. Place env file
sudo mkdir -p /etc/vps-coolify-bootstrap
sudo cp /tmp/bootstrap.env /etc/vps-coolify-bootstrap/bootstrap.env
sudo chmod 600 /etc/vps-coolify-bootstrap/bootstrap.env
# 3. Prepare (packages + sysctl + fail2ban)
sudo bash /opt/vps-coolify-bootstrap/scripts/prepare-existing-server.sh /etc/vps-coolify-bootstrap/bootstrap.env
# 4. Bootstrap (SSH, UFW, users, Coolify)
sudo bash /opt/vps-coolify-bootstrap/scripts/bootstrap-host.sh /etc/vps-coolify-bootstrap/bootstrap.env
# 5. Verify
sudo bash /opt/vps-coolify-bootstrap/scripts/verify-bootstrap-state.sh /etc/vps-coolify-bootstrap/bootstrap.env
Docmost env secret workflow (for Docmost deployment)
Use this when deploying Docmost on top of this bootstrap.
Local-first rule:
- generate infra env locally first (
bootstrap-artifacts/production-infra.env) - copy infra env to VPS and run infra setup there
- generate Docmost env locally second (
bootstrap-artifacts/docmost.env) - Docmost generator syncs infra-derived Docmost values automatically
Infra -> Docmost sync mapping:
POSTGRES_APPS_USER->DATABASE_URLuserPOSTGRES_APPS_PASSWORD->DATABASE_URLpasswordPOSTGRES_DOCMOST_DB->DATABASE_URLdatabasePOSTGRES_APPS_CONTAINER_NAME->DATABASE_URLhostAPPS_VALKEY_PASSWORD->REDIS_URLpasswordVALKEY_APPS_CONTAINER_NAME->REDIS_URLhostINFRA_NETWORK_NAME->INFRA_NETWORK_NAMEMAIL_DRIVER,SMTP_*,MAIL_FROM_*-> same keys in Docmost env (when present)DRAWIO_URL->DRAWIO_URLPLANE_S3_ACCESS_KEY->AWS_S3_ACCESS_KEY_IDPLANE_S3_SECRET_KEY->AWS_S3_SECRET_ACCESS_KEYPLANE_S3_BUCKET->AWS_S3_BUCKETSEAWEEDFS_PLANE_CONTAINER_NAME->AWS_S3_ENDPOINT(http://<container>:8333)AWS_S3_REGION,AWS_S3_ENDPOINT,AWS_S3_FORCE_PATH_STYLE-> same keys in Docmost env (when present)DISABLE_TELEMETRY->DISABLE_TELEMETRYFILE_UPLOAD_SIZE_LIMIT,FILE_IMPORT_SIZE_LIMIT-> same keys in Docmost env (when present)
Default generation:
bash scripts/generate-docmost-secrets.sh
PowerShell:
pwsh -File scripts/generate-docmost-secrets.ps1
Default output:
bootstrap-artifacts/docmost.env
Default infra source:
bootstrap-artifacts/production-infra.env
If production-infra.env is missing:
- Docmost generator does not fail; it prints a warning and skips infra sync
- script still generates local
APP_SECRETand writes/keeps env values - rerun after infra env exists to sync infra-derived Docmost values (
DATABASE_URL,REDIS_URL, SMTP/MAIL, DRAWIO, AWS_S3*, DISABLE_TELEMETRY, FILE*_SIZE_LIMIT)
Optional flags:
- custom env path:
- Bash:
--env-file <path> - PowerShell:
-EnvFile <path>
- Bash:
- custom infra env path:
- Bash:
--infra-env-file <path> - PowerShell:
-InfraEnvFile <path>
- Bash:
- disable infra sync:
- Bash:
--no-infra-sync - PowerShell:
-NoInfraSync
- Bash:
- rotate app secret:
- Bash:
--force-app-secret - PowerShell:
-ForceAppSecret
- Bash:
Details and deployment usage:
Docmost compose render workflow (apply docmost.env to template)
Use this when you want a ready-to-paste compose file that keeps ${VAR} syntax, but injects defaults from docmost.env (${VAR:-value}).
bash scripts/prepare-docmost-compose.sh
pwsh -File scripts/prepare-docmost-compose.ps1
Default inputs/outputs:
- env source:
bootstrap-artifacts/docmost.env - template source:
templates/docmost-coolify-compose.community.template.yml - rendered output:
bootstrap-artifacts/docmost-coolify-compose.community.yml - template includes a production-ready
docmosthealthcheck (/api/health)
Useful options:
--env-file <path>--template-file <path>--output-file <path>--overwrite
Plane env secret workflow (for Plane deployment)
Use this only when deploying Plane on top of this bootstrap.
Local-first rule:
- generate infra env locally first (
bootstrap-artifacts/production-infra.env) - copy infra env to VPS and run infra setup there (server-side render/deploy)
- generate Plane env locally second (
bootstrap-artifacts/plane.env) - Plane generator syncs infra-dependent values automatically
Default generation:
bash scripts/generate-plane-secrets.sh
PowerShell:
pwsh -File scripts/generate-plane-secrets.ps1
Default output:
bootstrap-artifacts/plane.env
Default infra source:
bootstrap-artifacts/production-infra.env
If production-infra.env is missing:
- Plane generator does not fail; it prints a warning and skips infra sync
- script still generates local Plane secrets/passwords in
bootstrap-artifacts/plane.env DATABASE_URL,REDIS_URL, andAMQP_URLare generated from current Plane env values- after generating infra env, rerun Plane generator to sync infra-derived values
Rerun after infra env creation:
bash scripts/generate-plane-secrets.sh
pwsh -File scripts/generate-plane-secrets.ps1
Override infra source:
- Bash:
--infra-env-file <path> - PowerShell:
-InfraEnvFile <path>
Disable infra sync (advanced/testing only):
- Bash:
--no-infra-sync - PowerShell:
-NoInfraSync
Force rotation:
- passwords only:
--force-passwords/-ForcePasswords - secrets only:
--force-secrets/-ForceSecrets - everything generated by script:
--force-all/-ForceAll
Infra -> Plane sync mapping:
POSTGRES_APPS_USER->POSTGRES_USERPOSTGRES_APPS_PASSWORD->POSTGRES_PASSWORDPOSTGRES_PLANE_DB->POSTGRES_DBPOSTGRES_APPS_CONTAINER_NAME->POSTGRES_HOSTAPPS_VALKEY_PASSWORD->REDIS_PASSWORDVALKEY_APPS_CONTAINER_NAME->REDIS_HOSTPLANE_RABBITMQ_USER->RABBITMQ_DEFAULT_USERPLANE_RABBITMQ_PASSWORD->RABBITMQ_DEFAULT_PASSPLANE_RABBITMQ_VHOST->RABBITMQ_VHOST+RABBITMQ_DEFAULT_VHOSTRABBITMQ_PLANE_CONTAINER_NAME->RABBITMQ_HOSTPLANE_S3_ACCESS_KEY->AWS_ACCESS_KEY_IDPLANE_S3_SECRET_KEY->AWS_SECRET_ACCESS_KEYPLANE_S3_BUCKET->AWS_S3_BUCKET_NAME+BUCKET_NAMESEAWEEDFS_PLANE_CONTAINER_NAME->AWS_S3_ENDPOINT_URL(http://<container>:8333)
URLs regenerated when needed:
DATABASE_URLREDIS_URLAMQP_URL
Details and deployment usage:
Plane compose render workflow (apply plane.env to template)
Use this when you want a ready-to-paste compose file that keeps ${VAR} syntax, but injects defaults from plane.env (${VAR:-value}).
bash scripts/prepare-plane-compose.sh
pwsh -File scripts/prepare-plane-compose.ps1
Default inputs/outputs:
- env source:
bootstrap-artifacts/plane.env - template source:
templates/plane-coolify-compose.community.v1.2.3.full-with-proxy.yml - rendered output:
bootstrap-artifacts/plane-coolify-compose.community.v1.2.3.full-with-proxy.yml
Useful options:
--env-file <path>--template-file <path>--output-file <path>--overwrite
Infra env + compose workflow (internal service layer)
Generate infra env locally:
bash scripts/generate-infra-secrets.sh
PowerShell:
pwsh -File scripts/generate-infra-secrets.ps1
Default local output:
bootstrap-artifacts/production-infra.env
Deployment and network preparation:
Copy env to VPS, then run server-side setup:
Linux/macOS:
scp -P <SSH_PORT> bootstrap-artifacts/production-infra.env <DEVOPS_USER>@<server-ip>:/tmp/production-infra.env
ssh -p <SSH_PORT> <DEVOPS_USER>@<server-ip>
sudo bash /opt/vps-coolify-bootstrap/scripts/setup-infra.sh --env-file /tmp/production-infra.env
sudo bash /opt/vps-coolify-bootstrap/scripts/verify-infra-state.sh --env-file /srv/infra/production-infra.env
Windows (PowerShell):
scp -P <SSH_PORT> .\bootstrap-artifacts\production-infra.env <DEVOPS_USER>@<server-ip>:/tmp/production-infra.env
ssh -p <SSH_PORT> <DEVOPS_USER>@<server-ip>
sudo bash /opt/vps-coolify-bootstrap/scripts/setup-infra.sh --env-file /tmp/production-infra.env
sudo bash /opt/vps-coolify-bootstrap/scripts/verify-infra-state.sh --env-file /srv/infra/production-infra.env
setup-infra.sh now also ensures the SeaweedFS S3 bucket defined by PLANE_S3_BUCKET (default plane-uploads) exists.
Optional PITR/WAL baseline:
env/infra.env.exampleincludesPOSTGRES_ENABLE_WAL_ARCHIVE,POSTGRES_WAL_ARCHIVE_TIMEOUT_SECONDS,POSTGRES_MAX_WAL_SENDERS,POSTGRES_REPLICATION_USER, andPOSTGRES_REPLICATION_PASSWORD- keep WAL archiving disabled unless you also run
pg_basebackupand replicate/srv/infra/postgres-wal-archive/off-site - when enabled,
setup-infra.shprepares the runtime archive directory andverify-infra-state.shchecks the required PostgreSQL settings
Use your actual server admin account for <DEVOPS_USER> (default: devops).
After first successful apply, use runtime env directly on reruns:
sudo bash /opt/vps-coolify-bootstrap/scripts/setup-infra.sh --env-file /srv/infra/production-infra.env
Backup automation workflow
Use this after setup-infra.sh is already applied successfully and the runtime env exists on the VPS.
Local backup baseline:
sudo bash /opt/vps-coolify-bootstrap/scripts/setup-backup-infra.sh \
--env-file /srv/infra/production-infra.env
Optional off-site bootstrap after rclone and /root/.config/rclone/rclone.conf are already prepared:
sudo bash /opt/vps-coolify-bootstrap/scripts/setup-backup-infra.sh \
--env-file /srv/infra/production-infra.env \
--install-offsite-example \
--offsite-remote-dest 'YOUR_RCLONE_REMOTE:vps-backups' \
--enable-offsite-timer
What happens server-side:
- installs
/usr/local/lib/vps-coolify-bootstrap/common.shfor strict env loading - installs backup scripts and matching
systemdunits - writes
/etc/default/pg-backup-infraand, when WAL is enabled,/etc/default/pg-basebackup-infra - enables only the timers that are actually ready
- runs the first local backup immediately unless
--skip-manual-runis used - runs the first off-site sync only when the off-site script is fully configured
Post-install verification:
sudo systemctl list-timers --all | grep -E 'pg-backup-infra|pg-basebackup-infra|offsite-backup-sync'
sudo systemctl status pg-backup-infra.service --no-pager
sudo systemctl status pg-basebackup-infra.service --no-pager || true
sudo systemctl status offsite-backup-sync.service --no-pager || true
sudo find /srv/backups -maxdepth 2 -type f | sort | tail -n 20
Design note:
- off-site replication is intentionally scheduled as a separate job instead of being chained implicitly from the local backup service
What happens server-side for setup-infra.sh:
- optional fill of unresolved placeholders in copied env
- compose/config render on VPS
- network ensure + deploy + validation
- optional WAL archive directory prepare + PostgreSQL PITR setting validation
- optional backup script + timer install for the shared infra layer
All-in-one server run is also available:
sudo bash scripts/setup-infra.sh
Useful options:
--env-file <path>: use a specific infra env file--force-passwords/--force-secrets: rotate generated values--runtime-dir <path>: change runtime directory (default/srv/infra)--skip-deploy: generate/sync only, withoutdocker compose up -d--skip-validate: skip health/network/exposure validation
Standalone validation script:
verify-infra-state.shrechecks the applied state after setup- default env path:
/srv/infra/production-infra.env - default runtime dir:
/srv/infra - useful flags:
--env-file <path>--runtime-dir <path>--network-name <name>--wait-seconds <n>
Port conflict check (before deploy/redeploy):
sudo ss -lntp | grep -E ':(5434|6379|5672|15672|8333)\b' || true
If a port is already occupied by another service, update the corresponding *_HOST_PORT in production-infra.env and rerun setup-infra.sh.
Back to Docs Home