GitHub Actions CI/CD: The Complete Guide for 2026

Published February 12, 2026 · 18 min read

GitHub Actions is the CI/CD platform built directly into GitHub. Every push, pull request, or scheduled event can trigger automated workflows that build, test, lint, and deploy your code — without leaving your repository. This guide covers everything from writing your first workflow to advanced patterns like matrix builds, Docker image publishing, reusable workflows, and production deployment strategies.

⚙ Try it: Validate your workflow files with our YAML Validator before committing them. Also bookmark the YAML Syntax Cheat Sheet for quick reference.

1. GitHub Actions Fundamentals

Before writing your first workflow, understand the four building blocks:

Repository
 └── .github/workflows/
      └── ci.yml              <-- Workflow file (YAML)
           ├── on: push        <-- Trigger (event)
           └── jobs:
                └── build:      <-- Job
                     ├── runs-on: ubuntu-latest  <-- Runner
                     └── steps:                  <-- Steps
                          ├── uses: actions/checkout@v4
                          └── run: npm test

How it works: when an event matches a workflow trigger, GitHub spins up a fresh virtual machine for each job, clones your code, runs the steps in order, and reports the result. Each job gets a clean environment — nothing persists between jobs unless you explicitly use artifacts or caching.

2. Workflow YAML Syntax

Every workflow file lives in .github/workflows/ and follows this structure:

# .github/workflows/ci.yml
name: CI Pipeline           # Display name in the Actions tab

on:                          # When to trigger
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:                 # Explicit permissions (security best practice)
  contents: read

env:                         # Workflow-level environment variables
  NODE_ENV: production

jobs:
  build:
    name: Build and Test
    runs-on: ubuntu-latest   # Runner OS
    timeout-minutes: 15      # Kill job if it hangs

    env:                     # Job-level environment variables
      CI: true

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build
⚙ Related: New to YAML? Read our YAML Complete Guide for a thorough explanation of the syntax.

3. Workflow Triggers

The on key controls when your workflow runs. Here are the most common triggers:

Push and Pull Request

on:
  push:
    branches: [main, develop]
    paths:
      - 'src/**'              # Only trigger when source files change
      - 'package.json'
    paths-ignore:
      - '**.md'               # Skip if only docs changed
      - 'docs/**'

  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

Schedule (Cron)

on:
  schedule:
    - cron: '0 6 * * 1'      # Every Monday at 6:00 AM UTC
    - cron: '0 0 * * *'      # Daily at midnight UTC

Cron syntax: minute hour day-of-month month day-of-week. Scheduled workflows run on the default branch only.

⚙ Try it: Use our Cron Job Schedule Guide to build and validate cron expressions.

Manual Dispatch

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        type: choice
        options:
          - staging
          - production
      dry_run:
        description: 'Dry run (skip actual deployment)'
        required: false
        type: boolean
        default: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to ${{ github.event.inputs.environment }}"
      - run: |
          if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
            echo "Dry run - skipping deployment"
          else
            echo "Deploying for real"
          fi

Other Useful Triggers

on:
  release:
    types: [published]         # When a GitHub Release is created

  workflow_run:
    workflows: ["CI"]          # Run after another workflow completes
    types: [completed]

  repository_dispatch:         # Triggered by external API call
    types: [deploy]

  issue_comment:               # React to comments on issues/PRs
    types: [created]

4. Common CI Workflows

Build, Test, and Lint

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    needs: lint                # Run tests only if lint passes
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm test -- --coverage
      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

  build:
    runs-on: ubuntu-latest
    needs: test                # Build only if tests pass
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/

Python CI with pytest

name: Python CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: '3.12', cache: pip }
      - run: pip install -r requirements.txt
      - run: pytest --cov=src --cov-report=xml
      - uses: codecov/codecov-action@v4
        with: { file: coverage.xml }

Go CI

name: Go CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with: { go-version: '1.22' }
      - run: go vet ./...
      - run: go test -race -coverprofile=coverage.out ./...
      - run: go build -v ./...

5. CD Workflows — Deploy to Cloud

Deploy to Vercel

name: Deploy to Vercel
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci && npm run build
      - uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

Deploy to Netlify

name: Deploy to Netlify
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci && npm run build
      - uses: nwtgck/actions-netlify@v3
        with:
          publish-dir: './dist'
          production-deploy: true
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

