CresignSys Learn — Lesson 065

Written by

in

Build the First Working hosting-create Script

We now create the first practical version of the CresignSys Hosting Platform provisioning engine.

The objective is:

sudo hosting-create example.com

and have it create the infrastructure required for a WordPress site.


1. Version 1 Architecture

The first version will automate:

Domain validation
      ↓
Site ID
      ↓
Linux user
      ↓
Website directories
      ↓
Permissions
      ↓
MySQL database
      ↓
MySQL user
      ↓
PHP-FPM pool
      ↓
Nginx configuration
      ↓
Nginx validation
      ↓
WordPress
      ↓
Health check

We will keep SSL as a separate stage initially. This is intentional: DNS and HTTP must work before certificate issuance.


2. Before Building the Script

Check the server:

php -v
mysql --version
nginx -v
wp --info

Check PHP-FPM:

systemctl list-units --type=service | grep php

You may see something like:

php8.3-fpm.service

Use your actual PHP version.


3. Create CresignSys Configuration Directory

sudo mkdir -p /etc/cresignsys

Create:

sudo nano /etc/cresignsys/hosting.conf

Put:

WEB_ROOT_BASE="/storage/websites"

NGINX_AVAILABLE="/etc/nginx/sites-available"
NGINX_ENABLED="/etc/nginx/sites-enabled"

PHP_VERSION="8.3"
PHP_FPM_SERVICE="php8.3-fpm"
PHP_FPM_SOCKET_BASE="/run/php"

LOG_DIR="/var/log/cresignsys"
STATE_DIR="/var/lib/cresignsys/sites"

If your server uses PHP 8.2, for example, change:

PHP_VERSION="8.2"
PHP_FPM_SERVICE="php8.2-fpm"

4. Why We Centralize PHP Version

Instead of putting:

php8.3

throughout the script, we define it once.

Later we can make the platform support:

PHP 8.2
PHP 8.3
PHP 8.4

without rewriting the entire provisioning engine.


5. Create Required Directories

sudo mkdir -p /var/log/cresignsys
sudo mkdir -p /var/lib/cresignsys/sites
sudo mkdir -p /storage/websites

Set the log directory appropriately for your server administration model.


6. Create the Script

sudo nano /usr/local/bin/hosting-create

Start with:

#!/usr/bin/env bash

set -Eeuo pipefail

7. Load Configuration

Add:

CONFIG="/etc/cresignsys/hosting.conf"

if [[ ! -f "$CONFIG" ]]; then
    echo "ERROR: Missing $CONFIG"
    exit 1
fi

source "$CONFIG"

Now the script receives:

WEB_ROOT_BASE
NGINX_AVAILABLE
NGINX_ENABLED
PHP_VERSION
PHP_FPM_SERVICE
PHP_FPM_SOCKET_BASE
LOG_DIR
STATE_DIR

8. Require Root

Add:

if [[ "$EUID" -ne 0 ]]; then
    echo "ERROR: Run this command with sudo."
    exit 1
fi

Correct:

sudo hosting-create example.com

Incorrect:

hosting-create example.com

9. Require Exactly One Argument

