feat(ci): add Docker build pipeline with version management

- Add multi-stage Dockerfile with pinned tool versions (Node 24.10.0, Go 1.25.1, Hugo 0.151.0)
- Create .env.versions as single source of truth for all tool versions
- Add GitHub Actions CI workflow for automated OCI image builds
  - Multi-arch support (amd64, arm64)
  - Automatic version loading from .env.versions
  - Docker registry push with metadata tags
- Add Taskfile tasks for local OCI image building and testing
  - task build:oci-image - Build with version-pinned dependencies
  - task test:oci-image - Build and test container locally
- Pin devbox.json to specific versions matching .env.versions
- Add comprehensive documentation (DOCKER.md, VERSIONS.md)
- Add helper script (scripts/get-versions.sh) for version extraction

This enables consistent development and production environments with
identical tool versions across local devbox, Docker builds, and CI/CD.
This commit is contained in:
Stephan Lo 2025-10-23 17:04:28 +02:00
parent 8e0aea2893
commit 4294524e81
9 changed files with 406 additions and 3 deletions

19
.dockerignore Normal file
View file

@ -0,0 +1,19 @@
.github
.gitignore
.vscode
.devbox
.devcontainer
node_modules
public
resources
tmp
*.md
!content/**/*.md
TESTING.md
Taskfile.yml
devbox.json
devbox.lock
.hugo_build.lock
.htmltest.yml
.htmlvalidate.json
.markdownlint.json

9
.env.versions Normal file
View file

@ -0,0 +1,9 @@
# Tool versions for development and CI/CD
# These versions are used in:
# - devbox.json (pinned versions)
# - Dockerfile (build arguments)
# - .github/workflows/ci.yaml (CI/CD pipeline)
NODE_VERSION=24.10.0
GO_VERSION=1.25.1
HUGO_VERSION=0.151.0

80
.github/workflows/ci.yaml vendored Normal file
View file

@ -0,0 +1,80 @@
name: ci
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Load versions from .env.versions
id: versions
run: |
# Source the versions file
set -a
source .env.versions
set +a
echo "node_version=${NODE_VERSION}" >> "$GITHUB_OUTPUT"
echo "go_version=${GO_VERSION}" >> "$GITHUB_OUTPUT"
echo "hugo_version=${HUGO_VERSION}" >> "$GITHUB_OUTPUT"
echo "Node: ${NODE_VERSION}"
echo "Go: ${GO_VERSION}"
echo "Hugo: ${HUGO_VERSION}"
- name: Repository meta
id: repository
run: |
registry=${{ github.server_url }}
registry=${registry##http*://}
echo "registry=${registry}" >> "$GITHUB_OUTPUT"
echo "registry=${registry}"
repository="$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')"
echo "repository=${repository}" >> "$GITHUB_OUTPUT"
echo "repository=${repository}"
- name: Docker meta
uses: docker/metadata-action@v5
id: docker
with:
images: ${{ steps.repository.outputs.registry }}/${{ steps.repository.outputs.repository }}
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ steps.repository.outputs.registry }}
username: "${{ secrets.PACKAGES_USER }}"
password: "${{ secrets.PACKAGES_TOKEN }}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
buildkitd-flags: '--allow-insecure-entitlement network.host'
driver-opts: network=host
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
allow: network.host
network: host
platforms: linux/amd64,linux/arm64
tags: ${{ steps.docker.outputs.tags }}
labels: ${{ steps.docker.outputs.labels }}
build-args: |
NODE_VERSION=${{ steps.versions.outputs.node_version }}
GO_VERSION=${{ steps.versions.outputs.go_version }}
HUGO_VERSION=${{ steps.versions.outputs.hugo_version }}

98
DOCKER.md Normal file
View file

