Files
aliasvault/.github/workflows/release-docker.yml
2026-07-24 21:24:39 +02:00

395 lines
17 KiB
YAML

name: Release Docker Images
# Docker image builds, split out of the main Release workflow because they take by far the
# longest and are the only part that benefits from building on our own hardware.
on:
release:
types: [published]
workflow_dispatch:
inputs:
version_tag:
description: 'Version tag for Docker images (e.g., 0.26.2). Auto-detected from release/* branch name if empty.'
required: false
type: string
tag_latest:
description: 'Also tag images as :latest'
required: false
default: false
type: boolean
build_multi_container:
description: 'Build and push multi-container images'
required: false
default: true
type: boolean
build_all_in_one:
description: 'Build and push all-in-one image'
required: false
default: true
type: boolean
runner:
description: 'Where to build. "self-hosted" needs an online runner labelled "docker-build" -- if it is offline the job queues for up to 24h and then fails. Release events always use GitHub-hosted.'
required: false
default: github-hosted
type: choice
options:
- github-hosted
- self-hosted
jobs:
# Guard job to prevent releases from main branch
valid-release:
if: github.event_name == 'release'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Check tag target
run: |
BRANCHES=$(git branch -r --contains $GITHUB_SHA)
echo "Tag is contained in:"
echo "$BRANCHES"
if ! echo "$BRANCHES" | grep -q "origin/release/"; then
echo "❌ Releases must come from a release/* branch, please recreate the release from a release branch"
exit 1
fi
echo "✅ Tag is on a release branch"
# Detect version from input, branch name, or release tag
detect-version:
if: always() && (github.event_name != 'release' || needs.valid-release.result == 'success')
needs: [valid-release]
runs-on: ubuntu-latest
outputs:
version: ${{ steps.detect.outputs.version }}
tag_latest: ${{ steps.detect.outputs.tag_latest }}
all_in_one_exists: ${{ steps.check-images.outputs.all_in_one_exists }}
multi_container_exists: ${{ steps.check-images.outputs.multi_container_exists }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
sparse-checkout: .github/actions/detect-release-version
- name: Detect version
id: detect
uses: ./.github/actions/detect-release-version
with:
version_tag: ${{ inputs.version_tag }}
tag_latest: ${{ inputs.tag_latest }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Check if Docker images already exist
id: check-images
run: |
VERSION="${{ steps.detect.outputs.version }}"
if [ -z "$VERSION" ]; then
echo "all_in_one_exists=false" >> $GITHUB_OUTPUT
echo "multi_container_exists=false" >> $GITHUB_OUTPUT
echo "No version specified, images will be built"
exit 0
fi
# Check all-in-one image
if docker manifest inspect ghcr.io/aliasvault/aliasvault:${VERSION} > /dev/null 2>&1; then
echo "all_in_one_exists=true" >> $GITHUB_OUTPUT
echo "✅ All-in-one image ghcr.io/aliasvault/aliasvault:${VERSION} exists"
else
echo "all_in_one_exists=false" >> $GITHUB_OUTPUT
echo "❌ All-in-one image does not exist"
fi
# Check multi-container images (check a few key ones)
MULTI_IMAGES=(
"ghcr.io/aliasvault/api"
"ghcr.io/aliasvault/client"
"ghcr.io/aliasvault/admin"
)
MULTI_EXISTS=true
for IMAGE in "${MULTI_IMAGES[@]}"; do
if ! docker manifest inspect ${IMAGE}:${VERSION} > /dev/null 2>&1; then
echo "❌ Multi-container image ${IMAGE}:${VERSION} does not exist"
MULTI_EXISTS=false
break
fi
echo "✅ ${IMAGE}:${VERSION} exists"
done
echo "multi_container_exists=${MULTI_EXISTS}" >> $GITHUB_OUTPUT
build-and-push-docker-multi-container:
needs: [valid-release, detect-version]
# Skip if images already exist during release (just tag them as latest instead), but allow manual rebuild via workflow_dispatch
if: always() && (github.event_name != 'release' || needs.valid-release.result == 'success') && (github.event_name == 'release' || inputs.build_multi_container) && (github.event_name != 'release' || needs.detect-version.outputs.multi_container_exists != 'true')
runs-on: ${{ inputs.runner == 'self-hosted' && 'docker-build' || 'ubuntu-latest' }}
permissions:
contents: read
packages: write
strategy:
# Don't cancel the other images if one fails.
fail-fast: false
# On GitHub-hosted runners: cap at 2 concurrent runner slots. The heavy Client build
# (~55 min: Rust→WASM + wasm-tools + WASM publish) holds one slot while the remaining
# light images cycle through the second slot (~35-45 min total). This frees up the rest
# of the CI worker pool for other workflows.
# On self-hosted: 1, since a single runner service handles one job at a time anyway. The
# persistent buildx cache carries the shared .NET layers from one image to the next.
# Raise this only if the machine runs multiple runner services.
max-parallel: ${{ inputs.runner == 'self-hosted' && 1 || 2 }}
matrix:
# Client is listed first so it claims a slot immediately and runs alongside the rest of the images.
include:
- image: client
dockerfile: apps/server/AliasVault.Client/Dockerfile
title: AliasVault Client
description: Blazor WebAssembly client UI for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.com)
- image: postgres
dockerfile: apps/server/Databases/AliasServerDb/Dockerfile
title: AliasVault PostgreSQL
description: PostgreSQL database for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.com)
- image: api
dockerfile: apps/server/AliasVault.Api/Dockerfile
title: AliasVault API
description: REST API backend for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.com)
- image: admin
dockerfile: apps/server/AliasVault.Admin/Dockerfile
title: AliasVault Admin
description: Admin portal for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.com)
- image: reverse-proxy
dockerfile: apps/server/Dockerfile
title: AliasVault Reverse Proxy
description: Nginx reverse proxy for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.com)
- image: smtp
dockerfile: apps/server/Services/AliasVault.SmtpService/Dockerfile
title: AliasVault SMTP Service
description: SMTP service for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.com)
- image: task-runner
dockerfile: apps/server/Services/AliasVault.TaskRunner/Dockerfile
title: AliasVault TaskRunner
description: Background task runner for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.com)
- image: installcli
dockerfile: apps/server/Utilities/AliasVault.InstallCli/Dockerfile
title: AliasVault Install CLI
description: Installation and configuration CLI for AliasVault. Used by install.sh for setup and configuration, not deployed as part of the application stack
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx (GitHub-hosted)
if: runner.environment == 'github-hosted'
uses: docker/setup-buildx-action@v4
# On self-hosted we reuse a named builder across runs so its build cache survives, which is
# what makes building the images back-to-back on a single machine fast. setup-buildx-action
# deletes its builder in a post-step, throwing away exactly that cache.
- name: Set up Docker Buildx (self-hosted)
if: runner.environment == 'self-hosted'
run: |
docker buildx inspect aliasvault >/dev/null 2>&1 \
|| docker buildx create --name aliasvault --driver docker-container --bootstrap
docker buildx use aliasvault
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v6
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ghcr.io/aliasvault/${{ matrix.image }}
tags: |
type=raw,value=${{ needs.detect-version.outputs.version }},enable=${{ needs.detect-version.outputs.version != '' }}
type=raw,value=latest,enable=${{ needs.detect-version.outputs.tag_latest == 'true' }}
type=ref,event=branch,enable=${{ needs.detect-version.outputs.version == '' }}
type=raw,value={{branch}}-{{sha}},enable=${{ needs.detect-version.outputs.version == '' }}
labels: |
org.opencontainers.image.title=${{ matrix.title }}
org.opencontainers.image.description=${{ matrix.description }}
annotations: |
org.opencontainers.image.description=${{ matrix.description }}
- name: Build and push image
uses: docker/build-push-action@v7
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64,linux/arm64/v8
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
# Keep the persistent builder cache from growing without bound on our own hardware.
- name: Prune stale build cache
if: always() && runner.environment == 'self-hosted'
run: docker buildx prune --force --filter until=168h
build-and-push-docker-all-in-one:
needs: [valid-release, detect-version]
# Skip if images already exist during release (just tag them as latest instead), but allow manual rebuild via workflow_dispatch
if: always() && (github.event_name != 'release' || needs.valid-release.result == 'success') && (github.event_name == 'release' || inputs.build_all_in_one) && (github.event_name != 'release' || needs.detect-version.outputs.all_in_one_exists != 'true')
runs-on: ${{ inputs.runner == 'self-hosted' && 'docker-build' || 'ubuntu-latest' }}
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
# Free up disk space of unused tools on the GitHub runner as the all-in-one build consumes a
# lot of disk space. Never do this on self-hosted: it would delete the machine's own
# toolchains and wipe the build cache we rely on.
- name: Free up disk space
if: runner.environment == 'github-hosted'
run: |
echo "Disk before cleanup:"
df -h /
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
sudo rm -rf /usr/local/share/boost "$AGENT_TOOLSDIRECTORY"
sudo docker image prune --all --force || true
echo "Disk after cleanup:"
df -h /
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx (GitHub-hosted)
if: runner.environment == 'github-hosted'
uses: docker/setup-buildx-action@v4
- name: Set up Docker Buildx (self-hosted)
if: runner.environment == 'self-hosted'
run: |
docker buildx inspect aliasvault >/dev/null 2>&1 \
|| docker buildx create --name aliasvault --driver docker-container --bootstrap
docker buildx use aliasvault
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata for all-in-one image
id: meta
uses: docker/metadata-action@v6
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: |
ghcr.io/aliasvault/aliasvault
aliasvault/aliasvault
tags: |
type=raw,value=${{ needs.detect-version.outputs.version }},enable=${{ needs.detect-version.outputs.version != '' }}
type=raw,value=latest,enable=${{ needs.detect-version.outputs.tag_latest == 'true' }}
type=ref,event=branch,enable=${{ needs.detect-version.outputs.version == '' }}
type=raw,value={{branch}}-{{sha}},enable=${{ needs.detect-version.outputs.version == '' }}
labels: |
org.opencontainers.image.title=AliasVault All-in-One
org.opencontainers.image.description=Self-contained AliasVault server including web app, with all services bundled using s6-overlay. Single container solution for easy deployment (see docs.aliasvault.com).
annotations: |
org.opencontainers.image.description=Self-contained AliasVault server including web app, with all services bundled using s6-overlay. Single container solution for easy deployment (see docs.aliasvault.com).
- name: Build and push all-in-one image
uses: docker/build-push-action@v7
with:
context: .
file: dockerfiles/all-in-one/Dockerfile
platforms: linux/amd64,linux/arm64/v8
# Mark this as the official published image (local builds default to "aio-build").
build-args: |
DEPLOYMENT_MODE=aio
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
- name: Prune stale build cache
if: always() && runner.environment == 'self-hosted'
run: docker buildx prune --force --filter until=168h
# Tag existing images as :latest without rebuilding (for release events when images already exist)
tag-docker-images-latest:
needs: [valid-release, detect-version]
if: github.event_name == 'release' && needs.valid-release.result == 'success' && needs.detect-version.outputs.tag_latest == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Tag all-in-one images as latest
if: needs.detect-version.outputs.all_in_one_exists == 'true'
run: |
VERSION="${{ needs.detect-version.outputs.version }}"
echo "Tagging ghcr.io/aliasvault/aliasvault:${VERSION} as latest..."
docker buildx imagetools create -t ghcr.io/aliasvault/aliasvault:latest ghcr.io/aliasvault/aliasvault:${VERSION}
docker buildx imagetools create -t aliasvault/aliasvault:latest aliasvault/aliasvault:${VERSION}
- name: Tag multi-container images as latest
if: needs.detect-version.outputs.multi_container_exists == 'true'
run: |
VERSION="${{ needs.detect-version.outputs.version }}"
IMAGES=(
"ghcr.io/aliasvault/postgres"
"ghcr.io/aliasvault/api"
"ghcr.io/aliasvault/client"
"ghcr.io/aliasvault/admin"
"ghcr.io/aliasvault/reverse-proxy"
"ghcr.io/aliasvault/smtp"
"ghcr.io/aliasvault/task-runner"
"ghcr.io/aliasvault/installcli"
)
for IMAGE in "${IMAGES[@]}"; do
if docker manifest inspect ${IMAGE}:${VERSION} > /dev/null 2>&1; then
echo "Tagging ${IMAGE}:${VERSION} as latest..."
docker buildx imagetools create -t ${IMAGE}:latest ${IMAGE}:${VERSION}
else
echo "⚠️ Skipping ${IMAGE}:${VERSION} (does not exist)"
fi
done