Build Support

Build Support #

Quad-Ops converts Docker Compose build configurations to Podman Quadlet build units, enabling containerized builds managed by systemd.

Build Configuration Overview #

Docker Compose build sections are converted to Podman Quadlet .build units that:

  • Build container images from Dockerfiles
  • Manage build dependencies through systemd
  • Handle build arguments and environment variables
  • Support multi-stage builds and build targets
  • Integrate with container units for automatic image usage

Basic Build Configuration #

Simple Build #

Docker Compose:

version: '3.8'
services:
  app:
    build: .
    ports:
      - "8080:80"

Generated build unit: myproject-app-build.build

[Unit]
Description=Build myproject-app

[Build]
ImageTag=localhost/myproject-app:latest
SetWorkingDirectory=/path/to/context

[Service]
Type=oneshot

[Install]
WantedBy=default.target

Generated container unit: myproject-app.container

[Unit]
Description=myproject-app container
After=myproject-app-build.service

[Container]
Image=localhost/myproject-app:latest
PublishPort=8080:80
NetworkAlias=app

[Install]
WantedBy=default.target

Advanced Build Configuration #

Complete Build Setup #

Docker Compose:

version: '3.8'
services:
  api:
    build:
      context: ./backend
      dockerfile: Dockerfile.prod
      target: production
      args:
        - NODE_ENV=production
        - VERSION=1.0.0
        - BUILD_DATE=${BUILD_DATE}
      labels:
        - "app=api"
        - "version=1.0.0"
      network: host
      pull: true
      secrets:
        - source: api_key
          target: /run/secrets/api_key
    environment:
      - NODE_ENV=production

Generated build unit: myproject-api-build.build

[Unit]
Description=Build myproject-api

[Build]
ImageTag=localhost/myproject-api:latest
SetWorkingDirectory=./backend
File=Dockerfile.prod
Target=production
BuildArg=NODE_ENV=production
BuildArg=VERSION=1.0.0
BuildArg=BUILD_DATE=2024-01-15T10:30:00Z
Label=app=api
Label=version=1.0.0
Pull=true
Network=host
Secret=api_key,target=/run/secrets/api_key

[Service]
Type=oneshot

[Install]
WantedBy=default.target

Build Arguments #

Static Build Arguments #

services:
  app:
    build:
      context: .
      args:
        NODE_ENV: production
        VERSION: "1.0.0"
        DEBIAN_FRONTEND: noninteractive

Dynamic Build Arguments #

services:
  app:
    build:
      context: .
      args:
        - NODE_ENV=${NODE_ENV}
        - VERSION=${GIT_TAG}
        - BUILD_DATE=${BUILD_DATE}

Environment file (.env):

NODE_ENV=production
GIT_TAG=v1.2.3
BUILD_DATE=2024-01-15T10:30:00Z

Build Arguments in Dockerfile #

Dockerfile:

FROM node:18-alpine
ARG NODE_ENV=development
ARG VERSION=latest
ARG BUILD_DATE

ENV NODE_ENV=${NODE_ENV}
LABEL version=${VERSION}
LABEL build_date=${BUILD_DATE}

COPY package.json package-lock.json ./
RUN npm ci --only=production

COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Multi-Stage Builds #

Development and Production Targets #

Dockerfile:

# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Development stage
FROM node:18-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]

# Production stage
FROM nginx:alpine AS production
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Docker Compose (Development):

services:
  app-dev:
    build:
      context: .
      target: development
    volumes:
      - .:/app
      - /app/node_modules

Docker Compose (Production):

services:
  app:
    build:
      context: .
      target: production
    ports:
      - "80:80"

Build Secrets #

Secret Mounting #

Docker Compose:

services:
  app:
    build:
      context: .
      secrets:
        - source: github_token
          target: /run/secrets/github_token
        - source: npm_token
          target: /run/secrets/npm_token

secrets:
  github_token:
    file: ./secrets/github_token.txt
  npm_token:
    file: ./secrets/npm_token.txt

Dockerfile:

FROM node:18-alpine

# Use secret during build
RUN --mount=type=secret,id=npm_token \
    echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > ~/.npmrc && \
    npm ci --only=production && \
    rm ~/.npmrc

COPY . .
CMD ["node", "server.js"]

SSH Authentication #

Docker Compose:

services:
  app:
    build:
      context: .
      ssh:
        - default=/home/user/.ssh/id_rsa

Dockerfile:

