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.
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.
Be sure you're adding the variable names as secrets (not environment variables)
👉 Required Secrets To Be Added
The following secrets are required to be set, based on the template that you're using.
Secret | Used In Template | Description |
---|---|---|
DEPLOYMENT_SSH_PRIVATE_KEY | all | The private key value for your deploy user. |
DEPLOYMENT_SSH_HOSTNAME | all | The DNS hostname of your server (example server01.example.com ) |
DB_ROOT_PASSWORD | laravel | The root password for your database instance. |
DB_NAME | laravel | The name of the database you want to use for your application |
DB_USERNAME | laravel | The username of the database user. |
DB_PASSWORD | laravel | The password for the database user. |
ENV_FILE_BASE64 | laravel | The 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