From cofounder
This skill should be used when the user asks to "deploy to Locaweb Cloud", "set up GitHub Actions deployment workflows", "create a preview environment", "add a production environment", "tear down an environment", "configure a custom domain", "connect to the database", "check deployment logs", "scale the VM", "recover from snapshots", "disaster recovery", "what is the app URL", "what is the deployed URL", "where is my app", or asks about architecture decisions (monolith vs microservices, vertical vs horizontal scaling), platform constraints (Postgres only, single container, port 80), managing secrets and environment variables, Dockerfile requirements, database migrations, or performing operations and troubleshooting on live infrastructure (SSH access, container logs, health checks).
How this skill is triggered — by the user, by Claude, or both
Slash command
/cofounder:app-deployThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
To deploy web apps, create for each environment:
examples/config/deploy.preview.ymlexamples/config/deploy.production.ymlexamples/config/deploy.ymlreferences/env-vars.mdreferences/kamal.mdreferences/operations.mdreferences/postgres-recipe.mdreferences/recovery.mdreferences/scaling.mdreferences/teardown.mdreferences/workflows.mdscripts/generate_pg_cmd.pyTo deploy web apps, create for each environment:
.github/workflows/deploy-{env}.yml.github/workflows/teardown-{env}.ymlconfig/deploy.{env}.yml.kamal/secrets.{env}And common to all environments:
config/deploy.yml.kamal/secrets-commonDockerfile at repo rootSee references/workflows.md for deploy and references/teardown.md for teardown workflow syntax. See the examples/ folder for .kamal/ (secrets) and config/ (Kamal config) example files. The .kamal/ and config/ folders live at the repo root so the Kamal deploy job can access them.
Caller workflow (.github/workflows/deploy-{env}.yml)
| |
provisioning job deploy job
| |
v v
+----------------------------------+ +----------------------------------+
| External workflow | | Kamal deployment |
|(gmautner/locaweb-cloud-provision)| | (runs in caller's deploy job) |
| | | |
| Provisions infrastructure: | | kamal setup / kamal deploy |
| VMs, networks, disks, IPs, | | Builds and pushes Docker image |
| firewalls, snapshots, DNS | | Deploys to provisioned VMs |
| | | Reboots scaled accessories |
| Outputs: infra_env, IPs, | | |
| infrastructure_changed, | | |
| scaled_accessories | | |
+----------------------------------+ +----------------------------------+
infra_env, infrastructure_changed, scaled_accessories, web_ip, worker_ips, accessory_ipsThese constraints apply to every application deployed to this platform. Communicate these upfront when starting any deployment work:
app_port in the proxy block of /config/deploy.ymlGET /up returning HTTP 200 when healthyforward_headers: false is non-negotiable -- VMs are directly exposed to the internet with no upstream proxy. The agent must never set this to true.web_plan (see references/scaling.md -- VM Plans for available sizes). Prefer runtimes and frameworks that scale well vertically.servers.workers.cmd in deploy.yml).volumes for app roles, directories for accessories -- For main app roles (web, workers), use Kamal's volumes keyword for persistent data mounts. For accessories, use directories (which support mode and owner options). Both are auto-created on the host by Kamal and map directly to host paths — making data visible, portable, and backed up. Never use named Docker volumes (myapp_data:/path). For directories syntax (string and hash formats), mode/owner options, and the distinction between volumes and directories, see references/kamal.md — Accessories./data/{subdir} -- both the web VM and each accessory VM have a persistent disk mounted at /data/. Always mount subdirectories of /data/, never /data/ root directly (see references/env-vars.md -- Disk Storage Path for web usage, references/postgres-recipe.md -- Volume Mount for the database example). Two reasons: (1) /data/ is an attached disk with scheduled snapshot policies that ensure disaster recovery — data outside /data/ is not backed up; (2) the ext4 filesystem creates lost+found at the mount root, which breaks Docker images that expect a clean directory on first boot (PostgreSQL initdb, Redis appendonly.aof, etc.).docker/build-push-action, no docker build, no docker push, no login to ghcr.io). Kamal handles the entire build-push-deploy lifecycle using the Dockerfile at the repo root.proxy.ssl: true in every environment config — both nip.io and custom domains get automatic Let's Encrypt certificates via HTTP-01 challenge. Let's Encrypt has never failed to increase rate limits for nip.io when asked, so nip.io subdomains are safe to use with TLS.supabase/postgres recipe — a Postgres image enriched with several extensions, as recommended by the tech-stack skill. Accessories that serve HTTP/HTTPS traffic through kamal-proxy need ports 80 and 443 opened at the firewall — pass "ports": "80,443" in the accessory's JSON entry (port 22/SSH is always open). See references/kamal.md — Accessories for proxy configuration details.kamal deploy does not update accessories, but the deploy workflow runs kamal accessory reboot all after kamal setup on every deploy. This ensures accessory config changes (image tag, env vars, volumes, ports, cmd) are always applied. Accessories have downtime during reboot (no rolling deploy). See references/kamal.md — Accessories for details.env_name and accessory name values must use only lowercase letters, digits, and underscores ([a-z0-9_]). No hyphens, uppercase, or special characters.If the application's current design conflicts with any of these, resolve the conflict before proceeding with deployment setup.
generate_pg_cmd.pyOutputs the complete PostgreSQL cmd string tuned for a VM plan, if using the supabase/postgres recipe. Use this for the accessories.db.cmd field in config/deploy.<environment>.yml.
mise x -- python3 scripts/generate_pg_cmd.py --plan medium
# Output: postgres -D /etc/postgresql -c shared_buffers=1GB -c effective_cache_size=3GB -c work_mem=10MB -c maintenance_work_mem=256MB -c max_connections=100
Valid plans: micro, small, medium, large, xlarge, 2xlarge, 4xlarge.
Follow these steps in order. Each step is idempotent -- safe to re-run across agent sessions.
/upgit remote -vorigin is already set, skip this stepCheck if an SSH key already exists for this repo:
test -f ~/.ssh/<repo-name> && echo "Key exists" || echo "Key missing"
If the key already exists, reuse it -- do not overwrite.
If the key does not exist, generate a new Ed25519 SSH key:
ssh-keygen -t ed25519 -f ~/.ssh/<repo-name> -N "" -C "<repo-name>-deploy"
chmod 600 ~/.ssh/<repo-name>
This key will be:
SSH_PRIVATE_KEY GitHub secret (the private key)gh secret list
If both CLOUDSTACK_API_KEY and CLOUDSTACK_SECRET_KEY appear → skip to Step 5.
Otherwise:
A. Check for a Locaweb Cloud account
Ask the user: "Você já tem uma conta na Locaweb Cloud?"
B. Find the API keys
Guide the user through these steps:
If the keys do not appear after waiting:
Wait for the user to confirm they have both keys in hand, then continue with C.
C. Create the GitHub secrets
Give them the GitHub secrets URL:
echo "$(gh repo view --json url -q .url)/settings/secrets/actions"
Ask them to open that URL and click "New repository secret" for each:
| Name | What to paste |
|---|---|
CLOUDSTACK_API_KEY | Copiar Chave da API |
CLOUDSTACK_SECRET_KEY | Copiar Chave secreta |
Wait for the user to confirm both secrets are saved, then continue to Step 5.
For the full secrets and environment variables reference, see references/env-vars.md.
Discover all app env vars: Read the project's .env file to get the full list of environment variables the application uses. Cross-reference with the application's config loading code (e.g., backend/internal/config/config.go) to confirm which variables are expected. For each variable, decide:
DEV_MODE must never be set in production) or already set directly in the Kamal config's env.clear or Dockerfile (example: PORT).kamal/secrets (DATABASE_URL from POSTGRES_PASSWORD)env.clear in the Kamal config (no GitHub Secret needed).kamal/secrets.<env> + entry in env.secret in config/deploy.<env>.yml + entry in the workflow env: blockKeep this classified list — it drives Steps 6 and 7.
Check which secrets already exist via gh secret list.
If the app uses the supabase/postgres recipe, set up database secrets:
Generate a random password for each environment:
# Preview password
mise x -- python -c "import secrets; print(secrets.token_urlsafe(32))"
# Production password (different from preview)
mise x -- python -c "import secrets; print(secrets.token_urlsafe(32))"
Set POSTGRES_PASSWORD as a GitHub Secret using gh secret set --body with the generated password (the agent does this directly -- never ask the user to set generated passwords)
DATABASE_URL is not a separate GitHub Secret -- it is derived from POSTGRES_PASSWORD in the .kamal/secrets file (see examples/ for the pattern)
The default preview environment uses unsuffixed names: POSTGRES_PASSWORD
Additional environments use suffixed names matching the environment name: e.g., POSTGRES_PASSWORD_PRODUCTION
For any other app secrets identified in the discovery step above, check whether the value already exists in the project's .env file:
OK to use same value locally and deployed (e.g., SMTP passwords, OAuth credentials, third-party API keys, depending on the case) — the agent sets the GitHub Secret directly from .env (see Step 6).
Different value or not in .env — the user must set it via the GitHub UI (see Step 6).
Never accept secret values through the chat
Use gh secret list to check which secrets already exist -- only create missing ones.
# SSH private key for preview (skip if already set)
gh secret set SSH_PRIVATE_KEY < ~/.ssh/<repo-name>
# Postgres password for preview (skip if already set)
gh secret set POSTGRES_PASSWORD --body "<generated password from Step 5>"
For additional environments, use suffixed names:
# SSH private key for production
gh secret set SSH_PRIVATE_KEY_PRODUCTION < ~/.ssh/<repo-name>-production
# Postgres password for production
gh secret set POSTGRES_PASSWORD_PRODUCTION --body "<generated password from Step 5>"
Note: DATABASE_URL does not need a GitHub Secret -- it is derived from POSTGRES_PASSWORD in the .kamal/secrets file.
For secrets only the user knows (app API keys, SMTP credentials, etc.), check the project's .env file first.
Reuse from .env: If the secret exists in .env and the same value may apply to the deployed environment, set it directly — the value never appears in chat:
# One-time setup (skip if already installed):
mise x -- pip install -q python-dotenv
# Set a secret from .env (value never appears in chat):
mise x -- python -c "from dotenv import dotenv_values; import sys; print(dotenv_values('.env')[sys.argv[1]], end='')" SECRET_NAME | gh secret set SECRET_NAME
Fall back to GitHub UI: If the secret is not in .env, or the deployed value should differ from the local one, give the user the GitHub secrets URL:
echo "$(gh repo view --json url -q .url)/settings/secrets/actions"
Ask them to click "New repository secret" and tell them the exact Name to enter and where to find the value. Wait for the user to confirm all manually-added secrets are saved before continuing.
Read the example files before writing. Each sub-step below names an example file — read it with the Read tool first, then adapt it for the project. Lines marked # do not change must be preserved exactly. Remove sections the project doesn't need (e.g., workers, volumes). Add project-specific values where the comments indicate.
config/deploy.yml)Read examples/config/deploy.yml and adapt it:
env.clear variables (non-sensitive config shared by all environments)volumes entries if the app stores files on disk (keep paths under /data/)servers.workers block if the project has no workersvolumes block if the app has no persistent storage# do not change exactly as-is — these are platform constraints (forward_headers: false, registry ERB templates, builder config, SSH user)env.secret here — arrays are replaced (not merged) by destination files, so any secrets listed here would be silently lost. Put ALL secrets in each config/deploy.<env>.ymlFor Postgres accessories, generate the cmd string using generate_pg_cmd.py --plan <chosen_plan>. See the supabase/postgres recipe.
config/deploy.{env}.yml)Read examples/config/deploy.preview.yml (or examples/config/deploy.production.yml for non-preview) and adapt it:
env.secret to list ALL secrets for this environment (common + env-specific) — this is the only place secrets are declaredenv.clear for environment-specific non-sensitive config (e.g., APP_ENV: preview)docs/INFRASTRUCTURE.md — match image, set host via ERB (<%= ENV['INFRA_<NAME>_IP'] %>), configure port, cmd, env, and directories (always under /data/)proxy.host — nip.io for preview (<%= ENV['INFRA_WEB_IP'] %>.nip.io), custom domain for production. Always ssl: trueservers.workers block if the project has no workersIMPORTANT — INFRA_*_IP is for Kamal and external URLs only: Use INFRA_*_IP only in host: fields, servers.*.hosts, and proxy.host. Never in env.clear or env.secret. For inter-component communication, use the CloudStack internal DNS hostname (the accessory name, e.g., db, redis).
.kamal/secrets-common)Read examples/.kamal/secrets-common and adapt it:
KAMAL_REGISTRY_PASSWORD line — this is NOT a GitHub Secret; it comes from secrets.GITHUB_TOKEN in the workflowSECRET_NAME=$SECRET_NAME).kamal/secrets.{env})Read examples/.kamal/secrets.preview (or examples/.kamal/secrets.production for non-preview) and adapt it:
POSTGRES_PASSWORD=$POSTGRES_PASSWORDPOSTGRES_PASSWORD=$POSTGRES_PASSWORD_PRODUCTIONDATABASE_URL) are composed inline — they don't need their own GitHub SecretGitHub Secrets --> Workflow env: block --> Shell environment --> .kamal/secrets* --> Kamal --> Container
POSTGRES_PASSWORD, API_KEY_PRODUCTION)env: block maps each GitHub Secret to an environment variable on the runner-d <destination>, it reads .kamal/secrets-common and .kamal/secrets.<destination> to resolve secret values from the shell environmentenv.secret in config/deploy.yml (common) and config/deploy.<env>.yml (env-specific) are injected into the containerThe left side of each secrets file line is the Kamal secret name (referenced in env.secret). The right side is the shell environment variable name (which matches the GitHub Secret name). For non-preview environments, the GitHub Secret is suffixed (e.g., POSTGRES_PASSWORD_PRODUCTION), so the right side maps to the suffixed name while the left side stays unsuffixed.
Accessory sync: The workflow's accessories JSON, config/deploy.<env>.yml, and docs/INFRASTRUCTURE.md must always describe the same set of services. Each accessory needs name, plan, disk_size_gb in the JSON; web-facing ones also need "ports": "80,443". Use INFRASTRUCTURE.md as source of truth in both directions — update it whenever accessories change.
WARNING: disk_size_gb is MANDATORY for every accessory. Even if the accessory does not need persistent storage, you must include disk_size_gb with a value in the range 10–4000 GB. Example: {"name":"nginx","plan":"small","disk_size_gb":10,"ports":"80,443"}.
Naming: accessory name must use only lowercase letters, digits, and underscores ([a-z0-9_]). No hyphens, uppercase, or special characters.
Example sync:
# In config/deploy.preview.yml:
accessories:
db:
image: supabase/postgres:17.6.1.136
# ... backend service, no proxy
n8n:
image: n8nio/n8n:latest
host: <%= ENV['INFRA_N8N_IP'] %>
proxy:
host: n8n.<%= ENV['INFRA_N8N_IP'] %>.nip.io
ssl: true
app_port: 5678
# ... web-facing, uses kamal-proxy
# In caller workflow (infra job):
with:
accessories: '[{"name":"db","plan":"medium","disk_size_gb":20},{"name":"n8n","plan":"small","disk_size_gb":10,"ports":"80,443"}]'
Start with a preview environment. Read the example workflows with the Read tool, then adapt them:
Deploy workflow: Read the Preview Workflow Example section in references/workflows.md. Copy the full workflow YAML and adapt it:
accessories JSON to match the accessories in config/deploy.preview.yml and docs/INFRASTRUCTURE.md — every accessory needs name, plan, and disk_size_gb; web-facing ones also need "ports": "80,443"env: block — one entry per GitHub Secret that .kamal/secrets files reference (derived secrets like DATABASE_URL don't need an entry)workers_replicas and workers_plan if the project has no workerssecrets: inherit — Kamal handles the entire build-push-deploy lifecycle.github/workflows/deploy-preview.ymlTeardown workflow: Read references/teardown.md for the teardown pattern. Save as .github/workflows/teardown-preview.yml
The preview workflow (triggered on push) gives immediate feedback on every change to the main branch, matching a typical developer flow. Other environments can be added depending on the team's processes.
A common choice is a "production" environment triggered on version tags (v*), where a tag signals that the pointed commit is ready for production. Feel free to create other environments with different triggers and workflow inputs to match your needs.
For each additional environment:
~/.ssh/<repo-name>-<env_name> (same procedure as Step 3)SSH_PRIVATE_KEY_PRODUCTIONPOSTGRES_PASSWORD_PRODUCTIONAPI_KEY_PRODUCTION.CLOUDSTACK_API_KEY, CLOUDSTACK_SECRET_KEY) don't need to be recreated -- just pass them in every caller workflowconfig/deploy.<env>.yml (same structure as the preview config, with the appropriate hosts, proxy host, and accessories).kamal/secrets.<env> file mapping Kamal secret names to suffixed env var names (e.g., POSTGRES_PASSWORD=$POSTGRES_PASSWORD_PRODUCTION).github/workflows/deploy-<env>.yml (same two-job pattern, with the environment's trigger, secrets, and inputs).github/workflows/teardown-<env>.ymlAll changes (Kamal config, workflows, application code) must be committed and pushed before entering this loop — deploy is triggered by git push.
+-----------------------------------------------------+
| |
| Push triggers GitHub Actions workflow |
| | |
| v |
| Monitor workflow run (gh run watch) |
| | |
| v |
| Workflow succeeds? --No--> Read logs, fix, |
| | commit/push, |
| Yes repeat |
| | |
| v |
| Health check: curl /up -> HTTP 200? |
| | |
| Yes --> Done (deployment verified) |
| | |
| No |
| | |
| v |
| SSH debug (logs, container state) |
| | |
| v |
| Return to tech-stack Local Development |
| Feedback Loop to diagnose and fix |
| |
+-----------------------------------------------------+
Give the user a clickable link first: gh run list --limit=1 --json databaseId,url -q '.[0].url'.
Present it prominently:
Your deploy is running! Follow it live here:
<URL from the command above>
Then monitor with gh run watch.
On failure: gh run view <run-id> --log-failed. Common causes: missing secrets, Dockerfile issues (port/health check), permission errors, invalid inputs. Fix, commit, push — preview auto-triggers on push.
curl -s -o /dev/null -w "%{http_code}" https://<web_ip>.nip.io/up (or https://<domain>/up). HTTP 200 = done. If it fails, SSH debug (see references/operations.md), then return to the tech-stack Local Development Feedback Loop to fix.
docs/INFRASTRUCTURE.mdconfig/deploy.<env>.yml — add/remove accessory block (image, host ERB, port, cmd, env, directories; web-facing: add proxy block)accessories JSON — disk_size_gb mandatory, web-facing need "ports": "80,443", name must match Kamal configenv.clear; secret → env.secret + GitHub Secret + .kamal/secrets.<env> + workflow env: blockQuick reference for interacting with deployed infrastructure. See references/operations.md for full details.
| Task | Command pattern |
|---|---|
| Get deployment IPs | rm -rf $HOME/tmp/provision-output && gh run download <run-id> --name provision-output --dir $HOME/tmp/provision-output |
| SSH into a VM | Resolve key first: REPO_NAME=$(gh repo view --json name -q .name), then ssh -i ~/.ssh/$REPO_NAME[-<env_name>] root@<ip> — always use -i, never rely on the default SSH key |
| Connect to accessory (e.g. Postgres) | SSH into accessory VM -> docker exec -it <repo-name>-db psql -U postgres |
| View app logs | SSH into web VM -> docker logs $(docker ps -q --filter "label=service=<repo-name>") --tail 100 |
| Check app health | curl -s https://<web_ip>.nip.io/up |
| Shell into app container | SSH into web VM -> docker exec -it $(docker ps -q --filter "label=service=<repo-name>") sh |
Dockerfile at repository rootGET /up → HTTP 200servers.workers.cmd in config/deploy.<env>.ymlDATABASE_URL env var — fail hard if missingFor Go+React apps, see the tech-stack skill's Dockerfile section. For other stacks, ensure port 80 and the health check endpoint are configured.
Single web VM → run migrations at container startup (no race conditions). Include migrations in the entrypoint before the web server starts. All migration dependencies must be bundled in the Docker image.
After deploy, information is available from: workflow outputs (web_ip, worker_ips, accessory_ips, infrastructure_changed, scaled_accessories, infra_env), the Actions step summary, and the provision-output artifact (90-day retention).
App URL: Without domain: https://<web_ip>.nip.io. With domain: https://<domain> (requires DNS A record). Both get TLS via Let's Encrypt HTTP-01.
When the user asks to configure a custom domain, always ask which environment should receive it — never default silently. Options:
Mention that this can be changed later. Then proceed with DNS Configuration. If production doesn't exist yet, create it via Step 9.
The web VM's public IP is not known until the first deployment completes. To set up a custom domain:
proxy.host domain from the destination config). The app will be accessible at https://<web_ip>.nip.io.web_ip from the workflow output or step summary.Type: A
Name: myapp.example.com (or @ for apex)
Value: <web_ip from step 2>
TTL: 300
config/deploy.<env>.yml) to set proxy.host to the domain (SSL is already true).Let's Encrypt HTTP-01 challenge requires the domain to resolve to the server before the certificate can be issued. The IP is stable across re-deployments to the same environment -- it only changes if the environment is torn down and recreated.
For apex domains (e.g., example.com), proactively include www. Use the hosts: array in the proxy config and instruct the user to create two DNS A records:
Type: A Name: @ Value: <web_ip> TTL: 300
Type: A Name: www Value: <web_ip> TTL: 300
The destination config uses proxy.hosts (array) instead of proxy.host (string):
proxy:
hosts:
- example.com
- www.example.com
ssl: true
kamal-proxy will provision separate Let's Encrypt certificates for each hostname. Both DNS records must resolve to the server before deployment.
Canonical redirect: BASE_URL is always the bare domain. The app must redirect www → bare (HTTP 301) at the application level — kamal-proxy does not do host-to-host redirects. This avoids inconsistent OAuth callbacks, cookies, and links.
Workers scale horizontally by changing workers_replicas in the caller workflow (see references/workflows.md -- Deploy Input Reference for all inputs). For scaling details, see references/scaling.md -- Scaling Workers. The worker command is set in config/deploy.yml under servers.workers.cmd, not as a workflow input.
workers_replicas: 0 means no workers (the workflow will not provision worker VMs)workers_replicas: 1 or higher provisions that many worker VMsWhen workers are enabled:
servers.workers.cmd and servers.workers.proxy: false to config/deploy.yml:# config/deploy.yml
servers:
workers:
cmd: "celery -A tasks worker --loglevel=info"
proxy: false
config/deploy.{env}.yml. The provision job exports INFRA_WORKER_IP_0, INFRA_WORKER_IP_1, etc. -- one for each workers_replicas:# config/deploy.preview.yml
servers:
web:
hosts:
- <%= ENV['INFRA_WEB_IP'] %>
workers:
hosts:
- <%= ENV['INFRA_WORKER_IP_0'] %>
- <%= ENV['INFRA_WORKER_IP_1'] %>
workers_replicas and workers_plan in the caller workflow's infra job:with:
workers_replicas: 2
workers_plan: "small"
Scaling happens by updating workflow inputs and/or Kamal config, then redeploying. See references/scaling.md for VM plans, disk sizes, and detailed instructions.
Change web_plan in the workflow and redeploy. No config file changes needed. Causes brief downtime.
workers_plan in the workflow and redeploy. No config file changes needed. Causes brief downtime.workers_replicas in the workflow. Also update the host list in config/deploy.<env>.yml to match the new replica count — see references/scaling.md -- Scaling Workers for details. Commit, push, and redeploy.Change the plan in the accessories JSON in the workflow. For database accessories using supabase/postgres, also regenerate the cmd — see references/scaling.md -- Scaling the Database for the full procedure.
Other accessories may also have memory-dependent configuration (e.g., maxmemory for Redis, JVM heap for Elasticsearch). When scaling any accessory, review its cmd, env, or container configuration in config/deploy.<env>.yml and adjust parameters to match the new plan's resources.
See references/teardown.md for tearing down environments, inferring zone/env_name from existing workflows, and reading last run outputs.
Every deployed environment has automatic daily snapshots of all data volumes. If an environment is lost (teardown, VM failure, or data corruption), it can be recovered by running the deploy workflow with recover: true.
When the user asks to "recover", "recuperar", "restore", "do DR", or "disaster recovery": the critical action is adding recover: true to the infra job inputs in the deploy workflow. Without this flag, the workflow creates blank disks and the data is lost. Simply changing the zone is not recovery — recovery means restoring from snapshots via the recover: true flag.
Recovery procedure:
zone input to recover in a different zone (snapshots are replicated across zones, so recovery works in either zone)recover: true to the infra job's with: block in the deploy workflow (e.g., .github/workflows/deploy-preview.yml)recover: true from the workflow file — if left in place, subsequent runs will fail because the network and volumes already existSee references/recovery.md for the full procedure, pre-flight requirements, and current limitations.
Without a local runtime, use the Deployment Feedback Loop as the primary cycle: commit/push → monitor workflow → health check → SSH debug if needed → repeat. Start with preview (push-triggered, no domain, TLS via nip.io). Add production later — especially important without local dev, since preview is the only pre-production check.
supabase/postgres image, a Postgres image enriched with extensions as recommended by the tech-stack skillnpx claudepluginhub gmautner/marketplaceCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.