Hosting
Deployment templates for Vercel, Firebase, Fly.io, Convex, and SSH
collabops/vercel-deploy@v1
On-Premise: ❌ — requires Vercel connectivity
Builds and deploys a Vercel project. Supports Preview and Production environments.
Prerequisites
1. Create a Vercel API Token
Go to Vercel Dashboard > Settings > Tokens and create a new token.
2. Create a Vercel Project
You need to create the project via the Vercel API without connecting a Git provider.
curl -X POST https://api.vercel.com/v10/projects \
-H "Authorization: Bearer {VERCEL_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name": "my-project", "framework": "nextjs"}'name: Project name
framework: Set according to your project (nextjs, vite, nuxtjs, svelte, etc.)
Use id from the response as VERCEL_PROJECT_ID
3. Find Your Organization ID
Go to Vercel Dashboard > Settings > General and find the Vercel ID (= Organization ID).
4. Register Secrets
Register the following secrets in your CollabOps project settings.
| Secret | Description |
|---|---|
VERCEL_TOKEN | Vercel API token |
VERCEL_ORG_ID | Organization / Team ID |
VERCEL_PROJECT_ID | Project ID |
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
vercel-token | YES | - | Vercel API token. $\{\{ secrets.VERCEL_TOKEN \}\} recommended |
vercel-org-id | YES | - | Vercel Organization ID |
vercel-project-id | YES | - | Vercel Project ID |
production | NO | "false" | Production deployment (false = Preview) |
working-directory | NO | "/workspace/source" | Project root path |
Usage Example
steps:
- name: Checkout
uses: collabops/checkout@v2
with:
repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
ref: ${{ collabops.ref_name }}
sha: ${{ collabops.sha }}
- name: Deploy to Vercel
uses: collabops/vercel-deploy@v1
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
production: "false"Production vs Preview routing
# Triggered by push and change_request. Pick production vs preview per Job using if.
triggers:
push:
branches: [main]
change_request:
branches: [main]
jobs:
deploy-production:
# Production only on main push — gate with a Job-level if.
if: "collabops.event_name == 'push'"
steps:
- name: checkout
uses: "collabops/checkout@v2"
with:
repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
- name: vercel-prod
uses: "collabops/vercel-deploy@v1"
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
production: "true"
deploy-preview:
# Change Request events deploy a preview.
if: "collabops.event_name == 'change_request'"
steps:
- name: checkout
uses: "collabops/checkout@v2"
with:
repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
- name: vercel-preview
uses: "collabops/vercel-deploy@v1"
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
production: "false"
Key points — Branch production using triggers plus a per-Job if: "collabops.event_name == ...". Use working-directory to scope multi-project monorepos.
collabops/firebase-deploy@v1
On-Premise: ❌ — requires Firebase connectivity
Deploys Firebase resources. Supports selective deployment of Functions, Hosting, Firestore Rules, and more.
| Input | Required | Default | Description |
|---|---|---|---|
service-account-key | YES | - | GCP service account key JSON. $\{\{ secrets.FIREBASE_SA_KEY \}\} recommended |
project-id | YES | - | Firebase project ID |
deploy-targets | NO | "" | Deploy targets (comma-separated: functions, hosting, firestore:rules, storage:rules). Deploys all if empty |
working-directory | NO | "/workspace/source" | Directory containing firebase.json |
Examples
Basic — deploy the full firebase.json
jobs:
deploy:
steps:
- name: checkout
uses: "collabops/checkout@v2"
with:
repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
- name: install-and-build
run: |
npm ci
npm run build
image: node:22-alpine
# Without deploy-targets, the entire firebase.json is deployed.
- name: firebase-deploy
uses: "collabops/firebase-deploy@v1"
with:
service-account-key: ${{ secrets.FIREBASE_SA_KEY }}
project-id: my-firebase-project
Targeted deploy — specific resources only
jobs:
deploy-hosting:
steps:
- name: checkout
uses: "collabops/checkout@v2"
with:
repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
- name: install-and-build
run: |
npm ci
npm run build
image: node:22-alpine
- name: firebase-deploy-targeted
uses: "collabops/firebase-deploy@v1"
with:
service-account-key: ${{ secrets.FIREBASE_SA_KEY }}
project-id: my-firebase-project
# Comma-separated — hosting, functions, firestore, storage, …
deploy-targets: "hosting,functions"
Key points — service-account-key is the complete JSON key downloaded from the GCP console. Use deploy-targets to ship only the hot path quickly; a full deploy is much slower. Point working-directory at the directory that contains firebase.json for monorepo setups.
collabops/fly-deploy@v1
On-Premise: ❌ — requires Fly.io connectivity
Deploys an app to Fly.io using flyctl with remote build.
| Input | Required | Default | Description |
|---|---|---|---|
api-token | YES | - | Fly.io API token. $\{\{ secrets.FLY_API_TOKEN \}\} recommended |
app-name | NO | "" | Fly.io app name (reads from fly.toml if empty) |
remote-only | NO | "true" | Use remote builder |
working-directory | NO | "/workspace/source" | Directory containing fly.toml |
Examples
Basic — pick app name from fly.toml
jobs:
deploy:
steps:
- name: checkout
uses: "collabops/checkout@v2"
with:
repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
# Omitting app-name uses the [app] value from the workspace's fly.toml.
- name: fly-deploy
uses: "collabops/fly-deploy@v1"
with:
api-token: ${{ secrets.FLY_API_TOKEN }}
Monorepo — env-specific app + local build
jobs:
deploy-staging:
steps:
- name: checkout
uses: "collabops/checkout@v2"
with:
repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
- name: fly-deploy-staging
uses: "collabops/fly-deploy@v1"
with:
api-token: ${{ secrets.FLY_API_TOKEN }}
# Override when the deploy targets a different app than fly.toml's [app].
app-name: web-staging
# Build inside the Job container instead of Fly's remote builder — needs BuildKit.
remote-only: "false"
working-directory: /workspace/source/apps/web
Key points — Only set app-name when the deploy targets an app different from [app] in fly.toml. remote-only defaults to true (uses Fly's remote builder) — set "false" only when you provide BuildKit yourself. Use the official flyio/flyctl image for the Job container.
collabops/convex-deploy@v1
On-Premise: ❌ — requires Convex connectivity
Deploys Convex Functions. Optionally runs a build command.
| Input | Required | Default | Description |
|---|---|---|---|
deploy-key | YES | - | Convex Deploy Key. $\{\{ secrets.CONVEX_DEPLOY_KEY \}\} recommended |
cmd | NO | "" | Build command to run after deploy |
working-directory | NO | "/workspace/source" | Project root path |
Examples
Basic — deploy Convex functions
jobs:
deploy:
steps:
- name: checkout
uses: "collabops/checkout@v2"
with:
repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
- name: npm-install
run: npm ci
image: node:22-alpine
# deploy-key is the Convex Production / Preview deploy key from the dashboard.
- name: convex-deploy
uses: "collabops/convex-deploy@v1"
with:
deploy-key: ${{ secrets.CONVEX_DEPLOY_KEY }}
Run a build command after deploy
jobs:
deploy:
steps:
- name: checkout
uses: "collabops/checkout@v2"
with:
repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
- name: npm-install
run: npm ci
image: node:22-alpine
- name: convex-deploy-with-cmd
uses: "collabops/convex-deploy@v1"
with:
deploy-key: ${{ secrets.CONVEX_DEPLOY_KEY }}
# Trigger downstream builds (e.g. Next.js prerender) immediately after the deploy.
cmd: "npm run build:next"
Key points — Store a separate deploy-key secret per environment (production / preview). cmd runs after Convex schema + codegen finishes, so it is the right place to trigger builds that depend on Convex types (Next.js prerender, etc.).
collabops/ssh-exec@v1
On-Premise: ✅ — works in air-gapped environments
Connects to a remote host over SSH and executes a shell script. Use this for deploy scripts, service restarts, migrations, or any ad-hoc remote command.
The runtime image is pinned to alpine/git:2.43.0 and performs no extra package installation at runtime (no apk/apt), so it works in air-gapped environments out of the box.
Prerequisites
You need an SSH private key for the target host, plus a known_hosts value obtained beforehand via ssh-keyscan -p <port> <host>. Store both as CollabOps secrets and inject them at runtime.
known-hosts is the host-key verification value that prevents MITM (man-in-the-middle) attacks, so it is required. Leaving it empty would let SSH connect even when the host key changes.
| Secret | Description |
|---|---|
DEPLOY_SSH_PRIVATE_KEY | OpenSSH private key for the remote host |
DEPLOY_KNOWN_HOSTS | Output of ssh-keyscan -p <port> <host> |
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
host | YES | - | Remote host (IP or domain) |
username | YES | - | SSH username |
port | NO | "22" | SSH port |
ssh-key | YES | - | SSH private key contents (OpenSSH format). $\{\{ secrets.DEPLOY_SSH_PRIVATE_KEY \}\} recommended |
known-hosts | YES | - | known_hosts contents (output of ssh-keyscan -p <port> <host>). Required to prevent MITM (man-in-the-middle) attacks |
script | YES | - | Shell script to run on the remote host (multi-line supported). set -eu is automatically prepended and the script is executed via bash -s to prevent silent failures |
Usage
steps:
- name: Restart service over SSH
uses: collabops/ssh-exec@v1
with:
host: ${{ vars.DEPLOY_HOST }}
username: ${{ vars.DEPLOY_USER }}
ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
script: |
cd /opt/myapp
docker compose pull
docker compose up -d --remove-orphansFan out the same script to multiple hosts
# strategy.matrix is unsupported. Declare a Job per host — Jobs run in parallel by default.
jobs:
restart-web1:
steps:
- name: ssh-restart
uses: "collabops/ssh-exec@v1"
with:
host: web1.prod
username: deploy
ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
script: |
sudo systemctl restart api
sudo systemctl status api --no-pager
restart-web2:
steps:
- name: ssh-restart
uses: "collabops/ssh-exec@v1"
with:
host: web2.prod
username: deploy
ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
script: |
sudo systemctl restart api
sudo systemctl status api --no-pager
restart-web3:
steps:
- name: ssh-restart
uses: "collabops/ssh-exec@v1"
with:
host: web3.prod
username: deploy
ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
script: |
sudo systemctl restart api
sudo systemctl status api --no-pager
Key points — strategy.matrix is unsupported, so declare a Job per host. Jobs without needs run in parallel by default. The known-hosts secret must include ssh-keyscan output for every host.
collabops/scp-upload@v1
On-Premise: ✅ — works in air-gapped environments
Uploads a file or directory to a remote host over SCP. Use this to ship compose files, static assets, or build artifacts to a deploy server.
The runtime image is pinned to alpine/git:2.43.0 with no extra package installation at runtime, so it works in air-gapped environments out of the box.
known-hosts is required for the same reason as ssh-exec: it prevents MITM (man-in-the-middle) attacks via host-key verification. Storing both values as CollabOps secrets and injecting them via inputs is recommended.
| Secret | Description |
|---|---|
DEPLOY_SSH_PRIVATE_KEY | OpenSSH private key for remote access |
DEPLOY_KNOWN_HOSTS | Output of ssh-keyscan -p <port> <host> |
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
host | YES | - | Remote host (IP or domain) |
username | YES | - | SSH username |
port | NO | "22" | SSH port |
ssh-key | YES | - | SSH private key contents (OpenSSH format). $\{\{ secrets.DEPLOY_SSH_PRIVATE_KEY \}\} recommended |
known-hosts | YES | - | known_hosts contents (output of ssh-keyscan -p <port> <host>). Required to prevent MITM (man-in-the-middle) attacks |
source | YES | - | Local path to upload (file or directory). Absolute path under /workspace/source recommended |
target | YES | - | Remote target path. Trailing slash recommended for directories. Paths containing spaces are not supported due to scp user@host:path syntax limits |
recursive | NO | "false" | Recursively upload a directory (true/false). Set to true when source is a directory |
Usage
steps:
- name: Upload compose file
uses: collabops/scp-upload@v1
with:
host: ${{ vars.DEPLOY_HOST }}
username: ${{ vars.DEPLOY_USER }}
ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
source: "/workspace/source/docker-compose.yml"
target: "/opt/myapp/docker-compose.yml"Upload a directory and trigger a remote reload
jobs:
release:
steps:
- name: checkout
uses: "collabops/checkout@v2"
with:
repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
- name: build-dist
run: |
npm ci && npm run build # writes dist/
image: node:22-alpine
# 1) Push build output to the remote host.
- name: upload-dist
uses: "collabops/scp-upload@v1"
with:
host: web1.prod
username: deploy
ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
source: dist
target: /var/www/app/current
# 2) Reload nginx remotely.
- name: reload-nginx
uses: "collabops/ssh-exec@v1"
with:
host: web1.prod
username: deploy
ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
script: |
sudo nginx -t
sudo systemctl reload nginx
Key points — Pairing scp-upload with an immediate ssh-exec reload/restart is the common deploy pattern. Both steps share the same SSH secrets, so prepare credentials once at the Job level.