Script Workflow

This page documents the detailed local workflow for:

  • scripts/generate-secrets.sh / scripts/generate-secrets.ps1
  • scripts/prepare-vps-coolify-init.sh / scripts/prepare-vps-coolify-init.ps1
  • scripts/prepare-existing-server.sh
  • scripts/generate-infra-secrets.sh / scripts/generate-infra-secrets.ps1
  • scripts/prepare-infra-compose.sh / scripts/prepare-infra-compose.ps1
  • scripts/setup-infra.sh
  • scripts/verify-infra-state.sh
  • scripts/setup-backup-infra.sh
  • scripts/pg-backup-infra.sh
  • scripts/pg-basebackup-infra.sh
  • scripts/offsite-backup-sync.example.sh
  • scripts/generate-docmost-secrets.sh / scripts/generate-docmost-secrets.ps1
  • scripts/prepare-docmost-compose.sh / scripts/prepare-docmost-compose.ps1
  • scripts/generate-plane-secrets.sh / scripts/generate-plane-secrets.ps1
  • scripts/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.env from env/bootstrap.env.example when missing
  • appends missing key-value lines from env/bootstrap.env.example when local env is older/stale
  • fills placeholder/empty secrets (non-destructive by default)
  • auto-detects local SSH public key and fills both:
    • SSH_PUBLIC_KEY
    • SSH_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

  1. Initialize env for fresh clone:
bash scripts/generate-secrets.sh
  1. Edit required business values in bootstrap-artifacts/bootstrap.env:
    • COOLIFY_PUBLIC_DOMAIN
    • COOLIFY_ROOT_USERNAME
    • COOLIFY_ROOT_USER_EMAIL
    • all remaining CHANGE_ME placeholders
  2. Optional: rotate only Coolify root password:
bash scripts/generate-secrets.sh --force-password

PowerShell:

pwsh -File scripts/generate-secrets.ps1 -ForcePassword
  1. Optional: rotate only encryption password:
bash scripts/generate-secrets.sh --force-encryption-password

PowerShell:

pwsh -File scripts/generate-secrets.ps1 -ForceEncryptionPassword
  1. Optional: force SSH key re-detection:
bash scripts/generate-secrets.sh --force-ssh-key

PowerShell:

pwsh -File scripts/generate-secrets.ps1 -ForceSshKey
  1. 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
  1. Render VPS init YAML:
bash scripts/prepare-vps-coolify-init.sh --overwrite

PowerShell:

pwsh -File scripts/prepare-vps-coolify-init.ps1 -Overwrite
  1. 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 / -EnvFile points outside repo, default TEMPLATE_FILE and OUTPUT_FILE are resolved with repo fallback
  • practical result: you can keep defaults from env/bootstrap.env.example; output still lands in repo bootstrap-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 is COOLIFY_REALTIME_DOMAIN or fallback COOLIFY_PUBLIC_DOMAIN
  • 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 current SSH_PUBLIC_KEY only if missing
  • SSH_KEY_ROTATE=1: replace authorized_keys with current SSH_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.local with SSH_PORT from 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 to bootstrap.env (default: /etc/vps-coolify-bootstrap/bootstrap.env)
    • used only to read SSH_PORT for the fail2ban jail config
    • if file is missing, defaults to SSH_PORT=2222

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_URL user
  • POSTGRES_APPS_PASSWORD -> DATABASE_URL password
  • POSTGRES_DOCMOST_DB -> DATABASE_URL database
  • POSTGRES_APPS_CONTAINER_NAME -> DATABASE_URL host
  • APPS_VALKEY_PASSWORD -> REDIS_URL password
  • VALKEY_APPS_CONTAINER_NAME -> REDIS_URL host
  • INFRA_NETWORK_NAME -> INFRA_NETWORK_NAME
  • MAIL_DRIVER, SMTP_*, MAIL_FROM_* -> same keys in Docmost env (when present)
  • DRAWIO_URL -> DRAWIO_URL
  • PLANE_S3_ACCESS_KEY -> AWS_S3_ACCESS_KEY_ID
  • PLANE_S3_SECRET_KEY -> AWS_S3_SECRET_ACCESS_KEY
  • PLANE_S3_BUCKET -> AWS_S3_BUCKET
  • SEAWEEDFS_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_TELEMETRY
  • FILE_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_SECRET and 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>
  • custom infra env path:
    • Bash: --infra-env-file <path>
    • PowerShell: -InfraEnvFile <path>
  • disable infra sync:
    • Bash: --no-infra-sync
    • PowerShell: -NoInfraSync
  • rotate app secret:
    • Bash: --force-app-secret
    • PowerShell: -ForceAppSecret

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 docmost healthcheck (/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, and AMQP_URL are 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_USER
  • POSTGRES_APPS_PASSWORD -> POSTGRES_PASSWORD
  • POSTGRES_PLANE_DB -> POSTGRES_DB
  • POSTGRES_APPS_CONTAINER_NAME -> POSTGRES_HOST
  • APPS_VALKEY_PASSWORD -> REDIS_PASSWORD
  • VALKEY_APPS_CONTAINER_NAME -> REDIS_HOST
  • PLANE_RABBITMQ_USER -> RABBITMQ_DEFAULT_USER
  • PLANE_RABBITMQ_PASSWORD -> RABBITMQ_DEFAULT_PASS
  • PLANE_RABBITMQ_VHOST -> RABBITMQ_VHOST + RABBITMQ_DEFAULT_VHOST
  • RABBITMQ_PLANE_CONTAINER_NAME -> RABBITMQ_HOST
  • PLANE_S3_ACCESS_KEY -> AWS_ACCESS_KEY_ID
  • PLANE_S3_SECRET_KEY -> AWS_SECRET_ACCESS_KEY
  • PLANE_S3_BUCKET -> AWS_S3_BUCKET_NAME + BUCKET_NAME
  • SEAWEEDFS_PLANE_CONTAINER_NAME -> AWS_S3_ENDPOINT_URL (http://<container>:8333)

URLs regenerated when needed:

  • DATABASE_URL
  • REDIS_URL
  • AMQP_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.example includes POSTGRES_ENABLE_WAL_ARCHIVE, POSTGRES_WAL_ARCHIVE_TIMEOUT_SECONDS, POSTGRES_MAX_WAL_SENDERS, POSTGRES_REPLICATION_USER, and POSTGRES_REPLICATION_PASSWORD
  • keep WAL archiving disabled unless you also run pg_basebackup and replicate /srv/infra/postgres-wal-archive/ off-site
  • when enabled, setup-infra.sh prepares the runtime archive directory and verify-infra-state.sh checks 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.sh for strict env loading
  • installs backup scripts and matching systemd units
  • writes /etc/default/pg-backup-infra and, 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-run is 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, without docker compose up -d
  • --skip-validate: skip health/network/exposure validation

Standalone validation script:

  • verify-infra-state.sh rechecks 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