Deploy to AWS (S3 + CloudFront)

name: Deploy to AWS

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write          # Required for OIDC
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions
          aws-region: us-east-1

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci && npm run build

      - name: Sync to S3
        run: aws s3 sync dist/ s3://my-bucket --delete

      - name: Invalidate CloudFront cache
        run: |
          aws cloudfront create-invalidation \
            --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} \
            --paths "/*"

Security note: AWS OIDC authentication (shown above) is strongly preferred over storing long-lived access keys as secrets. OIDC generates short-lived tokens scoped to each workflow run.

6. Docker Image Builds and Registry Push

name: Docker Build and Push

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

This workflow builds a Docker image on every push to main and on version tags, pushes it to GitHub Container Registry (ghcr.io), and uses GitHub Actions cache for layer caching. The metadata action automatically generates semantic version tags from git tags.

⚙ Related: Learn more about containers in our Docker Containers Beginner's Guide and build compose files with our Docker Compose Validator.

Multi-Platform Builds

      - name: Set up QEMU (for cross-platform builds)
        uses: docker/setup-qemu-action@v3

      - name: Build and push (multi-arch)
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}

7. Secrets and Environment Variables

Repository Secrets

Store sensitive values in Settings > Secrets and variables > Actions. Reference them with the secrets context:

steps:
  - name: Deploy
    run: ./deploy.sh
    env:
      API_KEY: ${{ secrets.API_KEY }}
      DATABASE_URL: ${{ secrets.DATABASE_URL }}
      # Secrets are masked in logs - GitHub replaces them with ***

GitHub Environments

Environments add protection rules like required reviewers, wait timers, and deployment branches:

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging        # Uses staging-specific secrets
    steps:
      - run: echo "Deploying to ${{ vars.API_URL }}"
        # vars.API_URL is set per-environment in repo settings

  deploy-production:
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment:
      name: production
      url: https://myapp.com    # Shows in the PR deployment status
    steps:
      - run: echo "Deploying to production"
        env:
          API_KEY: ${{ secrets.PROD_API_KEY }}

GITHUB_TOKEN

Every workflow run automatically gets a GITHUB_TOKEN with permissions scoped to the repository. Control its permissions explicitly:

permissions:
  contents: read
  packages: write
  pull-requests: write         # Needed to comment on PRs
  issues: write                # Needed to create/update issues

# Use it in steps:
steps:
  - name: Comment on PR
    uses: actions/github-script@v7
    with:
      github-token: ${{ secrets.GITHUB_TOKEN }}
      script: |
        github.rest.issues.createComment({
          owner: context.repo.owner,
          repo: context.repo.repo,
          issue_number: context.issue.number,
          body: 'All checks passed! Ready for review.'
        })

8. Matrix Builds

Matrix strategies run your job across multiple configurations in parallel. Test across Node versions, operating systems, or any combination:

Node.js Matrix

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false         # Don't cancel other jobs if one fails
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20, 22]
        exclude:
          - os: macos-latest
            node-version: 18   # Skip Node 18 on macOS
        include:
          - os: ubuntu-latest
            node-version: 22
            experimental: true # Flag for custom logic

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm
      - run: npm ci
      - run: npm test

Python Matrix

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.10', '3.11', '3.12', '3.13']
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
      - run: pip install -r requirements.txt
      - run: pytest

Key options:

9. Caching Dependencies

Caching avoids downloading dependencies on every run, cutting minutes off your workflow. GitHub provides 10 GB of cache per repository.

npm (Built-in Cache)

# The setup-node action has built-in caching:
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: npm               # Caches ~/.npm based on package-lock.json
- run: npm ci                # Installs from cache when available

pip (Built-in Cache)

- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: pip               # Caches pip packages based on requirements.txt
- run: pip install -r requirements.txt

Gradle (Manual Cache)

- name: Cache Gradle packages
  uses: actions/cache@v4
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
      gradle-${{ runner.os }}-

- name: Build with Gradle
  run: ./gradlew build

Generic Cache Action

# Cache any directory with actions/cache
- uses: actions/cache@v4
  with:
    path: ~/.cache/my-tool
    key: my-tool-${{ runner.os }}-${{ hashFiles('config.lock') }}
    restore-keys: |
      my-tool-${{ runner.os }}-

Cache tips:

10. Reusable Workflows and Composite Actions

Reusable Workflows