@ -0,0 +1,98 @@
# Docker Build
This project uses a multi-stage Docker build that matches the local devbox development environment.
## Version Management
All tool versions are defined in `.env.versions` as the single source of truth:
```bash
NODE_VERSION=24.10.0
GO_VERSION=1.25.1
HUGO_VERSION=0.151.0
```
These versions are used in:
- `devbox.json` - Local development environment
- `Dockerfile` - Docker build arguments (with defaults)
- `.github/workflows/ci.yaml` - CI/CD pipeline
**Important:** When updating versions, modify `.env.versions` and sync with `devbox.json`.
## Local Build
### Using Task (recommended)
The easiest way to build the OCI image:
```bash
task build:oci-image
```
This automatically:
- Loads versions from `.env.versions`
- Builds the image with correct build arguments
- Tags with `latest` and git commit hash
To build and test:
```bash
task test:oci-image
```
### Automatic version loading
Use the helper script to load versions from `.env.versions`:
```bash
source scripts/get-versions.sh
```
This will show you the Docker build command with the correct versions.
### Manual build
```bash
docker build --network=host \
--build-arg NODE_VERSION=24.10.0 \
--build-arg GO_VERSION=1.25.1 \
--build-arg HUGO_VERSION=0.151.0 \
-t ipceicis-developerframework:latest .
```
### Test the image
```bash
docker run -d -p 8080:80 --name hugo-test ipceicis-developerframework:latest
curl http://localhost:8080
docker stop hugo-test && docker rm hugo-test
```
## CI/CD Pipeline
The GitHub Actions workflow (`.github/workflows/ci.yaml`) automatically:
1. Extracts versions from devbox environment
2. Builds multi-arch images (amd64 + arm64)
3. Pushes to the container registry with appropriate tags
### Required Secrets
Configure these secrets in your GitHub repository:
- `PACKAGES_USER`: Container registry username
- `PACKAGES_TOKEN`: Container registry token/password
## Image Structure
- **Build Stage**: Uses Node.js base image, installs Go and Hugo
- **Runtime Stage**: Uses nginx:alpine to serve static content (~50MB)
The build process:
1. Installs npm dependencies
2. Downloads Hugo modules
3. Builds static site with `hugo --gc --minify`
4. Copies built site to minimal nginx container

66
Dockerfile Normal file
View file

@ -0,0 +1,66 @@
# Build arguments for version pinning (matching devbox.json)
ARG NODE_VERSION=24.10.0
ARG GO_VERSION=1.25.1
ARG HUGO_VERSION=0.151.0
# Build stage - use same versions as local devbox environment
FROM node:${NODE_VERSION}-bookworm AS builder
# Install Git (needed for Hugo's enableGitInfo)
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
# Install Go
ARG GO_VERSION
RUN wget -q https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz && \
tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz && \
rm go${GO_VERSION}.linux-amd64.tar.gz
ENV PATH="/usr/local/go/bin:${PATH}"
ENV GOPATH="/go"
ENV PATH="${GOPATH}/bin:${PATH}"
# Install Hugo extended
ARG HUGO_VERSION
RUN wget -q https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz && \
tar -xzf hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz && \
mv hugo /usr/local/bin/ && \
rm hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz && \
hugo version
WORKDIR /src
# Copy package files and install npm dependencies
COPY package*.json ./
RUN npm ci
# Copy all source files
COPY . .
# Build Hugo site (Git info wird aus dem aktuellen Kontext genommen, nicht aus .git)
# Hugo sucht nach .git, findet es nicht, und überspringt Git-Info automatisch
RUN hugo --gc --minify
# Runtime stage - nginx to serve static content
FROM nginx:1.27-alpine
# Copy built site from builder
COPY --from=builder /src/public /usr/share/nginx/html
# Copy custom nginx config
RUN echo 'server {' > /etc/nginx/conf.d/default.conf && \
echo ' listen 80;' >> /etc/nginx/conf.d/default.conf && \
echo ' server_name _;' >> /etc/nginx/conf.d/default.conf && \
echo ' root /usr/share/nginx/html;' >> /etc/nginx/conf.d/default.conf && \
echo ' index index.html;' >> /etc/nginx/conf.d/default.conf && \
echo '' >> /etc/nginx/conf.d/default.conf && \
echo ' location / {' >> /etc/nginx/conf.d/default.conf && \
echo ' try_files $uri $uri/ /index.html;' >> /etc/nginx/conf.d/default.conf && \
echo ' }' >> /etc/nginx/conf.d/default.conf && \
echo '' >> /etc/nginx/conf.d/default.conf && \
echo ' gzip on;' >> /etc/nginx/conf.d/default.conf && \
echo ' gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;' >> /etc/nginx/conf.d/default.conf && \
echo '}' >> /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View file

