Zero-downtime deployments with GitHub Actions

Spin is compatible with any CI/CD that can build Docker images and run SSH commands on a remote server. In this guide, we will walk you through how we do this with GitHub Actions

Important concepts

Zero-downtime deployments highly depend on your configuration with Docker. For a zero-downtime deployment to work, there are many things that need to align in order for this to happen:

  • A properly configured reverse proxy (like Traefik) and/or load balancer must be configured
  • This reverse proxy or load balancer must be able to access your container via the Docker Swarm Service
  • Container healthchecks must be implemented
  • Healthchecks must have an accurate definition of "healthy"
  • Container update configurations must be properly set
  • A CI/CD runner must be configured to build a container, upload it to a registry, and the deployment process

Spin takes care of all this for you when you run spin new or spin init. We give you templates with everything above to help you get started.

GitHub Actions: Zero-downtime Deployment

Deployment checklist with GitHub Actions

If you're using our templates, you'll need to add some secrets to your GitHub Actions environment. Your GitHub plan must have GitHub Environments available.

GitHub Actions Secrets

GitHub Actions Environment Secrets

👉 Required Secrets To Be Added

The following secrets are required to be set, based on the template that you're using.

SecretUsed In TemplateDescription
DEPLOYMENT_SSH_PRIVATE_KEYallThe private key value for your deploy user.
DEPLOYMENT_SSH_HOSTNAMEallThe DNS hostname of your server (example server01.example.com)
DB_ROOT_PASSWORDlaravelThe root password for your database instance.
DB_NAMElaravelThe name of the database you want to use for your application
DB_USERNAMElaravelThe username of the database user.
DB_PASSWORDlaravelThe password for the database user.
ENV_FILE_BASE64laravelThe base64 value of spin vault encode of your ENV file.

Default Triggering Action

By default, our actions only trigger a production deployment on a GitHub release only. You can change this to any GitHub event you'd like.

Example: action_deploy-production.yml

name: Production Deployment
on:
  release:
    types:
      - released

########################################################################
# 🚨 WARNING: You must set the following secrets in GitHub:
#
# - DEPLOYMENT_SSH_PRIVATE_KEY
# - DEPLOYMENT_SSH_HOSTNAME
# - DB_ROOT_PASSWORD
# - DB_NAME
# - DB_USERNAME
# - DB_PASSWORD
# - ENV_FILE_BASE64
#
# Ensure these secrets match the environment you're deploying to.
# https://github.com/<your-organization>/<your-repo>/settings/environments
########################################################################

# 👇 Set these variables to match your application needs. Most of them should work great by default.
env:
  DEPLOYMENT_URL_HOSTNAME: example.com
  DEPLOYMENT_URL: https://example.com

jobs:
  build:
    uses: ./.github/workflows/service_docker-build-and-publish.yml
    with:
      # 👇 Ensure these are the tags you want to publish to your registry.
      docker-tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }},ghcr.io/${{ github.repository }}:latest
      environment: production # 👈 Make sure you created this environment in GitHub with the secrets above.
    secrets: inherit
  
  deploy:
    needs: build
    runs-on: ubuntu-22.04
    environment:
      name: production # 👈 Make sure you created this environment in GitHub with the secrets above.
      url: "${{ env.DEPLOYMENT_URL }}"
    steps:

      - name: Get project name from repository name.
        run: | 
          echo "PROJECT_NAME=${GITHUB_REPOSITORY#*/}" >> $GITHUB_ENV

      - uses: serversideup/github-action-docker-swarm-deploy@v1
        with:
          # 👇 Ensure these are correct and that you've set the appropriate secrets.
          deployment_ssh_private_key: "${{ secrets.DEPLOYMENT_SSH_PRIVATE_KEY }}"
          remote_ssh_server_hostname: "${{ secrets.DEPLOYMENT_SSH_HOSTNAME }}"
          registry: "ghcr.io"
          registry-username: "${{ github.actor }}"
          registry-token: "${{ secrets.GITHUB_TOKEN }}"
          stack_name: "${{ env.PROJECT_NAME }}"
        env:
          # 👇 Ensure this makes sense for your application.
          TRAEFIK_HOST_RULE: "Host(`${{ env.DEPLOYMENT_URL_HOSTNAME }}`)"
          DB_ROOT_PASSWORD: "${{ secrets.DB_ROOT_PASSWORD }}"
          DB_NAME: "${{ secrets.DB_NAME }}"
          DB_USERNAME: "${{ secrets.DB_USERNAME }}"
          DB_PASSWORD: "${{ secrets.DB_PASSWORD }}"
          DEPLOYMENT_IMAGE_PHP: "ghcr.io/${{ github.repository }}:${{ github.ref_name }}"

