No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-06-17 19:11:52 +00:00
group_vars feat(tasks): add service action playbook 2026-06-12 16:01:49 +00:00
host_vars refactor structure 2026-05-23 18:53:44 +08:00
inventory update TODO.md 2026-05-30 03:04:43 +00:00
playbooks feat(tasks): add service action playbook 2026-06-12 16:01:49 +00:00
roles feat(app): expand node app support 2026-06-09 02:17:42 +00:00
.gitignore feat(results): emit job result JSON 2026-05-30 21:50:47 +00:00
ansible.cfg refactor structure 2026-05-23 18:53:44 +08:00
CLAUDE.md feat(tasks): add service action playbook 2026-06-12 16:01:49 +00:00
README.md docs(teset): add test runbook 2026-06-17 19:11:52 +00:00
requirements.yml feat(swap): manage server swapfile 2026-05-30 10:26:19 +00:00
TEST_PLAN.md docs(test): add test plan 2026-06-17 19:05:15 +00:00
TEST_RUNBOOK.md docs(teset): add test runbook 2026-06-17 19:11:52 +00:00
TODO.md docs(TODO): breakdown SaaS features 2026-06-17 18:54:18 +00:00

Forge-Style Ansible Provisioning

This project provisions Ubuntu servers and app sites in a Laravel Forge-style model. It is designed to work in two ways:

  • Manual CLI runs with ansible-playbook.
  • Future SaaS orchestration that runs playbooks non-interactively with explicit variables and reads JSON result files.

The first server provision is domainless. App/site configuration is handled separately.

What It Provisions

Server provisioning:

  • Ubuntu preflight checks
  • UFW, Fail2ban, unattended upgrades, and SSH hardening
  • Swap file and sysctl tuning
  • System user, sudoers, Git known hosts
  • Nginx
  • PHP-FPM
  • Composer v2 at a known path
  • MySQL
  • Redis
  • Supervisor
  • System-wide Node.js by major version
  • JSON result output for job tracking

App provisioning:

  • App directory layout
  • Nginx vhost
  • Optional MySQL database and user
  • Deploy environment variables
  • Optional zero-downtime layout
  • JSON result output for job tracking

Deploy action:

  • Repository checkout by branch
  • Optional release directory deployment
  • Shared .env and storage links
  • Composer install for PHP apps
  • Configurable deploy commands
  • Optional HTTP health check
  • JSON result output for job tracking

Certificate action:

  • Certbot with Nginx integration
  • App TLS certificate issue/renewal
  • Optional HTTP to HTTPS redirect
  • Nginx validation before and after certificate changes
  • JSON result output for job tracking

Worker action:

  • Per-app Supervisor program files
  • Configurable command, process count, user, working directory, and environment
  • Worker log paths
  • JSON result output for job tracking

Scheduler action:

  • Per-app Laravel scheduler cron entry
  • Configurable user, directory, schedule, command, environment, disabled state, and removal
  • JSON result output for job tracking

App lifecycle action:

  • Disable app Nginx site
  • Delete app Nginx config
  • Optional app file, database, and database user removal
  • JSON result output for job tracking

Log path conventions:

  • App, Nginx, PHP-FPM, Supervisor, worker, MySQL, Redis, and Certbot log paths
  • log_paths in JSON result output for SaaS display

Per-site Node/NVM:

  • Opt-in per-app NVM install under the app user
  • Configurable app Node.js version
  • Optional deploy-time Node install/build commands

App systemd service:

  • Opt-in systemd process manager for long-running app commands
  • Configurable service command, working directory, user, environment, and restart policy

Log discovery action:

  • Reports configured server/app log paths and whether each path exists
  • Optionally lists discovered app and worker log files
  • JSON result output for SaaS log display

Service action:

  • Restart, reload, or inspect server and app services
  • Supports Nginx, PHP-FPM, Supervisor programs, app systemd services, Redis, and MySQL
  • JSON result output for SaaS audit logs

Supported OS

  • Ubuntu 22.04 LTS
  • Ubuntu 24.04 LTS

Project Layout

ansible.cfg
inventory/
playbooks/
  site.yml      # imports server.yml
  server.yml    # first-time server provision
  app.yml       # create one app/site
  deploy.yml    # deploy one app repository
  certificate.yml # issue TLS for one app/site
  worker.yml    # manage one app Supervisor worker
  scheduler.yml # manage one app Laravel scheduler cron entry
  app_delete.yml # disable or delete one app/site
  logs.yml      # discover configured log paths and file presence
  tasks.yml     # run restart/reload/status service actions
