Ansible kamal
Skill mauriciovieira/skills/skills/engineering/ansible-kamal
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.From its SKILL.md
npx -y skills add mauriciovieira/skills --skill ansible-kamalAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
- runs commandsInstructs the agent to run 3 commands, including `~/.claude/skills/ansible-kamal/scripts/render.sh` and 2 more.
SKILL.md
8.8 KB, ~2.4k tokens by cl100k_base, as published. Nobody here has run it
ansible-kamal
Scaffolds a full Ansible + Kamal 2 infrastructure tree for a Rails app on a single Ubuntu VPS. Generates:
- Root
Makefile—make deploy ENV=…,make db-restore-from-vps ENV=…,make build,make run-local,make mailhog. - Root
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.ymlif staging is enabled)..kamal/secrets,.kamal/secrets-common(+.kamal/secrets.stagingif enabled).
When to Use
- A new Rails project that needs a single Ubuntu VPS with Kamal 2 + host PostgreSQL 16.
- An existing project missing
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.
Inputs to ask the user (in this order)
- App slug (snake_case) — e.g.
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 name (kebab-case) — Kamal
service:and image name. Default = slug with_→-(e.g.my-app). - Image repo — e.g.
myorg/<service>for GHCR. - VPS IP (IPv4) — host key in
inventory/production.ymlandconfig/deploy.ymlservers.web.hosts. - Inventory group name — Ansible group; default = slug (e.g.
myapp). - Environment mode — ONE of:
staging+production— two Kamal destinations, two DBs, two domains (recommended for production apps).single— only one domain / DB / Kamal destination, no staging plumbing emitted.
- Production domain — e.g.
example.com. - Staging domain — only if
staging+production. Default =staging.<production_domain>. - Let's Encrypt email — for kamal-proxy ACME (and Traefik if ever enabled).
- Deploy user — Linux user Ansible (
site.yml) + Kamal SSH as. Default =<slug>_deploy. Must NOT beroot. - Pass backend —
custom(uses a customPASSWORD_STORE_DIR, e.g.~/.password-store-custom) orpass(default~/.password-store). - Pass namespace — e.g.
infra/myapp. Default =infra/<inventory_group>.
Variable map
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 |
How to scaffold
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:
- Walks
templates/, applies placeholder substitution. - Strips blocks fenced by
# >>> staging-only/# <<< staging-onlywhenENV_MODE=single. - Skips files matching
*.staging.*andsecrets.stagingwhenENV_MODE=single. - Renames
*.tmpl→ no suffix. - Refuses to overwrite existing
infra/ansible/unlessFORCE=1.
After scaffold
Tell the user, in order:
cd infra/ansible && make setup && make test— bootstraps venv + galaxy collections + lints.- Generate deploy SSH key pair,
pass insert -m __PASS_NAMESPACE__/deploy_ssh_{private,public}_key. - Generate DB passwords:
openssl rand -base64 32, store at__PASS_NAMESPACE__/postgres___APP_SLUG___prod_password(and_staging_if applicable). - DNS
Arecords:__DOMAIN_PROD__(+__DOMAIN_STAGING__) →__VPS_IP__. - Open the VPS provider's firewall (or
ufw): allow 22/80/443 from anywhere (or your IP for 22). - Run
DEPLOY_SSH_KEY="$(pass show __PASS_NAMESPACE__/deploy_ssh_public_key)" make bootstrap(root reachable). - Run
make ansible(loads PG passwords from pass and provisionssite.ymlas__DEPLOY_USER__). - Wire GitHub Environments + secrets per
infra/kamal/README.md. bundle exec kamal setup(production) andbundle exec kamal setup -d staging(if staging).
Known constraints baked in
- PostgreSQL on host, not Docker accessory. Containers reach it via
host.docker.internal(add-host: host.docker.internal:host-gatewayin deploy.yml). - kamal-proxy owns 80/443 (Traefik role disabled by default; kept in tree).
- UFW runs in
bootstrap.ymlonly, withMANAGE_BUILTINS=yesandDEFAULT_FORWARD_POLICY=ACCEPTso SSH survives Docker's iptables hooks. Includes nft INPUT rehook task to recover from Docker orphaning UFW jumps. - sshd drop-in
05-ansible-sshd-hardening.confin bootstrap; cloud-init50-and60-removed. - Deploy user sudo restricted to
systemctl,apt,apt-get. No/etc/sshediting. - Docker daemon MTU 1450 for Let's Encrypt egress on cloud bridges.
Files emitted (full list)
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
Common mistakes
| 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. |
What ships with it: 46 files
79.9 KB alongside SKILL.md, 4 of them executable
scripts/
- render.shruns6.2 KB
templates/
- config/deploy.staging.yml536 B
- config/deploy.yml1.6 KB
- infra/ansible/ansible.cfg530 B
- infra/ansible/inventory/group_vars/all.yml4.1 KB
- infra/ansible/inventory/production.yml552 B
- infra/ansible/Makefile3.2 KB
- infra/ansible/playbooks/bootstrap.yml1.5 KB
- infra/ansible/playbooks/dump_db.yml2.5 KB
- infra/ansible/playbooks/restore_db.yml4.5 KB
- infra/ansible/playbooks/site.yml798 B
- infra/ansible/README.md3.6 KB
- infra/ansible/requirements.txt105 B
- infra/ansible/requirements.yml165 B
- infra/ansible/roles/common/tasks/main.yml992 B
- infra/ansible/roles/docker/handlers/main.yml92 B
- infra/ansible/roles/docker/tasks/main.yml3.2 KB
- infra/ansible/roles/docker/templates/daemon.json.j2253 B
- infra/ansible/roles/fail2ban/handlers/main.yml96 B
- infra/ansible/roles/fail2ban/tasks/main.yml496 B
- infra/ansible/roles/firewall/tasks/main.yml4.3 KB
- infra/ansible/roles/firewall/tasks/nft_input_rehook.yml1.1 KB
- infra/ansible/roles/opt/tasks/main.yml482 B
- infra/ansible/roles/postgresql/handlers/main.yml132 B
- infra/ansible/roles/postgresql/tasks/discover.yml3.2 KB
- infra/ansible/roles/postgresql/tasks/main.yml5.2 KB
- infra/ansible/roles/postgresql/tasks/ufw.yml504 B
- infra/ansible/roles/postgresql/templates/create_databases.sql.j2722 B
- infra/ansible/roles/postgresql/templates/pg_hba.conf.j2911 B
- infra/ansible/roles/postgresql/templates/postgresql.conf.j2283 B
- infra/ansible/roles/ssh_bootstrap/handlers/main.yml86 B
- infra/ansible/roles/ssh_bootstrap/tasks/main.yml897 B
- infra/ansible/roles/ssh_bootstrap/templates/sshd_ansible_hardening.conf.j2323 B
- infra/ansible/roles/traefik/tasks/main.yml1.2 KB
- infra/ansible/roles/traefik/templates/docker-compose.yml.j2949 B
- infra/ansible/roles/traefik/templates/traefik.yml.j2494 B
- infra/ansible/roles/users/library/managed_authorized_key.pyruns3.6 KB
- infra/ansible/roles/users/tasks/main.yml1.7 KB
- infra/kamal/README.md4.7 KB
- infra/README.md1.7 KB
6 more files not listed here. See all 46 in the repository.