Automated Builds
Automate your Docker builds so every release is pushed to the Self-Host Pro registry without manual intervention.
Release strategy
A common pattern uses three release channels:
| Channel | Trigger | Tags | Use case |
|---|---|---|---|
| Edge | Push to main | edge-main | Continuous deployment for testing |
| Prerelease | GitHub prerelease | prerelease, prerelease-v1.0.0-beta.1 | Beta testing with select customers |
| Stable | GitHub release | latest, 1.2.0, 1 | Production releases |
This gives you flexibility — push to main for instant edge builds, create a prerelease for beta feedback, then publish a stable release when ready.
Reusable workflow pattern
Instead of duplicating build logic, create a reusable workflow that handles the actual build, then call it from simple trigger workflows.
Build workflow
This workflow handles dependency caching, asset compilation, and pushing to the registry:
name: Docker Build & Publish
on:
workflow_call:
inputs:
docker-tags:
required: true
type: string
description: "Comma or newline separated list of Docker tags"
dockerfile:
type: string
default: "./Dockerfile"
environment:
type: string
required: true
version:
type: string
required: false
jobs:
docker-publish:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
# Add your build steps here (dependency install, asset compilation, etc.)
- name: Build and push
uses: serversideup/github-action-docker-build@v6
with:
tags: ${{ inputs.docker-tags }}
dockerfile: ${{ inputs.dockerfile }}
registry: "shpcr.io"
registry-username: ${{ secrets.SHPCR_USERNAME }}
registry-password: ${{ secrets.SHPCR_PASSWORD }}
Trigger workflows
Each release channel gets a simple workflow that calls the build workflow with appropriate tags. Replace your-org/your-app with your actual image path from the Docker Registry.
name: Edge Release
on:
push:
branches:
- main
jobs:
build:
uses: ./.github/workflows/service_docker-build-and-publish.yml
with:
docker-tags: "shpcr.io/your-org/your-app:edge-${{ github.ref_name }}"
environment: edge
version: "edge-${{ github.ref_name }}"
secrets: inherit
name: Pre-Release
on:
release:
types: [prereleased]
jobs:
build:
uses: ./.github/workflows/service_docker-build-and-publish.yml
with:
docker-tags: |
shpcr.io/your-org/your-app:prerelease
shpcr.io/your-org/your-app:prerelease-${{ github.ref_name }}
environment: prerelease
version: "${{ github.ref_name }}"
secrets: inherit
name: Stable Release
on:
release:
types: [released]
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.set-version.outputs.version }}
major: ${{ steps.set-version.outputs.major }}
steps:
- name: Parse version from tag
id: set-version
run: |
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#v}" # Strip 'v' prefix: v1.2.0 → 1.2.0
MAJOR="${VERSION%%.*}" # Extract major: 1.2.0 → 1
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "major=${MAJOR}" >> $GITHUB_OUTPUT
build:
needs: prepare
uses: ./.github/workflows/service_docker-build-and-publish.yml
with:
docker-tags: |
shpcr.io/your-org/your-app:latest
shpcr.io/your-org/your-app:${{ needs.prepare.outputs.version }}
shpcr.io/your-org/your-app:${{ needs.prepare.outputs.major }}
environment: production
version: "${{ needs.prepare.outputs.version }}"
secrets: inherit
How stable release versioning works
The stable release workflow is split into two jobs because GitHub Actions can't parse outputs and use them in the same job — the prepare job runs first, then passes values to build.
When you publish a GitHub release tagged v1.2.0, here's what happens:
github.ref_namegives you the raw tag:v1.2.0VERSION="${VERSION#v}"strips thevprefix →1.2.0MAJOR="${VERSION%%.*}"extracts everything before the first dot →1- Those two values are passed as outputs to the
buildjob
The result is three tags pushed to the registry in one build:
| Tag | Value | Purpose |
|---|---|---|
latest | Always latest | Default pull for new installs |
1.2.0 | Full semver | Pin to an exact release |
1 | Major version only | Stay on major, get patch updates |
Customers pinned to 1 automatically receive 1.2.0, 1.3.0, etc. — but not a breaking 2.0.0. Customers who need stability can pin to 1.2.0 and update manually.
Required secrets
In your GitHub repo, go to Settings → Secrets and variables → Actions and add:
| Secret | Value |
|---|---|
SHPCR_USERNAME | Your Self-Host Pro email |
SHPCR_PASSWORD | Your team access token |
See Docker Registry for how to generate an access token.
Version embedding
Pass version from the trigger workflow to embed the release version into your image at build time. In the reusable build workflow, use it to stamp your app's version file before the Docker build step:
- name: Embed version
if: inputs.version != ''
run: |
jq '.version = "${{ inputs.version }}"' composer.json > composer.json.tmp
mv composer.json.tmp composer.json
The example above is Laravel-specific (composer.json). Adapt it for your stack:
| Stack | Where to write the version |
|---|---|
| Node.js | package.json via jq |
| Python | A VERSION file or pyproject.toml |
| Go | A version.go constant or ldflags at build time |
| Any | A plain VERSION file your app reads at startup |
Other CI/CD systems
The same pattern works with any CI/CD system — GitLab CI, CircleCI, Jenkins, etc. The core steps are:
- Log in to the registry with
docker login shpcr.io - Build your image
- Tag with appropriate version(s)
- Push to
shpcr.io
The workflows above are just orchestration around standard Docker commands.