roles/
group_vars/
host_vars/

Install Dependencies

Run this on the controller machine:

ansible-galaxy collection install -r requirements.yml

Inventory

Edit inventory/production/hosts.yml:

all:
  children:
    zs_hosts:
      hosts:
        zs-01:
          ansible_host: 203.0.113.10
          ansible_user: ubuntu

Manual Usage

Configure non-secret defaults in group_vars/all.yml.

Important server variables:

zs_system_user: zabuser
zs_php_version: "8.3"
zs_node_major_version: 22
zs_nvm_version: v0.40.5

Optional secrets can be stored in group_vars/vault.yml:

cp group_vars/vault.example.yml group_vars/vault.yml
ansible-vault encrypt group_vars/vault.yml

For manual/local development, missing vault_zs_system_user_password, vault_mysql_root_password, or app_database_password can be generated under .generated-secrets/<inventory-host>/ on the controller when zs_generate_local_secrets=true.

Provision the server:

ansible-playbook playbooks/site.yml --ask-vault-pass

Create one app/site:

ansible-playbook playbooks/app.yml -e @group_vars/app.example.yml --ask-vault-pass

Deploy one app repository:

ansible-playbook playbooks/deploy.yml -e @group_vars/app.example.yml

Issue TLS for one app/site after DNS points to the server:

ansible-playbook playbooks/certificate.yml -e @group_vars/app.example.yml

Add or update one app queue worker:

ansible-playbook playbooks/worker.yml -e @group_vars/app.example.yml

Add or update one app Laravel scheduler:

ansible-playbook playbooks/scheduler.yml -e @group_vars/app.example.yml

Disable or delete one app/site:

ansible-playbook playbooks/app_delete.yml -e @group_vars/app.example.yml

Discover configured log paths:

ansible-playbook playbooks/logs.yml -e @group_vars/app.example.yml

Run a service action:

ansible-playbook playbooks/tasks.yml -e task_service=nginx -e task_action=status

If zs_php_version is empty during a manual run, the playbook prompts for it.

SaaS Usage

For SaaS execution, run Ansible non-interactively and pass all required variables explicitly. Do not rely on prompts.

SaaS jobs should set zs_load_vault_file=false. With that setting, zs_generate_local_secrets also defaults to false, so missing secrets fail validation instead of silently creating controller-local files. To use the local fallback anyway, pass -e zs_generate_local_secrets=true.

Server example:

ansible-playbook playbooks/server.yml \
  -i inventory/production/hosts.yml \
  -e zs_load_vault_file=false \
  -e zs_generate_local_secrets=false \
  -e zs_system_user=zabuser \
  -e zs_php_version=8.3 \
  -e zs_node_major_version=22 \
  -e vault_zs_system_user_password="$SYSTEM_USER_PASSWORD" \
  -e vault_mysql_root_password="$MYSQL_ROOT_PASSWORD"

App example:

ansible-playbook playbooks/app.yml \
  -i inventory/production/hosts.yml \
  -e zs_load_vault_file=false \
  -e zs_generate_local_secrets=false \
  -e app_name=example \
  -e app_domain=example.com \
  -e app_runtime=laravel \
  -e app_process_manager=php_fpm \
  -e app_php_version=8.3 \
  -e app_user=zabuser \
  -e app_branch=main \
  -e app_create_database=true \
  -e app_database=example \
  -e app_database_user=example \
  -e app_database_password="$APP_DATABASE_PASSWORD" \
  -e vault_mysql_root_password="$MYSQL_ROOT_PASSWORD"

Static app example:

ansible-playbook playbooks/app.yml \
  -i inventory/production/hosts.yml \
  -e zs_load_vault_file=false \
  -e app_name=docs \
  -e app_domain=docs.example.com \
  -e app_runtime=static \
  -e app_process_manager=none \
  -e app_user=zabuser \
  -e app_create_database=false \
  -e app_static_spa=true

Node app example:

ansible-playbook playbooks/app.yml \
  -i inventory/production/hosts.yml \
  -e zs_load_vault_file=false \
  -e app_name=api \
  -e app_domain=api.example.com \
  -e app_runtime=node \
  -e app_user=zabuser \
  -e app_node_version=22 \
  -e app_service_port=3000 \
  -e app_systemd_environment='{"NODE_ENV":"production"}'