FROM alpine/git AS source
RUN --mount=type=ssh \
    git clone git@github.com:private/repo.git /src

FROM node:18-alpine
COPY --from=source /src /app
WORKDIR /app
RUN npm ci
CMD ["node", "server.js"]

Build Networks #

Custom Build Network #

services:
  app:
    build:
      context: .
      network: build-network

networks:
  build-network:
    external: true

Host Network for Build #

services:
  app:
    build:
      context: .
      network: host  # Use host networking during build

Podman-Specific Build Extensions #

Build Cache Configuration #

Use Quad-Ops’ x-podman-buildargs extension for additional build arguments:

services:
  app:
    build:
      context: .
    x-podman-buildargs:
      BUILDAH_LAYERS: "true"
      BUILDKIT_INLINE_CACHE: "1"

Custom Build Volumes #

Use Quad-Ops’ x-podman-volumes extension for build-time volume mounts:

services:
  app:
    build:
      context: .
    x-podman-volumes:
      - "build-cache:/cache"
      - "/tmp/build:/tmp:rw"

Build Dependencies and Lifecycle #

Build Order Management #

Multi-service build order:

services:
  base:
    build:
      context: ./base
      dockerfile: Dockerfile.base

  app:
    build:
      context: ./app
    depends_on:
      - base  # Ensures base is built first

  frontend:
    build:
      context: ./frontend
    depends_on:
      - app   # Can reference app's API during build

Build Triggers #

Builds are triggered when:

  1. Dockerfile changes detected
  2. Build context files modified
  3. Build arguments change
  4. Manual rebuild requested

Build Unit Management #

Check build status:

# List build units
sudo quad-ops unit list -t build

# Check build service status
sudo systemctl status myproject-app-build

# View build logs
sudo journalctl -u myproject-app-build

Manual build operations:

# Trigger rebuild
sudo systemctl start myproject-app-build

# Restart dependent containers after rebuild
sudo systemctl restart myproject-app

Build Context Optimization #

.dockerignore Usage #

.dockerignore:

node_modules/
npm-debug.log
Dockerfile*
.dockerignore
.git
.gitignore
README.md
.env
.nyc_output
coverage/
.nyc_output/

Minimal Build Context #

services:
  app:
    build:
      context: .
      dockerfile: docker/Dockerfile
      # Only necessary files copied by Dockerfile

Dockerfile with selective copying:

FROM node:18-alpine
WORKDIR /app

# Copy package files first for better caching
COPY package*.json ./
RUN npm ci --only=production

# Copy source code
COPY src/ ./src/
COPY public/ ./public/

EXPOSE 3000
CMD ["node", "src/server.js"]

Debug Build Process #

Check generated build unit:

# View build unit file
cat /etc/containers/systemd/myproject-app-build.build

# Check build service status
systemctl status myproject-app-build.service

Manual build testing:

# Test build manually
cd /path/to/context
podman build -t test-image .

# Test with same args as quadlet
podman build \
  --build-arg NODE_ENV=production \
  --build-arg VERSION=1.0.0 \
  --target production \
  -t test-image .

Build logs analysis:

# View build logs
journalctl -u myproject-app-build.service -f

# Check for specific errors
journalctl -u myproject-app-build.service | grep -i error

Performance Optimization #

Layer caching optimization:

# ✅ Good - Dependencies cached separately
FROM node:18-alpine
COPY package*.json ./
RUN npm ci
COPY . .

# ❌ Bad - Everything rebuilt on any change
FROM node:18-alpine
COPY . .
RUN npm ci

Multi-stage optimization:

# Build stage with all dependencies
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Runtime stage with minimal footprint
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
EXPOSE 3000
CMD ["node", "dist/server.js"]

Integration with CI/CD #

Build in Development #

# Local development with live builds
services:
  app:
    build: .
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development

Production Builds #

# Production with optimized builds
services:
  app:
    build:
      context: .
      target: production
      args:
        - NODE_ENV=production
        - VERSION=${GIT_TAG}
    environment:
      - NODE_ENV=production

Automated Build Pipeline #

#!/bin/bash
# build-and-deploy.sh

# Set build variables
export GIT_TAG=$(git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD)
export BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

# Trigger quad-ops sync (includes builds)
quad-ops sync --repository myapp

# Verify builds completed
if systemctl is-active myproject-app-build.service; then
    echo "Build completed successfully"
    quad-ops unit list -t container
else
    echo "Build failed"
    journalctl -u myproject-app-build.service --no-pager
    exit 1
fi

Next Steps #