- Jinja 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| group_vars | ||
| host_vars | ||
| inventory | ||
| playbooks | ||
| roles | ||
| .gitignore | ||
| ansible.cfg | ||
| CLAUDE.md | ||
| README.md | ||
| requirements.yml | ||
| TEST_PLAN.md | ||
| TEST_RUNBOOK.md | ||
| TODO.md | ||
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
.envandstoragelinks - 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_pathsin 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
systemdprocess 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=falseif the SaaS passes all secrets directly and should ignore localgroup_vars/vault.yml. - Set
zs_generate_local_secrets=falsefor SaaS jobs so missing secrets fail fast instead of using.generated-secrets/. - Persist
.ansible-results/*.jsoninto the SaaS job record. - Treat
.generated-secrets/as a local development fallback only. - Always pass
app_php_versionorzs_php_versionforlaravelandphpapps 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