Go app example:

ansible-playbook playbooks/app.yml \
  -i inventory/production/hosts.yml \
  -e zs_load_vault_file=false \
  -e app_name=api \
  -e app_domain=api.example.com \
  -e app_runtime=go \
  -e app_user=zabuser \
  -e app_service_port=8080

Service action examples:

ansible-playbook playbooks/tasks.yml \
  -i inventory/production/hosts.yml \
  -e task_service=nginx \
  -e task_action=reload

ansible-playbook playbooks/tasks.yml \
  -i inventory/production/hosts.yml \
  -e task_service=supervisor \
  -e task_action=restart \
  -e task_supervisor_program=example-queue

ansible-playbook playbooks/tasks.yml \
  -i inventory/production/hosts.yml \
  -e task_service=systemd \
  -e task_action=restart \
  -e task_systemd_service=api

Supported task_service values are nginx, php_fpm, supervisor, systemd, redis, and mysql. Supported task_action values are status, restart, and reload; reload is limited to nginx, php_fpm, and systemd.

Deploy example:

ansible-playbook playbooks/deploy.yml \
  -i inventory/production/hosts.yml \
  -e app_name=example \
  -e app_domain=example.com \
  -e app_runtime=laravel \
  -e app_process_manager=php_fpm \
  -e app_php_version=8.3 \
  -e app_user=zabuser \
  -e app_branch=main \
  -e app_repository=git@github.com:example/example.git \
  -e app_zero_downtime=true \
  -e app_use_nvm=true \
  -e app_node_version=22 \
  -e deploy_node_install=true \
  -e deploy_node_build_commands='["npm run build"]' \
  -e app_health_check_enabled=true \
  -e app_health_check_path=/up \
  -e deploy_commands='["php artisan migrate --force", "php artisan optimize"]'

Certificate example:

ansible-playbook playbooks/certificate.yml \
  -i inventory/production/hosts.yml \
  -e app_name=example \
  -e app_domain=example.com \
  -e app_user=zabuser \
  -e app_tls_email=admin@example.com \
  -e app_tls_redirect=true

Worker example:

ansible-playbook playbooks/worker.yml \
  -i inventory/production/hosts.yml \
  -e app_name=example \
  -e app_domain=example.com \
  -e app_runtime=laravel \
  -e app_php_version=8.3 \
  -e app_user=zabuser \
  -e app_zero_downtime=true \
  -e worker_name=example-queue \
  -e worker_process_count=2 \
  -e worker_command='php artisan queue:work redis --sleep=3 --tries=3 --timeout=90' \
  -e worker_environment='{"APP_ENV":"production"}'

Scheduler example:

ansible-playbook playbooks/scheduler.yml \
  -i inventory/production/hosts.yml \
  -e app_name=example \
  -e app_domain=example.com \
  -e app_runtime=laravel \
  -e app_php_version=8.3 \
  -e app_user=zabuser \
  -e app_zero_downtime=true \
  -e scheduler_name=example-scheduler \
  -e scheduler_environment='{"APP_ENV":"production"}'

Disable app example:

ansible-playbook playbooks/app_delete.yml \
  -i inventory/production/hosts.yml \
  -e app_name=example \
  -e app_domain=example.com \
  -e app_runtime=laravel \
  -e app_user=zabuser \
  -e app_lifecycle_state=disabled

Delete app example:

ansible-playbook playbooks/app_delete.yml \
  -i inventory/production/hosts.yml \
  -e app_name=example \
  -e app_domain=example.com \
  -e app_runtime=laravel \
  -e app_user=zabuser \
  -e app_lifecycle_state=absent \
  -e app_remove_files=true \
  -e app_remove_database=true \
  -e app_remove_database_user=true \
  -e vault_mysql_root_password="$MYSQL_ROOT_PASSWORD"

