Deployment Guide for Infrastructure Providers Using EzyPlatform

Updated at 1789023681000
Cloud and VPS providers that want to offer EzyPlatform as a one-click application need a repeatable way to provision it: install the right runtime and database, wire up credentials, start the admin service, and keep it running without a human watching a terminal. This guide walks through a deployment script for Ubuntu/Debian hosts that automates all of that, and explains the auto-restart mechanism it sets up so the admin service recovers on its own if it ever stops responding.

Deployment flow

The script is idempotent: each step checks whether its target is already installed or running before acting, so re-running it on a host that already has EzyPlatform is safe.
flowchart TD
    A[Run install script] --> B[Install OpenJDK, MySQL, Nginx, Certbot]
    B --> C[Start MySQL and Nginx services]
    C --> D[Create EzyPlatform database and user]
    D --> E[Download and extract EzyPlatform]
    E --> F[Write MySQL connection into setup.properties]
    F --> G[Start EzyPlatform admin and web]
    G --> H[Install auto-restart watchdog for admin]
    H --> I[Configure Nginx + TLS for provided domains]
    I --> J[Store admin_url / web_url / websocket_url in the database]
    J --> K[Print database credentials and next steps]

What gets installed

  • OpenJDK 8, falling back to OpenJDK 11 if 8 isn't available in the package repositories
  • MySQL server
  • Nginx and Certbot, so a provider can add a virtual host and TLS afterward
  • unzip and wget, used to fetch and extract the EzyPlatform package
Each package is only installed if it's missing, and Java is skipped entirely if a working `java` binary is already on the `PATH`.

Database provisioning

The script creates a dedicated database and a MySQL user scoped to 'user'@'localhost' — never '%' — so the application account cannot be reached over the network, only from the host itself. The default MySQL package configuration also only listens on the loopback interface, and the script doesn't change that, so MySQL isn't exposed to the internet unless something outside the script (a custom image, a firewall rule) opens it up.
The database password is generated from /dev/urandom, filtered down to alphanumeric characters, at 24 characters long. On a re-run, it reuses whatever password is already sitting in setup.properties instead of rotating it — unless that password is still the packaged default placeholder, in which case it's replaced with a random one. The MySQL root password is only touched if you explicitly pass one in; otherwise the script leaves root's existing authentication method alone.

Starting the admin and web services, and keeping admin up

Once the database and files are in place, the script starts both the EzyPlatform admin and web processes (each skipped if its port is already listening), then installs a watchdog that keeps admin alive going forward.
flowchart TD
    A[systemd starts the watchdog service] --> B[Check script polls the admin port every 120s]
    B --> C{Port responding?}
    C -->|Yes| B
    C -->|No| D[Log the failure with a timestamp]
    D --> E[Restart admin via cli.sh]
    E --> B
