Author: cresignsys

  • CresignSys Learn — Lesson 074

    Build hosting-domain

    We now need a proper domain management layer.

    Until now, we treated:

    example.com

    as if one domain always equals one website.

    A real hosting platform needs more flexibility:

    example.com
    www.example.com
    blog.example.com
    shop.example.com

    may all have different relationships to the same site.


    1. Domain Architecture

    The relationship should become:

                             SITE
                              │
                ┌─────────────┼─────────────┐
                ▼             ▼             ▼
            Primary         Alias        Redirect
             Domain         Domain        Domain
                │             │             │
                └─────────────┼─────────────┘
                              ▼
                            Nginx
                              │
                         ┌────┴────┐
                         ▼         ▼
                       HTTP       HTTPS
                                   │
                                   ▼
                                  SSL

    2. Why Separate Domain From Site?

    Consider:

    example.com
    www.example.com

    Both may point to the same WordPress installation:

    /storage/websites/example.com/public

    Therefore:

    SITE
     └── example.com
          ├── example.com
          └── www.example.com

    The second name is an alias, not another website.


    3. Domain Types

    Use three basic types.

    Primary

    example.com

    The canonical domain.

    Alias

    www.example.com

    Another hostname serving the same site.

    Redirect

    old-example.com

    which redirects to:

    example.com

    4. Commands

    The first version:

    sudo hosting-domain-add example.com
    sudo hosting-domain-add www.example.com --alias example.com
    sudo hosting-domain-add old-example.com --redirect example.com

    List:

    sudo hosting-domain-list example.com

    Remove:

    sudo hosting-domain-remove www.example.com

    5. Primary Domain

    When creating a site:

    sudo hosting-create example.com

    the initial domain becomes:

    TYPE=PRIMARY

    Example metadata:

    SITE_ID=1027
    PRIMARY_DOMAIN=example.com

    6. Domain Registry

    Create:

    /var/lib/cresignsys/domains/

    For example:

    /var/lib/cresignsys/domains/example.com

    But rather than making the domain directory itself the only source of truth, maintain a structured registry.

    For the first version:

    /var/lib/cresignsys/sites/example.com/site.conf

    can contain the domain relationships.


    7. Better Site Metadata

    Example:

    SITE_ID=1027
    PRIMARY_DOMAIN=example.com
    DOMAIN_LIST="example.com www.example.com"

    But space-separated lists become fragile.

    A better long-term approach is a database.


    8. Domain Database Model

    Eventually:

    sites
    -----
    id
    primary_domain
    status
    
    domains
    -------
    id
    site_id
    domain
    type
    target
    status

    Example:

    sites
    ------------------------------------------------
    1027 | example.com | ACTIVE
    
    domains
    ------------------------------------------------
    1 | 1027 | example.com     | PRIMARY  | -
    2 | 1027 | www.example.com | ALIAS    | -
    3 | 1027 | old-example.com | REDIRECT | example.com

    This is much easier to manage.


    9. Domain Validation

    Before adding:

    example.com

    validate the hostname.

    Reject:

    https://example.com

    because that is a URL, not a hostname.

    Reject:

    example.com/path

    Reject:

    example.com:443

    The domain field should contain a valid hostname.


    10. Normalize the Domain

    Convert:

    Example.COM

    to:

    example.com

    DNS names are case-insensitive.

    Normalization prevents duplicate entries such as:

    example.com
    Example.com
    EXAMPLE.COM

    11. Validate Labels

    A domain contains labels:

    www
    example
    com

    Each label has restrictions.

    For basic hosting:

    letters
    numbers
    hyphen

    should be handled.

    Don’t accept arbitrary characters into Nginx configuration.


    12. Security Principle

    Never directly insert unvalidated user input into:

    Nginx configuration
    shell commands
    filesystem paths
    SQL

    For example, don’t construct:

    rm -rf "/storage/websites/$DOMAIN"

    without strict domain validation.


    13. Check Existing Domain

    Before adding:

    sudo hosting-domain-add example.com

    check whether it already belongs to:

    same site
    different site
    system configuration

    Possible result:

    Domain already belongs to example.com

    or:

    ERROR:
    example.com belongs to another site.

    Never silently reassign it.


    14. Primary Domain Rules

    Each site should have exactly one:

    PRIMARY

    For example:

    example.com → PRIMARY
    www.example.com → ALIAS

    You shouldn’t have:

    example.com → PRIMARY
    shop.com → PRIMARY

    inside the same site.

    If the user wants to change the primary domain, that should be a separate controlled operation.


    15. Alias

    Add:

    sudo hosting-domain-add www.example.com --alias example.com

    The result:

    example.com
       │
       └── www.example.com

    Both can serve the same application.


    16. Alias and Canonical Domain

    You must decide whether an alias:

    Option A

    Serves the same content:

    www.example.com
           ↓
    same WordPress

    or:

    Option B

    Redirects:

    www.example.com
           ↓
    301
           ↓
    example.com

    For WordPress, canonical-domain redirects are often preferable because they avoid duplicate hostname access.


    17. Recommended Model

    Separate:

    ALIAS

    from:

    REDIRECT

    So:

    www.example.com → ALIAS

    means:

    same site

    while:

    old-example.com → REDIRECT → example.com

    means:

    different hostname
         ↓
    301 redirect

    18. Nginx Server Name

    For an alias, Nginx can use:

    server_name example.com www.example.com;

    This means one server block handles both names.


    19. Redirect Domain

    For a redirect:

    server {
        server_name old-example.com;
    
        return 301 https://example.com$request_uri;
    }

    The redirect domain doesn’t need its own WordPress installation.


    20. SSL Implications

    This is extremely important.

    If:

    example.com
    www.example.com

    both serve HTTPS, the certificate needs to cover both names.

    Therefore SSL should understand:

    PRIMARY
    +
    ALIASES

    21. Certificate Names

    For example:

    example.com
    www.example.com

    can be included in the same certificate.

    Then:

    hosting-ssl example.com

    should eventually inspect all active domain names attached to the site.


    22. Adding an Alias After SSL

    Suppose:

    example.com

    already has SSL.

    Then:

    hosting-domain-add www.example.com --alias example.com

    creates a new requirement:

    certificate currently:
    example.com
    
    required:
    example.com
    www.example.com

    Therefore the platform should mark:

    SSL_STATE=RENEWAL_REQUIRED

    or:

    SSL_STATE=NEEDS_UPDATE

    until the certificate is updated.


    23. Don’t Automatically Break HTTPS

    If you modify:

    server_name

    before updating SSL, you could create:

    www.example.com
          ↓
    HTTPS
          ↓
    certificate mismatch

    Therefore domain addition and SSL update must be coordinated.


    24. Domain Addition Workflow

    For an alias:

    hosting-domain-add
            │
            ▼
    Validate domain
            │
            ▼
    Check uniqueness
            │
            ▼
    Register domain
            │
            ▼
    Generate Nginx config
            │
            ▼
    nginx -t
            │
            ▼
    Reload
            │
            ▼
    Update SSL requirement

    25. DNS Is Not Automatically Controlled

    Adding a domain to CHP does not necessarily create DNS records.

    These are separate layers:

    Domain Registration
            ↓
    DNS
            ↓
    CHP Domain Registry
            ↓
    Nginx
            ↓
    SSL

    If your platform doesn’t operate authoritative DNS, it should tell the administrator what DNS record is required.


    26. DNS Requirement Output

    For:

    www.example.com

    show:

    Required DNS:
    
    Type: A
    Name: www
    Value: YOUR_SERVER_IP

    If IPv6 is supported:

    Type: AAAA
    Name: www
    Value: YOUR_IPV6

    27. Don’t Assume DNS Is Ready

    After adding:

    www.example.com

    the system should be able to say:

    DNS:
    PENDING

    rather than:

    ACTIVE

    28. Domain State

    Each domain can have:

    PENDING_DNS
    ACTIVE
    SUSPENDED
    ERROR
    REMOVED

    For example:

    example.com
        DNS=ACTIVE
    
    www.example.com
        DNS=PENDING

    29. Domain Health

    Eventually:

    hosting-domain-list example.com

    can show:

    DOMAIN                 TYPE       DNS       SSL
    --------------------------------------------------
    example.com             PRIMARY    OK        OK
    www.example.com         ALIAS      OK        OK
    old-example.com         REDIRECT   OK        OK

    30. Create hosting-domain-add

    Create:

    sudo nano /usr/local/bin/hosting-domain-add

    Basic structure:

    #!/usr/bin/env bash
    
    set -Eeuo pipefail

    Require root:

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

    31. Parse Arguments

    Basic syntax:

    hosting-domain-add DOMAIN

    or:

    hosting-domain-add DOMAIN --alias TARGET

    or:

    hosting-domain-add DOMAIN --redirect TARGET

    For example:

    www.example.com --alias example.com

    means:

    DOMAIN=www.example.com
    TARGET=example.com
    TYPE=ALIAS

    32. Validate Target

    For:

    hosting-domain-add www.example.com --alias example.com

    verify:

    example.com

    exists as a CHP site.

    Otherwise:

    ERROR:
    Target site does not exist.

    33. Redirect Validation

    For:

    hosting-domain-add old-example.com --redirect example.com

    check:

    old-example.com != example.com

    and:

    target exists

    Also avoid redirect loops.


    34. Redirect Loop Example

    Don’t allow:

    old.example.com → example.com
    example.com → old.example.com

    or:

    A → B
    B → C
    C → A

    Your domain registry should detect cycles.


    35. Redirect Chain

    Prefer:

    old.example.com
           ↓
    example.com

    not:

    old.example.com
           ↓
    www.example.com
           ↓
    example.com

    Long redirect chains increase latency and complicate SEO/application behavior.


    36. Domain Database Constraints

    When you implement the database version, enforce:

    domain UNIQUE

    This prevents:

    example.com → site 1
    example.com → site 2

    from existing simultaneously.


    37. Domain Ownership

    A domain can only belong to one site.

    Example:

    example.com
       ↓
    SITE 1027

    Another site cannot claim:

    example.com

    unless the domain is explicitly removed/reassigned.


    38. Reassignment

    Changing:

    example.com
    SITE A

    to:

    example.com
    SITE B

    should be a separate operation:

    hosting-domain-move example.com SITE_B

    It should not happen implicitly.


    39. Why Move Is Dangerous

    Moving a domain changes:

    Nginx
    SSL
    WordPress canonical URL
    DNS expectations
    traffic routing

    Therefore it deserves its own controlled workflow.


    40. hosting-domain-list

    Create:

    sudo nano /usr/local/bin/hosting-domain-list

    Expected:

    CresignSys Domains
    ==================
    
    Site: example.com
    
    DOMAIN                TYPE       STATUS
    ------------------------------------------------
    example.com            PRIMARY    ACTIVE
    www.example.com        ALIAS      ACTIVE
    old-example.com        REDIRECT   ACTIVE

    41. hosting-domain-remove

    This command must be conservative.

    For:

    hosting-domain-remove www.example.com

    remove:

    domain registry entry
    Nginx hostname
    SSL hostname requirement

    but do not remove:

    website files
    database
    site itself

    42. Never Allow Primary Removal Without Replacement

    Suppose:

    example.com → PRIMARY

    and the user runs:

    hosting-domain-remove example.com

    Don’t allow it unless:

    another domain

    is promoted to primary.

    Otherwise:

    SITE
     ↓
    NO PRIMARY DOMAIN

    which breaks the site’s identity.


    43. Primary Domain Change

    A future command:

    hosting-domain-primary example.com www.example.com

    could change:

    OLD:
    example.com → PRIMARY
    www.example.com → ALIAS
    
    NEW:
    www.example.com → PRIMARY
    example.com → ALIAS

    But this should also update WordPress URLs when appropriate.


    44. Domain + WordPress

    Changing the primary domain may require:

    siteurl
    home
    canonical URLs
    redirects
    media URLs
    SSL
    Nginx

    Therefore domain management must eventually coordinate with the application layer.


    45. Domain + SSL

    The relationship becomes:

    Site
     │
     ├── Domain A
     ├── Domain B
     └── Domain C
           │
           ▼
     SSL Certificate
           │
           ├── A
           ├── B
           └── C

    So the certificate should be generated from the current domain registry rather than manually listing names.


    46. Domain + Backup

    Backup doesn’t need to create a separate backup for every alias.

    For:

    example.com
    www.example.com

    both use:

    same site data

    Therefore:

    one site backup

    is sufficient.


    47. Domain + Suspension

    If the site is:

    SUSPENDED

    all domains attached to the site should follow the suspension state.

    For example:

    example.com
    www.example.com

    both return:

    503

    A redirect domain may continue redirecting, depending on your policy, but the final target should remain unavailable.


    48. Domain + Health

    Health checks should eventually test every active hostname.

    For:

    example.com
    www.example.com

    check:

    DNS
    HTTP
    HTTPS
    certificate

    This catches:

    www.example.com

    being broken while:

    example.com

    works.


    49. Domain + Backup Restore

    When restoring:

    example.com

    the attached domain registry should not disappear.

    Restore customer data, but preserve current infrastructure relationships:

    example.com
    www.example.com

    remain attached to the site.


    50. Domain Registry vs Backup

    This reinforces our previous architecture:

    BACKUP DATA
        │
        ├── WordPress files
        └── Database
    
    CURRENT INFRASTRUCTURE
        │
        ├── Domains
        ├── Nginx
        ├── PHP
        └── SSL

    The current infrastructure should generally remain authoritative during restore.


    51. Domain Status Example

    A complete site might show:

    SITE
    --------------------------------
    Primary:
    example.com
    
    Domains:
    example.com       PRIMARY
    www.example.com   ALIAS
    old.example.com   REDIRECT
    
    DNS:
    OK
    
    SSL:
    OK
    
    Service:
    ACTIVE

    52. Domain Addition Output

    CresignSys Domain Add
    =====================
    
    Domain:
    www.example.com
    
    Type:
    ALIAS
    
    Target:
    example.com
    
    [OK] Domain validated
    [OK] Target site found
    [OK] Domain available
    [OK] Domain registered
    [OK] Nginx configuration generated
    [OK] nginx -t
    [OK] Nginx reloaded
    
    DNS:
    PENDING
    
    SSL:
    UPDATE REQUIRED

    This is an honest result.


    53. Why SSL UPDATE REQUIRED?

    Because adding the hostname changes the set of names the certificate should cover.

    The administrator can then run:

    sudo hosting-ssl example.com

    to update the certificate.

    Eventually this can be automated safely.


    54. Domain Removal Output

    CresignSys Domain Remove
    ========================
    
    Domain:
    www.example.com
    
    Type:
    ALIAS
    
    [OK] Domain removed
    [OK] Nginx configuration updated
    [OK] nginx -t
    [OK] Nginx reloaded
    
    SSL:
    UPDATE REQUIRED

    Again, the website itself remains intact.


    55. Redirect Output

    CresignSys Domain Add
    =====================
    
    Domain:
    old-example.com
    
    Type:
    REDIRECT
    
    Target:
    example.com
    
    [OK] Domain validated
    [OK] Target validated
    [OK] Redirect configuration generated
    [OK] nginx -t
    [OK] Nginx reloaded
    
    Status:
    ACTIVE

    56. Wildcard Domains

    Eventually you may want:

    *.example.com

    But don’t implement wildcard domains in the first version.

    They complicate:

    DNS
    SSL
    routing
    security
    customer isolation

    Start with explicit hostnames.


    57. Internationalized Domain Names

    International domain names introduce:

    Unicode
    Punycode

    For the initial CHP version, use normalized ASCII/Punycode hostnames.

    Internationalized domain support can be added later.


    58. Domain Security

    The domain-management layer is a major security boundary.

    Never allow a customer/site user to register:

    google.com

    or another customer’s domain unless your platform’s ownership verification process confirms control.


    59. Domain Ownership Verification

    For a reseller/hosting platform, eventually implement:

    Domain
     ↓
    DNS verification
     ↓
    Ownership confirmed
     ↓
    Attach to site

    For example:

    TXT
    _cresignsys-verification.example.com

    with a generated token.


    60. Why Ownership Verification Matters

    Without it, someone could attempt:

    hosting-domain-add bank-example.com --alias theirsite.com

    and potentially configure your infrastructure to respond for a domain they don’t control.

    Therefore domain attachment should eventually require proof of control.


    61. DNS Verification Token

    Conceptually:

    _cresignsys-verification.example.com

    TXT:

    cresignsys-site-1027-randomtoken

    CHP checks:

    DNS TXT
       ↓
    token matches
       ↓
    ownership verified

    Then the domain becomes attachable.


    62. But Don’t Mix This Into the First Script

    First version:

    hosting-domain-add

    can work for administrator-controlled domains.

    Later:

    hosting-domain-verify

    can provide customer self-service ownership verification.


    63. Multi-Domain Site Architecture

    We now have:

                          SITE 1027
                              │
            ┌─────────────────┼─────────────────┐
            ▼                 ▼                 ▼
         example.com     www.example.com   old-example.com
          PRIMARY           ALIAS             REDIRECT
            │                 │                 │
            └─────────────────┼─────────────────┘
                              ▼
                            Nginx
                              │
                        ┌─────┴─────┐
                        ▼           ▼
                      PHP         SSL
                        │
                        ▼
                     WordPress

    64. Updated CHP Model

    The platform is becoming:

                             CHP
                              │
           ┌──────────────────┼──────────────────┐
           ▼                  ▼                  ▼
         Sites              Domains            Services
           │                  │                  │
           ▼                  ▼                  ▼
     WordPress           Primary/Alias       PHP-FPM
     Files               Redirect            Nginx
     Database            DNS                 MySQL
     Backup              SSL                 SSL
     Restore

    65. Lesson 074 — Core Principle

    A domain is not the same thing as a website.

    The correct model is:

    SITE
      ├── Primary Domain
      ├── Aliases
      └── Redirect Domains

    This separation makes:

    SSL
    DNS
    Nginx
    WordPress
    suspension
    backup
    restore

    much easier to manage correctly.


    66. Current CHP Command Set

    hosting-create DOMAIN
    
    hosting-list
    
    hosting-info DOMAIN
    
    hosting-health DOMAIN
    
    hosting-repair DOMAIN --php
    hosting-repair DOMAIN --nginx
    hosting-repair DOMAIN --permissions
    hosting-repair DOMAIN --wordpress
    
    hosting-ssl DOMAIN
    
    hosting-backup DOMAIN
    hosting-backup-list DOMAIN
    hosting-backup-verify DOMAIN BACKUP_ID
    hosting-backup-prune DOMAIN --dry-run
    hosting-backup-prune DOMAIN --execute
    
    hosting-restore DOMAIN BACKUP_ID
    
    hosting-suspend DOMAIN
    hosting-unsuspend DOMAIN
    
    hosting-domain-add DOMAIN
    hosting-domain-add DOMAIN --alias TARGET
    hosting-domain-add DOMAIN --redirect TARGET
    
    hosting-domain-list DOMAIN
    
    hosting-domain-remove DOMAIN

    Next Lesson — 075

    Build hosting-dns-check

    The next layer connects the domain registry with actual DNS.

    We need a command such as:

    sudo hosting-dns-check example.com

    that determines:

    Domain
      ↓
    A record
      ↓
    AAAA record
      ↓
    CNAME
      ↓
    Nameserver resolution
      ↓
    Expected server
      ↓
    Actual server

    The output should distinguish:

    DNS = CORRECT
    DNS = PENDING
    DNS = WRONG
    DNS = MULTIPLE
    DNS = ERROR

    and eventually provide:

    Required DNS
    ----------------
    A      @       SERVER_IP
    A      www     SERVER_IP
    AAAA   @       SERVER_IPV6

    This will connect the domain layer → DNS → HTTP → HTTPS → SSL into one complete provisioning workflow.

  • CresignSys Learn — Lesson 073

    Build hosting-suspend and hosting-unsuspend

    We now have:

    hosting-create
    hosting-list
    hosting-info
    hosting-health
    hosting-repair
    hosting-ssl
    hosting-backup
    hosting-backup-list
    hosting-backup-verify
    hosting-backup-prune
    hosting-restore

    The next lifecycle feature is:

    sudo hosting-suspend example.com

    and:

    sudo hosting-unsuspend example.com

    The central rule is:

    Suspension disables service; it does not delete customer data.


    1. Why Suspension?

    A hosting platform needs to temporarily disable a website for situations such as:

    Payment overdue
    Abuse investigation
    Customer request
    Maintenance
    Security incident
    Resource violation
    Administrative action

    But the website must remain recoverable.


    2. Suspension vs Deletion

    These are completely different.

    Suspend

    Website access
          ↓
    BLOCKED
    
    Files       preserved
    Database    preserved
    SSL         preserved
    Backups     preserved
    Configuration preserved

    Delete

    Website
      ↓
    data removal
      ↓
    configuration removal

    Therefore:

    hosting-suspend
            ≠
    hosting-delete

    3. Site State Machine

    Our site lifecycle now becomes:

                        ┌─────────────┐
                        │   CREATED   │
                        └──────┬──────┘
                               │
                               ▼
                           ACTIVE
                           │    │
                 suspend   │    │ repair
                           ▼    │
                       SUSPENDED│
                           │    │
                 unsuspend  │    │
                           ▼    │
                         ACTIVE │
                                │
                                ▼
                             DELETED

    4. Important States

    Use explicit states:

    PROVISIONING
    ACTIVE
    SUSPENDING
    SUSPENDED
    UNSUSPENDING
    ERROR
    DELETING
    DELETED

    This is better than only:

    active=true

    5. Suspension Architecture

    The safe sequence:

    hosting-suspend example.com
              │
              ▼
         Verify site
              │
              ▼
         Acquire lock
              │
              ▼
       Optional final backup
              │
              ▼
         Set SUSPENDING
              │
              ▼
     Disable website access
              │
              ▼
       Validate configuration
              │
              ▼
          Set SUSPENDED

    6. What Should Suspension Do?

    For the first CHP implementation:

    HTTP access      blocked
    HTTPS access     blocked
    PHP execution    unavailable through site
    Files            preserved
    Database         preserved
    SSL              preserved
    Backups          preserved
    DNS              unchanged

    7. What Should Suspension NOT Do?

    Do not:

    delete files
    drop database
    delete SSL
    delete DNS
    delete backups
    remove site metadata

    The entire point is reversibility.


    8. Why Not Delete DNS?

    Suppose:

    example.com
       ↓
    SERVER IP

    During suspension, leave DNS alone.

    Changing DNS introduces unnecessary propagation delays and makes unsuspension less predictable.


    9. Suspension Methods

    There are several ways to block the site.

    Method A — Nginx suspension page

    HTTP/HTTPS
        ↓
    Nginx
        ↓
    Suspension page

    Method B — firewall

    Internet
       ↓
    firewall
       ↓
    BLOCK

    Method C — remove Nginx configuration

    Not recommended.

    For CHP:

    Prefer an Nginx suspension configuration.


    10. Why Nginx Is Better Here

    We want:

    DNS → server

    to remain valid.

    Nginx can respond:

    HTTP 403

    or:

    HTTP 503

    with:

    Website temporarily unavailable

    This is easier to reverse.


    11. HTTP Status Code

    For a suspended website, use:

    503 Service Unavailable

    rather than:

    404 Not Found

    Why?

    Because the website hasn’t disappeared.

    It is temporarily unavailable.


    12. Suspension Page

    Create a centralized page:

    /etc/cresignsys/suspended/index.html

    Example:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Website Temporarily Unavailable</title>
    </head>
    <body>
        <h1>Website Temporarily Unavailable</h1>
        <p>This website has been temporarily suspended.</p>
    </body>
    </html>

    Later you can make this branded as:

    CresignSys Hosting

    13. Don’t Put Suspension Pages in Every Website

    Avoid:

    example.com/public/suspended.html
    shop.com/public/suspended.html
    site3.com/public/suspended.html

    Instead use:

    /etc/cresignsys/suspended/

    This makes the infrastructure centrally managed.


    14. Nginx Suspension Configuration

    Conceptually:

    location / {
        return 503;
    }
    
    error_page 503 /suspended.html;
    
    location = /suspended.html {
        root /etc/cresignsys/suspended;
        internal;
    }

    This causes the website to respond with a suspension page.

    The exact configuration should be incorporated into your CHP Nginx template rather than manually appended to arbitrary configurations.


    15. Preserve HTTPS

    If the site already has:

    HTTPS

    don’t remove the certificate.

    The suspended site should still be able to respond over:

    https://example.com

    with:

    503 Service Unavailable

    This prevents unnecessary certificate reissuance when the site is unsuspended.


    16. Why Preserve SSL?

    Without this:

    SUSPEND
     ↓
    delete HTTPS configuration
     ↓
    UNSUSPEND
     ↓
    request certificate again

    creates unnecessary complexity.

    Better:

    SUSPEND
     ↓
    HTTPS remains
     ↓
    503
     ↓
    UNSUSPEND
     ↓
    normal HTTPS

    17. Create hosting-suspend

    Create:

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

    Start:

    #!/usr/bin/env bash
    
    set -Eeuo pipefail

    18. Root Check

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

    19. Domain Argument

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

    20. Load Site Metadata

    STATE_DIR="/var/lib/cresignsys/sites"
    SITE_DIR="${STATE_DIR}/${DOMAIN}"
    SITE_CONF="${SITE_DIR}/site.conf"
    
    if [[ ! -f "$SITE_CONF" ]]; then
        echo "ERROR: Site not found."
        exit 1
    fi
    
    source "$SITE_CONF"

    21. Check Current State

    If:

    STATUS=SUSPENDED

    then:

    hosting-suspend example.com

    should return:

    Site is already suspended.

    It should not perform the operation again unnecessarily.

    This is idempotency.


    22. Don’t Suspend a Site While Provisioning

    If:

    STATUS=PROVISIONING

    don’t allow:

    hosting-suspend

    The state machine should reject invalid transitions.

    For example:

    PROVISIONING → SUSPENDED

    should not be allowed.


    23. Valid Suspension Transition

    ACTIVE
      ↓
    SUSPENDING
      ↓
    SUSPENDED

    If an error occurs:

    SUSPENDING
         ↓
    ERROR

    The system must record what happened.


    24. Acquire Lock

    Use the site lock:

    /var/lock/cresignsys-example.com.lock

    This prevents:

    hosting-suspend

    from racing with:

    hosting-backup
    hosting-repair
    hosting-restore
    hosting-delete

    25. Optional Final Backup

    Before suspension, you can optionally create a final backup:

    ACTIVE
     ↓
    BACKUP
     ↓
    SUSPEND

    This is useful for administrative actions.

    However, don’t make this mandatory for every suspension if it creates unnecessary storage/cost.

    A configurable policy is better:

    SUSPEND_BACKUP=true

    26. Set State

    Before modifying Nginx:

    STATUS=SUSPENDING

    This prevents another operation from treating the site as fully active during the transition.


    27. Generate Suspended Configuration

    Don’t manually edit:

    /etc/nginx/sites-available/example.com.conf

    with random sed operations.

    Instead:

    Current site metadata
           ↓
    Nginx template
           ↓
    MODE=SUSPENDED
           ↓
    Generate temporary config

    28. Template Architecture

    Your Nginx template can eventually support:

    MODE=ACTIVE
    MODE=SUSPENDED

    For example:

    /etc/cresignsys/templates/nginx-site.conf

    The template chooses the correct behavior.


    29. Why Template-Based Suspension?

    Without a template:

    hosting-create
    hosting-ssl
    hosting-repair
    hosting-suspend
    hosting-unsuspend

    could each modify Nginx differently.

    Eventually the configuration becomes inconsistent.

    One source of truth is safer.


    30. Generate Temporary Configuration

    Example:

    /etc/nginx/sites-available/example.com.conf.tmp

    Then:

    nginx -t

    Only if successful:

    .tmp
     ↓
    actual configuration

    31. Reload Nginx

    systemctl reload nginx

    A reload is preferable to restarting the entire Nginx service because existing connections can be handled more gracefully.


    32. Verify Suspension

    Run:

    curl -I https://example.com

    Expected:

    HTTP/2 503

    or an equivalent configured response.


    33. Verify HTTP

    Also:

    curl -I http://example.com

    Depending on your architecture, HTTP may:

    HTTP → HTTPS → 503

    which is perfectly acceptable.

    The important thing is that the application is unavailable while HTTPS remains functional.


    34. Set Final State

    Only after verification:

    STATUS=SUSPENDED

    If the health test fails unexpectedly:

    STATUS=ERROR

    and investigate.


    35. Suspension Output

    A successful result:

    CresignSys Suspend
    ==================
    
    Domain: example.com
    
    [OK] Site verified
    [OK] Site lock acquired
    [OK] Pre-suspension backup
    [OK] Suspension configuration generated
    [OK] nginx -t
    [OK] Nginx reloaded
    [OK] HTTPS returns 503
    
    Status: SUSPENDED

    36. What the User Sees

    Instead of their WordPress site:

    Website Temporarily Unavailable
    
    This website has been temporarily suspended.
    Please contact the hosting administrator.

    37. Better Suspension Page

    Eventually use:

    CresignSys Hosting
    ------------------
    
    Website Temporarily Unavailable
    
    Domain:
    example.com
    
    Reason:
    This website is currently suspended.
    
    Contact:
    support@example.com

    However, be careful about exposing internal administrative information or suspension reasons publicly.


    38. Don’t Expose Sensitive Reasons

    Avoid displaying:

    Payment overdue by ₹4,521
    Abuse report #12345
    Security incident details

    on a public suspension page.

    Use generic messaging:

    This website is temporarily unavailable.
    Please contact the hosting provider.

    39. Build hosting-unsuspend

    Create:

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

    Start with the same structure:

    #!/usr/bin/env bash
    
    set -Eeuo pipefail

    40. Check State

    If:

    STATUS=ACTIVE

    then:

    Site is already active.

    If:

    STATUS=SUSPENDED

    continue.


    41. Unsuspend Workflow

    SUSPENDED
       ↓
    Acquire lock
       ↓
    UNSUSPENDING
       ↓
    Generate ACTIVE Nginx configuration
       ↓
    nginx -t
       ↓
    reload nginx
       ↓
    HTTP/HTTPS test
       ↓
    health check
       ↓
    ACTIVE

    42. Important: Don’t Just Flip the Database Flag

    This is insufficient:

    STATUS=ACTIVE

    because the Nginx configuration may still be:

    MODE=SUSPENDED

    The actual service state and metadata state must agree.


    43. Configuration + State Must Change Together

    Correct:

    Generate active configuration
            ↓
    Validate
            ↓
    Reload
            ↓
    Health check
            ↓
    STATUS=ACTIVE

    Incorrect:

    STATUS=ACTIVE
            ↓
    Nginx still suspended

    44. Unsuspend Validation

    Test:

    curl -I https://example.com

    Expected:

    HTTP/2 200

    or:

    HTTP/2 301

    depending on the application.

    It should no longer return:

    503

    45. Run Full Health Check

    After unsuspending:

    hosting-health example.com

    Expected:

    [OK] DNS
    [OK] Nginx
    [OK] PHP-FPM
    [OK] PHP socket
    [OK] WordPress
    [OK] Database
    [OK] HTTPS

    Only then:

    STATUS=ACTIVE

    46. Unsuspend Output

    CresignSys Unsuspend
    ====================
    
    Domain: example.com
    
    [OK] Site verified
    [OK] Site lock acquired
    [OK] Active configuration generated
    [OK] nginx -t
    [OK] Nginx reloaded
    [OK] HTTPS response
    [OK] Full health check
    
    Status: ACTIVE

    47. What If Unsuspend Fails?

    Suppose:

    Nginx configuration
            ↓
    FAIL

    Then:

    SUSPENDED

    should remain the logical state.

    Do not say:

    ACTIVE

    until the site actually works.


    48. Unsuspend Failure Model

    SUSPENDED
       ↓
    UNSUSPENDING
       ↓
    configuration error
       ↓
    restore previous suspended config
       ↓
    STATUS=SUSPENDED

    This is safer.


    49. Suspension Must Be Reversible

    The key requirement:

    ACTIVE
      ↓
    SUSPEND
      ↓
    SUSPENDED
      ↓
    UNSUSPEND
      ↓
    ACTIVE

    should work repeatedly.

    For example:

    ACTIVE
    → SUSPENDED
    → ACTIVE
    → SUSPENDED
    → ACTIVE

    without configuration accumulating duplicate directives.


    50. Avoid sed-Based Toggle Logic

    Bad design:

    sed -i 's/return 503/#return 503/' ...

    Then another command:

    sed -i ...

    After multiple operations, the configuration becomes fragile.

    Better:

    site metadata
          ↓
    template
          ↓
    desired state
          ↓
    complete configuration

    51. Desired-State Model

    This is an important platform architecture.

    Instead of asking:

    What changes should I make?

    ask:

    What should the final state look like?

    For example:

    DESIRED_STATE=ACTIVE

    or:

    DESIRED_STATE=SUSPENDED

    Then regenerate configuration from that state.


    52. This Helps Repair

    Suppose Nginx is manually changed and becomes incorrect.

    hosting-repair can simply say:

    Desired state:
    ACTIVE

    and regenerate the correct configuration.

    If suspended:

    Desired state:
    SUSPENDED

    and repair generates the suspended configuration.

    This integrates suspension with the repair system.


    53. Site Metadata

    Add:

    SERVICE_STATE=ACTIVE

    or:

    SERVICE_STATE=SUSPENDED

    You could also use:

    STATUS=ACTIVE
    SUSPENDED=false

    but avoid maintaining duplicate state variables unless necessary.

    Prefer one authoritative state.


    54. Suspension Reason

    You may want:

    SUSPENSION_REASON=ADMINISTRATIVE

    Possible values:

    ADMINISTRATIVE
    PAYMENT
    SECURITY
    ABUSE
    MAINTENANCE
    CUSTOMER_REQUEST

    But this should be internal metadata.


    55. Suspension Timestamp

    Store:

    SUSPENDED_AT=2026-08-13T18:30:00

    Then:

    hosting-info example.com

    can display:

    Status:
    SUSPENDED
    
    Since:
    2026-08-13 18:30

    56. Suspension Operator

    Audit information can include:

    SUSPENDED_BY=admin

    or an internal operator identity.

    This is useful for a multi-admin hosting platform.


    57. Audit Log

    Every lifecycle change should be recorded.

    Example:

    2026-08-13 18:30
    SUSPEND
    example.com
    operator=admin
    reason=MAINTENANCE

    Then:

    2026-08-13 19:15
    UNSUSPEND
    example.com
    operator=admin

    58. Why Audit Logs Matter

    If someone asks:

    Why is this website suspended?

    you can determine:

    who
    when
    why

    without relying on memory or shell history.


    59. hosting-info Enhancement

    It should now show:

    Domain          : example.com
    Status          : SUSPENDED
    Suspended Since : 2026-08-13 18:30
    Reason          : MAINTENANCE
    
    Web Root        : /storage/websites/example.com/public
    PHP             : 8.3
    SSL             : ACTIVE
    Backups         : ENABLED

    60. hosting-list Enhancement

    Instead of:

    example.com
    shop.com
    learn.com

    show:

    DOMAIN                    STATUS
    ---------------------------------------
    example.com               ACTIVE
    shop.com                  SUSPENDED
    learn.com                 ACTIVE

    This becomes much more useful operationally.


    61. Backup During Suspension

    Should backups continue?

    Generally:

    SUSPENDED
       ↓
    BACKUP

    should remain possible.

    Why?

    The site’s data still exists and should remain protected.

    However, your scheduled backup policy may decide whether suspended sites receive normal backups.


    62. Recommended Policy

    For an actively suspended site:

    Existing backups:
    PRESERVE
    
    Manual backup:
    ALLOW
    
    Restore:
    ALLOW ADMIN
    
    Scheduled backups:
    CONFIGURABLE

    Don’t automatically delete backups merely because the site is suspended.


    63. Restore a Suspended Site

    This should be allowed.

    Example:

    SUSPENDED
       ↓
    hosting-restore

    After restoration:

    SUSPENDED

    should remain suspended unless the administrator explicitly requests unsuspension.

    This is important.


    64. Why Restore Shouldn’t Unsuspend

    Suppose an administrator restores a suspended customer site.

    It would be dangerous if:

    restore
     ↓
    ACTIVE

    automatically.

    The intended state was:

    SUSPENDED

    Therefore:

    restore data
    +
    preserve lifecycle state

    is the safer model.


    65. Repair a Suspended Site

    Similarly:

    SUSPENDED
     ↓
    hosting-repair

    can repair infrastructure without automatically making the website publicly available.

    Desired state remains:

    SUSPENDED

    66. SSL Renewal for Suspended Sites

    This introduces an interesting issue.

    A suspended site may still have an SSL certificate that needs renewal.

    Therefore:

    SUSPENDED

    doesn’t necessarily mean:

    SSL automation stops

    If the certificate must remain valid for future unsuspension, renewal may need to continue.


    67. Better State Separation

    This reveals why we shouldn’t combine everything into one state.

    Use separate concepts:

    SERVICE_STATE
    ACTIVE / SUSPENDED
    
    SSL_STATE
    ACTIVE / ERROR
    
    BACKUP_STATE
    HEALTHY / ERROR
    
    HEALTH_STATE
    HEALTHY / DEGRADED

    Then:

    SERVICE_STATE=SUSPENDED
    SSL_STATE=ACTIVE
    BACKUP_STATE=HEALTHY

    is perfectly valid.


    68. This Is Better Than One STATUS

    Instead of:

    STATUS=SUSPENDED

    meaning everything, maintain:

    SERVICE_STATE=SUSPENDED
    HEALTH_STATE=HEALTHY
    SSL_STATE=ACTIVE
    BACKUP_STATE=HEALTHY

    Now the platform knows exactly what is happening.


    69. Example Dashboard

    example.com
    
    Service       SUSPENDED
    Health        HEALTHY
    SSL           ACTIVE
    Backup        HEALTHY
    Database      ONLINE
    PHP-FPM       ONLINE

    This is much more informative.


    70. Suspension and Database

    Do not stop MySQL globally.

    Never do:

    systemctl stop mysql

    because one website is suspended.

    The server hosts multiple websites.

    Instead:

    example.com
     ↓
    application access blocked
    
    shop.com
     ↓
    continues working

    71. Suspension and PHP-FPM

    Likewise, don’t stop:

    systemctl stop php8.3-fpm

    because one website is suspended.

    That would potentially affect every site using PHP 8.3.

    The suspension should happen at the site configuration layer.


    72. Multi-Tenant Principle

    This is a critical hosting-platform rule:

    A per-site operation must not unnecessarily disrupt other sites.

    Therefore:

    Suspend example.com

    should affect:

    example.com

    not:

    shop.com
    learn.com
    medical.example.com

    73. Nginx Isolation

    Because each site has its own server block:

    example.com
    shop.com
    learn.com

    you can suspend one site without affecting the others.

    This is one reason the per-domain Nginx configuration architecture is useful.


    74. Error Handling

    If:

    nginx -t

    fails while suspending:

    Don't reload.
    Don't change service state.
    Preserve working configuration.

    The safe workflow remains:

    generate
     ↓
    validate
     ↓
    activate

    75. Lesson 073 — Core Principle

    Suspension should be:

    REVERSIBLE
    SITE-SPECIFIC
    NON-DESTRUCTIVE
    AUDITABLE

    Therefore:

    hosting-suspend

    means:

    block access
    +
    preserve data
    +
    preserve SSL
    +
    preserve backups
    +
    preserve configuration

    and:

    hosting-unsuspend

    means:

    restore desired active configuration
    +
    validate
    +
    health check
    +
    activate

    76. Updated CHP Architecture

    The platform now has:

                             CHP
                              │
           ┌──────────────────┼──────────────────┐
           ▼                  ▼                  ▼
     Provisioning        Operations          Recovery
           │                  │                  │
           ▼                  ▼                  ▼
    hosting-create       hosting-health     hosting-backup
    hosting-list         hosting-repair     hosting-restore
    hosting-info         hosting-ssl        backup-list
                                             backup-verify
                                             backup-prune
                              │
                              ▼
                        Lifecycle
                              │
                     ┌────────┴────────┐
                     ▼                 ▼
                 suspend           unsuspend

    77. Current Command Set

    hosting-create DOMAIN
    
    hosting-list
    
    hosting-info DOMAIN
    
    hosting-health DOMAIN
    
    hosting-repair DOMAIN --php
    hosting-repair DOMAIN --nginx
    hosting-repair DOMAIN --permissions
    hosting-repair DOMAIN --wordpress
    
    hosting-ssl DOMAIN
    
    hosting-backup DOMAIN
    hosting-backup-list DOMAIN
    hosting-backup-verify DOMAIN BACKUP_ID
    hosting-backup-prune DOMAIN --dry-run
    hosting-backup-prune DOMAIN --execute
    
    hosting-restore DOMAIN BACKUP_ID
    
    hosting-suspend DOMAIN
    hosting-unsuspend DOMAIN

    Next Lesson — 074

    Build hosting-domain

    The next major requirement is domain management.

    We need to separate:

    SERVER
       │
       ├── IP
       ├── Nginx
       ├── PHP
       ├── MySQL
       └── SSL

    from:

    SITE
       │
       ├── domain
       ├── aliases
       ├── document root
       ├── DNS requirements
       └── SSL names

    The next layer will handle:

    sudo hosting-domain-add example.com
    sudo hosting-domain-add www.example.com --alias example.com
    sudo hosting-domain-list example.com
    sudo hosting-domain-remove example.com

    and establish the relationship:

                        SITE
                         │
              ┌──────────┼──────────┐
              ▼          ▼          ▼
          Primary      Alias      Redirect
           Domain      Domain      Domain
              │          │          │
              └──────────┼──────────┘
                         ▼
                       Nginx
                         │
                         ▼
                        SSL

    This is necessary before building a proper multi-domain WordPress hosting platform.

  • CresignSys Learn — Lesson 072

    Build hosting-backup-list and Backup Retention

    We now have:

    hosting-create
    hosting-list
    hosting-info
    hosting-health
    hosting-repair
    hosting-ssl
    hosting-backup
    hosting-restore

    The backup system now needs management.

    Without retention, this happens:

    Backup
     ↓
    Backup
     ↓
    Backup
     ↓
    Backup
     ↓
    Backup
     ↓
    DISK FULL

    So this lesson builds:

    sudo hosting-backup-list example.com

    and:

    sudo hosting-backup-prune example.com

    1. Backup Management Architecture

                        BACKUPS
                           │
              ┌────────────┼────────────┐
              ▼            ▼            ▼
            LIST         VERIFY       PRUNE
              │            │            │
              ▼            ▼            ▼
           Inventory    Integrity    Retention

    The commands have separate responsibilities.


    2. hosting-backup-list

    Purpose:

    Show available backups without modifying anything.

    Example:

    CresignSys Backups
    ==================
    
    Domain: example.com
    
    BACKUP ID             STATUS      SIZE       AGE
    --------------------------------------------------------
    2026-08-13_160000     COMPLETE    1.42 GB    2 hours
    2026-08-12_160000     COMPLETE    1.38 GB    1 day
    2026-08-11_160000     COMPLETE    1.35 GB    2 days
    2026-08-10_160000     COMPLETE    1.31 GB    3 days

    3. Backup Directory

    Our existing structure:

    /backup/cresignsys/
    └── example.com/
        ├── 2026-08-13_160000/
        ├── 2026-08-12_160000/
        └── 2026-08-11_160000/

    Each directory represents one backup.


    4. Backup Manifest

    Every backup should contain:

    manifest.json
    files.tar.gz
    database.sql.gz
    checksums.sha256
    site.conf

    Example:

    2026-08-13_160000/
    ├── files.tar.gz
    ├── database.sql.gz
    ├── checksums.sha256
    ├── site.conf
    └── manifest.json

    The manifest is what makes the backup self-describing.


    5. Create hosting-backup-list

    Create:

    sudo nano /usr/local/bin/hosting-backup-list

    Start:

    #!/usr/bin/env bash
    
    set -Eeuo pipefail
    
    BACKUP_BASE="/backup/cresignsys"

    6. Validate Domain

    if [[ $# -ne 1 ]]; then
        echo "Usage: hosting-backup-list DOMAIN"
        exit 1
    fi
    
    DOMAIN="$1"
    SITE_BACKUP_DIR="${BACKUP_BASE}/${DOMAIN}"

    Then:

    if [[ ! -d "$SITE_BACKUP_DIR" ]]; then
        echo "No backups found for $DOMAIN"
        exit 0
    fi

    7. List Backup Directories

    for backup_dir in "$SITE_BACKUP_DIR"/*; do
        [[ -d "$backup_dir" ]] || continue
    
        BACKUP_ID="$(basename "$backup_dir")"
    
        echo "$BACKUP_ID"
    done

    Initially this is enough to prove the structure works.


    8. Read Manifest Status

    For each backup:

    manifest.json

    read:

    status
    created_at

    For example:

    {
      "status": "COMPLETE",
      "created_at": "2026-08-13T16:00:00"
    }

    Then output:

    2026-08-13_160000    COMPLETE

    9. Calculate Backup Size

    Use:

    du -sh "$backup_dir"

    Example:

    2026-08-13_160000    COMPLETE    1.42G

    10. Sort Backups

    Backup IDs use:

    YYYY-MM-DD_HHMMSS

    which has the useful property that lexical sorting corresponds to chronological sorting.

    Therefore:

    2026-08-11...
    2026-08-12...
    2026-08-13...

    sort naturally.


    11. Newest First

    The administrator usually wants the newest backup first:

    2026-08-13
    2026-08-12
    2026-08-11

    Use reverse sorting.


    12. Better Output

    The final command should eventually produce:

    CresignSys Backup Inventory
    ============================
    
    Domain: example.com
    
    BACKUP ID             STATUS       SIZE
    ------------------------------------------------
    2026-08-13_160000     COMPLETE     1.42G
    2026-08-12_160000     COMPLETE     1.38G
    2026-08-11_160000     COMPLETE     1.35G
    2026-08-10_160000     COMPLETE     1.31G
    
    Total backups: 4
    Total storage: 5.46G

    13. Failed Backups

    Suppose:

    2026-08-13_160000

    failed during database backup.

    The inventory should show:

    2026-08-13_160000     ERROR        1.40G

    Do not treat it as a valid restore point.


    14. Why Failed Backups Matter

    A failed backup can consume substantial storage.

    For example:

    files backup ✓
    database backup ✗

    might still leave:

    1.4 GB

    on disk.

    These should eventually be cleaned up.


    15. Backup Integrity Command

    Eventually add:

    sudo hosting-backup-verify example.com BACKUP_ID

    Example:

    sudo hosting-backup-verify example.com 2026-08-13_160000

    Output:

    [OK] Manifest
    [OK] Files archive
    [OK] Database archive
    [OK] Checksums
    
    Backup: VALID

    This should remain separate from backup-list.


    16. Why Separate Verification?

    Listing should be fast.

    Verification may involve:

    large archive
     ↓
    checksum calculation
     ↓
    CPU + disk I/O

    You don’t want:

    hosting-backup-list

    to recalculate every backup’s checksum every time.


    17. Retention

    Now the important part.

    Suppose we create:

    1 backup/day

    for a year:

    365 backups

    If each is:

    1 GB

    we have:

    365 GB

    for one website.

    At scale this becomes expensive.


    18. Retention Policy

    A retention policy answers:

    Which backups should remain, and which may be deleted?

    Example:

    Daily:
    7
    
    Weekly:
    4
    
    Monthly:
    3

    So you might retain:

    7 recent daily backups
    4 weekly backups
    3 monthly backups

    19. Retention Is Not Simply “Delete Older Than X”

    A naive policy:

    delete backups older than 30 days

    is easy.

    But:

    30 days

    might not give you useful historical coverage.

    A better policy preserves representative restore points.


    20. Example

    Suppose:

    Daily backups:
    every day
    
    Weekly backups:
    every Sunday
    
    Monthly backups:
    first day of month

    You might keep:

    Recent 7 days
    +
    4 weekly points
    +
    3 monthly points

    This provides both recent and historical recovery.


    21. Retention Tags

    The backup manifest can eventually include:

    {
      "backup_id": "2026-08-13_160000",
      "retention": "DAILY"
    }

    or:

    {
      "retention": "WEEKLY"
    }

    or:

    {
      "retention": "MONTHLY"
    }

    22. Better: Don’t Hard-Code Tags at Creation

    The retention engine can calculate the category from the timestamp.

    For example:

    today
     ↓
    daily
    
    Sunday
     ↓
    weekly
    
    first day of month
     ↓
    monthly

    This allows retention policy changes without rewriting old backups.


    23. Retention Configuration

    Add to:

    /etc/cresignsys/hosting.conf

    for example:

    BACKUP_DAILY_RETENTION=7
    BACKUP_WEEKLY_RETENTION=4
    BACKUP_MONTHLY_RETENTION=3

    These are example defaults.


    24. Per-Plan Retention

    Eventually:

    Starter
        daily = 3
        weekly = 2
        monthly = 1
    
    Business
        daily = 7
        weekly = 4
        monthly = 2
    
    Professional
        daily = 14
        weekly = 8
        monthly = 6

    This lets backup service become part of the hosting plan.


    25. Never Delete the Only Backup

    The prune engine must protect against:

    Total valid backups = 1

    It should never delete the final usable backup simply because it is old.

    Minimum safety rule:

    Keep at least 1 valid backup.

    26. Never Delete the Newest Valid Backup

    Another important rule:

    newest valid backup

    should always be protected.

    Even if retention calculation says it is old.


    27. Failed Backup Cleanup

    Failed backups can be handled separately.

    Example policy:

    ERROR backups:
    delete after 2 days

    because they aren’t valid recovery points.

    But retain enough logs/metadata to diagnose failures.


    28. hosting-backup-prune

    Create:

    sudo nano /usr/local/bin/hosting-backup-prune

    Start:

    #!/usr/bin/env bash
    
    set -Eeuo pipefail

    Arguments:

    hosting-backup-prune DOMAIN

    and later:

    hosting-backup-prune DOMAIN --dry-run

    29. Dry Run Is Essential

    Never make deletion the default while developing.

    Run:

    sudo hosting-backup-prune example.com --dry-run

    Output:

    CresignSys Backup Prune
    =======================
    
    Domain: example.com
    
    KEEP:
    2026-08-13_160000
    2026-08-12_160000
    2026-08-11_160000
    
    DELETE:
    2026-07-01_160000
    2026-06-24_160000
    
    No files deleted.

    30. Why Dry Run Matters

    A retention bug can become:

    backup-prune
     ↓
    wrong date calculation
     ↓
    DELETE ALL

    A dry-run stage allows you to inspect the selection first.


    31. Explicit Confirmation

    For actual deletion:

    sudo hosting-backup-prune example.com --execute

    This makes the destructive nature obvious.

    Avoid vague:

    --force

    32. Selection Algorithm

    Conceptually:

    1. Find all backups
    2. Read status
    3. Ignore invalid backups for restore retention
    4. Sort chronologically
    5. Protect newest valid backup
    6. Identify daily backups to keep
    7. Identify weekly backups to keep
    8. Identify monthly backups to keep
    9. Mark remaining backups as deletable
    10. Show plan
    11. Delete only after explicit execution

    33. Backup Selection Example

    Suppose you have:

    Aug 13
    Aug 12
    Aug 11
    Aug 10
    Aug 09
    Aug 08
    Aug 07
    Aug 06
    Aug 05
    Aug 04
    ...

    Daily retention:

    7

    means:

    Aug 13
    Aug 12
    Aug 11
    Aug 10
    Aug 09
    Aug 08
    Aug 07

    are protected as recent daily points.


    34. Weekly Protection

    Suppose Sundays are:

    Aug 09
    Aug 02
    Jul 26
    Jul 19

    If weekly retention is:

    4

    all four remain even if they’re older than the daily window.


    35. Monthly Protection

    Suppose:

    Aug 01
    Jul 01
    Jun 01

    and monthly retention is:

    3

    those remain.


    36. Result

    The system retains:

    recent daily
    +
    historical weekly
    +
    historical monthly

    instead of simply:

    latest N backups

    37. Don’t Delete a Backup That Is Being Restored

    The restore operation must acquire the same site lock.

    Therefore:

    restore
       ↓
    LOCK

    and:

    prune
       ↓
    LOCK

    Only one operation can modify backup state for that site at a time.


    38. Better: Global Backup Lock

    There may also be a global backup storage lock:

    /var/lock/cresignsys-backup.lock

    This prevents two processes from simultaneously manipulating the same backup repository metadata.

    For the initial implementation, a per-site lock plus safe filesystem operations may be sufficient.


    39. Backup Storage Limit

    Retention alone isn’t enough.

    Suppose:

    Backup storage limit = 500 GB

    and usage reaches:

    490 GB

    The system should report:

    WARNING:
    Backup storage 98% full.

    40. Backup Monitoring

    Add:

    Backup Storage
    --------------
    Used:       490 GB
    Available:   10 GB
    Usage:       98%

    This should become part of the server dashboard.


    41. What Happens When Storage Is Full?

    The worst design:

    backup
     ↓
    disk full
     ↓
    partial backup
     ↓
    database dump fails

    Better:

    check available storage
            ↓
    enough?
       ┌────┴────┐
       ▼         ▼
      YES        NO
       │          │
     backup      alert

    42. Preflight Storage Check

    Before backup:

    df -P "$BACKUP_BASE"

    Calculate available bytes.

    For a large site, estimate required space.

    A simple safety margin can be used.

    For example:

    required:
    current site size
    +
    database size
    +
    safety margin

    43. Don’t Promise Exact Backup Size

    Compression changes the final size.

    A website containing:

    JPEG
    PNG
    MP4
    ZIP
    PDF

    may already contain compressed data.

    Therefore:

    uncompressed size

    does not directly predict:

    compressed backup size

    Use estimates only as safety checks.


    44. Backup Storage Directory Permissions

    Protect:

    /backup/cresignsys/

    from normal web users.

    The directory should not be writable by:

    www-data

    unless there is a deliberate architecture requiring it.

    Ideally:

    root
     ↓
    backup engine
     ↓
    backup repository

    45. Don’t Let WordPress Write Backups

    Avoid:

    WordPress
     ↓
    /backup/cresignsys/

    The web application should not have unrestricted access to all customer backups.

    This creates unnecessary security risk.


    46. Backup Ownership

    A sensible architecture is:

    backup repository
    owner: root
    group: dedicated backup group

    with carefully controlled access.

    Exact permissions should follow your operational requirements.


    47. Backup Encryption

    If backups remain on the same server, encryption may still be useful.

    Especially because backups contain:

    database
    customer files
    credentials

    For off-site storage, encryption should be considered mandatory for sensitive customer data.


    48. Remote Backup Retention

    Eventually:

    Local retention
    +
    Remote retention

    can be separate.

    For example:

    Local:
    7 daily
    
    Remote:
    30 daily

    because local storage is cheaper/faster while remote storage provides disaster recovery.


    49. Remote Backup Architecture

                       CHP
                        │
                        ▼
                     Backup
                        │
                 ┌──────┴──────┐
                 ▼             ▼
               Local         Remote
                 │             │
                 ▼             ▼
             Fast restore   Disaster recovery

    50. Remote Storage Should Not Be Mounted Publicly

    Avoid exposing:

    remote backup storage

    through:

    Nginx
    FTP
    public directory

    Use an appropriate storage API/protocol with least-privilege credentials.


    51. Backup Encryption Key

    Do not store:

    backup encryption key

    inside:

    files.tar.gz

    or:

    manifest.json

    The encryption key needs separate protection.


    52. Backup Deletion Audit

    Every deletion should be logged:

    2026-08-13 18:00
    DELETE BACKUP
    Domain: example.com
    Backup: 2026-06-01_160000
    Reason: RETENTION
    Operator: root

    This is important for accountability.


    53. Don’t Permanently Delete Immediately

    For a more mature system, consider:

    eligible
     ↓
    scheduled deletion
     ↓
    deleted

    This creates a grace period.

    But this consumes additional storage, so it must be balanced against capacity.


    54. Backup Inventory Database

    Eventually you don’t want to scan:

    /backup/cresignsys/

    every time.

    Create metadata:

    backups
    -------
    id
    site_id
    backup_id
    created_at
    status
    files_size
    database_size
    storage_location
    retention_class
    verified

    Then:

    hosting-backup-list

    can query metadata quickly.


    55. Why This Matters at Scale

    With:

    10 sites

    filesystem scanning is easy.

    With:

    1,000 sites
    ×
    365 backups

    you have:

    365,000 backup directories

    A database-backed inventory becomes much more practical.


    56. Backup Database Model

    Eventually:

    sites
      │
      │ 1:N
      ▼
    backups

    Example:

    sites
    -----
    id
    domain
    
    backups
    -------
    id
    site_id
    backup_id
    status
    created_at
    size
    location

    57. Backup Storage Locations

    Don’t assume every backup lives locally.

    Use:

    storage_location

    Examples:

    LOCAL
    REMOTE_S3
    REMOTE_OBJECT

    Then:

    hosting-backup-list

    can show:

    BACKUP                 LOCATION
    ----------------------------------
    2026-08-13_160000      LOCAL
    2026-08-12_160000      REMOTE

    58. Backup Health

    A backup itself can have health:

    VALID
    INVALID
    VERIFYING
    ERROR

    So you eventually have:

    SITE HEALTH
    +
    BACKUP HEALTH

    59. Dashboard

    The future CHP dashboard could show:

    Backup Overview
    ================================
    
    Sites:              42
    Backup Healthy:     40
    Backup Warning:      1
    Backup Failed:       1
    
    Storage:
    Local:              380 GB
    Remote:             1.2 TB
    
    Oldest valid backup:
    2026-05-01
    
    Failed backups:
    3

    60. Backup Policy Per Site

    site.conf can eventually contain:

    BACKUP_ENABLED=true
    BACKUP_DAILY=7
    BACKUP_WEEKLY=4
    BACKUP_MONTHLY=3

    Then each site can have a different policy.


    61. Don’t Let Customers Arbitrarily Set Huge Retention

    If the hosting plan allows:

    7 daily

    a customer shouldn’t be able to request:

    10,000 daily backups

    without the platform accounting for the resulting storage.

    This is another example of:

    plan
     ↓
    resource policy

    62. Backup Quota

    Eventually:

    Backup Quota:
    20 GB

    and:

    Current:
    17.5 GB

    The system can show:

    87.5% used

    63. Backup Quota vs Website Storage

    These are different resources.

    Website Storage
          +
    Backup Storage

    For example:

    Website:
    5 GB
    
    Backup:
    20 GB

    A site using 4 GB doesn’t necessarily mean it has only 1 GB of total backup capacity.


    64. Lesson 072 — Core Principle

    Backup management has three separate jobs:

    Inventory

    What backups exist?

    Integrity

    Which backups are valid?

    Retention

    Which backups should remain?

    Therefore:

    hosting-backup-list
    hosting-backup-verify
    hosting-backup-prune

    should remain separate operations.


    65. Current Backup Architecture

                     BACKUP SYSTEM
                           │
              ┌────────────┼────────────┐
              ▼            ▼            ▼
            CREATE        LIST         VERIFY
              │            │            │
              └────────────┼────────────┘
                           ▼
                         RETAIN
                           │
                           ▼
                         PRUNE
                           │
                           ▼
                        RESTORE

    This is now a complete basic backup lifecycle.


    66. Current CHP Command Set

    hosting-create DOMAIN
    
    hosting-list
    
    hosting-info DOMAIN
    
    hosting-health DOMAIN
    
    hosting-repair DOMAIN --php
    hosting-repair DOMAIN --nginx
    hosting-repair DOMAIN --permissions
    hosting-repair DOMAIN --wordpress
    
    hosting-ssl DOMAIN
    
    hosting-backup DOMAIN
    
    hosting-backup-list DOMAIN
    
    hosting-backup-verify DOMAIN BACKUP_ID
    
    hosting-backup-prune DOMAIN --dry-run
    hosting-backup-prune DOMAIN --execute
    
    hosting-restore DOMAIN BACKUP_ID

    Next Lesson — 073

    Build hosting-suspend and hosting-unsuspend

    The next requirement is account/site lifecycle control.

    We need to be able to temporarily disable a website without deleting it:

    sudo hosting-suspend example.com

    and later restore access:

    sudo hosting-unsuspend example.com

    The architecture will become:

    ACTIVE
      │
      ▼
    SUSPENDED
      │
      ├── website access blocked
      ├── database preserved
      ├── files preserved
      ├── SSL preserved
      └── backups preserved
      │
      ▼
    UNSUSPEND
      │
      ▼
    HEALTH CHECK
      │
      ▼
    ACTIVE

    This will also introduce an important hosting-platform concept:

    suspension must be reversible and must never mean deletion.

  • CresignSys Learn — Lesson 071

    Build hosting-restore

    We now have:

    hosting-create
    hosting-list
    hosting-info
    hosting-health
    hosting-repair
    hosting-ssl
    hosting-backup

    The next component completes the backup system:

    BACKUP
      ↓
    RESTORE

    The target command:

    sudo hosting-restore example.com BACKUP_ID

    For example:

    sudo hosting-restore example.com 2026-08-13_160000

    1. Restore Is a High-Risk Operation

    Unlike:

    hosting-info
    hosting-health

    restore modifies customer data.

    Therefore the workflow must be much stricter.

    SELECT BACKUP
          ↓
    VERIFY BACKUP
          ↓
    LOCK SITE
          ↓
    CREATE PRE-RESTORE BACKUP
          ↓
    RESTORE
          ↓
    VERIFY
          ↓
    HEALTH CHECK
          ↓
    ACTIVE

    If anything fails:

    RESTORE ERROR
          ↓
    ROLLBACK

    2. Never Restore Directly Over the Live Site

    Avoid:

    backup
     ↓
    rm -rf public
     ↓
    extract

    This is dangerous.

    If extraction fails halfway:

    public/
    ├── some files restored
    ├── some files missing
    └── site broken

    Instead use a temporary restore directory.


    3. Safe Restore Architecture

    LIVE SITE
       │
       │ remains intact
       ▼
    TEMPORARY RESTORE
       │
       ▼
    VERIFY
       │
       ▼
    ACTIVATE

    This is much safer.


    4. Example Directory Structure

    Current:

    /storage/websites/example.com/public

    Temporary:

    /storage/websites/example.com/.restore/

    For example:

    .restore/
    └── 2026-08-13_160000/

    5. Better Temporary Location

    For very large websites, temporary restoration can require substantial disk space.

    Therefore eventually you may want:

    /storage/cresignsys/restore-work/

    instead of putting temporary data inside the website.

    For the first implementation, choose a dedicated location outside the public document root.


    6. Restore States

    Use:

    id="v4l0q8"
    RESTORE_REQUESTED
          ↓
    VERIFYING
          ↓
    PREPARING
          ↓
    RESTORING_FILES
          ↓
    RESTORING_DATABASE
          ↓
    VERIFYING_SITE
          ↓
    HEALTH_CHECK
          ↓
    ACTIVE

    Failure:

    RESTORE_ERROR

    7. First Step — Find the Backup

    Given:

    sudo hosting-restore example.com 2026-08-13_160000

    the script constructs:

    /backup/cresignsys/example.com/2026-08-13_160000/

    Then checks:

    files archive
    database dump
    manifest
    checksums

    8. Verify Manifest

    Read:

    manifest.json

    Check:

    domain
    site_id
    backup ID
    status
    files archive
    database dump

    The backup should say:

    status = COMPLETE

    If:

    status = ERROR

    stop immediately.


    9. Verify Domain

    This is a critical safety check.

    Suppose you accidentally execute:

    hosting-restore shop.example.com BACKUP_FROM_OTHER_SITE

    You must not restore it.

    The manifest should contain:

    DOMAIN=example.com

    and the requested domain must match.


    10. Verify Site ID

    Also compare:

    Current site:
    SITE_ID=1027
    
    Backup:
    SITE_ID=1027

    If they don’t match:

    ERROR:
    Backup belongs to a different site.

    This prevents accidental cross-site restoration.


    11. Verify Checksums

    Run:

    sha256sum -c checksums.sha256

    Expected:

    files.tar.gz: OK
    database.sql.gz: OK

    If a checksum fails:

    RESTORE STOPPED

    Do not continue.


    12. Why Checksum Verification Comes Before Restore

    Without verification:

    corrupted backup
          ↓
    restore
          ↓
    corrupted website

    With verification:

    corrupted backup
          ↓
    checksum failure
          ↓
    restore stopped

    13. Acquire Site Lock

    Before modifying anything:

    /var/lock/cresignsys-example.com.lock

    The lock prevents:

    hosting-backup
    hosting-repair
    hosting-delete
    hosting-restore

    from simultaneously modifying the same site.


    14. Create Pre-Restore Backup

    This is one of the most important steps.

    Before restoring:

    CURRENT SITE
         ↓
    BACKUP
         ↓
    RESTORE OLD BACKUP

    If restoration fails:

    OLD CURRENT SITE
         ↓
    ROLLBACK

    Therefore:

    Never perform a production restore without preserving the current state.


    15. Pre-Restore Backup

    You can call:

    hosting-backup example.com

    from the restore workflow, but ideally the backup engine should be implemented as a reusable internal module rather than invoking a separate command blindly.


    16. Maintenance Mode

    For database/file replacement, users shouldn’t continue modifying the site.

    WordPress maintenance mode can be enabled with WP-CLI:

    sudo -u "$SITE_USER" \
    wp --path="$WEB_ROOT" maintenance-mode activate

    However, if the site is already inaccessible, this may not work.

    Therefore maintenance mode is an enhancement, not the primary safety mechanism.


    17. Database Restore Is Different From File Restore

    Files:

    archive
     ↓
    extract

    Database:

    SQL dump
     ↓
    MySQL

    They need separate verification.


    18. Never Drop the Live Database First

    A dangerous workflow:

    DROP DATABASE
          ↓
    CREATE DATABASE
          ↓
    IMPORT

    If import fails:

    database gone

    Instead use a safer strategy.


    19. Database Restore Strategy

    For a first controlled implementation:

    current database
           ↓
    pre-restore backup
           ↓
    prepare restore database
           ↓
    import
           ↓
    verify
           ↓
    activate

    There are several ways to implement this depending on your MySQL architecture.


    20. Option A — Temporary Database

    Example:

    Current:
    csp1027_wp
    
    Temporary:
    csp1027_restore_20260813

    Import the dump into:

    csp1027_restore_20260813

    Then verify it.

    This is safer than destroying the current database first.


    21. Verify Temporary Database

    After import:

    temporary database
            ↓
    WordPress tables?
            ↓
    wp_options?
            ↓
    wp_posts?
            ↓
    wp_users?

    You can query the temporary database to ensure the expected WordPress structure exists.


    22. Database Swap

    Once the temporary database is verified, the final database transition must be performed carefully.

    Depending on your MySQL setup, this may involve:

    rename/swap strategy

    or:

    controlled replacement

    The exact mechanism should be tested extensively before production use.


    23. Why Not Just Import Into Existing Database?

    You could:

    mysql existing_database < backup.sql

    but this can leave old tables or conflicting schema/data depending on the dump.

    For example:

    current:
    wp_posts
    wp_users
    wp_options
    old_custom_table
    
    backup:
    wp_posts
    wp_users
    wp_options

    The old custom table may remain.

    A clean restore requires a controlled database replacement strategy.


    24. File Restore Strategy

    Create:

    restore-work/
    └── public/

    Extract:

    files.tar.gz

    into the temporary location.

    Then verify:

    wp-admin/
    wp-content/
    wp-includes/
    wp-config.php

    or the site’s expected structure.


    25. Don’t Assume Every Site Is WordPress

    Your CHP platform is currently WordPress-focused.

    But the architecture should eventually support:

    WordPress
    PHP application
    static site
    Laravel
    custom PHP

    Therefore the restore engine should eventually know:

    APPLICATION_TYPE=wordpress

    rather than assuming WordPress forever.


    26. Current WordPress Implementation

    For now:

    APPLICATION_TYPE=wordpress

    can be stored in:

    site.conf

    Example:

    APPLICATION_TYPE=wordpress

    Then:

    hosting-restore
          ↓
    application type
          ↓
    WordPress restore verification

    27. File Ownership

    After restoring files:

    chown -R "$SITE_USER:$SITE_USER" "$WEB_ROOT"

    But ownership should match the architecture established during provisioning.

    Don’t blindly apply ownership rules to unrelated server paths.


    28. Permissions

    Then restore expected permissions.

    Typical baseline:

    directories → 755
    files       → 644

    But special files may require different permissions.

    For example:

    wp-config.php

    may need tighter permissions depending on your PHP-FPM and deployment model.


    29. Don’t Run chmod -R 777

    Never use:

    chmod -R 777

    as a restore solution.

    It hides permission problems while creating unnecessary security exposure.


    30. Verify WordPress

    After restoring:

    sudo -u "$SITE_USER" \
    wp --path="$WEB_ROOT" core is-installed

    Then:

    sudo -u "$SITE_USER" \
    wp --path="$WEB_ROOT" db check

    And:

    sudo -u "$SITE_USER" \
    wp --path="$WEB_ROOT" core verify-checksums

    31. Verify Site URL

    Check:

    sudo -u "$SITE_USER" \
    wp --path="$WEB_ROOT" option get siteurl

    and:

    sudo -u "$SITE_USER" \
    wp --path="$WEB_ROOT" option get home

    Make sure they match the intended domain/environment.


    32. Important Restore Problem

    Suppose the backup was created when the site used:

    https://example.com

    but the current site configuration is:

    https://new.example.com

    Restoring the old database may restore:

    https://example.com

    This can create redirects or incorrect URLs.

    Therefore restore should preserve the backup’s application state unless the administrator explicitly requests a migration.


    33. Restore vs Migration

    These are different operations.

    Restore

    example.com
         ↓
    example.com

    Migration

    old.example.com
         ↓
    new.example.com

    Don’t mix them.

    A future command should be something like:

    hosting-migrate old.example.com new.example.com

    34. SSL During Restore

    The database/files backup doesn’t necessarily mean the certificate should be restored as raw certificate files.

    Certificates should remain managed by the SSL subsystem.

    After restore:

    website restored
           ↓
    hosting-ssl status
           ↓
    certificate still valid?

    If necessary:

    hosting-ssl

    can repair/reconfigure HTTPS.


    35. Nginx Configuration

    Similarly, restore should not blindly overwrite the entire server’s Nginx configuration.

    Your backup may contain:

    site.conf

    as metadata, but Nginx configuration should be generated from the current CHP templates.

    Therefore:

    Backup
     ↓
    site metadata
     ↓
    current CHP template
     ↓
    Nginx configuration

    This avoids restoring obsolete server configuration.


    36. PHP Configuration

    Same principle.

    Don’t blindly restore an old:

    /etc/php/8.3/fpm/pool.d/

    configuration from a backup.

    Instead:

    site metadata
     ↓
    current PHP template
     ↓
    current PHP configuration

    The backup should restore customer/application data, not necessarily obsolete infrastructure configuration.


    37. This Is a Critical Architecture Principle

    Separate:

    Customer Data

    WordPress
    uploads
    database
    themes
    plugins

    from:

    Infrastructure Configuration

    Nginx
    PHP-FPM
    SSL
    systemd
    firewall
    server packages

    The restore engine should treat them differently.


    38. Restore Architecture

                      BACKUP
                        │
              ┌─────────┴─────────┐
              ▼                   ▼
         Customer Data       Site Metadata
              │                   │
              ▼                   ▼
           Restore            Regenerate
              │              infrastructure
              │                   │
              └─────────┬─────────┘
                        ▼
                   HEALTH CHECK

    39. Final Health Check

    After restoration:

    hosting-health example.com

    must run.

    Expected:

    [OK] Web Root
    [OK] PHP-FPM
    [OK] PHP Socket
    [OK] Nginx
    [OK] WordPress
    [OK] Database
    [OK] HTTP
    [OK] HTTPS

    Only then:

    STATUS=ACTIVE

    40. Restore Failure

    Suppose:

    Files        ✓
    Database     ✓
    WordPress    ✓
    HTTP         ✗

    Do not mark:

    ACTIVE

    Instead:

    RESTORE_ERROR

    Then rollback if appropriate.


    41. Rollback

    The safest high-level sequence is:

    Current site
        ↓
    Pre-restore backup
        ↓
    Selected backup
        ↓
    Restore
        ↓
    Health failure
        ↓
    Restore pre-restore backup

    This creates a recovery chain.


    42. Restore State Machine

    ACTIVE
      │
      ▼
    RESTORE_REQUESTED
      │
      ▼
    VERIFYING
      │
      ▼
    PRE_RESTORE_BACKUP
      │
      ▼
    RESTORING
      │
      ▼
    VERIFYING
      │
      ▼
    HEALTH_CHECK
      │
     ┌┴──────────┐
     ▼           ▼
    PASS        FAIL
     │           │
     ▼           ▼
    ACTIVE     ROLLBACK
                 │
                 ▼
               ACTIVE

    If rollback itself fails:

    ROLLBACK_ERROR

    and immediate administrator intervention is required.


    43. Restore Logs

    Example:

    2026-08-13 17:00 RESTORE_START
    2026-08-13 17:00 BACKUP_VERIFIED
    2026-08-13 17:01 PRE_RESTORE_BACKUP_COMPLETE
    2026-08-13 17:02 FILES_RESTORED
    2026-08-13 17:03 DATABASE_RESTORED
    2026-08-13 17:03 PERMISSIONS_FIXED
    2026-08-13 17:03 WORDPRESS_VERIFIED
    2026-08-13 17:04 HEALTH_CHECK
    2026-08-13 17:04 RESTORE_COMPLETE

    44. Restore Output

    A successful restore:

    CresignSys Restore
    ==================
    
    Domain:
    example.com
    
    Backup:
    2026-08-13_160000
    
    [OK] Backup verified
    [OK] Current site backed up
    [OK] Files restored
    [OK] Database restored
    [OK] Ownership verified
    [OK] WordPress verified
    [OK] Nginx verified
    [OK] PHP verified
    [OK] Health check
    
    Status:
    ACTIVE

    45. Failed Restore

    CresignSys Restore
    ==================
    
    Domain:
    example.com
    
    [OK] Backup verified
    [OK] Current site backed up
    [OK] Files restored
    [OK] Database restored
    [FAIL] WordPress verification
    
    Attempting rollback...
    
    [OK] Previous files restored
    [OK] Previous database restored
    [OK] Health check
    
    Status:
    ACTIVE

    This is the kind of behavior you want from a production system.


    46. Dry Run

    Because restore is dangerous, add:

    sudo hosting-restore example.com 2026-08-13_160000 --dry-run

    The dry run should show:

    Restore Plan
    ============
    
    Domain:
    example.com
    
    Backup:
    2026-08-13_160000
    
    Files:
    1.4 GB
    
    Database:
    28 MB
    
    Actions:
    [ ] Create pre-restore backup
    [ ] Restore files
    [ ] Restore database
    [ ] Fix permissions
    [ ] Verify WordPress
    [ ] Health check
    
    No changes made.

    47. Confirmation

    For destructive operations, interactive confirmation can provide another safety barrier.

    For example:

    WARNING:
    This operation will replace the current website state.
    
    Current:
    2026-08-13 17:00
    
    Restore:
    2026-08-10 16:00
    
    Continue? [yes/no]

    For automated systems, use an explicit non-interactive approval flag rather than relying on a prompt.


    48. Why --force Is Dangerous

    Avoid:

    hosting-restore example.com BACKUP --force

    unless --force has a precisely defined meaning.

    Better:

    hosting-restore example.com BACKUP --confirm

    where the semantics are explicitly:

    I understand this operation will modify the live site.

    49. Restore Database Credentials

    The restored wp-config.php may contain credentials corresponding to the backup environment.

    If the current database credentials differ, WordPress may stop working.

    Therefore your restore system should decide which configuration is authoritative.

    For CHP:

    CURRENT INFRASTRUCTURE
            ↓
    current DB credentials

    should generally remain authoritative.

    Customer data is restored separately.


    50. Important Separation

    Don’t blindly restore:

    old wp-config.php

    over the current environment if it contains environment-specific configuration.

    A safer design is:

    backup wp-config.php
            ↓
    extract application settings
            ↓
    preserve current CHP-managed credentials/configuration

    This is another reason to separate application data from infrastructure configuration.


    51. WordPress Restore Strategy

    A mature CHP restore can therefore do:

    Restore:
    wp-admin
    wp-content
    wp-includes
    application files
    database
    
    Preserve/regenerate:
    wp-config.php
    Nginx
    PHP-FPM
    SSL
    server configuration

    The exact boundary depends on your deployment architecture.


    52. Backup Manifest Improvement

    Record which components were backed up:

    {
      "domain": "example.com",
      "site_id": "1027",
      "application": "wordpress",
      "files": true,
      "database": true,
      "site_metadata": true,
      "nginx_config": false,
      "php_config": false,
      "ssl_private_key": false
    }

    This prevents ambiguity during restoration.


    53. Restore Compatibility

    A backup may have been created under:

    PHP 8.2

    while the current server uses:

    PHP 8.3

    The restore system should report:

    Backup PHP:
    8.2
    
    Current PHP:
    8.3

    rather than silently assuming compatibility.


    54. Compatibility Warning

    For example:

    WARNING:
    Backup was created under PHP 8.2.
    Current site uses PHP 8.3.
    
    Continue?

    This is especially important for old WordPress plugins and custom PHP code.


    55. Database Version

    Similarly record:

    MySQL version

    in the backup manifest.

    For example:

    {
      "mysql_version": "8.0"
    }

    Then the restore process can identify major compatibility changes.


    56. Restore Testing

    The best way to test restore is:

    Production
        ↓
    Backup
        ↓
    Temporary environment
        ↓
    Restore
        ↓
    Health check

    This is called a:

    Restore Test

    You should eventually automate this.


    57. Disaster Recovery Testing

    A serious hosting platform should periodically test:

    Can we actually restore this site?

    not simply:

    Does a backup file exist?

    This distinction is critical.


    58. Recovery Point Objective

    RPO means approximately:

    How much recent data can we afford to lose?

    Example:

    RPO = 24 hours

    means a failure could potentially lose up to a day’s changes.

    If:

    RPO = 1 hour

    you need much more frequent backup/snapshot mechanisms.


    59. Recovery Time Objective

    RTO means:

    How quickly must the site be restored?

    Example:

    RTO = 4 hours

    versus:

    RTO = 15 minutes

    The second requires much more sophisticated infrastructure.


    60. CHP Backup Strategy

    Eventually hosting plans could define:

    Plan
     │
     ├── Backup frequency
     ├── Retention
     ├── Remote backup
     ├── RPO
     └── RTO

    This makes backup a proper hosting service rather than simply a Bash script.


    61. Current CHP Recovery Layer

    We now have:

                       RECOVERY
                          │
              ┌───────────┴───────────┐
              ▼                       ▼
           Backup                  Restore
              │                       │
              ▼                       ▼
           Verify                  Verify
              │                       │
              └───────────┬───────────┘
                          ▼
                      Health Check
                          │
                          ▼
                       ACTIVE

    62. Complete Website Lifecycle

    The platform now looks like:

    CREATE
      ↓
    CONFIGURE
      ↓
    SSL
      ↓
    ACTIVE
      ↓
    HEALTH
      ↓
    BACKUP
      ↓
    REPAIR
      ↓
    RESTORE
      ↓
    ACTIVE

    63. Current Command Set

    hosting-create DOMAIN
    
    hosting-list
    
    hosting-info DOMAIN
    
    hosting-health DOMAIN
    
    hosting-repair DOMAIN --php
    hosting-repair DOMAIN --nginx
    hosting-repair DOMAIN --permissions
    hosting-repair DOMAIN --wordpress
    
    hosting-ssl DOMAIN
    
    hosting-backup DOMAIN
    
    hosting-restore DOMAIN BACKUP_ID

    This is becoming a real operational platform.


    64. Lesson 071 — Core Principle

    The safest restore architecture is:

    VERIFY
      ↓
    PROTECT CURRENT STATE
      ↓
    RESTORE
      ↓
    VERIFY
      ↓
    HEALTH CHECK
      ↓
    ACTIVATE

    Never:

    DELETE CURRENT DATA
      ↓
    HOPE RESTORE WORKS

    The key rule is:

    A restore operation must have a recovery path of its own.


    Next Lesson — 072

    Build hosting-backup-list and Retention

    We now need to manage multiple backups.

    The next commands will be:

    sudo hosting-backup-list example.com
    sudo hosting-backup-delete example.com BACKUP_ID

    and eventually:

    sudo hosting-backup-prune example.com

    The system will calculate:

    Daily backups
    Weekly backups
    Monthly backups
    Retention
    Backup size
    Oldest backup
    Newest backup
    Failed backups

    and safely remove only backups that are eligible for deletion.

  • CresignSys Learn — Lesson 070

    Build hosting-backup

    We now have:

    hosting-create
    hosting-list
    hosting-info
    hosting-health
    hosting-repair
    hosting-ssl

    The next essential component is backup.

    A hosting platform without a reliable restore system is incomplete.

    The target command:

    sudo hosting-backup example.com

    should protect:

    Website files
    +
    Database
    +
    Site metadata
    +
    Configuration

    1. Backup Architecture

    The basic flow:

                    hosting-backup
                          │
                          ▼
                    Check website
                          │
                          ▼
                   Create backup ID
                          │
              ┌───────────┴───────────┐
              ▼                       ▼
         WordPress files          MySQL database
              │                       │
              ▼                       ▼
          Compress                 Dump
              │                       │
              └───────────┬───────────┘
                          ▼
                    Create manifest
                          │
                          ▼
                    Verify backup
                          │
                          ▼
                      COMPLETE

    2. What Must Be Backed Up?

    For a WordPress website:

    /storage/websites/example.com/

    contains important files.

    But the database is separate.

    Therefore:

    BACKUP
    │
    ├── Files
    │   ├── wp-content
    │   ├── wp-config.php
    │   ├── WordPress core
    │   └── other site files
    │
    ├── Database
    │   └── MySQL dump
    │
    └── Metadata
        └── CHP configuration

    3. Files Alone Are Not Enough

    Suppose you back up only:

    public/

    You still don’t have:

    wp_posts
    wp_users
    wp_options
    wp_terms
    ...

    Those are in MySQL.

    So:

    Files + Database

    are both required for a meaningful WordPress backup.


    4. Database Alone Is Not Enough

    A database backup does not contain:

    uploads
    themes
    plugins
    WordPress core
    custom PHP files

    Therefore:

    Database + Files

    is the fundamental backup pair.


    5. Recommended Backup Layout

    Create:

    /backup/cresignsys/

    Then:

    /backup/cresignsys/example.com/

    and:

    /backup/cresignsys/example.com/
    └── 2026-08-13_160000/
        ├── files.tar.zst
        ├── database.sql.gz
        ├── site.conf
        └── manifest.json

    6. Why Use a Timestamp?

    Suppose you have:

    2026-08-13_080000
    2026-08-13_120000
    2026-08-13_160000

    You can restore a particular point in time.

    This gives you:

    Point-in-Time Backup Selection


    7. Create Backup Configuration

    Add to:

    /etc/cresignsys/hosting.conf

    something like:

    BACKUP_BASE="/backup/cresignsys"

    Then:

    sudo mkdir -p /backup/cresignsys

    8. Don’t Put Backups Under public

    Never use:

    /storage/websites/example.com/public/backups/

    because a web server could potentially expose them.

    Bad:

    https://example.com/backups/database.sql

    Good:

    /backup/cresignsys/example.com/

    outside the website document root.


    9. Create hosting-backup

    Create:

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

    Start:

    #!/usr/bin/env bash
    
    set -Eeuo pipefail

    10. Require Root

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

    11. Require Domain

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

    12. Load Configuration

    source /etc/cresignsys/hosting.conf

    Then:

    STATE_DIR="/var/lib/cresignsys/sites"
    SITE_DIR="${STATE_DIR}/${DOMAIN}"
    SITE_CONF="${SITE_DIR}/site.conf"

    13. Verify Website Exists

    if [[ ! -f "$SITE_CONF" ]]; then
        echo "ERROR: Site not found: $DOMAIN"
        exit 1
    fi

    Load:

    source "$SITE_CONF"

    Now we know:

    DOMAIN
    SITE_ID
    SITE_USER
    WEB_ROOT
    DB_NAME
    DB_USER

    14. Generate Backup ID

    BACKUP_ID="$(date '+%Y-%m-%d_%H%M%S')"

    Then:

    BACKUP_DIR="${BACKUP_BASE}/${DOMAIN}/${BACKUP_ID}"

    Create:

    mkdir -p "$BACKUP_DIR"

    15. Example

    For:

    example.com

    you get:

    /backup/cresignsys/example.com/2026-08-13_160000/

    16. Create Backup Manifest

    The manifest describes the backup.

    Example:

    {
      "domain": "example.com",
      "site_id": "1027",
      "created_at": "2026-08-13T16:00:00",
      "database": "csp1027_wp",
      "status": "STARTED"
    }

    The final manifest should say:

    status = COMPLETE

    17. Why a Manifest?

    Imagine you have:

    200 backup files

    You need to know:

    Which website?
    Which date?
    Which database?
    Which version?
    Was it complete?

    The manifest provides that information.


    18. Backup the Site Configuration

    Copy:

    cp "$SITE_CONF" "$BACKUP_DIR/site.conf"

    Remember:

    Do not store database passwords in site.conf.

    Therefore the backup doesn’t accidentally become a password archive.


    19. Backup Website Files

    A common approach is tar.

    For example:

    tar -C "$(dirname "$WEB_ROOT")" \
        -czf "$BACKUP_DIR/files.tar.gz" \
        "$(basename "$WEB_ROOT")"

    This creates:

    files.tar.gz

    containing the website’s public files.


    20. Why -C?

    Instead of creating an archive containing:

    /storage/websites/example.com/public/...

    you can create a cleaner archive structure:

    public/
        wp-admin/
        wp-content/
        wp-includes/

    This makes restoration easier.


    21. What About private and logs?

    You need to decide what constitutes the site’s authoritative data.

    For example:

    site root
    ├── public
    ├── private
    └── logs

    You may want:

    public       → backup
    private      → backup
    logs         → optional

    Logs usually don’t need the same retention policy as customer data.


    22. Recommended Initial Policy

    For the first CHP version:

    public      ✓
    private     ✓
    logs        optional

    Later:

    logs → separate operational retention

    23. Database Backup

    Use mysqldump or the appropriate MySQL logical backup tool.

    For a basic WordPress database:

    mysqldump \
        --single-transaction \
        --routines \
        --triggers \
        "$DB_NAME" \
        | gzip > "$BACKUP_DIR/database.sql.gz"

    The exact options should match your MySQL version and backup requirements.


    24. Why --single-transaction?

    For transactional tables such as InnoDB, it can provide a consistent logical snapshot without locking the entire database for the duration of the dump.

    This is generally preferable for active WordPress sites.


    25. Database Credentials

    The backup command needs database access.

    Instead of exposing passwords on the command line, use a secure credentials mechanism.

    For example, the MySQL client can use a protected configuration file or another secret-management approach.

    Avoid:

    mysqldump -u user -pPASSWORD ...

    because command-line secrets can be exposed through process inspection or shell history in some environments.


    26. Important Backup Principle

    The backup system itself must not create a security problem.

    Bad:

    backup
     ↓
    password exposed

    Good:

    backup
     ↓
    protected credentials
     ↓
    database dump

    27. Compress Database Backup

    The resulting file:

    database.sql.gz

    is much smaller than:

    database.sql

    For many WordPress databases this saves substantial storage.


    28. File Compression

    You can also use:

    gzip

    or:

    zstd

    For modern Linux systems, zstd is often attractive because of its speed and compression tradeoff.

    For example:

    files.tar.zst

    could replace:

    files.tar.gz

    once your backup/restore tooling standardizes on it.


    29. Don’t Mix Formats Randomly

    Choose one platform standard.

    For example:

    Website files → tar.zst
    Database → sql.gz

    or:

    Website files → tar.gz
    Database → sql.gz

    Consistency makes automation much easier.


    30. Calculate Checksums

    After creating:

    files.tar.gz
    database.sql.gz

    calculate SHA-256:

    sha256sum \
        "$BACKUP_DIR/files.tar.gz" \
        "$BACKUP_DIR/database.sql.gz"

    Store the result:

    checksums.sha256

    31. Why Checksums?

    Suppose:

    backup created

    but later:

    disk corruption

    The archive may still exist but be damaged.

    Checksum:

    original hash
           ↓
    current hash
           ↓
    compare

    can detect corruption.


    32. Verify the Backup

    A backup isn’t complete merely because:

    files.tar.gz exists

    You should verify:

    archive readable
    database dump readable
    checksum correct
    manifest complete

    33. Test the Archive

    For gzip:

    gzip -t "$BACKUP_DIR/database.sql.gz"

    For tar:

    tar -tzf "$BACKUP_DIR/files.tar.gz" >/dev/null

    If using zstd, use the corresponding verification commands.


    34. Test the Database Dump

    At minimum:

    zcat "$BACKUP_DIR/database.sql.gz" | head

    should produce SQL content.

    But don’t rely only on seeing text.

    A stronger backup verification is:

    restore dump to a temporary database
           ↓
    run database checks

    This is much more meaningful.


    35. Backup Verification Levels

    Level 1

    file exists

    Weak.

    Level 2

    checksum valid

    Better.

    Level 3

    archive can be read

    Better.

    Level 4

    database dump can be imported

    Strong.

    Level 5

    full restore tested

    Best.


    36. The Most Important Backup Principle

    A backup is not proven until restoration has been tested.

    A 500 GB backup that cannot restore is not useful.


    37. Create Manifest

    A more complete manifest:

    {
      "domain": "example.com",
      "site_id": "1027",
      "created_at": "2026-08-13T16:00:00",
      "database": "csp1027_wp",
      "files_archive": "files.tar.gz",
      "database_dump": "database.sql.gz",
      "status": "COMPLETE"
    }

    Add checksums separately.


    38. File Size Information

    Record:

    files_size
    database_size

    For example:

    {
      "files_size": 1452389120,
      "database_size": 28347120
    }

    This will later help with:

    backup monitoring
    storage planning
    billing

    39. Backup Status

    The process should have states:

    STARTED
      ↓
    FILES_COMPLETE
      ↓
    DATABASE_COMPLETE
      ↓
    VERIFIED
      ↓
    COMPLETE

    Failure:

    BACKUP_ERROR

    40. Why State Matters

    The control panel can display:

    Last Backup:
    2026-08-13 16:00
    
    Status:
    COMPLETE

    instead of merely:

    Backup folder exists

    41. Backup Log

    Create:

    /var/log/cresignsys/backup.log

    Example:

    2026-08-13 16:00 example.com BACKUP_START
    2026-08-13 16:01 example.com FILES_COMPLETE
    2026-08-13 16:01 example.com DATABASE_COMPLETE
    2026-08-13 16:01 example.com CHECKSUM_COMPLETE
    2026-08-13 16:01 example.com VERIFY_COMPLETE
    2026-08-13 16:01 example.com BACKUP_COMPLETE

    42. Backup Lock

    Backup should use the same per-site locking mechanism.

    Why?

    Avoid:

    backup
    +
    repair
    +
    delete

    running against the same site simultaneously.


    43. Site Operation Lock

    Eventually all operations should use:

    /var/lock/cresignsys-example.com.lock

    Operations:

    hosting-create
    hosting-repair
    hosting-backup
    hosting-restore
    hosting-delete

    should coordinate through this lock.


    44. Backup While Website Is Online

    You generally don’t want:

    website
     ↓
    STOP
     ↓
    backup
     ↓
    START

    for every backup.

    Instead:

    website remains online
           ↓
    consistent database snapshot
           +
    filesystem archive

    For larger/high-write systems, filesystem/database consistency becomes more complicated and may require snapshots or application-aware backup strategies.


    45. WordPress Uploads

    The largest directory in many WordPress sites is:

    wp-content/uploads/

    Therefore backup size may be dominated by:

    images
    videos
    PDFs
    documents

    This should influence your storage and retention design.


    46. Backup Retention

    Suppose a site gets:

    1 backup/day

    for:

    30 days

    That’s:

    30 backups

    If each backup is 2 GB:

    60 GB

    for one site.

    With 100 sites:

    6 TB

    Therefore retention must be designed carefully.


    47. Retention Policy

    A simple initial policy:

    Daily:
    7 backups
    
    Weekly:
    4 backups
    
    Monthly:
    3 backups

    This is only an example policy.

    Your actual retention should depend on:

    storage
    customer plan
    business requirements
    recovery objectives

    48. Don’t Keep Infinite Backups

    Bad:

    backup
    backup
    backup
    backup
    ...

    Eventually:

    DISK FULL

    and then:

    WordPress sites fail
    MySQL fails
    backups fail

    A backup system can itself become an availability problem.


    49. Backup Storage Separation

    Eventually:

    Server
      │
      ├── Websites
      │
      └── Local backup

    is not enough.

    If the entire server dies:

    server
     ↓
    websites lost
     ↓
    local backups lost

    Therefore use:

    Off-Site Backup


    50. 3-2-1 Backup Principle

    A useful general strategy is:

    3 copies
    2 different storage media/systems
    1 off-site copy

    For example:

    Primary server
         +
    local backup storage
         +
    off-site object storage

    51. Local Backup

    Advantages:

    fast
    cheap
    quick restore

    Disadvantages:

    same server/storage failure

    52. Remote Backup

    Advantages:

    survives server failure

    Disadvantages:

    network dependency
    storage cost
    transfer cost
    credential management

    53. Recommended CHP Architecture

                        BACKUP
                           │
              ┌────────────┴────────────┐
              ▼                         ▼
        Local Backup              Remote Backup
              │                         │
              ▼                         ▼
       Fast Restore             Disaster Recovery

    54. Don’t Upload Plain Backup Files Without Protection

    A remote backup may contain:

    wp-config.php
    database contents
    customer uploads
    customer information

    Therefore remote backups should be protected with appropriate encryption and access controls.


    55. Encryption

    The ideal architecture:

    Website
       ↓
    Backup archive
       ↓
    Encryption
       ↓
    Remote storage

    The storage provider should not necessarily have access to the plaintext backup contents.


    56. Encryption Key Management

    This introduces another problem:

    Where is the encryption key?

    If the key is stored:

    on same server

    and the server is compromised:

    backup + key

    may both be compromised.

    Therefore key management needs separate consideration.


    57. Backup Security

    Treat backups as sensitive data.

    Protect:

    database dumps
    wp-config.php
    customer uploads
    SSH keys
    API credentials
    SSL-related metadata

    Never place them in:

    public_html

    or:

    public/

    58. hosting-backup Output

    A successful command could show:

    CresignSys Backup
    =================
    
    Domain: example.com
    Backup ID: 2026-08-13_160000
    
    [OK] Files archived
    [OK] Database dumped
    [OK] Checksums generated
    [OK] Archive verified
    [OK] Database dump verified
    [OK] Manifest created
    
    Files:
    1.4 GB
    
    Database:
    28 MB
    
    Status:
    COMPLETE

    59. Failed Backup

    Example:

    CresignSys Backup
    =================
    
    Domain: example.com
    
    [OK] Files archived
    [FAIL] Database dump
    
    Status:
    BACKUP_ERROR
    
    Backup is NOT marked as complete.

    Do not pretend that:

    files backup exists

    means:

    website fully backed up

    60. Backup Manifest Should Say Exactly What Happened

    For example:

    {
      "status": "ERROR",
      "files": "COMPLETE",
      "database": "FAILED"
    }

    This is much better than:

    {
      "status": "COMPLETE"
    }

    when the database was missing.


    61. Backup Listing

    The next useful command will be:

    sudo hosting-backup-list example.com

    Output:

    BACKUP ID                STATUS       SIZE
    ------------------------------------------------
    2026-08-13_160000        COMPLETE     1.4G
    2026-08-12_160000        COMPLETE     1.3G
    2026-08-11_160000        COMPLETE     1.3G

    This will feed directly into restore.


    62. Restore Architecture

    We are building toward:

    hosting-backup
          ↓
    backup repository
          ↓
    hosting-restore

    The restore operation should be:

    select backup
          ↓
    verify backup
          ↓
    lock site
          ↓
    backup current state
          ↓
    restore files
          ↓
    restore database
          ↓
    verify
          ↓
    health check

    63. Never Restore Blindly

    Suppose:

    example.com

    is currently working.

    You select an old backup.

    A safe restore should first create:

    pre-restore backup

    so that if the restore is wrong:

    restore
     ↓
    problem
     ↓
    restore previous state

    64. Disaster Recovery Model

    Eventually:

    CURRENT SITE
         │
         ▼
    PRE-RESTORE BACKUP
         │
         ▼
    RESTORE SELECTED BACKUP
         │
         ▼
    HEALTH CHECK
         │
     ┌───┴────┐
     ▼        ▼
    PASS     FAIL
     │        │
     ▼        ▼
    ACTIVE   ROLLBACK

    65. Backup + Repair

    These systems now connect:

    Health
      ↓
    Problem
      ↓
    Backup
      ↓
    Repair
      ↓
    Health

    For risky repairs:

    Backup
     ↓
    Repair
     ↓
    Verify

    This gives administrators a safety net.


    66. Backup + Delete

    Eventually:

    hosting-delete example.com

    should not immediately destroy data.

    A safer policy could be:

    ACTIVE
     ↓
    SUSPENDED
     ↓
    FINAL BACKUP
     ↓
    DELETED

    with configurable retention.


    67. Backup + Hosting Plans

    Your hosting plans can eventually define:

    Starter:
    7-day backup retention
    
    Business:
    14-day retention
    
    Professional:
    30-day retention
    
    Premium:
    90-day retention

    Again, these are example policies.


    68. Backup + Billing

    Your control panel can calculate:

    Website storage
    +
    Backup storage

    and eventually price backup storage separately if your hosting product requires it.


    69. Backup + Monitoring

    The dashboard can show:

    Website              Last Backup        Status
    ------------------------------------------------
    learn.cresignsys.com  2 hours ago        ✓
    shop.cresignsys.com   1 day ago          ✓
    example.com           5 days ago         !

    This allows administrators to find sites that have quietly stopped backing up.


    70. The Complete Data Protection Layer

    We now have the architecture:

                        DATA PROTECTION
                              │
                 ┌────────────┼────────────┐
                 ▼            ▼            ▼
              Backup       Restore       Retention
                 │            │            │
                 ▼            ▼            ▼
              Local         Recovery     Cleanup
                 │
                 ▼
              Remote

    71. Lesson 070 — Core Principle

    A hosting backup system should guarantee three things:

    1. Completeness

    Files + Database + Metadata

    2. Integrity

    Checksum + Verification

    3. Recoverability

    Tested restoration

    Therefore:

    BACKUP
      ≠
    FILES COPIED

    Instead:

    BACKUP
    =
    COPY
    +
    VERIFY
    +
    STORE
    +
    RECOVERABILITY

    72. Current CresignSys Architecture

    Your platform is now developing into:

                             CHP
                              │
          ┌───────────────────┼───────────────────┐
          ▼                   ▼                   ▼
     Provisioning        Observability        Data Protection
          │                   │                   │
          ▼                   ▼                   ▼
    hosting-create      hosting-list         hosting-backup
                        hosting-info
                        hosting-health
          │
          ▼
    hosting-repair
          │
          ▼
    hosting-ssl
          │
          ▼
         SITE

    The next major component is:

    hosting-restore

    because a backup that cannot be restored is not yet a complete recovery system.


    Next Lesson — 071

    Build hosting-restore

    We will design:

    sudo hosting-restore example.com BACKUP_ID

    with:

    Backup verification
            ↓
    Current-site safety backup
            ↓
    Site lock
            ↓
    Maintenance mode
            ↓
    Restore files
            ↓
    Restore database
            ↓
    Fix ownership
            ↓
    Verify WordPress
            ↓
    Verify Nginx/PHP
            ↓
    Health check
            ↓
    ACTIVE

    and, most importantly, a rollback path if the restoration itself fails.

  • CresignSys Learn — Lesson 069

    Build hosting-ssl

    We now have:

    hosting-create
    hosting-list
    hosting-info
    hosting-health
    hosting-repair

    The next component is SSL automation.

    The target command is:

    sudo hosting-ssl example.com

    It should convert:

    HTTP

    into:

    HTTPS

    without disturbing the existing website.


    1. SSL Architecture

    The complete flow becomes:

                    hosting-ssl
                         │
                         ▼
                  Check domain
                         │
                         ▼
                  Check DNS
                         │
                         ▼
                  Check HTTP
                         │
                         ▼
              Request certificate
                         │
                         ▼
                 Let's Encrypt
                         │
                         ▼
                 Configure Nginx
                         │
                         ▼
                 nginx -t
                         │
                         ▼
               Reload Nginx
                         │
                         ▼
              Test HTTPS
                         │
                         ▼
              Enable auto-renewal

    2. Most Important Rule

    SSL provisioning must not break HTTP.

    Bad workflow:

    request certificate
          ↓
    modify Nginx
          ↓
    configuration fails
          ↓
    website DOWN

    Better:

    HTTP working
          ↓
    certificate
          ↓
    generate HTTPS configuration
          ↓
    validate
          ↓
    activate

    3. Why DNS Comes First

    For:

    example.com

    the DNS record should point to your server:

    example.com
         ↓
    SERVER IP

    For a subdomain:

    learn.cresignsys.com
         ↓
    SERVER IP

    The certificate authority must be able to verify that the domain resolves correctly.


    4. Check DNS

    From the server:

    dig +short example.com

    or:

    nslookup example.com

    Expected:

    YOUR_SERVER_IP

    If DNS points somewhere else:

    hosting-ssl
          ↓
    STOP

    5. Don’t Guess DNS

    Your SSL script should never assume:

    DNS is correct

    It should verify the domain’s DNS resolution.

    For a production system, DNS verification should be explicit.


    6. Check HTTP First

    Before SSL:

    curl -I http://example.com

    You want a valid HTTP response.

    For example:

    HTTP/1.1 200 OK

    or potentially:

    HTTP/1.1 301 Moved Permanently

    depending on your existing configuration.


    7. Why HTTP Must Work

    With common HTTP-based certificate validation:

    Certificate Authority
            ↓
    http://example.com/.well-known/...
            ↓
    Nginx
            ↓
    server

    If Nginx isn’t correctly serving the domain, certificate issuance can fail.


    8. Install Certbot

    On Ubuntu, check whether Certbot is already installed:

    certbot --version

    If not installed, install it using the package-management method appropriate for your Ubuntu deployment.

    For a production CHP server, keep the certificate-management tool standardized rather than mixing several ACME clients.


    9. Check Nginx Plugin

    If using Certbot with Nginx:

    certbot plugins

    Look for:

    nginx

    The important idea is:

    Certbot
       ↓
    ACME
       ↓
    Let's Encrypt

    10. First Certificate Test

    Before allowing automated production changes, use a staging/test environment when developing the automation.

    This avoids hitting certificate authority rate limits while debugging.

    The general flow is:

    staging
     ↓
    test successful
     ↓
    production certificate

    11. Why Staging Matters

    Imagine your script has a bug and requests:

    20 certificates

    during development.

    You could encounter certificate authority rate limits.

    Therefore:

    DEVELOPMENT
    → staging
    
    PRODUCTION
    → production CA

    12. Create hosting-ssl

    Create:

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

    Start:

    #!/usr/bin/env bash
    
    set -Eeuo pipefail

    13. Require Root

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

    14. Require Domain

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

    15. Locate Site

    STATE_DIR="/var/lib/cresignsys/sites"
    SITE_DIR="${STATE_DIR}/${DOMAIN}"
    SITE_CONF="${SITE_DIR}/site.conf"

    Then:

    if [[ ! -f "$SITE_CONF" ]]; then
        echo "ERROR: CresignSys site not found."
        exit 1
    fi

    16. Load Metadata

    source "$SITE_CONF"

    Now the script knows:

    DOMAIN
    SITE_ID
    SITE_USER
    WEB_ROOT
    PHP_VERSION
    PHP_SOCKET
    STATUS

    17. Verify Required Commands

    Check:

    command -v certbot
    command -v dig
    command -v nginx
    command -v curl

    If anything important is missing:

    ERROR: Required dependency missing.

    Stop before modifying anything.


    18. Check Nginx

    if ! systemctl is-active --quiet nginx; then
        echo "ERROR: Nginx is not running."
        exit 1
    fi

    19. Check Existing Configuration

    NGINX_CONF="/etc/nginx/sites-available/${DOMAIN}.conf"

    Verify:

    if [[ ! -f "$NGINX_CONF" ]]; then
        echo "ERROR: Nginx site configuration not found."
        exit 1
    fi

    20. Validate Nginx Before SSL

    Always:

    nginx -t

    If it fails:

    SSL provisioning
          ↓
    STOP

    Fix the existing website first:

    hosting-repair example.com --nginx

    21. Check DNS

    A simple check:

    DNS_IP="$(dig +short "$DOMAIN" A | tail -n 1)"

    Then display:

    Domain:
    example.com
    
    Resolved IP:
    xxx.xxx.xxx.xxx

    22. Compare With Server IP

    You can determine the server’s public addresses using the server/network configuration.

    But be careful with:

    IPv4
    IPv6
    NAT
    reverse proxies
    CDNs
    load balancers

    A simplistic:

    DNS IP == local interface IP

    check is not universally correct.

    Therefore DNS validation should be configurable.


    23. HTTP Validation

    Run:

    curl -fsSI --max-time 10 "http://${DOMAIN}"

    If this fails:

    ERROR: HTTP validation failed.

    Do not request SSL yet.


    24. Why This Prevents Problems

    The dependency chain is:

    DNS
     ↓
    HTTP
     ↓
    Certificate
     ↓
    HTTPS

    You shouldn’t attempt step 3 when step 2 is broken.


    25. Certificate Request

    Once DNS and HTTP are confirmed, request the certificate.

    Conceptually:

    certbot certonly \
        --nginx \
        -d "$DOMAIN"

    For automated server provisioning, use non-interactive settings and a defined administrative email.

    Do not hard-code a personal email address into the platform.


    26. Why certonly?

    We want CHP to remain the owner of the Nginx configuration.

    Therefore:

    Certbot
       ↓
    certificate management
    
    CHP
       ↓
    Nginx configuration

    This gives you greater control.

    An alternative architecture is to let Certbot modify Nginx automatically, but mixing two configuration managers can make your platform harder to reason about.


    27. Certificate Location

    Certificates are commonly maintained under:

    /etc/letsencrypt/

    For a domain, you’ll typically find a lineage such as:

    /etc/letsencrypt/live/example.com/

    with certificate-related files.

    Don’t copy private keys into:

    /storage/websites/example.com/public/

    Never.


    28. Private Key Security

    The TLS private key is sensitive.

    It belongs in:

    /etc/letsencrypt/

    or another protected system location managed by your certificate system.

    It must never be:

    publicly accessible
    downloadable
    stored in WordPress
    stored under document root

    29. Generate HTTPS Nginx Configuration

    Your Nginx configuration now needs:

    HTTP
    HTTPS

    A common architecture is:

    HTTP :80
       ↓
    redirect
       ↓
    HTTPS :443

    and:

    HTTPS :443
       ↓
    PHP-FPM

    30. HTTP Server Block

    Conceptually:

    server {
        listen 80;
        server_name example.com;
    
        return 301 https://$host$request_uri;
    }

    But there is an important issue:

    ACME HTTP validation may need access to the challenge path.

    Therefore your certificate issuance and final redirect configuration need to be coordinated.


    31. HTTPS Server Block

    Conceptually:

    server {
        listen 443 ssl http2;
        server_name example.com;
    
        root /storage/websites/example.com/public;
    
        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
        index index.php index.html;
    
        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:/run/php/csp1027.sock;
        }
    }

    Exact TLS directives should be maintained in a tested central template rather than copied independently for every site.


    32. Don’t Duplicate Configuration

    Your architecture should eventually use:

    /etc/cresignsys/templates/
    ├── nginx-http.conf
    └── nginx-https.conf

    or one carefully designed template with SSL variables.


    33. Certificate Variables

    The template needs:

    DOMAIN
    WEB_ROOT
    PHP_SOCKET
    SSL_CERTIFICATE
    SSL_CERTIFICATE_KEY

    For example:

    SSL_CERTIFICATE:
    /etc/letsencrypt/live/example.com/fullchain.pem
    
    SSL_CERTIFICATE_KEY:
    /etc/letsencrypt/live/example.com/privkey.pem

    34. Validate Before Activation

    Generate the configuration.

    Then:

    nginx -t

    Only if successful:

    systemctl reload nginx

    This rule remains unchanged.


    35. Test HTTPS

    After reload:

    curl -I https://example.com

    Expected:

    HTTP/2 200

    or another legitimate successful/redirect response depending on your configuration.


    36. Verify Certificate

    Use:

    openssl s_client \
        -connect "${DOMAIN}:443" \
        -servername "$DOMAIN" \
        </dev/null

    This lets you inspect the TLS connection.

    For automated health checking, a simpler certificate-expiry inspection can also be used.


    37. Certificate Expiry

    A certificate has an expiration date.

    Your platform should know:

    Certificate:
    ACTIVE
    
    Expires:
    2026-11-11
    
    Days remaining:
    90

    The exact lifetime depends on the certificate authority’s current policies.


    38. Why Renewal Is Essential

    SSL automation isn’t complete when the certificate is issued.

    It is complete only when:

    certificate
       ↓
    renewal
       ↓
    renewal validation
       ↓
    Nginx reload

    works automatically.


    39. Check Renewal Configuration

    Certbot typically provides a renewal mechanism through the system’s scheduled service/timer.

    Check:

    systemctl list-timers | grep -i certbot

    Depending on how Certbot was installed, the exact scheduling mechanism can differ.


    40. Test Renewal

    Do not wait until the certificate is nearly expired.

    Use the certificate tool’s dry-run renewal mechanism.

    For Certbot:

    certbot renew --dry-run

    This tests the renewal process without replacing the production certificate.


    41. Renewal Architecture

    The desired system:

    Certificate
         │
         ▼
    Renewal Scheduler
         │
         ▼
    Renew Certificate
         │
         ▼
    Validate Nginx
         │
         ▼
    Reload Nginx
         │
         ▼
    HTTPS Health Check

    42. Important: Renewal Must Not Break Nginx

    A renewal system should verify:

    new certificate
           ↓
    Nginx configuration
           ↓
    nginx -t
           ↓
    reload

    before declaring success.


    43. Update site.conf

    After successful SSL:

    SSL_STATUS=ACTIVE

    Add:

    SSL_CERTIFICATE=/etc/letsencrypt/live/example.com/fullchain.pem
    SSL_CERTIFICATE_KEY=/etc/letsencrypt/live/example.com/privkey.pem

    Again, storing the path is fine; don’t store the private key itself.


    44. Update Site State

    Before:

    STATUS=ACTIVE
    SSL_STATUS=NOT_CONFIGURED

    After:

    STATUS=ACTIVE
    SSL_STATUS=ACTIVE

    45. Health Check Changes

    Previously:

    HTTP ✓

    Now:

    HTTP → redirect
    HTTPS ✓
    Certificate ✓

    Your hosting-health command should understand the new expected state.


    46. Final Health Model

    For an SSL-enabled website:

    DNS              ✓
    HTTP             ✓
    HTTPS            ✓
    Certificate      ✓
    Certificate age  ✓
    Nginx            ✓
    PHP-FPM          ✓
    PHP socket       ✓
    WordPress        ✓
    Database         ✓
    Filesystem       ✓

    47. HTTP Redirect Test

    After SSL:

    curl -I http://example.com

    Expected:

    HTTP/1.1 301 Moved Permanently
    Location: https://example.com/...

    Then:

    curl -I https://example.com

    should succeed.


    48. Avoid Redirect Loops

    A common mistake is creating:

    HTTP → HTTPS
    HTTPS → HTTP

    or proxy configurations that cause:

    HTTP
     ↓
    HTTPS
     ↓
    HTTP
     ↓
    HTTPS

    Your health check should detect this.


    49. HTTPS Redirect Architecture

    The desired flow is:

    http://example.com
            │
            ▼
    301
            │
            ▼
    https://example.com
            │
            ▼
    WordPress

    Never:

    HTTPS → HTTP

    unless deliberately required for a specific architecture.


    50. WordPress URL Update

    After enabling HTTPS, WordPress must know that its canonical URL is HTTPS.

    Check:

    sudo -u "$SITE_USER" \
    wp --path="$WEB_ROOT" option get siteurl

    and:

    sudo -u "$SITE_USER" \
    wp --path="$WEB_ROOT" option get home

    They should eventually reflect:

    https://example.com

    51. Update WordPress URLs Carefully

    A controlled command can update:

    wp option update siteurl "https://example.com"
    wp option update home "https://example.com"

    But this is only part of the HTTPS migration.

    Existing database content may contain:

    http://example.com

    inside serialized data or content.


    52. Don’t Use Blind SQL Replacement

    Avoid:

    UPDATE wp_posts
    SET post_content = REPLACE(...)

    as a generic WordPress migration technique.

    WordPress data can contain serialized structures.

    Use WP-CLI’s WordPress-aware search/replace functionality for appropriate migrations.


    53. Mixed Content

    After HTTPS:

    Page = HTTPS
    Image = HTTP
    CSS = HTTP
    JS = HTTP

    can produce:

    mixed content

    The site may technically load but still have insecure resources.

    Therefore SSL health should eventually include an application-level check.


    54. SSL Is More Than a Certificate

    A working HTTPS deployment means:

    DNS
    +
    certificate
    +
    Nginx
    +
    HTTPS
    +
    WordPress URL
    +
    resources
    +
    renewal

    not simply:

    certificate exists

    55. hosting-ssl Output

    A good result:

    CresignSys SSL
    ==============
    
    Domain: example.com
    
    [OK] DNS resolution
    [OK] HTTP service
    [OK] Certificate issued
    [OK] Certificate files
    [OK] Nginx configuration
    [OK] Nginx reload
    [OK] HTTPS response
    [OK] Certificate verification
    [OK] WordPress HTTPS URL
    
    SSL Status: ACTIVE

    56. Failure Example

    Suppose DNS is wrong:

    CresignSys SSL
    ==============
    
    Domain: example.com
    
    [FAIL] DNS resolution
    
    SSL Status: NOT CONFIGURED
    
    HTTP website has not been modified.

    That final line is extremely important.


    57. Another Failure

    Suppose certificate issuance succeeds but Nginx validation fails:

    [OK] Certificate issued
    [OK] Certificate files
    [FAIL] Nginx configuration
    
    SSL Status: ERROR
    
    Existing HTTP configuration preserved.

    The site should remain available over its previous working configuration.


    58. This Requires Atomic Configuration Updates

    Don’t overwrite the working Nginx configuration directly.

    Instead:

    current.conf

    then generate:

    current.conf.tmp

    Validate:

    nginx -t

    Then replace:

    current.conf.tmp
           ↓
    current.conf

    and reload.


    59. Why This Matters

    You want:

    WORKING CONFIG
          │
          ▼
    NEW CONFIG
          │
          ▼
    VALIDATE
          │
       ┌──┴──┐
       ▼     ▼
    PASS   FAIL
     │       │
     ▼       ▼
    activate preserve old

    This is a general safe deployment pattern.


    60. SSL State Machine

    The SSL subsystem can have:

    NOT_CONFIGURED
          ↓
    REQUESTING
          ↓
    ISSUED
          ↓
    CONFIGURING
          ↓
    ACTIVE

    Failure:

    REQUESTING
         ↓
    ERROR

    or:

    CONFIGURING
         ↓
    ERROR

    61. Renewal States

    Later:

    ACTIVE
      ↓
    RENEWAL_DUE
      ↓
    RENEWING
      ↓
    RENEWED
      ↓
    ACTIVE

    If renewal fails:

    RENEWAL_ERROR

    This allows the control panel to warn administrators before a certificate expires.


    62. Certificate Monitoring

    Your dashboard could eventually show:

    DOMAIN                    SSL
    ------------------------------------------------
    learn.cresignsys.com      ACTIVE — 72 days
    shop.cresignsys.com       ACTIVE — 81 days
    example.com               ERROR
    oldsite.com               EXPIRING — 8 days

    This is much more useful than simply:

    SSL: ON

    63. Add SSL to hosting-info

    Now:

    sudo hosting-info example.com

    can show:

    SSL Status       : ACTIVE
    Certificate      : /etc/letsencrypt/live/example.com/fullchain.pem
    Certificate Exp. : 2026-11-11

    64. Add SSL to hosting-health

    The health check becomes:

    [OK] DNS
    [OK] HTTP
    [OK] HTTPS
    [OK] Certificate
    [OK] PHP-FPM
    [OK] PHP Socket
    [OK] Nginx
    [OK] WordPress
    [OK] Database

    65. Current CHP Architecture

    We now have:

                           CHP
                            │
           ┌────────────────┼────────────────┐
           │                │                │
           ▼                ▼                ▼
     Provisioning      Observability       Repair
           │                │                │
    hosting-create     hosting-list      hosting-repair
                       hosting-info
                       hosting-health
                            │
                            ▼
                           SSL
                            │
                            ▼
                      hosting-ssl

    66. Full Website Lifecycle

    The lifecycle is becoming:

    CREATE
      ↓
    HTTP
      ↓
    SSL
      ↓
    HTTPS
      ↓
    HEALTH
      ↓
    REPAIR
      ↓
    BACKUP
      ↓
    SUSPEND
      ↓
    RESTORE
      ↓
    DELETE

    67. Lesson 069 — Core Principle

    SSL automation should be treated as a deployment process, not a single certificate command.

    The safe workflow is:

    DNS
     ↓
    HTTP
     ↓
    Certificate
     ↓
    Temporary configuration
     ↓
    Nginx validation
     ↓
    Activation
     ↓
    HTTPS test
     ↓
    WordPress HTTPS
     ↓
    Renewal test
     ↓
    HEALTHY

    And the most important safety rule is:

    Never sacrifice a working website merely because SSL provisioning failed.


    Next Lesson — 070

    Build hosting-backup

    The next major component is data protection.

    We will design:

    sudo hosting-backup example.com

    to back up:

    WordPress files
          +
    MySQL database
          +
    site configuration
          +
    SSL metadata

    and produce a structured backup such as:

    /backup/cresignsys/example.com/
    ├── 2026-08-13/
    │   ├── files.tar.zst
    │   ├── database.sql.gz
    │   ├── site.conf
    │   └── manifest.json

    Then we can safely make hosting-repair, hosting-restore, and eventually automatic disaster recovery work together.

  • CresignSys Learn — Lesson 068

    Build hosting-repair

    We now have:

    hosting-create
    hosting-list
    hosting-info
    hosting-health

    The next layer is repair.

    The principle is:

    Repair the failed component without rebuilding or deleting the entire website.


    1. Why hosting-repair?

    Suppose:

    example.com

    has:

    Filesystem       ✓
    PHP-FPM          ✓
    PHP socket       ✗
    Nginx            ✓
    Database         ✓
    WordPress        ✓

    You don’t want:

    hosting-delete example.com
    hosting-create example.com

    That could destroy configuration or data.

    Instead:

    sudo hosting-repair example.com --php

    2. Repair Architecture

                        hosting-repair
                               │
                 ┌─────────────┼─────────────┐
                 ▼             ▼             ▼
                PHP          Nginx       Permissions
                 │             │             │
                 ▼             ▼             ▼
              Validate       Validate      Validate
                 │             │             │
                 └─────────────┼─────────────┘
                               ▼
                          Health Check

    3. Supported Repair Operations

    Initial version:

    sudo hosting-repair example.com --php
    sudo hosting-repair example.com --nginx
    sudo hosting-repair example.com --permissions
    sudo hosting-repair example.com --wordpress

    And eventually:

    sudo hosting-repair example.com --all

    4. Important Rule

    hosting-repair should not blindly recreate everything.

    For example:

    --php

    should repair:

    PHP-FPM configuration
    PHP-FPM service
    PHP socket

    It should not:

    drop database
    delete WordPress
    delete website

    5. Find Site Metadata

    The repair command reads:

    /var/lib/cresignsys/sites/example.com/site.conf

    which provides:

    SITE_ID
    SITE_USER
    WEB_ROOT
    DB_NAME
    DB_USER
    PHP_VERSION
    PHP_SOCKET

    6. Basic Script

    Create:

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

    Start:

    #!/usr/bin/env bash
    
    set -Eeuo pipefail
    
    STATE_DIR="/var/lib/cresignsys/sites"
    
    if [[ $# -lt 2 ]]; then
        echo "Usage: hosting-repair DOMAIN --php|--nginx|--permissions|--wordpress|--all"
        exit 1
    fi
    
    DOMAIN="$1"
    ACTION="$2"

    7. Load Site Configuration

    SITE_DIR="${STATE_DIR}/${DOMAIN}"
    SITE_CONF="${SITE_DIR}/site.conf"
    
    if [[ ! -f "$SITE_CONF" ]]; then
        echo "ERROR: Site not found: $DOMAIN"
        exit 1
    fi
    
    source "$SITE_CONF"

    8. Validate Action

    case "$ACTION" in
        --php|--nginx|--permissions|--wordpress|--all)
            ;;
        *)
            echo "ERROR: Unknown repair action: $ACTION"
            exit 1
            ;;
    esac

    9. Repair PHP

    The PHP repair sequence should be:

    Read site configuration
           ↓
    Check PHP version
           ↓
    Generate PHP-FPM pool
           ↓
    Validate configuration
           ↓
    Reload PHP-FPM
           ↓
    Check socket

    10. PHP Pool Path

    For PHP 8.3:

    /etc/php/8.3/fpm/pool.d/SITE_ID.conf

    For example:

    /etc/php/8.3/fpm/pool.d/csp1027.conf

    11. Generate Pool From Template

    Rather than duplicating configuration in multiple scripts, use:

    /etc/cresignsys/templates/php-fpm.conf

    Example template:

    [{{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 = {{PM_MAX_CHILDREN}}
    pm.start_servers = {{PM_START_SERVERS}}
    pm.min_spare_servers = {{PM_MIN_SPARE_SERVERS}}
    pm.max_spare_servers = {{PM_MAX_SPARE_SERVERS}}
    pm.max_requests = {{PM_MAX_REQUESTS}}

    12. Why Use a Template?

    Without templates:

    hosting-create → PHP configuration
    hosting-repair → PHP configuration

    could gradually become inconsistent.

    With one template:

    template
       ↓
    create
       ↓
    repair

    both use the same configuration standard.


    13. PHP Repair

    After generating:

    php-fpm8.3 -t

    If successful:

    systemctl reload php8.3-fpm

    Then:

    test -S "$PHP_SOCKET"

    14. If the Socket Is Missing

    Investigate:

    journalctl -u php8.3-fpm --no-pager -n 50

    This may reveal:

    configuration error
    permission error
    pool error
    resource error

    The repair command should ideally report the failure rather than hiding it.


    15. Repair Nginx

    Nginx repair:

    Read site metadata
           ↓
    Generate Nginx config
           ↓
    Check symlink
           ↓
    nginx -t
           ↓
    reload nginx
           ↓
    HTTP health check

    16. Nginx Configuration Path

    /etc/nginx/sites-available/example.com.conf

    Enabled link:

    /etc/nginx/sites-enabled/example.com.conf

    17. Recreate Only the Site Configuration

    The repair command can regenerate:

    example.com.conf

    from the standard template.

    It should not modify:

    /storage/websites/example.com/

    or:

    database

    18. Validate

    Always:

    nginx -t

    before:

    systemctl reload nginx

    If:

    nginx -t

    fails:

    DO NOT RELOAD

    19. Repair Permissions

    Use:

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

    and:

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

    But this must be adapted to the actual ownership model.


    20. Important Warning

    Do not automatically run:

    chmod -R 777

    during repair.

    A repair command should never turn a permission problem into a security problem.


    21. WordPress Repair

    --wordpress needs to be handled differently.

    We shouldn’t blindly reinstall WordPress.

    First inspect:

    wp core is-installed

    If installed:

    WordPress already exists

    then repair may mean:

    verify files
    verify database
    verify permissions
    verify core

    22. Verify WordPress Core

    WP-CLI provides:

    wp core verify-checksums

    Run as the site user:

    sudo -u "$SITE_USER" \
    wp --path="$WEB_ROOT" core verify-checksums

    This can identify modified or corrupted WordPress core files.


    23. WordPress Core Repair

    If the core is damaged, WP-CLI can reinstall core files without necessarily removing site content.

    A controlled repair could use:

    sudo -u "$SITE_USER" \
    wp --path="$WEB_ROOT" core download --force

    But do not execute this blindly on a production site.

    Before repair:

    backup
     ↓
    verify
     ↓
    repair
     ↓
    verify again

    24. Database Repair

    WP-CLI:

    wp db check

    tests connectivity/database integrity at a basic level.

    Don’t automatically run database repair commands on every health failure.

    A database failure needs diagnosis first.


    25. --all

    The --all operation can eventually perform:

    PHP
     ↓
    Nginx
     ↓
    Permissions
     ↓
    WordPress verification
     ↓
    Health check

    But it should still avoid destructive operations.


    26. Repair Locking

    Repair also needs locking.

    Why?

    Imagine:

    Admin A → hosting-repair example.com --php
    Admin B → hosting-repair example.com --nginx

    Running simultaneously may be okay in some cases, but concurrent modifications can still create conflicts.

    Use the same per-site lock strategy as provisioning.


    27. Lock Model

    example.com
         ↓
    /var/lock/cresignsys-example.com.lock

    Then:

    repair A
       ↓
    gets lock
       ↓
    repair
    
    repair B
       ↓
    wait/fail

    28. Repair Status

    When repair starts:

    STATUS=REPAIRING

    When successful:

    STATUS=ACTIVE

    When unsuccessful:

    STATUS=ERROR

    29. Don’t Hide Existing Errors

    Suppose the site was:

    STATUS=ERROR

    and PHP repair succeeds.

    Don’t automatically declare:

    ACTIVE

    until the complete health check succeeds.

    Instead:

    repair PHP
       ↓
    health check
       ↓
    healthy?

    Only then:

    ACTIVE

    30. Repair Workflow

    STATUS=ERROR
           ↓
    hosting-repair example.com --php
           ↓
    PHP repaired
           ↓
    health check
           ↓
    healthy
           ↓
    STATUS=ACTIVE

    If another problem remains:

    PHP repaired
           ↓
    HTTP still failing
           ↓
    STATUS=ERROR

    This is honest system reporting.


    31. Repair Logs

    Example:

    2026-08-13 16:20 REPAIR START
    2026-08-13 16:20 PHP CONFIG GENERATED
    2026-08-13 16:20 PHP CONFIG VALIDATED
    2026-08-13 16:20 PHP-FPM RELOADED
    2026-08-13 16:20 SOCKET VERIFIED
    2026-08-13 16:20 HEALTH CHECK
    2026-08-13 16:20 STATUS ACTIVE

    32. hosting-repair --php

    Example expected output:

    CresignSys Repair
    =================
    
    Domain: example.com
    Repair: PHP-FPM
    
    [OK]   Configuration generated
    [OK]   Configuration validated
    [OK]   PHP-FPM reloaded
    [OK]   PHP socket available
    [OK]   Health check
    
    Status: ACTIVE

    33. hosting-repair --nginx

    CresignSys Repair
    =================
    
    Domain: example.com
    Repair: Nginx
    
    [OK]   Configuration generated
    [OK]   nginx -t
    [OK]   Nginx reloaded
    [OK]   HTTP response
    
    Status: ACTIVE

    34. hosting-repair --permissions

    CresignSys Repair
    =================
    
    Domain: example.com
    Repair: Permissions
    
    [OK]   Ownership
    [OK]   Directory permissions
    [OK]   File permissions
    [OK]   WordPress access
    [OK]   Health check
    
    Status: ACTIVE

    35. hosting-repair --wordpress

    A safer workflow:

    Check WordPress
          ↓
    Check database
          ↓
    Check checksums
          ↓
    Backup if modification required
          ↓
    Repair
          ↓
    Verify

    Don’t simply replace the entire installation automatically.


    36. Why Backups Matter Before WordPress Repair

    A repair operation may change:

    core files
    plugins
    themes
    database

    Therefore:

    repair
     ↓
    backup
     ↓
    modify
     ↓
    verify

    should become the eventual model.


    37. Repair Is Not Backup

    Keep separate commands:

    hosting-backup
    hosting-restore
    hosting-repair

    A repair may use backup functionality, but shouldn’t become a backup system itself.


    38. Repair Should Be Deterministic

    If you run:

    hosting-repair example.com --nginx

    twice, the second run should produce essentially the same correct Nginx configuration.

    That is another form of idempotency.


    39. Repair vs Reprovision

    Important distinction:

    Repair

    Fix an existing installation.

    Reprovision

    Build infrastructure again.

    Restore

    Recover data from a backup.

    Delete

    Remove a site.

    These should never be treated as the same operation.


    40. Command Architecture

    hosting-create
            ↓
    new site
    
    hosting-repair
            ↓
    existing site
    
    hosting-backup
            ↓
    copy site data
    
    hosting-restore
            ↓
    recover site
    
    hosting-delete
            ↓
    remove site

    41. What hosting-repair Should Never Do Automatically

    Unless explicitly requested and carefully implemented, don’t automatically:

    DROP DATABASE
    rm -rf public
    delete uploads
    delete plugins
    delete themes
    change domain
    change DNS

    Repair should be conservative.


    42. Site Recovery Matrix

    You can think about failures like this:

    FailureRepair
    PHP socket missing--php
    PHP-FPM config broken--php
    Nginx config broken--nginx
    Wrong ownership--permissions
    WordPress core damaged--wordpress
    Database unavailableDiagnose MySQL
    Website deletedrestore
    Bad deploymentrestore

    43. Database Failure

    If:

    wp db check

    fails, don’t automatically run:

    hosting-repair example.com --database

    yet.

    First determine whether the problem is:

    MySQL service
    database user
    password
    database existence
    permissions
    disk
    corruption

    This deserves a separate database repair module.


    44. Next Repair Module

    Eventually:

    hosting-repair example.com --database

    can inspect:

    MySQL service
    database existence
    database user
    grants
    connection

    without dropping anything.


    45. Health → Repair Loop

    The operational cycle becomes:

    HEALTH CHECK
         │
         ▼
    PROBLEM DETECTED
         │
         ▼
    IDENTIFY COMPONENT
         │
     ┌───┼────┬─────┐
     ▼   ▼    ▼     ▼
    PHP Nginx Files WordPress
     │   │    │     │
     └───┴────┴─────┘
            │
            ▼
          REPAIR
            │
            ▼
       HEALTH CHECK
            │
            ▼
          ACTIVE

    46. This Is the Beginning of Self-Healing

    Eventually, the platform could automatically detect:

    PHP socket missing

    and recommend:

    Run PHP repair

    or, for safe predefined failures, automatically repair them.

    However, automatic repair should be introduced cautiously.


    47. Don’t Automatically Repair Everything

    Some failures require human judgment.

    For example:

    WordPress database corruption

    should not trigger:

    automatic destructive repair

    Instead:

    ALERT
     ↓
    diagnose
     ↓
    backup
     ↓
    human-approved repair

    48. hosting-repair as an Operator Tool

    At this stage:

    Administrator
        ↓
    hosting-health
        ↓
    find problem
        ↓
    hosting-repair
        ↓
    health check

    This gives you a safe operational workflow.


    49. Lesson 068 — Core Principle

    The purpose of repair is:

    Fix the smallest possible component while preserving the site’s data and configuration.

    Therefore:

    PHP problem
       → repair PHP
    
    Nginx problem
       → repair Nginx
    
    permission problem
       → repair permissions
    
    WordPress problem
       → verify/repair WordPress

    rather than:

    any problem
       → delete and recreate website

    50. Current CHP Command Set

    You now have the conceptual design for:

    hosting-create DOMAIN
    hosting-list
    hosting-info DOMAIN
    hosting-health DOMAIN
    hosting-repair DOMAIN --php
    hosting-repair DOMAIN --nginx
    hosting-repair DOMAIN --permissions
    hosting-repair DOMAIN --wordpress

    Next we need to solve an important missing piece:

    DOMAIN
      ↓
    DNS
      ↓
    HTTP
      ↓
    HTTPS
      ↓
    SSL certificate

    Next Lesson — 069

    Build hosting-ssl

    We will design:

    sudo hosting-ssl example.com

    to handle:

    DNS verification
           ↓
    HTTP validation
           ↓
    Let's Encrypt certificate
           ↓
    Nginx HTTPS configuration
           ↓
    HTTP → HTTPS redirect
           ↓
    Certificate verification
           ↓
    Auto-renewal
           ↓
    Health check

    We will also design it so SSL failures do not break an already-working HTTP website.

  • CresignSys Learn — Lesson 067

    Build hosting-list, hosting-info & hosting-health

    A hosting platform needs two capabilities:

    PROVISION
       ↓
    OBSERVE
       ↓
    DIAGNOSE
       ↓
    REPAIR

    We already started the provisioning engine.

    Now we build the observability layer.


    1. Three New Commands

    We want:

    sudo hosting-list
    sudo hosting-info example.com
    sudo hosting-health example.com

    They have different purposes.


    2. hosting-list

    Purpose:

    Show all websites managed by CresignSys.

    Example:

    DOMAIN                         STATUS
    ------------------------------------------------------
    learn.cresignsys.com           ACTIVE
    shop.cresignsys.com            ACTIVE
    medical.cresignsys.com         ACTIVE
    example.com                    ERROR

    3. hosting-info

    Purpose:

    Show the configuration and metadata of one website.

    Example:

    Domain:
    example.com
    
    Site ID:
    1027
    
    Status:
    ACTIVE
    
    Linux User:
    csp1027
    
    Web Root:
    /storage/websites/example.com/public
    
    PHP Version:
    8.3
    
    Database:
    csp1027_wp

    4. hosting-health

    Purpose:

    Test whether the website is actually functioning.

    Example:

    Filesystem       ✓
    PHP-FPM          ✓
    PHP Socket       ✓
    Nginx            ✓
    Database         ✓
    WordPress        ✓
    HTTP             ✓
    HTTPS            ✓

    5. Why Separate Them?

    Don’t make:

    hosting-info

    perform repairs.

    Don’t make:

    hosting-health

    modify configuration.

    Observability commands should ideally be:

    READ ONLY

    This is an important operational principle.


    6. Source of Truth

    We previously created:

    /var/lib/cresignsys/sites/

    For example:

    /var/lib/cresignsys/sites/
    ├── site1.example.com/
    ├── site2.example.com/
    └── learn.cresignsys.com/

    Each site can contain:

    state
    site.conf
    provision.log

    7. site.conf

    Example:

    DOMAIN=learn.cresignsys.com
    SITE_ID=1027
    SITE_USER=csp1027
    WEB_ROOT=/storage/websites/learn.cresignsys.com/public
    DB_NAME=csp1027_wp
    DB_USER=csp1027_db
    PHP_VERSION=8.3
    PHP_SOCKET=/run/php/csp1027.sock
    STATUS=ACTIVE

    Notice:

    Do not store the database password here.


    8. Build hosting-list

    Create:

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

    Start:

    #!/usr/bin/env bash
    
    set -Eeuo pipefail
    
    STATE_DIR="/var/lib/cresignsys/sites"
    
    if [[ ! -d "$STATE_DIR" ]]; then
        echo "No CresignSys sites found."
        exit 0
    fi

    9. Print Header

    printf "%-35s %-12s\n" "DOMAIN" "STATUS"
    printf "%-35s %-12s\n" "-----------------------------------" "------------"

    10. Read Site Directories

    for site_dir in "$STATE_DIR"/*; do
    
        [[ -d "$site_dir" ]] || continue
    
        DOMAIN="$(basename "$site_dir")"
        STATE_FILE="${site_dir}/state"
    
        STATUS="UNKNOWN"
    
        if [[ -f "$STATE_FILE" ]]; then
            STATUS="$(
                awk -F= '$1=="STATUS" {print $2}' "$STATE_FILE"
            )"
        fi
    
        printf "%-35s %-12s\n" "$DOMAIN" "$STATUS"
    done

    11. Make It Executable

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

    Run:

    sudo hosting-list

    12. First Improvement — Sort the List

    Instead of relying on filesystem ordering:

    for site_dir in "$STATE_DIR"/*;

    you can collect and sort the output.

    This becomes useful once you have dozens or hundreds of websites.


    13. Better Output

    Eventually:

    DOMAIN                         STATUS
    ------------------------------------------------------
    acme.com                      ACTIVE
    learn.cresignsys.com          ACTIVE
    shop.cresignsys.com           ACTIVE
    test.example.com              ERROR

    Alphabetical ordering makes administration easier.


    14. Build hosting-info

    Create:

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

    Start:

    #!/usr/bin/env bash
    
    set -Eeuo pipefail
    
    STATE_DIR="/var/lib/cresignsys/sites"
    
    if [[ $# -ne 1 ]]; then
        echo "Usage: hosting-info DOMAIN"
        exit 1
    fi
    
    DOMAIN="$1"
    SITE_DIR="${STATE_DIR}/${DOMAIN}"
    SITE_CONF="${SITE_DIR}/site.conf"

    15. Verify Site Exists

    if [[ ! -f "$SITE_CONF" ]]; then
        echo "ERROR: Site not found: $DOMAIN"
        exit 1
    fi

    16. Load Configuration

    source "$SITE_CONF"

    Now variables become available:

    DOMAIN
    SITE_ID
    SITE_USER
    WEB_ROOT
    DB_NAME
    DB_USER
    PHP_VERSION
    PHP_SOCKET
    STATUS

    17. Display Site Information

    echo
    echo "CresignSys Site Information"
    echo "============================"
    echo
    echo "Domain       : $DOMAIN"
    echo "Site ID      : $SITE_ID"
    echo "Status       : $STATUS"
    echo "Linux User   : $SITE_USER"
    echo "Web Root     : $WEB_ROOT"
    echo "PHP Version  : $PHP_VERSION"
    echo "PHP Socket   : $PHP_SOCKET"
    echo "Database     : $DB_NAME"
    echo "DB User      : $DB_USER"
    echo

    18. Show Directory Status

    Add:

    if [[ -d "$WEB_ROOT" ]]; then
        echo "Web Root     : EXISTS"
    else
        echo "Web Root     : MISSING"
    fi

    19. Show Disk Usage

    Use:

    du -sh "$(dirname "$WEB_ROOT")" 2>/dev/null || true

    Example:

    Disk Usage   : 1.2G

    20. Don’t Use du as a Quota System

    This is important.

    du tells you approximately:

    how much space is currently used

    It does not enforce:

    maximum storage

    Quota enforcement is a separate feature.


    21. Show PHP-FPM Status

    Use the configured service:

    systemctl is-active "php${PHP_VERSION}-fpm" \
        2>/dev/null || true

    Output:

    PHP-FPM      : active

    22. Check PHP Socket

    if [[ -S "$PHP_SOCKET" ]]; then
        echo "PHP Socket   : EXISTS"
    else
        echo "PHP Socket   : MISSING"
    fi

    This is a very useful diagnostic.


    23. Check Nginx Configuration

    NGINX_CONF="/etc/nginx/sites-available/${DOMAIN}.conf"
    
    if [[ -f "$NGINX_CONF" ]]; then
        echo "Nginx Config : EXISTS"
    else
        echo "Nginx Config : MISSING"
    fi

    24. Check Enabled Site

    NGINX_LINK="/etc/nginx/sites-enabled/${DOMAIN}.conf"
    
    if [[ -L "$NGINX_LINK" ]]; then
        echo "Nginx Site   : ENABLED"
    else
        echo "Nginx Site   : NOT ENABLED"
    fi

    25. Check WordPress

    if [[ -f "$WEB_ROOT/wp-config.php" ]]; then
        echo "WordPress    : CONFIGURED"
    else
        echo "WordPress    : NOT CONFIGURED"
    fi

    26. Don’t Run wp as Root

    If you need deeper WordPress information:

    sudo -u "$SITE_USER" \
    wp --path="$WEB_ROOT" core is-installed

    The principle remains:

    WordPress operations
            ↓
    site user

    not:

    root

    27. Build hosting-health

    Now the most useful command.

    Create:

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

    Start:

    #!/usr/bin/env bash
    
    set -Eeuo pipefail
    
    STATE_DIR="/var/lib/cresignsys/sites"
    
    if [[ $# -ne 1 ]]; then
        echo "Usage: hosting-health DOMAIN"
        exit 1
    fi
    
    DOMAIN="$1"
    SITE_DIR="${STATE_DIR}/${DOMAIN}"
    SITE_CONF="${SITE_DIR}/site.conf"

    28. Load Site Information

    if [[ ! -f "$SITE_CONF" ]]; then
        echo "ERROR: Site not found."
        exit 1
    fi
    
    source "$SITE_CONF"

    29. Health Check Philosophy

    Each test should return:

    PASS

    or:

    FAIL

    Do not let one failed check hide the other checks.


    30. Helper Function

    Create:

    check() {
        local name="$1"
        shift
    
        if "$@" >/dev/null 2>&1; then
            printf "[OK]   %-20s\n" "$name"
            return 0
        else
            printf "[FAIL] %-20s\n" "$name"
            return 1
        fi
    }

    Now:

    check "Web Root" test -d "$WEB_ROOT"

    prints:

    [OK] Web Root

    31. Don’t Stop After One Failure

    For health checking, we want:

    Filesystem    ✓
    PHP-FPM       ✓
    Socket        ✗
    Nginx         ✓
    Database      ✓
    WordPress     ✓

    rather than stopping at:

    Socket failed

    We want to know everything that is wrong.


    32. Maintain Health Status

    HEALTH_OK=1

    If a test fails:

    HEALTH_OK=0

    At the end:

    if [[ "$HEALTH_OK" -eq 1 ]]; then
        echo
        echo "Overall Status: HEALTHY"
        exit 0
    else
        echo
        echo "Overall Status: UNHEALTHY"
        exit 1
    fi

    33. Filesystem Check

    if ! check "Web Root" test -d "$WEB_ROOT"; then
        HEALTH_OK=0
    fi

    34. WordPress Check

    if ! check "WordPress" \
        sudo -u "$SITE_USER" \
        wp --path="$WEB_ROOT" core is-installed
    then
        HEALTH_OK=0
    fi

    35. Database Check

    if ! check "Database" \
        sudo -u "$SITE_USER" \
        wp --path="$WEB_ROOT" db check
    then
        HEALTH_OK=0
    fi

    This tests the actual WordPress database connection.

    That is better than merely checking whether MySQL is running.


    36. PHP-FPM Service Check

    PHP_SERVICE="php${PHP_VERSION}-fpm"
    
    if ! check "PHP-FPM" \
        systemctl is-active "$PHP_SERVICE"
    then
        HEALTH_OK=0
    fi

    37. PHP Socket Check

    if ! check "PHP Socket" test -S "$PHP_SOCKET"; then
        HEALTH_OK=0
    fi

    38. Nginx Check

    Global Nginx:

    if ! check "Nginx Service" \
        systemctl is-active nginx
    then
        HEALTH_OK=0
    fi

    39. Nginx Configuration Check

    if ! check "Nginx Config" nginx -t; then
        HEALTH_OK=0
    fi

    40. Domain HTTP Check

    Now test the actual website:

    if ! check "HTTP" \
        curl -fsSI --max-time 10 "http://${DOMAIN}"
    then
        HEALTH_OK=0
    fi

    This is important because:

    Nginx service running

    does not necessarily mean:

    your domain works

    41. HTTP Health Check

    The path is now:

    Browser
     ↓
    DNS
     ↓
    Server
     ↓
    Nginx
     ↓
    Domain server block
     ↓
    WordPress

    This tests multiple layers simultaneously.


    42. HTTPS Check

    If SSL is installed:

    if ! check "HTTPS" \
        curl -fsSI --max-time 10 "https://${DOMAIN}"
    then
        HEALTH_OK=0
    fi

    But if SSL hasn’t been configured yet, don’t mark HTTPS as a required failure.


    43. SSL-Aware Health Check

    You can detect whether HTTPS is expected.

    For example:

    SSL_STATUS=ACTIVE

    in site.conf.

    Then:

    SSL ACTIVE
        ↓
    test HTTPS
    
    SSL NOT ACTIVE
        ↓
    skip HTTPS test

    44. Health Output

    A good final result:

    CresignSys Health Check
    ========================
    
    Domain          : learn.cresignsys.com
    
    [OK]   Web Root
    [OK]   PHP-FPM
    [OK]   PHP Socket
    [OK]   Nginx Service
    [OK]   Nginx Config
    [OK]   WordPress
    [OK]   Database
    [OK]   HTTP
    [OK]   HTTPS
    
    Overall Status: HEALTHY

    45. Failed Example

    CresignSys Health Check
    ========================
    
    Domain          : learn.cresignsys.com
    
    [OK]   Web Root
    [OK]   PHP-FPM
    [FAIL] PHP Socket
    [OK]   Nginx Service
    [OK]   Nginx Config
    [OK]   WordPress
    [OK]   Database
    [FAIL] HTTP
    
    Overall Status: UNHEALTHY

    This immediately tells you where to investigate.


    46. Why Socket Failure Can Cause HTTP Failure

    The chain:

    Nginx
     ↓
    PHP-FPM socket
     ↓
    PHP

    If the socket is missing:

    Nginx
     ↓
    socket ✗

    dynamic WordPress requests can return:

    502 Bad Gateway

    47. Health Checks Are Diagnostic Clues

    For example:

    Nginx ✓

    Socket ✗

    Likely area:

    PHP-FPM pool

    Socket ✓

    HTTP ✗

    Investigate:

    Nginx routing
    DNS
    firewall
    domain configuration

    HTTP ✓

    WordPress ✗

    Investigate:

    database
    filesystem
    WordPress installation

    48. HTTP Status Codes

    Your health checker can eventually inspect HTTP status.

    For example:

    200 → healthy
    301/302 → redirect
    403 → access problem
    404 → routing/resource problem
    500 → application error
    502 → PHP-FPM problem
    503 → service unavailable

    This makes the health system more intelligent.


    49. Capture HTTP Status

    Instead of:

    curl -fsSI

    you can eventually capture:

    curl -o /dev/null -s -w "%{http_code}"

    Then:

    HTTP_STATUS=200

    and your health engine can interpret it.


    50. Don’t Treat Every Redirect as Failure

    A website may legitimately return:

    301

    because:

    HTTP → HTTPS

    So:

    301

    is not necessarily unhealthy.

    Your health system should understand expected behavior.


    51. Add SSL Status

    In your site.conf:

    SSL_STATUS=ACTIVE

    Then:

    echo "SSL Status : $SSL_STATUS"

    52. hosting-info Should Be Read Only

    This command should never do:

    systemctl restart nginx

    or:

    mysql ...

    to modify anything.

    Its role is:

    inspect

    53. hosting-health Should Also Be Read Only

    It can run:

    wp db check
    nginx -t
    curl
    systemctl is-active

    but should not:

    restart services
    change permissions
    recreate databases

    That belongs to:

    hosting-repair

    54. This Creates a Clean Operational Model

    hosting-create
         ↓
    CREATE
    
    hosting-list
         ↓
    OBSERVE
    
    hosting-info
         ↓
    INSPECT
    
    hosting-health
         ↓
    DIAGNOSE
    
    hosting-repair
         ↓
    FIX

    This separation is extremely useful.


    55. Add hosting-list --json

    Eventually your control panel will need machine-readable output.

    Human output:

    DOMAIN STATUS

    Machine output:

    {
      "domain": "example.com",
      "status": "ACTIVE"
    }

    This allows the web panel to consume the CLI safely.


    56. But Don’t Parse Human Text

    Avoid designing the control panel to parse:

    DOMAIN       STATUS
    example.com  ACTIVE

    Human formatting can change.

    Instead:

    hosting-list --json

    should produce structured JSON.


    57. JSON Becomes the API Boundary

    Eventually:

    CHP Web Panel
           ↓
    API
           ↓
    Provisioning Engine
           ↓
    JSON result

    For example:

    {
      "domain": "example.com",
      "status": "ACTIVE",
      "php": "8.3",
      "database": "healthy",
      "http": 200
    }

    58. This Is Better Than Shell Scraping

    Bad architecture:

    Web Panel
     ↓
    grep output from Bash

    Better:

    Web Panel
     ↓
    structured API
     ↓
    provisioning engine

    We will eventually move toward the second architecture.


    59. Add Timestamps

    Health results should eventually contain:

    checked_at

    For example:

    2026-08-13 15:45:22

    This matters because:

    healthy now

    does not mean:

    healthy forever

    60. Health History

    Eventually store:

    site_id
    timestamp
    http_status
    php_status
    database_status
    ssl_status
    overall_status

    Then the panel can show:

    Health
    ──────────────
    15:00 ✓
    15:05 ✓
    15:10 ✓
    15:15 ✗
    15:20 ✓

    61. This Enables Monitoring

    Once health checks exist, you can later build:

    automatic monitoring

    such as:

    if website unhealthy
            ↓
    alert administrator

    This is where the observability layer becomes operational monitoring.


    62. Disk Usage in hosting-health

    Add:

    du -sh "$(dirname "$WEB_ROOT")"

    Then:

    Disk Usage : 1.4G

    Eventually compare against the hosting plan.


    63. Storage Warning

    Suppose the plan allows:

    5 GB

    and usage is:

    4.8 GB

    The system can report:

    WARNING: 96% storage used

    This is the beginning of resource monitoring.


    64. Database Size

    You can also measure database size.

    Conceptually:

    SELECT
        ROUND(SUM(data_length + index_length) / 1024 / 1024, 2)
    FROM information_schema.tables
    WHERE table_schema = 'database_name';

    Then show:

    Database Size : 420 MB

    65. Don’t Query Every Table Excessively

    For a small number of websites, this is fine.

    For hundreds or thousands:

    health check
     ↓
    hundreds of SQL queries

    can itself become a performance problem.

    Later you will want:

    cached metrics

    and periodic collection.


    66. Website Health vs Server Health

    These are different.

    Server health

    CPU
    RAM
    Disk
    MySQL
    Nginx
    PHP-FPM

    Website health

    DNS
    HTTP
    WordPress
    Database
    SSL
    Filesystem

    You need both.


    67. Example

    Server:

    CPU ✓
    RAM ✓
    Disk ✓
    Nginx ✓
    MySQL ✓

    But:

    example.com → HTTP 502

    Therefore:

    server healthy
    site unhealthy

    This distinction prevents misleading diagnostics.


    68. Site Health Model

    Eventually:

    SITE HEALTH
    │
    ├── DNS
    ├── HTTP
    ├── HTTPS
    ├── Nginx
    ├── PHP-FPM
    ├── PHP socket
    ├── WordPress
    ├── Database
    ├── Filesystem
    └── SSL

    69. Server Health Model

    SERVER HEALTH
    │
    ├── CPU
    ├── Memory
    ├── Storage
    ├── Inodes
    ├── Nginx
    ├── PHP-FPM
    ├── MySQL
    └── Network

    70. Control Panel Dashboard

    These commands now provide the foundation for a dashboard:

                        CHP DASHBOARD
    
    Sites
    ────────────────────────────────────
    Total: 42
    
    Active: 40
    Error: 2
    
    Server
    ────────────────────────────────────
    CPU:      Healthy
    Memory:   Healthy
    Storage:  62%
    MySQL:    Healthy
    Nginx:    Healthy
    
    Websites
    ────────────────────────────────────
    learn.cresignsys.com       ✓
    shop.cresignsys.com        ✓
    example.com                ✗

    71. The Architecture Is Growing

    We now have:

                  CHP
                   │
         ┌─────────┴─────────┐
         ▼                   ▼
    Provisioning         Observability
         │                   │
    hosting-create      hosting-list
                        hosting-info
                        hosting-health

    Next:

    Repair

    Then:

    SSL

    Then:

    Backup

    72. Lesson 067 — Core Principle

    A professional hosting platform must be able to answer:

    What websites exist, how are they configured, and are they actually working?

    That requires three separate capabilities:

    hosting-list
         ↓
    What exists?
    
    hosting-info
         ↓
    How is it configured?
    
    hosting-health
         ↓
    Is it working?

    This is the foundation of reliable hosting operations.


    Next Lesson — 068

    Build hosting-repair

    We now have:

    CREATE
      ↓
    LIST
      ↓
    INFO
      ↓
    HEALTH

    The next logical component is:

    HEALTH
      ↓
    FAILURE
      ↓
    REPAIR

    We will build:

    sudo hosting-repair example.com

    with targeted repair operations such as:

    sudo hosting-repair example.com --php
    sudo hosting-repair example.com --nginx
    sudo hosting-repair example.com --permissions
    sudo hosting-repair example.com --wordpress

    without deleting the website or database.

  • CresignSys Learn — Lesson 066

    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.

  • CresignSys Learn — Lesson 065

    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.