Recommended SaaS behavior:

  • Store secrets outside this repo.
  • Pass secrets per job through Ansible vars or a secure runtime mechanism.
  • Set zs_load_vault_file=false if the SaaS passes all secrets directly and should ignore local group_vars/vault.yml.
  • Set zs_generate_local_secrets=false for SaaS jobs so missing secrets fail fast instead of using .generated-secrets/.
  • Persist .ansible-results/*.json into the SaaS job record.
  • Treat .generated-secrets/ as a local development fallback only.
  • Always pass app_php_version or zs_php_version for laravel and php apps to avoid interactive prompts.

Expected secret inputs:

Job Required secrets
Server provisioning vault_zs_system_user_password, vault_mysql_root_password
App with app_create_database=true vault_mysql_root_password, app_database_password
App without database None, unless custom app variables contain secrets
Deploy None by default; treat deploy_env_content, repository credentials, and custom deploy vars as secrets when provided
App delete with database removal vault_mysql_root_password

Result Files

Playbooks write JSON job results on the controller:

.ansible-results/server-<inventory-host>.json
.ansible-results/app-<inventory-host>-<app-name>.json
.ansible-results/deploy-<inventory-host>-<app-name>.json
.ansible-results/certificate-<inventory-host>-<app-name>.json
.ansible-results/worker-<inventory-host>-<app-name>-<worker-name>.json
.ansible-results/scheduler-<inventory-host>-<app-name>-<scheduler-name>.json
.ansible-results/app-lifecycle-<inventory-host>-<app-name>.json
.ansible-results/logs-<inventory-host>-<app-name>.json
.ansible-results/task-<inventory-host>-<service>-<action>[-<app-name>].json

These files include users, roots, versions, database names, service paths, deploy variables, log paths, and timestamps. They do not include raw passwords.

Server Layout

The server playbook creates a domainless default site:

/home/<zs_system_user>/default/public

Domain apps are created only by playbooks/app.yml.

App Layout

Supported app runtimes:

laravel
php
static
node
go

laravel is the default runtime and uses php_fpm as its default process manager. static is a first-class runtime for Nginx-served files and defaults to no PHP-FPM, no database, no app .env, and no Laravel storage links. node and go are first-class systemd-managed runtimes behind an Nginx reverse proxy, with deploy-time build/install metadata and per-site NVM support for Node apps.

For laravel and php apps, app_php_version can override the server default zs_php_version. The target PHP-FPM version must already be installed on the server.

Static apps serve app_public_root directly with these defaults:

app_process_manager: none
app_create_database: false
app_static_spa: false
app_index_files:
  - index.html
  - index.htm

When app_static_spa: true, Nginx falls back to /index.html for routes that do not match a file. Override app_static_try_files for custom static routing.

Node apps default to a systemd service, per-site NVM, and Nginx proxy:

app_process_manager: systemd
app_use_nvm: true
app_node_version: "22"
app_service_host: 127.0.0.1
app_service_port: 3000
app_proxy_url: http://127.0.0.1:3000
app_systemd_command: npm start
app_node_start_on_provision: false

During app provisioning, Node systemd units are rendered but not started by default because dependencies and build output usually exist only after deployment. Set app_node_start_on_provision: true only when the app is already deployable. playbooks/deploy.yml restarts the service after dependency/build commands finish.

Go apps default to a systemd service and Nginx proxy:

app_process_manager: systemd
app_service_host: 127.0.0.1
app_service_port: 8080
app_proxy_url: http://127.0.0.1:8080
app_go_binary_name: <app_name>
app_go_binary_path: /home/<app_user>/<app_domain>/<app_name>
app_systemd_command: /home/<app_user>/<app_domain>/<app_name>
app_go_start_on_provision: false

During app provisioning, Go systemd units are rendered but not started by default because the binary usually exists only after deployment. Set app_go_start_on_provision: true only when the binary already exists. playbooks/deploy.yml restarts the service after a successful Go build.

Default app layout:

ZS_SITE_PATH=/home/<app_user>/<app_domain>
ZS_SITE_ROOT=/home/<app_user>/<app_domain>/public

The app playbook writes deploy variables to:

/home/<app_user>/<app_domain>/.zs-deploy-env

Available deploy variables:

ZS_APP_RUNTIME
ZS_SITE_ROOT
ZS_SITE_PATH
ZS_SITE_USER
ZS_SITE_BRANCH
ZS_PHP
ZS_PHP_FPM
ZS_COMPOSER
ZS_NVM_ENABLED
ZS_NVM_DIR
ZS_NVM_VERSION
ZS_NODE_VERSION
ZS_NODE
ZS_NPM
ZS_NPX
ZS_GO_BINARY
ZS_GO_BUILD_PACKAGE
ZS_APP_SERVICE_HOST
ZS_APP_SERVICE_PORT
ZS_APP_PROXY_URL

Enable zero-downtime layout with:

app_zero_downtime: true

That creates:

/home/<app_user>/<app_domain>/releases
/home/<app_user>/<app_domain>/current -> releases/initial
/home/<app_user>/<app_domain>/shared

With zero-downtime enabled:

ZS_SITE_PATH=/home/<app_user>/<app_domain>
ZS_SITE_ROOT=/home/<app_user>/<app_domain>/current/public

Laravel zero-downtime apps also create shared/.env and shared/storage. Static apps skip those by default; set app_manage_env_file: true or pass deploy_env_content if a static deployment needs an .env file.

Deployment

playbooks/deploy.yml requires app_repository and uses app_branch as the checkout version.

Default layout:

/home/<app_user>/<app_domain>

Zero-downtime layout:

/home/<app_user>/<app_domain>/releases/<deploy_release_id>
/home/<app_user>/<app_domain>/current -> releases/<deploy_release_id>
/home/<app_user>/<app_domain>/shared/.env
/home/<app_user>/<app_domain>/shared/storage

Useful deploy variables:

app_repository: git@github.com:example/example.git
app_branch: main
app_zero_downtime: true
deploy_php_composer_install: true
deploy_composer_options: "--no-dev --no-interaction --prefer-dist --optimize-autoloader"
deploy_commands:
  - php artisan migrate --force
  - php artisan optimize

For static deployments, deploy_php_composer_install defaults to false and deploy_manage_env_file defaults to false. If deploy_env_content is provided, the playbook writes it to the app .env file. Treat that value as a secret in SaaS orchestration.

For Node deployments, deploy_node_install defaults to true, deploy_node_install_command defaults to npm ci, and deploy_node_build_commands can run one or more build steps through the per-site NVM runtime:

app_runtime: node
app_process_manager: systemd
app_use_nvm: true
app_node_version: "22"
app_service_port: 3000
deploy_node_install: true
deploy_node_install_command: npm ci
deploy_node_build_commands:
  - npm run build

Set deploy_node_install: false for prebuilt artifacts or repos without package installs.

For Go deployments, deploy_go_build defaults to true and builds from the repository root:

app_runtime: go
app_process_manager: systemd
app_service_port: 8080
app_go_binary_name: api
app_go_build_package: "."
deploy_go_build: true

The default build command is equivalent to:

go build -o /home/<app_user>/<app_domain>/<app_go_binary_name> <app_go_build_package>

With zero-downtime enabled, the binary is built inside the new release and the systemd command points at /home/<app_user>/<app_domain>/current/<app_go_binary_name>.

The target server must already have the Go toolchain available on PATH for the default build command. If builds happen in CI, set deploy_go_build: false and deploy a prebuilt binary, or pass deploy_go_build_command for a custom artifact step.

Per-Site Node/NVM

The server still installs system-wide Node.js from zs_node_major_version. Per-site NVM is separate, isolated per app, and enabled by default for app_runtime: node:

app_use_nvm: true
app_node_version: "22"
app_nvm_version: v0.40.5

By default, per-site NVM installs under:

/home/<app_user>/.zs-nvm/<app_domain>

The app playbook writes NVM deploy metadata to:

/home/<app_user>/<app_domain>/.zs-nvm-env

Useful deploy variables:

deploy_node_install: true
deploy_node_install_command: npm ci
deploy_node_build_commands:
  - npm run build

Node deploy commands run after checkout and Composer install, with NVM_DIR set to the per-site NVM directory and nvm use <app_node_version> applied. This keeps custom app Node.js versions isolated from the server default.

App Systemd Services

Set app_process_manager: systemd to manage a long-running app command with systemd. This is intended for Node/Go services and other app runtimes that should run as a persistent process.

Useful systemd variables:

app_process_manager: systemd
app_systemd_service_name: api
app_systemd_command: npm start
app_systemd_environment:
  NODE_ENV: production
app_systemd_restart: always
app_systemd_restart_sec: 5
app_systemd_state: started
app_systemd_enable: true

The default working directory is /home/<app_user>/<app_domain> or current when zero-downtime is enabled. When app_use_nvm is true, the unit runs through the per-site NVM runtime before executing app_systemd_command.

playbooks/deploy.yml reapplies the unit after deployment when app_process_manager=systemd; its default service state is restarted so the process picks up the new release.

Health Checks

playbooks/app.yml and playbooks/deploy.yml can run an HTTP health check when app_health_check_enabled is true. By default the check runs from the server against http://127.0.0.1/ with Host: <app_domain>, which avoids depending on public DNS propagation.

Useful health variables:

app_health_check_enabled: true
app_health_check_scheme: http
app_health_check_host: 127.0.0.1
app_health_check_path: /up
app_health_check_expected_status:
  - 200
app_health_check_timeout: 10

If app_health_check_url is set, it overrides the scheme, host, and path composition. Health results are written into the app/deploy JSON result before the play fails on an unhealthy response.

TLS Certificates

playbooks/certificate.yml issues or renews an app certificate with Certbot's Nginx integration. Run playbooks/app.yml first so the Nginx site exists, then point DNS for app_domain and any app_tls_extra_domains to the server before requesting a real certificate.

Useful certificate variables:

app_tls_email: admin@example.com
app_tls_extra_domains: []
app_tls_staging: false
app_tls_redirect: true

Use app_tls_staging: true while testing automation to avoid Let's Encrypt rate limits. If no email is available, the playbook requires explicit app_tls_register_unsafely_without_email: true.

Workers

playbooks/worker.yml manages one Supervisor program for an app. It defaults to a Laravel queue worker in the app root, or current when app_zero_downtime is true.

Useful worker variables:

worker_name: example-queue
worker_process_count: 2
worker_user: zabuser
worker_directory: /home/zabuser/example.com/current
worker_command: php artisan queue:work redis --sleep=3 --tries=3 --timeout=90
worker_environment:
  APP_ENV: production

The playbook writes /etc/supervisor/conf.d/<worker_name>.conf, runs supervisorctl reread and supervisorctl update when the config changes, starts stopped processes, and writes worker logs under /home/<app_user>/<app_domain>/worker-logs by default.

Scheduler

playbooks/scheduler.yml manages one cron entry for Laravel schedule:run. It defaults to every minute in the app root, or current when app_zero_downtime is true.

Useful scheduler variables:

scheduler_name: example-scheduler
scheduler_user: zabuser
scheduler_directory: /home/zabuser/example.com/current
scheduler_command: php artisan schedule:run
scheduler_minute: "*"
scheduler_hour: "*"
scheduler_day: "*"
scheduler_month: "*"
scheduler_weekday: "*"
scheduler_disabled: false
scheduler_state: present
scheduler_environment:
  APP_ENV: production

Set scheduler_disabled: true to keep the cron entry but comment it out. Set scheduler_state: absent to remove it.

Service Actions

playbooks/tasks.yml runs focused service operations for SaaS buttons and audit records. It does not change service configuration; it only runs the requested action and captures final status.

Useful task variables:

task_service: nginx
task_action: status
task_php_fpm_service: php8.3-fpm
task_supervisor_program: example-queue
task_systemd_service: api
task_redis_service: redis-server
task_mysql_service: mysql

For task_service=php_fpm, the playbook derives task_php_fpm_service from app_php_version or zs_php_version when possible. For task_service=systemd, it derives task_systemd_service from app_systemd_service_name or app_name when possible.

App Lifecycle

playbooks/app_delete.yml disables or deletes one app/site. By default it is non-destructive:

app_lifecycle_state: disabled

That removes only /etc/nginx/sites-enabled/<app_domain>.conf and reloads Nginx. To delete the Nginx site config too:

app_lifecycle_state: absent

Files and database cleanup are separate opt-in flags:

app_remove_files: true
app_remove_database: true
app_remove_database_user: true

Database and database-user removal require vault_mysql_root_password, either from group_vars/vault.yml or explicit SaaS vars.

Log Paths

Playbook result files include a log_paths object so a SaaS can display log links without rediscovering server conventions.

playbooks/logs.yml returns those conventions plus path metadata from the target server. It does not modify services or log files.

Default server log paths:

nginx_default_access: /var/log/nginx/default.access.log
nginx_default_error: /var/log/nginx/default.error.log
php_fpm: /var/log/php<version>-fpm.log
supervisor: /var/log/supervisor/supervisord.log
mysql_error: /var/log/mysql/error.log
redis: /var/log/redis/redis-server.log

Default app log paths:

app: /home/<app_user>/<app_domain>/storage/logs/laravel.log
app_directory: /home/<app_user>/<app_domain>/storage/logs
nginx_access: /var/log/nginx/<app_domain>.access.log
nginx_error: /var/log/nginx/<app_domain>.error.log
php_fpm: /var/log/php<app_php_version>-fpm.log
supervisor: /var/log/supervisor/supervisord.log
mysql_error: /var/log/mysql/error.log
redis: /var/log/redis/redis-server.log

When app_zero_downtime is true, app log paths point at current/storage/logs. Worker result files also include worker_stdout, worker_stderr, and worker_directory. Certificate result files include certbot.

Log discovery includes:

log_paths:
  app: /home/<app_user>/<app_domain>/storage/logs/laravel.log
  app_directory: /home/<app_user>/<app_domain>/storage/logs
  nginx_access: /var/log/nginx/<app_domain>.access.log
  nginx_error: /var/log/nginx/<app_domain>.error.log
  nginx_default_access: /var/log/nginx/default.access.log
  nginx_default_error: /var/log/nginx/default.error.log
  php_fpm: /var/log/php<app_php_version>-fpm.log
  supervisor: /var/log/supervisor/supervisord.log
  worker_stdout: /home/<app_user>/<app_domain>/worker-logs/<worker_name>.out.log
  worker_stderr: /home/<app_user>/<app_domain>/worker-logs/<worker_name>.err.log
  worker_directory: /home/<app_user>/<app_domain>/worker-logs
  scheduler_directory: /home/<app_user>/<app_domain>
  systemd_unit: /etc/systemd/system/<app_systemd_service_name>.service
  mysql_error: /var/log/mysql/error.log
  redis: /var/log/redis/redis-server.log
  certbot: /var/log/letsencrypt/letsencrypt.log

Each discovered.paths[] entry includes key, category, path, kind, exists, is_file, is_directory, readable, size, mtime, mode, uid, and gid. Set log_discovery_include_globs: false to skip app/worker directory file listing.

Useful log path overrides:

app_nginx_access_log: /var/log/nginx/example.com.access.log
app_nginx_error_log: /var/log/nginx/example.com.error.log
app_log_dir: /home/zabuser/example.com/storage/logs
app_primary_log: /home/zabuser/example.com/storage/logs/laravel.log

Tags

Common tags:

preflight
apt_lock
common
users
security
swap
mysql
php
composer
redis
supervisor
nodejs
nvm
systemd
nginx
sudoers
app
database
deploy
result
git
health
certificate
tls
certbot
worker
scheduler
cron
lifecycle
files
logs
tasks
status

List tags:

ansible-playbook playbooks/server.yml --list-tags
ansible-playbook playbooks/app.yml -e @group_vars/app.example.yml --list-tags

Verification

Run syntax checks before executing against real servers:

ansible-playbook playbooks/site.yml --syntax-check
ansible-playbook playbooks/server.yml --syntax-check
ansible-playbook playbooks/app.yml --syntax-check -e @group_vars/app.example.yml
ansible-playbook playbooks/app.yml --syntax-check -e @group_vars/app.example.yml -e app_zero_downtime=true
ansible-playbook playbooks/deploy.yml --syntax-check -e @group_vars/app.example.yml
ansible-playbook playbooks/certificate.yml --syntax-check -e @group_vars/app.example.yml
ansible-playbook playbooks/worker.yml --syntax-check -e @group_vars/app.example.yml
ansible-playbook playbooks/scheduler.yml --syntax-check -e @group_vars/app.example.yml
ansible-playbook playbooks/logs.yml --syntax-check -e @group_vars/app.example.yml
ansible-playbook playbooks/tasks.yml --syntax-check -e @group_vars/app.example.yml
ansible-playbook playbooks/app_delete.yml --syntax-check -e @group_vars/app.example.yml

For full VM validation with snapshots, follow TEST_PLAN.md. For a step-by-step testing workflow with prompts to use with Codex, follow TEST_RUNBOOK.md.

Current Scope

Included now:

  • Server provisioning
  • App/site creation
  • Optional database creation
  • Deploy vars
  • Optional zero-downtime directories
  • Repository deployment
  • Optional health checks
  • TLS certificates
  • Queue workers
  • Laravel scheduler
  • App disable/delete lifecycle
  • Log path conventions
  • Service actions
  • Per-site Node/NVM
  • App systemd services
  • JSON result output