GitHub-Actions-driven preview environments for Rails apps deployed with Kamal 2. Open a PR, get a fresh app at a unique URL with a freshly cloned staging database. Close the PR, everything goes away.
Pull Request opened ───▶ web-ascender/github-actions-kamal-previews ─┬─▶ kamal deploy -d <slug>
├─▶ clone staging DB → myapp_<slug>
├─▶ post URL on PR comment
└─▶ register GitHub Deployment
Pull Request closed ───▶ web-ascender/github-actions-kamal-previews ─┬─▶ kamal app remove -d <slug>
├─▶ drop database myapp_<slug>
├─▶ update PR comment
└─▶ deactivate GitHub Deployment
Rails + Kamal is a great combination, but Kamal alone doesn't ship with a "review apps" / "preview environments" feature out of the box the way Heroku or Render do. This repo packages the missing piece: one composite action that any Kamal-deployed Rails app can adopt in a single workflow step.
- One Kamal service per PR, deployed to your existing staging host
(multiple feature apps coexist on one server via
kamal-proxy). - Database clone per PR. PostgreSQL, MySQL, and SQLite all supported.
- Full lifecycle. Deploys on PR open/sync, tears down on PR close and on branch delete.
- Native GitHub UX. Deployments API integration, transient environment, rolling status comment on the PR with the live URL.
- Sweeper that reconciles orphaned deployments daily so nothing leaks.
- Resource caps + concurrency caps + branch filter to bound preview
cost. (Scale-to-zero is on the upstream Kamal roadmap; see
docs/resource-limits.md.) - No Ruby gem in your app. Pure GitHub Action — invoke with one
uses:line.
In your Rails app's repo, drop one workflow file:
# .github/workflows/preview.yml
name: Preview environment
on:
pull_request:
types: [opened, synchronize, reopened, closed]
delete:
permissions:
contents: read
packages: write # GHCR push
pull-requests: write # PR comments
deployments: write # Deployments API
# Cancel a superseded deploy when a new commit lands on the same PR,
# but never cancel a teardown — letting a `closed` event get cancelled
# would leave the preview app + databases orphaned on the host.
concurrency:
group: kamal-previews-${{ github.event.pull_request.number || github.event.ref || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' && github.event.action != 'closed' }}
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: web-ascender/github-actions-kamal-previews@v1
with:
base-deploy-file: config/deploy.staging.yml
base-secrets-file: .kamal/secrets.staging
domain-suffix: preview.example.com
deploy-host: staging.example.com
database-engine: postgres
databases: |
DATABASE_URL=myapp_staging:myapp_{db_slug}
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
# No DB credentials needed here — the action sources
# base-secrets-file and reads DATABASE_URL for the admin
# connection. Override with the DATABASE_ADMIN_URL secret if
# the staging app role lacks CREATEDB.The repo-level GitHub Actions secrets the workflow expects:
| Secret | When |
|---|---|
DEPLOY_SSH_KEY |
Always — the deploy host SSH key. |
DATABASE_ADMIN_URL |
Only if the URL exposed by your base-secrets-file doesn't have CREATEDB. Format: postgres://admin:secret@host:5432/postgres?sslmode=verify-full. |
Add them under Settings → Secrets and variables → Actions. The first PR you open after merging the workflow file will provision a preview environment.
See docs/getting-started.md for the full
walkthrough including DNS, TLS, secrets, and host setup.
Prefer the reusable workflow form
(uses: web-ascender/github-actions-kamal-previews/.github/workflows/preview.yml@v1)
when you want job-level features like matrix-fanned parallel orphan
cleanup or GitHub's job-level concurrency UI. See
examples/README.md for the comparison.
Private deploy host? Stick with the composite action form above and add your VPN action as a sibling step — the reusable workflow form can't host sibling steps. See getting-started.
The work splits into four layers:
- Top-level composite action (
action.yml) — the simpleuses: web-ascender/github-actions-kamal-previews@v1entry point. Auto-dispatches deploy vs. teardown based on the triggering event. - Reusable workflow (
.github/workflows/preview.yml) — alternate entry point for adopters who want job-level features (matrix parallelism, separate concurrency groups for deploy vs. teardown). - Sub-composite actions (
.github/actions/*/action.yml) — the building blocks both entry points share:setup,generate-config,clone-database,deploy,teardown,pr-comment,deployment-status. Independently usable. - Stdlib-only Ruby library (
lib/kamal_previews/) — branch-name sanitization and Kamal config generation. Runs on any Ruby ≥ 3.0 without Bundler.
The database engines are pluggable shell scripts under
scripts/{postgres,mysql,sqlite}/ invoked by the clone-database and
drop-database composite actions. Adding a new engine is one new directory.
See docs/architecture.md for a deeper look.
- Kamal 2.x. Kamal 1 is not supported.
- Rails 7.1+ is the tested baseline. Earlier Rails works as long as your Dockerfile and Kamal config are Kamal-2-compatible.
- GitHub Actions runners —
ubuntu-latestis the default; the workflow is portable. - Databases: PostgreSQL 14+, MySQL 8.0+, SQLite 3.
- Getting started — adoption walkthrough.
- Architecture — how the pieces fit together.
- Databases — engine-specific notes, sanitization hooks, and clone modes.
- Secrets — Kamal
kamal secretsintegration with 1Password, AWS, GCP, Doppler, etc. - DNS and TLS — wildcard DNS, per-host vs. wildcard certs, the cookie-domain footgun.
- Resource limits — memory / CPU caps, branch-pattern filtering, max-concurrent-previews cap, and notes on the scale-to-zero gap.
- Reference — every input, output, and secret the reusable workflow consumes.
- Troubleshooting — common failure modes.
MIT. See LICENSE.