Make hosting-create Production-Safe
Lesson 065 created the first working provisioning engine.
Now we improve it so that a failed operation does not leave the server in an inconsistent state.
The new philosophy is:
REQUEST
↓
VALIDATE EVERYTHING POSSIBLE
↓
DRY RUN / PLAN
↓
CREATE RESOURCES
↓
VALIDATE EACH RESOURCE
↓
ACTIVATE
↓
HEALTH CHECK
↓
ACTIVE
If something fails:
PROVISIONING
↓
ERROR
↓
RECOVERY / ROLLBACK
1. Why Production Safety Matters
Imagine:
Linux user ✓
directories ✓
database ✓
DB user ✓
PHP-FPM ✓
Nginx ✗
WordPress not reached
You now have a partially created website.
If you simply run:
hosting-create example.com
again, the script might say:
ERROR: user already exists
and stop.
This is why provisioning needs a defined state model.
2. Provisioning States
Use:
NEW
↓
VALIDATING
↓
PROVISIONING
↓
TESTING
↓
ACTIVE
Failure:
PROVISIONING
↓
ERROR
Recovery:
ERROR
↓
REPAIR
↓
TESTING
↓
ACTIVE
3. Never Automatically Delete Customer Data
A dangerous rollback design is:
failure
↓
rm -rf website
This is unacceptable once real customer data exists.
Instead, distinguish:
Newly created resource
Can potentially be rolled back.
Existing customer resource
Must not be destroyed automatically.
This distinction is critical.
4. Add a State File
Create:
/var/lib/cresignsys/sites/example.com/
with:
state
site.conf
provision.log
For example:
/var/lib/cresignsys/sites/example.com/
├── state
├── site.conf
└── provision.log
5. State File
Example:
STATUS=PROVISIONING
DOMAIN=example.com
SITE_ID=example_com
When everything succeeds:
STATUS=ACTIVE
If something fails:
STATUS=ERROR
6. Why State Is Important
Your control panel can later ask:
What happened to example.com?
and get:
STATUS=ERROR
instead of guessing from:
directory exists?
user exists?
database exists?
7. Create a State Function
Conceptually:
set_state() {
local state="$1"
printf 'STATUS=%s\n' "$state" > \
"${SITE_STATE_DIR}/state"
}
Then:
set_state "PROVISIONING"
or:
set_state "ACTIVE"
8. Add Provisioning ID
Every provisioning attempt should have an identifier.
For example:
PROV-20260813-153501-12345
This is useful when several operations occur simultaneously.
Generate something unique, such as:
PROVISION_ID="$(date +%Y%m%d-%H%M%S)-$$"
Then log:
PROVISION_ID=20260813-153501-12345
9. Per-Site Log
Instead of putting everything only in:
/var/log/cresignsys/hosting-create.log
also create:
/var/lib/cresignsys/sites/example.com/provision.log
Now troubleshooting a single website is much easier.
10. Better Logging
Use:
INFO
WARNING
ERROR
SUCCESS
Example:
[INFO] Creating Linux user
[INFO] Creating database
[SUCCESS] Database created
[INFO] Creating PHP-FPM pool
[ERROR] PHP-FPM validation failed
11. Centralized Error Trap
Bash can trap unexpected failures.
Conceptually:
trap 'handle_error $LINENO' ERR
Then:
handle_error() {
set_state "ERROR"
log "Provisioning failed at line $1"
}
This gives you a safety net for unexpected errors.
12. But Don’t Rely Only on trap
A trap tells you:
something failed
It doesn’t necessarily tell you:
what should be safely removed
Therefore explicit stage tracking is better.
13. Track Completed Stages
For example:
USER_CREATED=1
DIRECTORIES_CREATED=1
DATABASE_CREATED=1
DB_USER_CREATED=1
PHP_POOL_CREATED=1
NGINX_CREATED=1
WORDPRESS_CREATED=1
Initially:
USER_CREATED=0
After success:
USER_CREATED=1
14. Why Track Stages?
Suppose:
USER_CREATED=1
DIRECTORIES_CREATED=1
DATABASE_CREATED=1
DB_USER_CREATED=1
PHP_POOL_CREATED=0
You know exactly where provisioning stopped.
15. Rollback Logic
A simplified development model:
if PHP pool creation fails:
remove newly-created DB user
remove newly-created DB
remove newly-created directories
remove newly-created Linux user
But only if those resources were created by this provisioning attempt.
16. Never Roll Back Blindly
Bad:
mysql -e "DROP DATABASE example_db"
without first proving:
this database
was created by this provisioning transaction
Production systems must protect against accidental deletion.
17. Resource Ownership Record
Record:
RESOURCE
CREATED_BY_THIS_RUN
For example:
USER_CREATED=1
DB_CREATED=1
DB_USER_CREATED=1
PHP_POOL_CREATED=1
NGINX_CREATED=1
Then rollback can act only on those resources.
18. --dry-run
This is one of the most useful additions.
Run:
sudo hosting-create example.com --dry-run
It should not modify the server.
Instead:
CresignSys Provisioning Plan
----------------------------
Domain:
example.com
Linux user:
example_com
Website:
/storage/websites/example.com/public
Database:
example_com_db
PHP socket:
/run/php/example_com.sock
Nginx:
/etc/nginx/sites-available/example.com.conf
Actions:
[ ] Create user
[ ] Create directories
[ ] Create database
[ ] Create DB user
[ ] Create PHP-FPM pool
[ ] Create Nginx configuration
[ ] Install WordPress
[ ] Health check
19. Why Dry Run?
It allows you to catch:
wrong domain
wrong PHP version
wrong paths
wrong database names
existing resources
before modifying anything.
20. Argument Parsing
Instead of accepting only:
hosting-create example.com
support:
hosting-create example.com --dry-run
and perhaps:
hosting-create example.com --php=8.3
later.
For now, keep the options limited.
21. Don’t Add --force Casually
A dangerous option is:
hosting-create example.com --force
because users may assume it means:
repair
while the implementation might accidentally mean:
overwrite everything
Instead, define explicit operations:
hosting-repair
hosting-reprovision
hosting-delete
rather than a vague --force.
22. Duplicate Detection
Before provisioning, check:
domain exists?
site state exists?
Linux user exists?
database exists?
PHP pool exists?
Nginx config exists?
If any conflict exists:
ERROR: Existing resource detected.
Do not continue.
23. Example
Suppose:
/storage/websites/example.com
already exists.
Output:
ERROR: Website directory already exists.
Domain: example.com
Action: use hosting-info or hosting-repair.
This is safer than overwriting it.
24. Better Domain Validation
A domain consists of labels:
example.com
or:
learn.cresignsys.com
Each label should be handled safely.
Your validation should reject obvious filesystem/control characters such as:
/
\
:
;
'
"
$
`
and whitespace.
25. Domain vs URL
Don’t accept:
https://example.com
when the command expects:
example.com
Similarly:
example.com/path
should be rejected.
The provisioning command should receive the hostname, not a URL.
26. Validate Maximum Length
A production domain validator should enforce appropriate DNS length constraints.
At minimum, your system should not permit arbitrarily long values to become:
Linux usernames
database identifiers
file names
Nginx configuration names
27. Separate Display Name and Technical ID
This is an important architectural improvement.
Customer-facing:
Domain:
my-business.example
Internal:
Site ID:
site_000127
Then resources become:
Linux user:
csp127
PHP pool:
csp127
Database:
csp127_wp
instead of deriving everything directly from the domain.
28. Why Site IDs Are Better
Consider:
very-long-business-name-with-many-words.example.com
A domain-derived Linux/database identifier can become cumbersome.
Instead:
site_id = 127
and:
site_127
is much easier internally.
29. Recommended CHP Model
Use:
Domain
↓
Site ID
↓
Resource names
Example:
DOMAIN
learn.cresignsys.com
SITE ID
1027
LINUX USER
csp1027
DATABASE
csp1027_wp
DB USER
csp1027_db
PHP POOL
csp1027
30. This Also Prevents Naming Collisions
Two domains may produce awkward normalized names.
A unique site ID guarantees:
site_1027
site_1028
site_1029
are unique.
The domain remains the public identifier.
31. Site Registry
Your control database should eventually contain:
sites
-----
id
domain
status
linux_user
database_name
database_user
php_version
document_root
created_at
Example:
1027
learn.cresignsys.com
ACTIVE
csp1027
csp1027_wp
csp1027_db
8.3
/storage/websites/learn.cresignsys.com/public
32. This Changes hosting-create
Instead of:
hosting-create
↓
generate everything from domain
the future architecture becomes:
hosting-create
↓
reserve Site ID
↓
create resources from Site ID
↓
store metadata
This is much stronger.
33. Concurrency Problem
Imagine two administrators run:
hosting-create example.com
at exactly the same time.
Both processes might check:
Does site exist?
NO
Then both attempt to create it.
This is called a:
Race Condition
34. Use a Lock
The script should acquire a global or per-domain lock.
For example:
/var/lock/cresignsys-example.com.lock
Then:
Process A
↓
gets lock
↓
provisions
Process B
↓
cannot get lock
↓
waits/fails
35. Why Locks Matter
Without locking:
Process A → create DB
Process B → create DB
Process A → create user
Process B → create user
and you can get unpredictable failures.
36. flock
Linux provides:
flock
which is useful for this.
Conceptually:
flock -n "$LOCK_FILE" ...
This gives your provisioning operation a simple concurrency-control mechanism.
37. Disk Space Validation
Before creating:
df -P "$WEB_ROOT_BASE"
Check available space.
If insufficient:
ERROR:
Insufficient disk space.
Stop before creating resources.
38. Inode Check
Disk space isn’t the only issue.
Check:
df -Pi "$WEB_ROOT_BASE"
A filesystem can have free gigabytes but run out of inodes.
This matters on servers hosting many small files.
39. Service Health
Before provisioning:
Nginx ✓
MySQL ✓
PHP-FPM ✓
Storage ✓
WP-CLI ✓
If one is unavailable:
don't start provisioning
This avoids unnecessary partial deployments.
40. PHP Version Validation
Check:
php-fpm8.3 -t
and:
systemctl is-active php8.3-fpm
If your configured PHP version doesn’t exist:
ERROR:
Configured PHP version 8.3 is unavailable.
41. WordPress Compatibility
Your platform should eventually define supported PHP versions centrally.
For example:
PHP profile:
wordpress-default
rather than letting arbitrary users request unsupported versions.
42. Secret Handling Improvement
The first version generated:
DB_PASSWORD
inside the shell.
Now consider where it goes.
It must eventually be stored in:
wp-config.php
but should not be:
logged
printed
stored in shell history
43. Don’t Log Secrets
Never do:
log "DB_PASSWORD=$DB_PASSWORD"
Never.
Logs should contain:
database created
not:
database password = ...
44. WordPress Admin Password
The same rule applies.
Do not print:
ADMIN_PASSWORD
to:
terminal
log file
control panel logs
unless the product deliberately provides a secure one-time credential mechanism.
45. Better Future Design
Eventually:
CHP
↓
credential generator
↓
secure credential storage
↓
one-time display / password reset
rather than:
Bash output
↓
password
46. File Permission for wp-config.php
Because wp-config.php contains database credentials, its permissions should be deliberately controlled.
After installation, inspect:
ls -l wp-config.php
The exact mode should be chosen based on your PHP-FPM/Nginx ownership architecture.
The goal is:
PHP can read it
unnecessary users cannot modify it
47. Don’t Over-Harden Blindly
You may see recommendations like:
chmod 400 wp-config.php
But if PHP or your management process cannot access it, WordPress breaks.
Security must be balanced with actual process identities.
48. Nginx Security Rules
Your Nginx template should also prevent access to hidden files.
For example:
location ~ /\. {
deny all;
}
This helps block requests to files such as:
.git/
.env
.htaccess
depending on the exact Nginx configuration.
49. Protect Sensitive Files
You should also think about:
backup.zip
database.sql
.env
.git
composer.json
composer.lock
Some files should never be publicly downloadable.
A hardened template should explicitly define what should and should not be exposed.
50. PHP File Upload Limits
WordPress sites may need settings such as:
upload_max_filesize
post_max_size
memory_limit
max_execution_time
Don’t hard-code huge values for every site.
These should become:
hosting plan settings
later.
51. Example Plan Configuration
Starter
memory_limit = 256M
upload_max_filesize = 64M
Business
memory_limit = 512M
upload_max_filesize = 128M
Professional
memory_limit = 768M
upload_max_filesize = 256M
These are example policy values, not universal recommendations.
52. Resource Profiles
Instead of putting resource settings directly into hosting-create, create:
/etc/cresignsys/plans/
For example:
starter.conf
business.conf
professional.conf
Then:
site
↓
plan
↓
resource configuration
53. This Is the Beginning of Resource Governance
Your platform eventually controls:
Storage
CPU-related PHP concurrency
Memory-related PHP settings
Database limits
Backup retention
Bandwidth policy
This is what transforms a server into a hosting service.
54. Add hosting-info
A useful next command:
sudo hosting-info example.com
Output:
Domain:
example.com
Status:
ACTIVE
Site ID:
1027
Linux user:
csp1027
Web root:
/storage/websites/example.com/public
PHP:
8.3
Database:
csp1027_wp
SSL:
ACTIVE
This should become a read-only diagnostic command.
55. Add hosting-list
Eventually:
sudo hosting-list
could show:
DOMAIN STATUS
------------------------------------------------
learn.cresignsys.com ACTIVE
shop.cresignsys.com ACTIVE
example.com ERROR
This will become the CLI equivalent of the control panel’s website list.
56. Add hosting-health
For one site:
sudo hosting-health example.com
Output:
Linux user ✓
Website files ✓
PHP-FPM ✓
PHP socket ✓
Nginx ✓
Database ✓
WordPress ✓
HTTPS ✓
57. Repair Instead of Recreate
If:
PHP socket ✗
you should eventually be able to run:
sudo hosting-repair example.com --php
If:
Nginx ✗
then:
sudo hosting-repair example.com --nginx
This is much safer than recreating the entire site.
58. Provisioning Becomes Modular
Eventually:
hosting-create
│
├── validate
├── filesystem
├── database
├── php
├── nginx
├── wordpress
└── health
and:
hosting-repair
│
├── filesystem
├── database
├── php
├── nginx
└── health
can reuse the same modules.
59. Recommended File Layout
A more mature CHP server might look like:
/etc/cresignsys/
├── hosting.conf
├── plans/
│ ├── starter.conf
│ ├── business.conf
│ └── professional.conf
├── templates/
│ ├── nginx-wordpress.conf
│ └── php-fpm.conf
└── scripts/
├── common.sh
├── filesystem.sh
├── database.sh
├── php.sh
├── nginx.sh
├── wordpress.sh
└── health.sh
60. The Main Command Becomes Small
Instead of one enormous Bash file:
hosting-create
the main script eventually becomes an orchestrator:
validate
↓
filesystem.create
↓
database.create
↓
php.create
↓
nginx.create
↓
wordpress.create
↓
health.check
This is a much better long-term architecture.
61. Production-Safe Provisioning Flow
The improved workflow is:
hosting-create
│
▼
Validate request
│
▼
Acquire lock
│
▼
Dry-run plan
│
▼
Create state record
│
▼
Create filesystem
│
▼
Create database
│
▼
Create PHP-FPM
│
▼
Create Nginx
│
▼
Install WP
│
▼
Health checks
│
▼
ACTIVE
Failure anywhere:
ERROR
│
┌────────┴────────┐
▼ ▼
recover rollback
with rollback restricted to resources created by that operation.
62. What We Should Not Automate Yet
Don’t add everything at once.
Avoid immediately combining:
DNS API
SSL
backups
billing
email
resource quotas
firewall changes
into one Bash script.
First make:
hosting-create
reliable.
Then add each subsystem separately.
63. Lesson 066 — Core Principle
The difference between a script and a hosting platform is reliability.
A simple script says:
run commands
A provisioning engine says:
validate
lock
plan
create
verify
record
recover
activate
That distinction is fundamental.
64. Target CHP Command Set
The architecture we are building toward is:
hosting-create DOMAIN
hosting-list
hosting-info DOMAIN
hosting-health DOMAIN
hosting-repair DOMAIN
hosting-ssl DOMAIN
hosting-backup DOMAIN
hosting-restore DOMAIN
hosting-suspend DOMAIN
hosting-unsuspend DOMAIN
hosting-delete DOMAIN
Each command should perform one clearly defined operational task.
Next Lesson — 067
Build hosting-info, hosting-list and hosting-health
Before adding SSL, we should build the observability layer.
You will learn to create:
sudo hosting-list
sudo hosting-info example.com
sudo hosting-health example.com
These commands will inspect:
Domain
Site ID
Status
Linux user
Directory
Disk usage
PHP-FPM
PHP socket
Nginx
Database
WordPress
SSL
This gives your CresignSys Hosting Platform the ability to see and diagnose every hosted website before we add more automation.
Leave a Reply