Extract common workflow patterns into reusable templates that other workflows can call:

# .github/workflows/reusable-deploy.yml
name: Reusable Deploy

on:
  workflow_call:               # This makes it callable
    inputs:
      environment:
        required: true
        type: string
      app_version:
        required: true
        type: string
    secrets:
      deploy_key:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
      - run: |
          echo "Deploying version ${{ inputs.app_version }}"
          echo "to ${{ inputs.environment }}"
        env:
          DEPLOY_KEY: ${{ secrets.deploy_key }}
# .github/workflows/release.yml — caller workflow
name: Release

on:
  push:
    tags: ['v*']

jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      app_version: ${{ github.ref_name }}
    secrets:
      deploy_key: ${{ secrets.STAGING_DEPLOY_KEY }}

  deploy-production:
    needs: deploy-staging
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
      app_version: ${{ github.ref_name }}
    secrets:
      deploy_key: ${{ secrets.PROD_DEPLOY_KEY }}

Composite Actions

Bundle multiple steps into a single reusable action that you call with uses:

# .github/actions/setup-and-build/action.yml
name: 'Setup and Build'
description: 'Install deps and build the project'
inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '20'
runs:
  using: composite
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: npm
    - run: npm ci
      shell: bash
    - run: npm run build
      shell: bash
# Use it in a workflow:
steps:
  - uses: actions/checkout@v4
  - uses: ./.github/actions/setup-and-build
    with:
      node-version: '20'
  - run: npm test

When to use which:

11. Self-Hosted Runners

Self-hosted runners give you full control over hardware, OS, and software. Use them for GPU access, special hardware, persistent caches, or to avoid per-minute billing.

# Use self-hosted runner in a workflow:
jobs:
  build:
    runs-on: self-hosted       # Matches any self-hosted runner
    # Or be specific with labels:
    # runs-on: [self-hosted, linux, gpu]
    steps:
      - uses: actions/checkout@v4
      - run: nvidia-smi        # Access GPU on your machine
      - run: python train.py

Install a runner by downloading the binary from your repo's Settings > Actions > Runners, running ./config.sh with your repo URL and token, then sudo ./svc.sh install to run as a service.

Best practices: run in Docker or ephemeral VMs for isolation, never use self-hosted runners with public repos (anyone can fork and run code on your machine), assign custom labels for job routing, and use runner groups in organizations to control access.

12. Common Patterns

Concurrency — Cancel Redundant Runs

# Cancel previous runs of the same workflow on the same branch
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

# For production deploys, wait instead of canceling:
concurrency:
  group: deploy-production
  cancel-in-progress: false

Monorepo — Run Only Affected Packages