if [[ $# -ne 1 ]]; then
    echo "Usage: hosting-create DOMAIN"
    exit 1
fi

DOMAIN="$1"

10. Domain Validation

For the first version, use a conservative validation rule:

if [[ ! "$DOMAIN" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$ ]]; then
    echo "ERROR: Invalid domain name."
    exit 1
fi

This is deliberately conservative.

A production-grade domain validator should also account for DNS rules, IDNs, label lengths, reserved names, and other edge cases.


11. Normalize Domain

Use lowercase:

DOMAIN="$(printf '%s' "$DOMAIN" | tr '[:upper:]' '[:lower:]')"

Therefore:

Example.COM

becomes:

example.com

12. Generate Site ID

Create a safe identifier:

SITE_ID="$(printf '%s' "$DOMAIN" | tr '.-' '__')"

Example:

learn.cresignsys.com

becomes approximately:

learn_cresignsys_com

13. Generate Resource Names

Now:

SITE_USER="$SITE_ID"
DB_NAME="${SITE_ID}_db"
DB_USER="${SITE_ID}_dbuser"
PHP_SOCKET="${PHP_FPM_SOCKET_BASE}/${SITE_ID}.sock"
WEB_ROOT="${WEB_ROOT_BASE}/${DOMAIN}/public"
SITE_ROOT="${WEB_ROOT_BASE}/${DOMAIN}"
NGINX_CONF="${NGINX_AVAILABLE}/${DOMAIN}.conf"

Conceptually:

Domain
learn.cresignsys.com

Site user
learn_cresignsys_com

Database
learn_cresignsys_com_db

Database user
learn_cresignsys_com_dbuser

PHP socket
/run/php/learn_cresignsys_com.sock

14. Generate Database Password

Use a secure random generator:

DB_PASSWORD="$(openssl rand -hex 32)"

This produces a random value rather than a predictable password.


15. Log Function

Add:

LOG_FILE="${LOG_DIR}/hosting-create.log"

log() {
    local message="$1"
    printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$message" \
        | tee -a "$LOG_FILE"
}

Now:

log "Starting provisioning for $DOMAIN"

writes to the log.


16. Error Function

Add:

fail() {
    log "ERROR: $1"
    exit 1
}

Then:

fail "Nginx configuration failed."

stops provisioning.


17. Display the Plan

Before making changes:

echo
echo "CresignSys Hosting Provisioner"
echo "=============================="
echo "Domain       : $DOMAIN"
echo "Site ID      : $SITE_ID"
echo "Web root     : $WEB_ROOT"
echo "Database     : $DB_NAME"
echo "DB user      : $DB_USER"
echo "PHP version  : $PHP_VERSION"
echo "PHP socket   : $PHP_SOCKET"
echo

This is useful during development.


18. Check Required Commands

Add:

for command in \
    useradd \
    mysql \
    openssl \
    nginx \
    wp
do
    if ! command -v "$command" >/dev/null 2>&1; then
        fail "Required command not found: $command"
    fi
done

Now the script refuses to start if important dependencies are missing.


19. Check PHP-FPM

if ! systemctl is-enabled "$PHP_FPM_SERVICE" >/dev/null 2>&1; then
    log "WARNING: $PHP_FPM_SERVICE is not enabled."
fi

if ! systemctl is-active "$PHP_FPM_SERVICE" >/dev/null 2>&1; then
    fail "$PHP_FPM_SERVICE is not running."
fi

20. Check MySQL

if ! systemctl is-active mysql >/dev/null 2>&1; then
    fail "MySQL is not running."
fi

21. Check Nginx

if ! systemctl is-active nginx >/dev/null 2>&1; then
    fail "Nginx is not running."
fi

22. Check Existing Site

Never overwrite an existing website.

Add:

if [[ -d "$SITE_ROOT" ]]; then
    fail "Website directory already exists: $SITE_ROOT"
fi

Also check:

if id "$SITE_USER" >/dev/null 2>&1; then
    fail "Linux user already exists: $SITE_USER"
fi

23. Check Existing Database

Inside MySQL:

if mysql -Nse "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME='${DB_NAME}'" \
    | grep -Fxq "$DB_NAME"; then
    fail "Database already exists: $DB_NAME"
fi

24. Create Linux User

Now:

log "Creating Linux user: $SITE_USER"

useradd \
    --system \
    --create-home \
    --shell /usr/sbin/nologin \
    "$SITE_USER"

For a hosting account, nologin is useful when you don’t want the site account to have normal interactive SSH login.


25. Why --system?

A system account is intended for services rather than ordinary human login.

This fits the role:

site user

whose purpose is to execute website processes.


26. Create Website Directories

log "Creating website directories"

mkdir -p "$SITE_ROOT/public"
mkdir -p "$SITE_ROOT/private"
mkdir -p "$SITE_ROOT/logs"

Result:

/storage/websites/example.com/
├── public/
├── private/
└── logs/

27. Set Ownership

chown -R "$SITE_USER:$SITE_USER" "$SITE_ROOT"

28. Initial Permissions

For directories:

find "$SITE_ROOT" -type d -exec chmod 755 {} \;

For files:

find "$SITE_ROOT" -type f -exec chmod 644 {} \;

At this point, WordPress has not yet been installed.


29. Create MySQL Database

Now:

log "Creating database: $DB_NAME"

Then execute SQL through MySQL.

Conceptually:

mysql <<SQL
CREATE DATABASE \`$DB_NAME\`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
SQL

30. Create Database User

mysql <<SQL
CREATE USER '$DB_USER'@'localhost'
IDENTIFIED BY '$DB_PASSWORD';
SQL

Because the password is generated rather than directly supplied by the user, this is safer than accepting arbitrary SQL input.

Still, production code should carefully handle SQL identifier/value escaping.


31. Grant Only Required Database Access

mysql <<SQL
GRANT ALL PRIVILEGES
ON \`$DB_NAME\`.*
TO '$DB_USER'@'localhost';
FLUSH PRIVILEGES;
SQL

Now:

Site
 ↓
DB user
 ↓
Only its own database

32. Important Security Point

We are not doing:

GRANT ALL PRIVILEGES ON *.*

The site user gets access only to:

example_db.*

33. Create PHP-FPM Configuration

Create a temporary configuration:

PHP_POOL_CONF="/etc/php/${PHP_VERSION}/fpm/pool.d/${SITE_ID}.conf"

Then:

cat > "$PHP_POOL_CONF" <<EOF
[${SITE_ID}]

user = ${SITE_USER}
group = ${SITE_USER}

listen = ${PHP_SOCKET}

listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
EOF

These worker values are only initial examples and should later be replaced with plan/resource-based tuning.


34. Why www-data Socket Ownership?

Our model is:

PHP-FPM workers
    ↓
site user

Nginx
    ↓
www-data

Nginx needs access to the PHP-FPM socket.

Therefore the socket permissions must allow the Nginx process to communicate with the pool.


35. Validate PHP-FPM

Before reloading:

php-fpm${PHP_VERSION} -t

Depending on the installed PHP package, the executable may instead be:

php-fpm8.3 -t

Use the correct binary for your server.

If validation fails:

STOP

Do not continue to Nginx.


36. Reload PHP-FPM

If validation succeeds:

systemctl reload "$PHP_FPM_SERVICE"

Then:

sleep 1

Check the socket:

if [[ ! -S "$PHP_SOCKET" ]]; then
    fail "PHP-FPM socket was not created: $PHP_SOCKET"
fi

37. This Is an Important Validation

We don’t merely assume:

PHP-FPM reload succeeded

We verify:

socket actually exists

38. Generate Nginx Configuration

Create:

cat > "$NGINX_CONF" <<EOF
server {
    listen 80;
    server_name ${DOMAIN};

    root ${WEB_ROOT};
    index index.php index.html;

    access_log ${SITE_ROOT}/logs/access.log;
    error_log ${SITE_ROOT}/logs/error.log;

    location / {
        try_files \$uri \$uri/ /index.php?\$args;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
        fastcgi_pass unix:${PHP_SOCKET};
    }

    location ~ /\. {
        deny all;
    }
}
EOF

39. Why Escape $?

Inside a Bash heredoc:

$uri

would normally be interpreted by Bash.

We therefore use:

\$uri

so that the generated Nginx configuration contains:

$uri

40. Enable the Site

ln -s "$NGINX_CONF" \
    "${NGINX_ENABLED}/${DOMAIN}.conf"

41. Validate Nginx

Now:

nginx -t

If successful:

systemctl reload nginx

If it fails:

STOP

42. Why This Sequence?

We are deliberately doing:

Generate
 ↓
Validate
 ↓
Activate

not:

Generate
 ↓
Activate
 ↓
Hope

This is one of the most important principles in automation.


43. Download WordPress

Now:

log "Downloading WordPress"

Move into the public directory:

cd "$WEB_ROOT"

Run WP-CLI as the website user:

sudo -u "$SITE_USER" wp core download

This ensures the downloaded files are owned by the site account.


44. Why Run WP-CLI as the Site User?

If you run:

sudo wp core download

as root, you may create:

root:root

files.

Then PHP running as:

siteuser

may not be able to modify them.

Better:

WP-CLI
 ↓
site user
 ↓
website files

45. Create wp-config.php

Run:

sudo -u "$SITE_USER" wp config create \
    --dbname="$DB_NAME" \
    --dbuser="$DB_USER" \
    --dbpass="$DB_PASSWORD" \
    --dbhost="localhost"

46. Check wp-config.php

ls -l "$WEB_ROOT/wp-config.php"

Then:

grep -E "DB_NAME|DB_USER|DB_HOST" \
"$WEB_ROOT/wp-config.php"

Do not print:

DB_PASSWORD

to your terminal or logs.


47. Install WordPress

For example:

sudo -u "$SITE_USER" wp core install \
    --url="http://${DOMAIN}" \
    --title="${DOMAIN}" \
    --admin_user="siteadmin" \
    --admin_password="GENERATED_ADMIN_PASSWORD" \
    --admin_email="admin@example.com"

For production, generate the administrator password securely and do not expose it in logs.


48. Better Site Title

Instead of using:

example.com

as the title, your control panel can later collect:

Site Name

from the customer.

Then:

--title="$SITE_TITLE"

49. Check WordPress

sudo -u "$SITE_USER" wp core is-installed

Then:

sudo -u "$SITE_USER" wp db check

Both should succeed.


50. Final Permissions

After installation:

chown -R "$SITE_USER:$SITE_USER" "$SITE_ROOT"

Then:

find "$SITE_ROOT" -type d -exec chmod 755 {} \;
find "$SITE_ROOT" -type f -exec chmod 644 {} \;

Remember that WordPress/plugin-specific writable directories may require a deliberately designed permission model.


51. Verify Nginx

curl -I "http://${DOMAIN}"

At this point DNS must point to the server.

You may receive:

HTTP/1.1 200 OK

or another legitimate response depending on WordPress behavior.


52. Verify PHP

A useful temporary test is to create a PHP file:

echo '<?php echo "PHP_OK";' \
> "$WEB_ROOT/test.php"

Then:

curl "http://${DOMAIN}/test.php"

Expected:

PHP_OK

Immediately remove it:

rm "$WEB_ROOT/test.php"

Never leave diagnostic PHP files on a production website.


53. Full Health Check

The script can now test:

systemctl is-active nginx
systemctl is-active "$PHP_FPM_SERVICE"
systemctl is-active mysql
test -S "$PHP_SOCKET"
test -f "$WEB_ROOT/wp-config.php"
sudo -u "$SITE_USER" wp core is-installed
sudo -u "$SITE_USER" wp db check

54. Store Site Metadata

Create:

SITE_STATE_DIR="${STATE_DIR}/${DOMAIN}"
mkdir -p "$SITE_STATE_DIR"

Then:

cat > "${SITE_STATE_DIR}/site.conf" <<EOF
DOMAIN=${DOMAIN}
SITE_ID=${SITE_ID}
SITE_USER=${SITE_USER}
WEB_ROOT=${WEB_ROOT}
DB_NAME=${DB_NAME}
DB_USER=${DB_USER}
PHP_VERSION=${PHP_VERSION}
PHP_SOCKET=${PHP_SOCKET}
STATUS=ACTIVE
EOF

Do not put the database password into this metadata file unless you deliberately secure it and have a specific need.


55. Why Metadata?

Later your control panel can ask:

What websites are installed?

and read:

/var/lib/cresignsys/sites/

or, preferably, a central control database.


56. Final Output

The command can display:

========================================
CresignSys Hosting Provisioning Complete
========================================

Domain      : example.com
Web root    : /storage/websites/example.com/public
PHP-FPM     : example_...
Database    : example_db
Status      : ACTIVE

Next:
https://example.com

57. Important: SSL Is Not Yet Included

For Version 1:

hosting-create

creates:

HTTP website

Then SSL can be handled by:

sudo hosting-ssl example.com

This separation makes troubleshooting much easier.


58. Why Separate hosting-ssl?

Imagine:

hosting-create example.com

fails.

You know the problem is somewhere in:

Linux
MySQL
PHP-FPM
Nginx
WordPress

If SSL is also involved, there are more failure points.

Separate commands make the system easier to debug.


59. Future Command Structure

Eventually:

hosting-create
hosting-delete
hosting-suspend
hosting-unsuspend
hosting-ssl
hosting-backup
hosting-restore
hosting-list
hosting-info
hosting-repair
hosting-health

This becomes a real hosting CLI.


60. Make the Script Executable

After saving:

sudo chmod 755 /usr/local/bin/hosting-create

Check:

ls -l /usr/local/bin/hosting-create

61. First Test

Use a test domain/subdomain that points to this server:

sudo hosting-create test.example.com

Do not start with an important production website.

Use a disposable test site first.


62. Watch the Log

In another terminal:

sudo tail -f /var/log/cresignsys/hosting-create.log

Then run:

sudo hosting-create test.example.com

You can watch the provisioning stages.


63. Test Every Layer

After provisioning:

Linux

id test_example_com

Files

ls -la /storage/websites/test.example.com/public

PHP-FPM

ls -l /run/php/

Nginx

sudo nginx -t

MySQL

sudo mysql -e "SHOW DATABASES;"

WordPress

sudo -u test_example_com wp \
--path=/storage/websites/test.example.com/public \
core is-installed

64. Test Database Isolation

Try:

sudo mysql

Then verify the dedicated database exists.

The important architectural relationship is:

test site
    ↓
test DB user
    ↓
test database

65. Test Filesystem Isolation

Create a second test site:

site1.example.com
site2.example.com

Then test:

sudo -u site1_example_com \
touch /storage/websites/site2.example.com/public/test.txt

It should fail.

This verifies that your site users aren’t unintentionally sharing write access.


66. Test PHP Isolation

Check that:

site1

has:

site1 PHP-FPM pool

and:

site2

has:

site2 PHP-FPM pool

Run:

ps aux | grep php-fpm

67. The Result

After two sites:

/storage/websites/
│
├── site1.example.com/
│   └── public/
│
└── site2.example.com/
    └── public/

PHP-FPM:

site1 pool → site1 user
site2 pool → site2 user

MySQL:

site1_db → site1_dbuser
site2_db → site2_dbuser

Nginx:

site1.example.com → site1 socket
site2.example.com → site2 socket

This is the architecture we wanted.


68. What We Have Built

The original manual process:

Create directory
Create user
Set permissions
Create database
Create DB user
Configure PHP
Configure Nginx
Download WordPress
Configure WordPress
Test everything

has become:

sudo hosting-create example.com

69. But This Is Only Version 1

The script is useful for learning and controlled server deployment.

A production hosting platform still needs:

resource limits
backup system
SSL automation
DNS integration
site deletion
site suspension
security hardening
quotas
monitoring
rate limiting
audit logs
secret management
rollback
database maintenance
PHP version management

We will add these progressively rather than making one giant script.


70. The Most Important Design Principle

Do not make:

hosting-create

a huge collection of unvalidated shell commands.

Make it:

VALIDATE
   ↓
PREPARE
   ↓
CREATE
   ↓
VALIDATE
   ↓
ACTIVATE
   ↓
HEALTH CHECK

Every stage should have a known success condition.


71. Final CresignSys Architecture

You now have:

                     CHP
                      │
                      ▼
               hosting-create
                      │
          ┌───────────┼───────────┐
          ▼           ▼           ▼
       Linux         MySQL       PHP-FPM
          │           │            │
          ▼           ▼            ▼
       Files        Database      Socket
          │           │            │
          └───────────┼────────────┘
                      ▼
                    Nginx
                      │
                      ▼
                  WordPress
                      │
                      ▼
                    HTTP

Then SSL will extend it to:

HTTPS
  ↓
Nginx
  ↓
PHP-FPM
  ↓
WordPress
  ↓
MySQL

72. Lesson 065 — Core Principle

The most important lesson is:

A hosting platform is not just a collection of server commands. It is a controlled provisioning system that validates every layer before declaring a website active.

Your first-generation workflow is:

DOMAIN
 ↓
SITE ID
 ↓
LINUX USER
 ↓
FILES
 ↓
DATABASE
 ↓
DATABASE USER
 ↓
PHP-FPM
 ↓
NGINX
 ↓
WORDPRESS
 ↓
HEALTH CHECK
 ↓
ACTIVE

Next Lesson — 066

Make hosting-create Production-Safe

The next stage is to improve the first script rather than immediately adding more features.

We will add:

1. Safe rollback
2. Transaction-like provisioning
3. Better domain validation
4. Duplicate detection
5. Secure secret handling
6. PHP-FPM configuration validation
7. Nginx configuration validation
8. Detailed per-site logs
9. Provisioning status
10. Failure recovery
11. `--dry-run`
12. `--force` protection

Then we can safely move to:

hosting-ssl
hosting-list
hosting-info
hosting-delete
hosting-backup
hosting-restore

and eventually connect the CLI to the CresignSys Hosting Platform web control panel.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *