Skip to content

GitHub Action

Griffen Fargo edited this page Aug 1, 2026 · 3 revisions

GitHub Action

Deploy a strut-managed Docker stack to a VPS from GitHub Actions in one step.

- uses: gfargo/strut-action@v1
  with:
    stack: my-app
    command: deploy
    env: prod
    host: ${{ secrets.STRUT_HOST }}
    ssh-key: ${{ secrets.STRUT_SSH_KEY }}

Prerequisites

  1. Your repository contains strut.conf and a stacks/<stack>/ directory (run strut init + strut scaffold <stack> locally first).
  2. The VPS already has strut bootstrapped (strut remote:init or manual setup).
  3. Two repository secrets are configured (Settings → Secrets and variables → Actions):
    • STRUT_HOST — VPS hostname or IP address
    • STRUT_SSH_KEY — Private SSH key whose public half is in ~/.ssh/authorized_keys on the VPS

The workflow must actions/checkout before the strut action so strut.conf and the stacks/ tree are present on the runner.


Inputs

Input Required Default Description
stack Stack name (subdirectory under stacks/)
command deploy strut command — see Command semantics
env Environment name — resolves to .<env>.env in the project root. Empty uses .env
ssh-key Private SSH key contents (VPS_SSH_KEY) — always pass via a secret
host VPS hostname or IP (VPS_HOST). Optional if your repo's topology already resolves the stack to a host
user SSH user on the VPS (VPS_USER)
port 22 SSH port on the VPS (VPS_PORT)
known-hosts known_hosts entries for the VPS. If empty, the host key is fetched via ssh-keyscan
services Services profile passed as --services <profile>
strict false Pass --strict — treat migration failures as fatal
dry-run false Pass --dry-run — print the plan without making changes
args Extra raw arguments appended to the strut command
env-file Full contents of the .<env>.env file — always pass via a secret
strut-version latest strut version to install: latest, main, or vX.Y.Z
working-directory . Project root containing strut.conf and stacks/

Minimal example

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: gfargo/strut-action@v1
        with:
          stack: my-app
          host: ${{ secrets.STRUT_HOST }}
          ssh-key: ${{ secrets.STRUT_SSH_KEY }}

Full example

name: Deploy

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      dry-run:
        description: 'Preview without deploying'
        default: 'false'
        type: boolean

jobs:
  deploy:
    runs-on: ubuntu-latest
    concurrency:
      group: deploy-prod
      cancel-in-progress: false
    steps:
      - uses: actions/checkout@v4

      - uses: gfargo/strut-action@v1
        with:
          stack: my-app
          command: deploy
          env: prod
          host: ${{ secrets.STRUT_HOST }}
          ssh-key: ${{ secrets.STRUT_SSH_KEY }}
          strut-version: v0.45.0    # pin to a specific strut release
          strict: true              # fail on migration errors
          dry-run: ${{ github.event.inputs.dry-run || 'false' }}
          env-file: ${{ secrets.STRUT_PROD_ENV }}

Note: strut health runs against the runner's local Docker daemon, not the VPS. It cannot verify stack health on a hosted runner. To check the VPS after deploying, use your own smoke-test step (e.g. curl the health endpoint) or run strut <stack> status over SSH.


Command semantics

Command Runs where Use case
deploy Runner (SSH to VPS) Primary CI command. Runs sync → migrate → deploy → verify on the VPS the stack maps to. The action adds --require-remote, so a stack that resolves to no VPS fails loudly instead of deploying to the runner.
release Runner (SSH to VPS) Alias for deploy. Accepted so existing workflows keep working.
ship Runner (git + SSH to VPS) Like deploy but also commits and pushes from the runner. Needs contents: write permission and full checkout. See security warning below. Prefer deploy in most CI pipelines.
health Runner's local Docker Checks docker compose ps on the runner — not the VPS. Not useful on a hosted runner (Docker is empty). Omit from hosted CI; use a smoke-test curl or SSH step instead.

Rule of thumb: use deploy from a hosted runner. Use health only on a self-hosted runner running on the VPS itself.

Security warning: command: ship

ship runs git add -A on the runner before pushing. The action writes a .<env>.env file (containing VPS_HOST and your env-file contents) to the repository root. If your repo does not gitignore *.env, this file will be committed and pushed, leaking secrets.

Before using command: ship:

  1. Add *.env (or .prod.env, .staging.env, etc.) to your repo's .gitignore.
  2. Confirm the gitignore is committed before the workflow runs.

For hosted CI, deploy is the safe default — it does not run any git commands on the runner.


Pinning the strut version

Use the strut-version input to pin to a specific release tag, ensuring reproducible deploys:

- uses: gfargo/strut-action@v1
  with:
    strut-version: v0.45.0
    # ...

The action installs strut at that tag and logs the version to the action output.


Passing extra secrets (registry tokens, etc.)

The env-file input takes the entire contents of the env file, written to .<env>.env before the strut command runs. Store the whole file as one secret:

- uses: gfargo/strut-action@v1
  with:
    env-file: ${{ secrets.STRUT_PROD_ENV }}
    # ...

Where STRUT_PROD_ENV holds the file verbatim:

GH_PAT=ghp_…
REGISTRY_TOKEN=…

The file is written with 600 permissions and never echoed to the action log.

This input takes a whole file, not a list of KEY=VALUE additions. Connection values (host, user, port) are merged in separately by the action, so you do not need to repeat them here.


Security notes

  • The SSH key is written to $RUNNER_TEMP/strut_deploy_key with mode 600 and never printed.
  • The env file is written with mode 600. Values are materialized via printf, not echo.
  • VPS_HOST is masked in the action log with ::add-mask::.
  • Secrets passed as ${{ secrets.* }} are automatically redacted by GitHub Actions.
  • strut uses StrictHostKeyChecking=no (consistent with its standard SSH behavior) — no known_hosts pinning.
  • --dry-run shows the SSH plan (host/user only) without leaking key or env values.

Troubleshooting

strut: command not found after install Install adds a symlink to /usr/local/bin (writable on hosted runners) or ~/.local/bin. If the Verify strut version step fails, the runner environment may be unusual — open an issue with the runner OS and version.

Not inside a strut project The runner must have strut.conf at the repo root. Ensure actions/checkout runs before this action.

Stack not found The stack input must match a directory under stacks/ in your repository.

Deploy hangs or fails on SSH Verify STRUT_HOST resolves, port 22 (or your port) is open, and the public key matching STRUT_SSH_KEY is in ~/.ssh/authorized_keys on the VPS.

deploy deployed nothing, but the job passed The stack resolved to no VPS, so the deploy fell back to the runner's own Docker daemon. The action passes --require-remote to turn this into a hard failure — if you invoke strut directly in a run: step, pass it yourself. Check that the stack is mapped under [stacks] in strut.conf or that VPS_HOST is set for the environment.

Clone this wiki locally