The watchdog itself is two small pieces:
  • A polling script that checks the admin port (via /dev/tcp, falling back to ss or curl depending on what's available) every 120 seconds, and restarts the admin process through EzyPlatform's own cli.sh if the check fails. Failures are logged with a timestamp to /var/log/check-ezyplatform-status.log.
  • A systemd unit with Restart=always and RestartSec=10 wrapping that script, so the checker itself comes back if it ever dies, and starts automatically on boot.
This is the same check-script-plus-systemd-unit approach used by EzySupport's own auto-restart feature, an official EzyPlatform admin plugin. Using the same mechanism and naming convention means a provider can hand a server to EzySupport later — from the admin UI — and it will recognize the watchdog this script installed instead of creating a duplicate one.

Custom domains and TLS

By default, admin and web are only reachable by IP and port. If you pass ADMIN_DOMAIN and/or WEB_DOMAIN, the script goes further for each domain provided:
flowchart TD
    A[Domain provided] --> B[Write an Nginx reverse-proxy site for the domain]
    B --> C[Reload Nginx]
    C --> D[Request a certificate with Certbot's Nginx plugin]
    D --> E[Certbot adds the HTTPS block and HTTP-to-HTTPS redirect]
    E --> F[Write the matching *_url setting into the database]
Each domain's DNS A record has to point at the server before the script runs, since Certbot validates ownership over plain HTTP before it will issue a certificate. Once a domain is live, its URL is written straight into the `ezy_settings` table — `admin_url` for `ADMIN_DOMAIN`, and `web_url` plus `websocket_url` for `WEB_DOMAIN` — using an `INSERT ... ON DUPLICATE KEY UPDATE`, so re-running the script with the same domain just refreshes the value instead of erroring out. Those writes go through the same database user created earlier, scoped to `localhost`, using a temporary credentials file rather than a command-line password, so the password doesn't end up visible to other local users in the process list.
Admin and web sitting on two different domains also means EzyPlatform needs to know that both are allowed to receive the admin's SSO callback and share its access token — otherwise the login flow between them gets rejected outright. That's controlled by a separate setting, admin_sso_allowed_origins, stored as a JSON object mapping each allowed origin to the domain used for its cookie.
The script only touches this setting when you also pass a third variable, SSO_COOKIE_DOMAIN — the registrable domain both ADMIN_DOMAIN and WEB_DOMAIN are subdomains of (e.g. example.com for admin.example.com and www.example.com). It's an explicit, separate input on purpose: computing a shared domain automatically from `ADMIN_DOMAIN`/`WEB_DOMAIN` would mean trusting whatever they happen to have in common, and for domains like `admin.example.io.vn` and `shop.io.vn`, the "common" part is `io.vn` — a public registry suffix, not anything either domain's owner actually controls. Scoping a cookie to a public suffix leaks it to every unrelated domain under that suffix, so the script never infers it — it has to be told.
When SSO_COOKIE_DOMAIN is set, the script first checks that any configured ADMIN_DOMAIN/WEB_DOMAIN really are that domain or a subdomain of it (rejecting, for instance, a domain that merely ends with the same characters, like evilexample.com against example.com), then writes:
{"localhost": "localhost", "admin.example.com": ".example.com", "www.example.com": ".example.com"}
If SSO_COOKIE_DOMAIN isn't provided, `admin_sso_allowed_origins` is left exactly as it was — the script doesn't fall back to guessing.
The generated Nginx site follows [EzyPlatform's official Ubuntu deployment guide](https://youngmonkeys.org/ezyplatform/guides/deploy-ezyplatform-on-ubuntu): the same proxy headers, a `client_max_body_size`, and, for the admin domain only, a dedicated `/api/v1/media/add` location with buffering disabled so large media uploads stream straight through instead of being held in memory by Nginx first.
The script writes websocket_url for WEB_DOMAIN into the database along with web_url, but it doesn't start or configure the socket service itself — that's intentionally out of scope here.

Pre-installing plugins for a Vibe Coding deployment

If a provider wants to offer EzyPlatform as a Vibe Coding service, the script can install and activate a plugin set for that when you pass INSTALL_VIBE_CODING_PLUGINS=1, in dependency order:
#PluginDepends on
1ezyarticle(none)
2ezymailezyarticle
3freestyleezyarticle
4logs-monitor(none)
5ezysupportezyarticle, ezymail
6ecommerce(none) — runs scripts.sql before activating
7accountingecommerce
8graphql(none)
9ezypaymentecommerce
10ezydeliveryecommerce, accounting, ezyarticle
flowchart TD
    A[INSTALL_VIBE_CODING_PLUGINS=1] --> B{market.access_token already in setup.properties?}
    B -->|No| C[Skip, print instructions to log in to Market then re-run]
    B -->|Yes| D[For each plugin, in dependency order]
    D --> E["cli.sh install <plugin>"]
    E --> F{Plugin is ecommerce?}
    F -->|Yes| G[Run scripts.sql first]
    F -->|No| H["cli.sh activate all <plugin>"]
    G --> H
    H --> D
    D --> I[Restart admin and web]
Installing a plugin through cli.sh install calls EzyPlatform's marketplace using `market.access_token`, and that token is **only** written to `settings/setup.properties` after you open the admin UI and log in to the Market once through the browser (an SSO redirect and callback) — a manual step the script cannot automate. As a result:
  • On a first run, before that token exists, the plugin-installation step skips itself and prints instructions: finish setup-admin, log in to the Market once from the admin UI, then re-run the script with INSTALL_VIBE_CODING_PLUGINS=1.
  • Because the script is already idempotent, re-running it after logging in to the Market is safe — the database/EzyPlatform/watchdog/domain steps skip themselves since they're already done, and only the plugin-installation step actually runs.
  • ecommerce ships a scripts.sql file (views/seed data the ORM doesn't create automatically) that has to run before it's activated; the script runs it using the same application database user created earlier. If it's re-run and hits duplicate-key/table-exists errors because it already applied on a previous run, the script logs a warning instead of aborting the whole install.
  • If installing or activating a given plugin fails (for example, the linked Market account hasn't purchased it), the script logs the failure and moves on to the next plugin instead of stopping entirely.

Building a golden image to spin up VPS instances faster

If a provider sells EzyPlatform to many customers, re-running the full script on every new VPS — pulling down OS packages, MySQL, Nginx, EzyPlatform, and each plugin — is slow and wastes bandwidth. Instead, build a template server once, snapshot it into an image, and boot every purchased VPS as a clone of that image rather than installing from scratch each time.
flowchart TD
    A[Run install-ezyplatform.sh on the template, without ADMIN_DOMAIN/WEB_DOMAIN] --> B[Optionally bake in Vibe Coding plugins: INSTALL_VIBE_CODING_PLUGINS=1]
    B --> C[By hand: stop admin/web/MySQL, strip platform key + Market access token + MySQL server UUID]
    C --> D[Regenerate SSH host keys / machine-id, then shut down]
    D --> E[Snapshot the image through your infrastructure provider's tooling]
    E --> F[Every purchased VPS = one clone booted from the image]
    F --> G[Customer opens the admin URL, completes setup-admin]
    G --> H[Re-run the script with that customer's own ADMIN_DOMAIN/WEB_DOMAIN/SSO_COOKIE_DOMAIN]
This is operational guidance, not something the script automates — the infrastructure provider runs the commands below by hand on the template host. The critical thing to get right: some of what the script produces is per-instance identity. Leave it in the image, and every clone shares it — causing collisions or leaking credentials:
  • settings/platform-key.txt: EzyPlatform uses this to identify itself to the marketplace. Left in the image, every cloned VPS would register as the same instance.
  • The market.access_token line in settings/setup.properties: this is the template-builder's own Market login token. Leave it in the image and any customer who can read that file could use your Market account to install paid plugins.
  • MySQL's server UUID (the `auto.cnf` file in its data directory): reset it so each VPS generates its own on first boot, instead of every clone sharing one.
So the recommended workflow has two phases:
  1. On the template host (once): run install-ezyplatform.sh as usual but without ADMIN_DOMAIN/WEB_DOMAIN/CERTBOT_EMAIL/SSO_COOKIE_DOMAIN (those are tied to each customer's own domain and can't be shared), optionally with INSTALL_VIBE_CODING_PLUGINS=1 to bake in the Vibe Coding plugin set — the downloaded plugin files and ecommerce's `scripts.sql` schema are safe shared data, not identity. **Don't** open the admin UI to complete setup-admin on the template — doing so would give every cloned VPS the same initial admin account/password. Then, before taking the snapshot, run the following on the template host yourself (replace <ezyplatform dir> with the actual path, e.g. ~/ezyplatform):
cd <ezyplatform dir>
bash cli.sh "stop admin"
bash cli.sh "stop web"
rm -f settings/platform-key.txt
sed -i '/^market.access_token=/d' settings/setup.properties
sudo systemctl stop mysql
sudo rm -f /var/lib/mysql/auto.cnf
sudo rm -f /etc/ssh/ssh_host_*
sudo truncate -s 0 /etc/machine-id
sudo shutdown now
Then snapshot the image through your infrastructure provider's tooling.
  1. On each purchased VPS (booted from the image): on first boot, start mysql/nginx/EzyPlatform admin/web (the systemd services from the template's install run are already enabled), open the admin URL to complete setup-admin, then re-run `install-ezyplatform.sh` with that customer's own ADMIN_DOMAIN/WEB_DOMAIN/CERTBOT_EMAIL/SSO_COOKIE_DOMAIN so the script configures Nginx + TLS for their actual domains (this has to happen per VPS, since Certbot validates the domain over live DNS/HTTP). If the Vibe Coding plugins weren't baked into the image, the customer logs in to the Market with their own account and re-runs with `INSTALL_VIBE_CODING_PLUGINS=1`.
The database password (DB_PASSWORD) doesn't need to be cleared or rotated as part of this workflow: each VPS's MySQL only listens on its own loopback interface, so clones sharing one password doesn't add real attack surface — an attacker with shell access to a VPS can just read the config file, which is faster than guessing a shared password anyway. If you still want to rotate it, you'll need to edit datasource.password in setup.properties by hand before snapshotting, since the script's idempotent re-run logic intentionally skips rewriting that file once it detects EzyPlatform is already installed (`EZYPLATFORM_ALREADY_EXISTS`).

Configuration

Every value the script needs can be overridden with an environment variable, or for the root password and download URL, with positional arguments:
VariableDefault
DB_NAMEezyplatform
DB_USERezyplatform
DB_PASSWORDrandom 24-character password
MYSQL_ROOT_PASSWORDunchanged, unless passed as the first argument
EZYPLATFORM_DIR<current directory>/ezyplatform
EZYPLATFORM_URLlatest EzyPlatform release, or the second argument
EZYPLATFORM_ADMIN_PORT9090
EZYPLATFORM_WEB_PORT8080
ADMIN_DOMAINunset — falls back to IP:port
WEB_DOMAINunset — falls back to IP:port
CERTBOT_EMAILunset — Certbot registers without a contact email
SSO_COOKIE_DOMAINunset — admin_sso_allowed_origins is left untouched
INSTALL_VIBE_CODING_PLUGINSunset — no plugins are installed
sudo bash install-ezyplatform.sh
sudo bash install-ezyplatform.sh 'RootPassword123!'
sudo DB_PASSWORD='AppPassword123!' EZYPLATFORM_ADMIN_PORT=9091 bash install-ezyplatform.sh
sudo ADMIN_DOMAIN='admin.example.com' WEB_DOMAIN='www.example.com' CERTBOT_EMAIL='ops@example.com' \
     SSO_COOKIE_DOMAIN='example.com' bash install-ezyplatform.sh
sudo INSTALL_VIBE_CODING_PLUGINS=1 bash install-ezyplatform.sh

What this script does not do

  • Configure Nginx or request a TLS certificate for domains that aren't passed via `ADMIN_DOMAIN`/`WEB_DOMAIN`
  • Update admin_sso_allowed_origins unless SSO_COOKIE_DOMAIN is explicitly provided — it never guesses a shared domain from ADMIN_DOMAIN/WEB_DOMAIN
  • Complete the web-based EzyPlatform admin setup wizard — you still open the admin URL once to finish that
  • Log in to the Market on your behalf — that's a manual, browser-based step, and a prerequisite for `INSTALL_VIBE_CODING_PLUGINS` to do anything
  • Prepare a template host into a golden image — that's a set of manual commands the infrastructure provider runs themselves, described under "Building a golden image" above

Download the script

The full script is provided as a plain-text download rather than inline in this article, so shell syntax like [[ ... ]] or doesn't get mangled by HTML rendering along the way:
Download install-ezyplatform.sh
Read through it before running it against production infrastructure, and run bash install-ezyplatform.sh --help first to see the current usage, arguments, and environment variables in one place.

Table Of Contents