Skip to main content

Containerization

Applications on the Ybor Platform run as containers. This guide covers Dockerfile patterns, security best practices, and publishing to the container registry.

What You'll Learn

  • Multi-stage Dockerfile patterns for production images
  • Security best practices (non-root users, minimal base images)
  • Layer caching strategies
  • Publishing images to the Artifactory Docker registry

Dockerfile Patterns

The platform supports two approaches to containerization:

Build and test your application in a prior CI step, then copy the compiled artifacts into the container. This approach:

  • Avoids duplicate build work between CI and Docker
  • Speeds up container builds
  • Produces consistent artifacts (same build runs tests and creates the image)
# Runtime only - artifacts built externally
FROM <runtime-image>
# Copy pre-built artifacts from CI
COPY dist/ /app/
# Configure non-root user
# Set health check
# Define entrypoint

Approach 2: Multi-Stage Build in Container

Build the application inside the Dockerfile using multi-stage builds. This approach:

  • Self-contained—everything needed is in the Dockerfile
  • Useful when you need specific build environment isolation
  • Produces larger build contexts
# Stage 1: Build
FROM <build-image> AS builder
# Install build dependencies
# Copy source code
# Compile/build application

# Stage 2: Runtime
FROM <runtime-image>
# Copy only runtime artifacts from builder
# Configure non-root user
# Set health check
# Define entrypoint
tip

The p6m-actions build actions (e.g., python-uv-build, dotnet-build) handle building and testing, then the corresponding docker-publish actions package the pre-built artifacts. This follows Approach 1.

Language-Specific Dockerfiles

Each language has its own patterns and base images. See the language reference for Dockerfile guidance:

  • JavaScript — pnpm with Node.js slim
  • Python — uv-based builds with slim images
  • .NET — SDK build, alpine runtime
  • Java — Maven with Temurin JRE
  • Rust — cargo-chef for dependency caching

Security Best Practices

Non-Root User

Always run containers as a non-root user. This limits the impact of container escapes.

# Create user and group with explicit IDs
RUN groupadd --gid 1000 appgroup && \
useradd --uid 1000 --gid appgroup --shell /bin/bash appuser

# Set file ownership
RUN chown -R appuser:appgroup /app

# Switch to non-root user
USER appuser

Minimal Base Images

Use slim or alpine variants to reduce attack surface:

LanguageBuild ImageRuntime Image
Pythonpython:3.11-slimpython:3.11-slim
.NETdotnet/sdk:8.0dotnet/aspnet:8.0-alpine
Rustrust:1.75-slimdebian:bookworm-slim
Javaeclipse-temurin:21-jdkeclipse-temurin:21-jre-alpine

Health Checks

Include HEALTHCHECK instructions for container orchestration:

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1

The platform's deployment resources use these health checks for readiness and liveness probes.

Layer Caching

Order Dockerfile instructions to maximize cache hits. Dependencies change less frequently than source code, so install them first:

# Good: Dependencies cached separately from source
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY src/ ./src/

# Bad: Any source change invalidates dependency cache
COPY . .
RUN pip install -r requirements.txt

Multi-Architecture Builds

The platform supports both linux/amd64 and linux/arm64 architectures. See Multi-Architecture Builds for detailed guidance on:

  • Using Docker Buildx with QEMU emulation
  • Parallel native builds for compiled languages
  • Creating multi-arch manifests

Registry Publishing

Registry Structure

Images are published to Artifactory with this naming convention:

<ARTIFACTORY_HOSTNAME>/<ARTIFACTORY_PROJECT>-docker-local/applications/<service-name>:<tag>

Example:

artifacts.example.com/myproject-docker-local/applications/user-service:1.2.3

Authentication

Use the docker-repository-login action:

- name: Login to registry
uses: p6m-actions/docker-repository-login@v1
with:
registry: ${{ vars.ARTIFACTORY_HOSTNAME }}
username: ${{ secrets.ARTIFACTORY_USERNAME }}
password: ${{ secrets.ARTIFACTORY_IDENTITY_TOKEN }}

Tagging Strategy

Apply multiple tags to each image:

TagPurpose
1.2.3Immutable version tag
1.2Latest patch in minor version
1Latest minor in major version
latestMost recent build

For deployments, always use the full version tag or digest for reproducibility.

Image Digests

The build workflow should output the image digest for use in deployments:

- name: Build and push
id: docker
uses: p6m-actions/python-uv-docker-publish@v1
with:
image: ${{ env.REGISTRY }}/my-service
tags: ${{ env.VERSION }}

- name: Save digest
run: echo "${{ steps.docker.outputs.digest }}" > digest.txt

- name: Upload digest artifact
uses: actions/upload-artifact@v4
with:
name: digest
path: digest.txt

The digest is used by the promotion workflow to deploy specific, immutable image versions.

Secrets in Builds

For builds that need credentials (e.g., private package registries), use BuildKit secrets:

# Mount secret at build time only - not persisted in image
RUN --mount=type=secret,id=artifactory-token \
ARTIFACTORY_TOKEN=$(cat /run/secrets/artifactory-token) \
pip install --extra-index-url https://user:${ARTIFACTORY_TOKEN}@artifacts.example.com/pypi/simple my-private-package

Pass secrets from the workflow:

- name: Build with secrets
run: |
docker buildx build \
--secret id=artifactory-token,env=ARTIFACTORY_IDENTITY_TOKEN \
--push \
.
env:
ARTIFACTORY_IDENTITY_TOKEN: ${{ secrets.ARTIFACTORY_IDENTITY_TOKEN }}