GitHub Actions

A pipeline runs with access to your secrets. Configure it like it matters.

1guide Feed

GitHub Actions is CI that lives next to the code, which is most of its appeal and all of its risk. A workflow file is code that runs with access to your secrets, and it is edited by everyone who can open a pull request.

The useful pipeline shape for a self-hosted application is three jobs chained with needs: test, build and push an image, deploy. Each runs only if the previous passed, and what gets deployed is the exact artifact that was tested — tagged with the commit SHA, not latest.

Reach for Actions when the repository already lives on GitHub and the pipeline is straightforward. Reach for something else when builds need hardware the hosted runners do not have, or when minutes cost more than a small self-hosted runner would.

Four settings matter more than the rest. Per-job permissions, so a test job cannot push packages. A workflow-level concurrency group, so two deploys never run at once. Environment secrets with a required reviewer, so an accidental merge becomes a prompt instead of a release. And SHA-pinned third-party actions, so a moved tag cannot swap in new code that reads your secrets.

The failure that costs the most time is a deploy step that exits zero while the application is broken — docker compose up without --wait, or services with no health check for it to wait on. A smoke test after the deploy step turns that into a red build instead of a silent outage.

Articles here show complete workflow files rather than fragments, and state which runner image they were verified on.

Start here

How to Build a GitHub Actions Deployment Pipeline

Test, build a container image, push it to GHCR and deploy it to a VPS over SSH — with the concurrency guard, environment approval and token scoping that keep the pipeline from becoming the weak point.

Intermediate · 4 min read

Beginner to advanced

A reading order that builds Actions knowledge in the sequence it is actually needed.

  1. Build How to Build a GitHub Actions Deployment Pipeline 4 min

Common problems

Failure modes that come up repeatedly with Actions, and where each one is solved.

Pushing an image fails with: denied: permission_denied
The job is missing `permissions: packages: write`, or the repository's default workflow token is read-only.
SSH deploy fails with: Host key verification failed
known_hosts is empty. Pin the host key in a secret rather than running ssh-keyscan at deploy time, which trusts whatever answers.
The deploy step succeeds but the application is down
`docker compose up` returns as soon as containers are created. Add `--wait` and give the services real health checks.
Two deploys run at the same time
No concurrency group. Add one at the workflow level with `cancel-in-progress: false` so the second run queues instead.

Latest Actions guides

Browse the archive
  • intermediate

    How to Build a GitHub Actions Deployment Pipeline

    Test, build a container image, push it to GHCR and deploy it to a VPS over SSH — with the concurrency guard, environment approval and token scoping that keep the pipeline from becoming the weak point.

    4 min AI-assisted

Frequently asked questions

Why pin actions to a commit SHA instead of a tag?

Tags are mutable. The author can move `v3` to different code at any time, and that code runs with access to the secrets available to its job. Pin the SHA and let Dependabot raise the update as a reviewable diff.

Repository secrets or environment secrets?

Environment secrets for anything that touches production. They are only readable by jobs that declare the environment, and environments support required reviewers and wait timers.

What is the most dangerous workflow misconfiguration?

`pull_request_target` combined with a checkout of the pull request head. It runs untrusted code with write-scoped secrets, and it is the pattern most commonly exploited in the wild.

Sources

  1. GitHub Actions documentation GitHub Primary source
  2. Security hardening for GitHub Actions GitHub Primary source
  3. Docker build with GitHub Actions Docker Primary source