Example: service_docker-build-and-publish.yml

on:
  workflow_call:
    inputs:
      platforms:
        type: string
        default: 'linux/amd64'
      docker-tags:
        required: true
        type: string
      dockerfile:
        type: string
        default: './Dockerfile'
      target:
        type: string
        default: ''
      environment:
        type: string
        required: true

env:
  DOCKER_COMPOSE_CMD: docker compose -f docker-compose.yml -f docker-compose.ci.yml

jobs:
  docker-publish:
    runs-on: ubuntu-22.04
    environment:
      name: ${{ inputs.environment }}
    steps:

      - name: Checkout
        uses: actions/checkout@v4

      - name: Restore composer cache (if available)
        id: composer-vendor-restore
        uses: actions/cache/restore@v3
        with:
          path: vendor/
          key: ${{ runner.os }}-composer-vendor-${{ hashFiles('composer.lock') }}

      - if: ${{ steps.composer-vendor-restore.outputs.cache-hit != 'true' }}
        name: List the composer packages
        continue-on-error: true
        run: |
          $DOCKER_COMPOSE_CMD \
          run \
          php \
          composer show --locked
          
      - if: ${{ steps.composer-vendor-restore.outputs.cache-hit != 'true' }}
        name: Install Composer dependencies
        run: |
          $DOCKER_COMPOSE_CMD \
          run \
          php \
          composer install --optimize-autoloader --no-interaction --no-progress --no-ansi
      
      - name: Set env file
        run: |
          echo $BASE_64_SECRET | base64 -d > .env
          chmod 600 .env
        env:
          BASE_64_SECRET: ${{ secrets.ENV_FILE_BASE64 }}
      
      - name: docker-build-action
        uses: serversideup/github-action-docker-build@v5
        with:
          tags: "${{ inputs.docker-tags }}"
          dockerfile: "${{ inputs.dockerfile }}"
          registry: "ghcr.io"
          registry-username: "${{ github.actor }}"
          registry-token: "${{ secrets.GITHUB_TOKEN }}"
          platforms: "${{ inputs.platforms }}"
          target: "${{ inputs.target }}"

Using your own GitHub Action

Spin does not require you to use our template. You can put use whatever you'd like for your deployment process.

The only thing that will be very helpful to include on your side is the GitHub Action runner's ability to connect to your server over SSH and run the deployment. We created an open source GitHub Action called serversideup/docker-swarm-deploy-github-action.

You can see that being used below:

  # Rest of GitHub Actions file
    steps:

      - name: Get project name from repository name.
        run: | 
          echo "PROJECT_NAME=${GITHUB_REPOSITORY#*/}" >> $GITHUB_ENV

      - uses: serversideup/github-action-docker-swarm-deploy@v1
        with:
          # 👇 Ensure these are correct and that you've set the appropriate secrets.
          deployment_ssh_private_key: "${{ secrets.DEPLOYMENT_SSH_PRIVATE_KEY }}"
          remote_ssh_server_hostname: "${{ secrets.DEPLOYMENT_SSH_HOSTNAME }}"
          registry: "ghcr.io"
          registry-username: "${{ github.actor }}"
          registry-token: "${{ secrets.GITHUB_TOKEN }}"
          stack_name: "${{ env.PROJECT_NAME }}"
        env:
          # 👇 Ensure this makes sense for your application.
          TRAEFIK_HOST_RULE: "Host(`${{ env.DEPLOYMENT_URL_HOSTNAME }}`)"
          DB_ROOT_PASSWORD: "${{ secrets.DB_ROOT_PASSWORD }}"
          DB_NAME: "${{ secrets.DB_NAME }}"
          DB_USERNAME: "${{ secrets.DB_USERNAME }}"
          DB_PASSWORD: "${{ secrets.DB_PASSWORD }}"
          DEPLOYMENT_IMAGE_PHP: "ghcr.io/${{ github.repository }}:${{ github.ref_name }}"

Security Considerations

Be aware that you're taking a sensitive deployment key, putting that into GitHub actions, and allowing SSH connections from anywhere to connect to your production server. If you want to further harden your server, you may consider:

  • Deploying your own Self-hosted GitHub Runner
  • Locking down SSH access to your server from specific IP addresses