name: Monorepo CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      frontend: ${{ steps.filter.outputs.frontend }}
      backend: ${{ steps.filter.outputs.backend }}
      shared: ${{ steps.filter.outputs.shared }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            frontend:
              - 'packages/frontend/**'
            backend:
              - 'packages/backend/**'
            shared:
              - 'packages/shared/**'

  test-frontend:
    needs: detect-changes
    if: needs.detect-changes.outputs.frontend == 'true' || needs.detect-changes.outputs.shared == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: cd packages/frontend && npm ci && npm test

  test-backend:
    needs: detect-changes
    if: needs.detect-changes.outputs.backend == 'true' || needs.detect-changes.outputs.shared == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: cd packages/backend && npm ci && npm test

Conditional Deployment

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    # Only deploy on main branch pushes, not on PRs
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: ./deploy.sh

Automated Releases with Changelog

name: Release
on:
  push:
    tags: ['v*']
permissions:
  contents: write
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0       # Full history for changelog
      - name: Generate changelog
        id: changelog
        run: |
          PREV=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
          [ -n "$PREV" ] && RANGE="${PREV}..HEAD" || RANGE="HEAD~20..HEAD"
          CHANGES=$(git log $RANGE --pretty=format:"- %s (%h)" --no-merges)
          echo "changes<> $GITHUB_OUTPUT
          echo "$CHANGES" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT
      - uses: softprops/action-gh-release@v2
        with:
          body: |
            ## What's Changed
            ${{ steps.changelog.outputs.changes }}
          generate_release_notes: true

Service Containers for Integration Tests

jobs:
  integration-test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      redis:
        image: redis:7
        ports:
          - 6379:6379
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test:integration
        env:
          DATABASE_URL: postgres://postgres:testpass@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379
⚙ Related: For Docker networking in CI, see our Docker Networking Guide and learn Kubernetes deployments in Kubernetes: The Complete Guide.

13. Security Best Practices

# Security-hardened workflow example:
permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      # Pin to SHA instead of tag
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29  # v4
      - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8  # v4
        with:
          node-version: 20

14. Debugging Failed Workflows

# Enable debug logging via repository secrets:
# ACTIONS_RUNNER_DEBUG = true    (runner diagnostic logs)
# ACTIONS_STEP_DEBUG = true      (step debug output)

# Debug context in a step:
- run: |
    echo "Event: ${{ github.event_name }}"
    echo "Ref: ${{ github.ref }} | SHA: ${{ github.sha }}"
    echo "Actor: ${{ github.actor }} | Runner: ${{ runner.os }}"

# Interactive SSH debugging on failure:
- uses: mxschmitt/action-tmate@v3
  if: failure()
  timeout-minutes: 15

Common failure causes: permission denied (check permissions block), secret not found (secrets are unavailable from fork PRs), cache miss (verify hashFiles() glob patterns), timeouts (increase timeout-minutes), and rate limits (reduce parallelism with max-parallel).

Frequently Asked Questions

How much does GitHub Actions cost?

GitHub Actions is free for public repositories with unlimited minutes. For private repositories, free plans include 2,000 minutes per month, Pro plans include 3,000 minutes, and Team plans include 3,000 minutes. Additional minutes are billed per minute with different rates for Linux ($0.008/min), Windows ($0.016/min), and macOS ($0.08/min) runners. Self-hosted runners are always free regardless of plan. Storage for artifacts and caches is shared with GitHub Packages and has a 500 MB free tier on the free plan.

What is the difference between a job and a step in GitHub Actions?

A job is a set of steps that execute on the same runner (virtual machine). Jobs run in parallel by default unless you define dependencies with the needs keyword. Each job gets a fresh runner environment. A step is a single task within a job — either a shell command with run or a reusable action with uses. Steps within a job always run sequentially and share the same filesystem, environment variables, and working directory.

How do I pass secrets to GitHub Actions workflows?

Store secrets in your repository settings under Settings > Secrets and variables > Actions. Reference them in workflows using the secrets context: ${{ secrets.MY_SECRET }}. Secrets are encrypted at rest, masked in log output, and not passed to workflows triggered by pull requests from forks. For environment-specific secrets (staging vs production), create GitHub Environments and assign secrets to each one. Organization-level secrets can be shared across multiple repositories.

How can I speed up GitHub Actions workflows?

Use dependency caching with actions/cache or built-in cache options in setup actions (actions/setup-node has a cache parameter). Run independent jobs in parallel instead of sequentially. Use path filters to skip workflows when irrelevant files change. Use matrix strategies to parallelize testing across versions. Use concurrency groups with cancel-in-progress: true to cancel redundant runs when new commits are pushed. Consider larger runners for compute-heavy tasks.

Can I run GitHub Actions locally for testing?

Yes, use the open-source tool act (github.com/nektos/act) to run GitHub Actions workflows locally. Install it with brew install act or by downloading the binary, then run act in your repository root. It uses Docker to simulate the GitHub runner environment. Note that some features like secrets, GITHUB_TOKEN, and certain runner features may behave differently locally. You can pass secrets with act -s MY_SECRET=value or use a .secrets file.

Conclusion

GitHub Actions turns your repository into a complete CI/CD platform. Start with a simple workflow that runs your tests on every push, then layer on caching, matrix builds, and deployment steps as your project grows. The key principles that make workflows reliable are: keep jobs focused and fast, cache aggressively, use environments with protection rules for production, pin action versions for security, and set explicit permissions on every workflow.

For large teams, invest in reusable workflows and composite actions to standardize your CI/CD patterns across repositories. Use concurrency groups to avoid wasting runner minutes on outdated commits. And remember that the best workflow is one that runs fast enough that developers never think about skipping it.

Related Resources

Related Resources

Docker Containers Guide
Learn Docker from scratch with hands-on examples
Git Branching Strategies
Choose the right branching model for your CI/CD pipeline
YAML Validator
Validate your GitHub Actions workflow YAML files
Docker Commands Cheat Sheet
Quick reference for all essential Docker commands