@ -86,3 +86,37 @@ tasks:
desc: Run CI pipeline locally desc: Run CI pipeline locally
deps: deps:
- test - test
build:oci-image:
desc: Build OCI/Docker image with versions from .env.versions
cmds:
- |
set -a
source .env.versions
set +a
echo "Building OCI image with versions:"
echo " NODE_VERSION=${NODE_VERSION}"
echo " GO_VERSION=${GO_VERSION}"
echo " HUGO_VERSION=${HUGO_VERSION}"
docker build --network=host \
--build-arg NODE_VERSION=${NODE_VERSION} \
--build-arg GO_VERSION=${GO_VERSION} \
--build-arg HUGO_VERSION=${HUGO_VERSION} \
-t ipceicis-developerframework:latest \
-t ipceicis-developerframework:$(git rev-parse --short HEAD) \
.
test:oci-image:
desc: Test the built OCI image
deps:
- build:oci-image
cmds:
- |
echo "Starting container on port 8080..."
docker run -d -p 8080:80 --name hugo-test ipceicis-developerframework:latest
sleep 2
echo "Testing endpoint..."
curl -f http://localhost:8080 > /dev/null && echo "✓ Container is running and responding" || echo "✗ Container test failed"
echo "Cleaning up..."
docker stop hugo-test
docker rm hugo-test

69
VERSIONS.md Normal file
View file

@ -0,0 +1,69 @@
# Version Management
## Single Source of Truth: `.env.versions`
All tool versions are centrally managed in `.env.versions`:
```bash
NODE_VERSION=24.10.0
GO_VERSION=1.25.1
HUGO_VERSION=0.151.0
```
## Where are versions used?
1. **devbox.json** - Local development environment (manual sync required)
2. **Dockerfile** - Build arguments with defaults
3. **.github/workflows/ci.yaml** - CI/CD pipeline (automatic)
4. **scripts/get-versions.sh** - Helper script for local builds
## Updating Versions
### Step 1: Update `.env.versions`
Edit the file with new versions:
```bash
NODE_VERSION=24.12.0
GO_VERSION=1.25.2
HUGO_VERSION=0.152.0
```
### Step 2: Update `devbox.json`
Manually sync the versions in `devbox.json`:
```json
{
"packages": [
"hugo@0.152.0",
"go@1.25.2",
"nodejs@24.12.0",
...
]
}
```
### Step 3: Rebuild devbox environment
```bash
devbox shell --refresh
```
### Step 4: Test Docker build
```bash
source scripts/get-versions.sh
# Follow the printed docker build command
```
## Why not automatic devbox sync?
- devbox.json uses a different version format (e.g., `@latest` vs specific versions)
- devbox package names may differ from Docker image names
- Keeps devbox.json simple and readable
- Manual sync ensures intentional version updates
## CI/CD
The GitHub Actions workflow automatically loads versions from `.env.versions` - no manual intervention needed.

View file

@ -1,10 +1,10 @@
{ {
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.10.5/.schema/devbox.schema.json", "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.10.5/.schema/devbox.schema.json",
"packages": [ "packages": [
"hugo@latest", "hugo@0.151.0",
"dart-sass@latest", "dart-sass@latest",
"go@latest", "go@1.25.1",
"nodejs@latest", "nodejs@24.10.0",
"htmltest@latest", "htmltest@latest",
"go-task@latest" "go-task@latest"
], ],

28
scripts/get-versions.sh Executable file
View file

@ -0,0 +1,28 @@
#!/bin/bash
# Load versions from .env.versions for Docker build
# Usage: source scripts/get-versions.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERSIONS_FILE="${SCRIPT_DIR}/../.env.versions"
if [ ! -f "$VERSIONS_FILE" ]; then
echo "Error: .env.versions not found at $VERSIONS_FILE"
exit 1
fi
# Load versions
set -a
source "$VERSIONS_FILE"
set +a
echo "Loaded versions from .env.versions:"
echo " NODE_VERSION=${NODE_VERSION}"
echo " GO_VERSION=${GO_VERSION}"
echo " HUGO_VERSION=${HUGO_VERSION}"
echo ""
echo "Build Docker image with:"
echo " docker build --network=host \\"
echo " --build-arg NODE_VERSION=${NODE_VERSION} \\"
echo " --build-arg GO_VERSION=${GO_VERSION} \\"
echo " --build-arg HUGO_VERSION=${HUGO_VERSION} \\"
echo " -t ipceicis-developerframework:latest ."