Use when bootstrapping a Rails project that needs Ubuntu VPS provisioning (Ansible) and Kamal 2 deploy (kamal-proxy on 80/443) — scaffolds infra/ansible, infra/kamal, config/deploy*.yml, .kamal/secrets* templates from a hardened reference layout, asking IP, DNS, and whether the host runs both staging+production or only a single domain.
How this skill is triggered — by the user, by Claude, or both
Slash command
/mauriciovieira-skills:ansible-kamalThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Scaffolds a full **Ansible + Kamal 2** infrastructure tree for a Rails app on a single Ubuntu VPS. Generates:
scripts/render.shtemplates/Makefiletemplates/config/deploy.staging.ymltemplates/config/deploy.ymltemplates/infra/README.mdtemplates/infra/ansible/Makefiletemplates/infra/ansible/README.mdtemplates/infra/ansible/ansible.cfgtemplates/infra/ansible/inventory/group_vars/all.ymltemplates/infra/ansible/inventory/production.ymltemplates/infra/ansible/playbooks/bootstrap.ymltemplates/infra/ansible/playbooks/dump_db.ymltemplates/infra/ansible/playbooks/restore_db.ymltemplates/infra/ansible/playbooks/site.ymltemplates/infra/ansible/requirements.txttemplates/infra/ansible/requirements.ymltemplates/infra/ansible/roles/common/tasks/main.ymltemplates/infra/ansible/roles/docker/handlers/main.ymltemplates/infra/ansible/roles/docker/tasks/main.ymltemplates/infra/ansible/roles/docker/templates/daemon.json.j2Scaffolds a full Ansible + Kamal 2 infrastructure tree for a Rails app on a single Ubuntu VPS. Generates:
Makefile — make deploy ENV=…, make db-restore-from-vps ENV=…, make build, make run-local, make mailhog.scripts/ — kamal-deploy-from-pass.sh, db-restore-from-vps.sh (provider-agnostic — works with Hetzner, DigitalOcean, OVH, Hostinger, AWS Lightsail, etc., as long as the Ansible inventory points at the box).infra/ansible/ — playbooks (bootstrap, site, dump_db, restore_db), roles (common, users, ssh_bootstrap, firewall, fail2ban, docker, postgresql, traefik, opt), inventory, Makefile (setup, test, bootstrap, ansible), README.infra/kamal/README.md — secrets / pass / GitHub Actions guide.config/deploy.yml (+ config/deploy.staging.yml if staging is enabled)..kamal/secrets, .kamal/secrets-common (+ .kamal/secrets.staging if enabled).infra/ansible/ where you want the same hardened baseline (UFW, fail2ban, sshd drop-in, Docker, PG on host).Do NOT use when the project already has infra/ansible/ — diff manually instead, this skill overwrites.
myapp, petshop. Used for DB names (<slug>_production, <slug>_staging), DB users (<slug>_prod_user, <slug>_staging_user), and env var prefix (POSTGRES_<SLUG_UPPER>_PROD_PASSWORD).service: and image name. Default = slug with _→- (e.g. my-app).myorg/<service> for GHCR.inventory/production.yml and config/deploy.yml servers.web.hosts.myapp).staging+production — two Kamal destinations, two DBs, two domains (recommended for production apps).single — only one domain / DB / Kamal destination, no staging plumbing emitted.example.com.staging+production. Default = staging.<production_domain>.site.yml) + Kamal SSH as. Default = <slug>_deploy. Must NOT be root.custom (uses a custom PASSWORD_STORE_DIR, e.g. ~/.password-store-custom) or pass (default ~/.password-store).infra/myapp. Default = infra/<inventory_group>.After collecting inputs, derive:
| Placeholder | Value |
|---|---|
__APP_SLUG__ | snake_case slug |
__APP_SERVICE__ | kebab-case service |
__APP_SLUG_UPPER__ | uppercase snake_case (env var prefix) |
__INVENTORY_GROUP__ | inventory group |
__VPS_IP__ | IPv4 |
__DOMAIN_PROD__ | production FQDN |
__DOMAIN_STAGING__ | staging FQDN (or empty in single mode) |
__LETSENCRYPT_EMAIL__ | ACME email |
__DEPLOY_USER__ | deploy linux user |
__IMAGE_REPO__ | container image (no tag) |
__PASS_BACKEND__ | custom or pass |
__PASS_STORE_DIR__ | $(HOME)/.password-store-custom or empty for default |
__PASS_NAMESPACE__ | e.g. infra/myapp |
__ENV_MODE__ | staging+production or single |
Run the render script from the target project root (the repo to scaffold INTO):
APP_SLUG=myapp \
APP_SERVICE=my-app \
APP_SLUG_UPPER=MYAPP \
INVENTORY_GROUP=myapp \
VPS_IP=203.0.113.10 \
DOMAIN_PROD=example.com \
DOMAIN_STAGING=staging.example.com \
[email protected] \
DEPLOY_USER=myapp_deploy \
IMAGE_REPO=myorg/my-app \
PASS_BACKEND=custom \
PASS_STORE_DIR='$(HOME)/.password-store-custom' \
PASS_NAMESPACE=infra/myapp \
ENV_MODE=staging+production \
TARGET_DIR=. \
~/.claude/skills/ansible-kamal/scripts/render.sh
For ENV_MODE=single leave DOMAIN_STAGING empty (or unset). PASS_STORE_DIR may be empty when PASS_BACKEND=pass.
The script:
templates/, applies placeholder substitution.# >>> staging-only / # <<< staging-only when ENV_MODE=single.*.staging.* and secrets.staging when ENV_MODE=single.*.tmpl → no suffix.infra/ansible/ unless FORCE=1.Tell the user, in order:
cd infra/ansible && make setup && make test — bootstraps venv + galaxy collections + lints.pass insert -m __PASS_NAMESPACE__/deploy_ssh_{private,public}_key.openssl rand -base64 32, store at __PASS_NAMESPACE__/postgres___APP_SLUG___prod_password (and _staging_ if applicable).A records: __DOMAIN_PROD__ (+ __DOMAIN_STAGING__) → __VPS_IP__.ufw): allow 22/80/443 from anywhere (or your IP for 22).DEPLOY_SSH_KEY="$(pass show __PASS_NAMESPACE__/deploy_ssh_public_key)" make bootstrap (root reachable).make ansible (loads PG passwords from pass and provisions site.yml as __DEPLOY_USER__).infra/kamal/README.md.bundle exec kamal setup (production) and bundle exec kamal setup -d staging (if staging).host.docker.internal (add-host: host.docker.internal:host-gateway in deploy.yml).bootstrap.yml only, with MANAGE_BUILTINS=yes and DEFAULT_FORWARD_POLICY=ACCEPT so SSH survives Docker's iptables hooks. Includes nft INPUT rehook task to recover from Docker orphaning UFW jumps.05-ansible-sshd-hardening.conf in bootstrap; cloud-init 50- and 60- removed.systemctl, apt, apt-get. No /etc/ssh editing.Makefile # root: deploy, db-restore-from-vps, build, run-local, mailhog
scripts/kamal-deploy-from-pass.sh
scripts/db-restore-from-vps.sh
infra/README.md
infra/ansible/ansible.cfg
infra/ansible/Makefile # ansible: setup, test, bootstrap, ansible
infra/ansible/README.md
infra/ansible/requirements.txt
infra/ansible/requirements.yml
infra/ansible/playbooks/{bootstrap,site,dump_db,restore_db}.yml
infra/ansible/inventory/production.yml
infra/ansible/inventory/group_vars/all.yml
infra/ansible/roles/common/tasks/main.yml
infra/ansible/roles/users/tasks/main.yml
infra/ansible/roles/users/library/managed_authorized_key.py # idempotent single-key authorized_keys module
infra/ansible/roles/ssh_bootstrap/{tasks/main.yml,templates/sshd_ansible_hardening.conf.j2,handlers/main.yml}
infra/ansible/roles/firewall/tasks/{main,nft_input_rehook}.yml
infra/ansible/roles/fail2ban/{tasks/main.yml,handlers/main.yml}
infra/ansible/roles/docker/{tasks/main.yml,templates/daemon.json.j2,handlers/main.yml}
infra/ansible/roles/postgresql/{tasks/{main,discover,ufw}.yml,templates/{postgresql.conf,pg_hba.conf,create_databases.sql}.j2,handlers/main.yml}
infra/ansible/roles/opt/tasks/main.yml
infra/ansible/roles/traefik/{tasks/main.yml,templates/{traefik.yml,docker-compose.yml}.j2}
infra/kamal/README.md
config/deploy.yml
config/deploy.staging.yml # only if ENV_MODE=staging+production
.kamal/secrets
.kamal/secrets-common
.kamal/secrets.staging # only if ENV_MODE=staging+production
| Mistake | Fix |
|---|---|
Forgot __VPS_IP__ substitution → bootstrap connects to placeholder | Re-run render.sh; verify inventory/production.yml and config/deploy.yml have the IP. |
Used __APP_SLUG__ with hyphens | Slug must be snake_case; service name is the kebab-case form. Postgres identifiers reject hyphens. |
single mode but staging files present | Set ENV_MODE=single, leave DOMAIN_STAGING empty, re-render. |
Deploy user = root | Disallowed; users role creates it as a non-sudo account with restricted sudoers. Pick <slug>_deploy. |
| Pass paths don't match Makefile | __PASS_NAMESPACE__ flows into Makefile make ansible target. Match what's in pass. |
npx claudepluginhub mauriciovieira/skillsCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.