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.
Leave a Reply