Files
Charon/Dockerfile
GitHub Actions 3fd85ce34f fix: upgrade Go to 1.25 for Caddy 2.10.2 compatibility
Caddy 2.10.2 requires Go 1.25 (declared in its go.mod). The previous
commit incorrectly downgraded to Go 1.23 based on the false assumption
that Go 1.25.5 doesn't exist.

This fix:
- Updates Dockerfile Go images from 1.23-alpine to 1.25-alpine
- Updates backend/go.mod to go 1.25
- Updates go.work to go 1.25

Fixes CI Docker build failures in xcaddy stage.
2025-12-14 01:06:03 +00:00

296 lines
13 KiB
Docker

# Multi-stage Dockerfile for Charon with integrated Caddy
# Single container deployment for simplified home user setup
# Build arguments for versioning
ARG VERSION=dev
ARG BUILD_DATE
ARG VCS_REF
# Allow pinning Caddy version - Renovate will update this
# Build the most recent Caddy 2.x release (keeps major pinned under v3).
# Setting this to '2' tells xcaddy to resolve the latest v2.x tag so we
# avoid accidentally pulling a v3 major release. Renovate can still update
# this ARG to a specific v2.x tag when desired.
## Try to build the requested Caddy v2.x tag (Renovate can update this ARG).
## If the requested tag isn't available, fall back to a known-good v2.10.2 build.
ARG CADDY_VERSION=2.10.2
## When an official caddy image tag isn't available on the host, use a
## plain Alpine base image and overwrite its caddy binary with our
## xcaddy-built binary in the later COPY step. This avoids relying on
## upstream caddy image tags while still shipping a pinned caddy binary.
ARG CADDY_IMAGE=alpine:3.23
# ---- Cross-Compilation Helpers ----
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0 AS xx
# ---- Frontend Builder ----
# Build the frontend using the BUILDPLATFORM to avoid arm64 musl Rollup native issues
FROM --platform=$BUILDPLATFORM node:24.12.0-alpine AS frontend-builder
WORKDIR /app/frontend
# Copy frontend package files
COPY frontend/package*.json ./
# Build-time project version (propagated from top-level build-arg)
ARG VERSION=dev
# Make version available to Vite as VITE_APP_VERSION during the frontend build
ENV VITE_APP_VERSION=${VERSION}
# Set environment to bypass native binary requirement for cross-arch builds
ENV npm_config_rollup_skip_nodejs_native=1 \
ROLLUP_SKIP_NODEJS_NATIVE=1
RUN npm ci
# Copy frontend source and build
COPY frontend/ ./
RUN --mount=type=cache,target=/app/frontend/node_modules/.cache \
npm run build
# ---- Backend Builder ----
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS backend-builder
# Copy xx helpers for cross-compilation
COPY --from=xx / /
WORKDIR /app/backend
# Install build dependencies
# xx-apk installs packages for the TARGET architecture
ARG TARGETPLATFORM
# hadolint ignore=DL3018
RUN apk add --no-cache clang lld
# hadolint ignore=DL3018,DL3059
RUN xx-apk add --no-cache gcc musl-dev sqlite-dev
# Install Delve (cross-compile for target)
# Note: xx-go install puts binaries in /go/bin/TARGETOS_TARGETARCH/dlv if cross-compiling.
# We find it and move it to /go/bin/dlv so it's in a consistent location for the next stage.
# hadolint ignore=DL3059,DL4006
RUN CGO_ENABLED=0 xx-go install github.com/go-delve/delve/cmd/dlv@latest && \
DLV_PATH=$(find /go/bin -name dlv -type f | head -n 1) && \
if [ -n "$DLV_PATH" ] && [ "$DLV_PATH" != "/go/bin/dlv" ]; then \
mv "$DLV_PATH" /go/bin/dlv; \
fi && \
xx-verify /go/bin/dlv
# Copy Go module files
COPY backend/go.mod backend/go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
# Copy backend source
COPY backend/ ./
# Build arguments passed from main build context
ARG VERSION=dev
ARG VCS_REF=unknown
ARG BUILD_DATE=unknown
# Build the Go binary with version information injected via ldflags
# xx-go handles CGO and cross-compilation flags automatically
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
CGO_ENABLED=1 xx-go build \
-ldflags "-s -w -X github.com/Wikid82/charon/backend/internal/version.Version=${VERSION} \
-X github.com/Wikid82/charon/backend/internal/version.GitCommit=${VCS_REF} \
-X github.com/Wikid82/charon/backend/internal/version.BuildTime=${BUILD_DATE}" \
-o charon ./cmd/api
# ---- Caddy Builder ----
# Build Caddy from source to ensure we use the latest Go version and dependencies
# This fixes vulnerabilities found in the pre-built Caddy images (e.g. CVE-2025-59530, stdlib issues)
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS caddy-builder
ARG TARGETOS
ARG TARGETARCH
ARG CADDY_VERSION
# hadolint ignore=DL3018
RUN apk add --no-cache git
# hadolint ignore=DL3062
RUN --mount=type=cache,target=/go/pkg/mod \
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
# Build Caddy for the target architecture with security plugins.
# We use XCADDY_SKIP_CLEANUP=1 to keep the build environment, then patch dependencies.
# hadolint ignore=SC2016
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
sh -c 'set -e; \
export XCADDY_SKIP_CLEANUP=1; \
# Run xcaddy build - it will fail at the end but create the go.mod
GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_VERSION} \
--with github.com/greenpau/caddy-security \
--with github.com/corazawaf/coraza-caddy/v2 \
--with github.com/hslatman/caddy-crowdsec-bouncer \
--with github.com/zhangjiayin/caddy-geoip2 \
--with github.com/mholt/caddy-ratelimit \
--output /tmp/caddy-temp || true; \
# Find the build directory
BUILDDIR=$(ls -td /tmp/buildenv_* 2>/dev/null | head -1); \
if [ -d "$BUILDDIR" ] && [ -f "$BUILDDIR/go.mod" ]; then \
echo "Patching dependencies in $BUILDDIR"; \
cd "$BUILDDIR"; \
# Upgrade transitive dependencies to pick up security fixes.
# These are Caddy dependencies that lag behind upstream releases.
# Renovate tracks these via regex manager in renovate.json
# TODO: Remove this block once Caddy ships with fixed deps (check v2.10.3+)
# renovate: datasource=go depName=github.com/expr-lang/expr
go get github.com/expr-lang/expr@v1.17.6 || true; \
# renovate: datasource=go depName=github.com/quic-go/quic-go
go get github.com/quic-go/quic-go@v0.57.1 || true; \
# renovate: datasource=go depName=github.com/smallstep/certificates
go get github.com/smallstep/certificates@v0.29.0 || true; \
go mod tidy || true; \
# Rebuild with patched dependencies
echo "Rebuilding Caddy with patched dependencies..."; \
GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /usr/bin/caddy \
-ldflags "-w -s" -trimpath -tags "nobadger,nomysql,nopgx" . && \
echo "Build successful"; \
else \
echo "Build directory not found, using standard xcaddy build"; \
GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_VERSION} \
--with github.com/greenpau/caddy-security \
--with github.com/corazawaf/coraza-caddy/v2 \
--with github.com/hslatman/caddy-crowdsec-bouncer \
--with github.com/zhangjiayin/caddy-geoip2 \
--with github.com/mholt/caddy-ratelimit \
--output /usr/bin/caddy; \
fi; \
rm -rf /tmp/buildenv_* /tmp/caddy-temp; \
/usr/bin/caddy version'
# ---- CrowdSec Installer ----
# CrowdSec requires CGO (mattn/go-sqlite3), so we cannot build from source
# with CGO_ENABLED=0. Instead, we download prebuilt static binaries for amd64
# or install from packages. For other architectures, CrowdSec is skipped.
FROM alpine:3.23 AS crowdsec-installer
WORKDIR /tmp/crowdsec
ARG TARGETARCH
# CrowdSec version - Renovate can update this
# renovate: datasource=github-releases depName=crowdsecurity/crowdsec
ARG CROWDSEC_VERSION=1.7.4
# hadolint ignore=DL3018
RUN apk add --no-cache curl tar
# Download static binaries (only available for amd64)
# For other architectures, create empty placeholder files so COPY doesn't fail
# hadolint ignore=DL3059,SC2015
RUN set -eux; \
mkdir -p /crowdsec-out/bin /crowdsec-out/config; \
if [ "$TARGETARCH" = "amd64" ]; then \
echo "Downloading CrowdSec binaries for amd64..."; \
curl -fSL "https://github.com/crowdsecurity/crowdsec/releases/download/v${CROWDSEC_VERSION}/crowdsec-release.tgz" \
-o /tmp/crowdsec.tar.gz && \
tar -xzf /tmp/crowdsec.tar.gz -C /tmp && \
# Binaries are in cmd/crowdsec-cli/cscli and cmd/crowdsec/crowdsec
cp "/tmp/crowdsec-v${CROWDSEC_VERSION}/cmd/crowdsec-cli/cscli" /crowdsec-out/bin/ && \
cp "/tmp/crowdsec-v${CROWDSEC_VERSION}/cmd/crowdsec/crowdsec" /crowdsec-out/bin/ && \
chmod +x /crowdsec-out/bin/* && \
# Copy config files from the release tarball
if [ -d "/tmp/crowdsec-v${CROWDSEC_VERSION}/config" ]; then \
cp -r "/tmp/crowdsec-v${CROWDSEC_VERSION}/config/"* /crowdsec-out/config/; \
fi && \
echo "CrowdSec binaries installed successfully"; \
else \
echo "CrowdSec binaries not available for $TARGETARCH - skipping"; \
# Create empty placeholder so COPY doesn't fail
touch /crowdsec-out/bin/.placeholder /crowdsec-out/config/.placeholder; \
fi; \
# Show what we have
ls -la /crowdsec-out/bin/ /crowdsec-out/config/ || true
# ---- Final Runtime with Caddy ----
FROM ${CADDY_IMAGE}
WORKDIR /app
# Install runtime dependencies for Charon (no bash needed)
# hadolint ignore=DL3018
RUN apk --no-cache add ca-certificates sqlite-libs tzdata curl gettext \
&& apk --no-cache upgrade
# Download MaxMind GeoLite2 Country database
# Note: In production, users should provide their own MaxMind license key
# This uses the publicly available GeoLite2 database
RUN mkdir -p /app/data/geoip && \
curl -L "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
-o /app/data/geoip/GeoLite2-Country.mmdb
# Copy Caddy binary from caddy-builder (overwriting the one from base image)
COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy
# Copy CrowdSec binaries from the crowdsec-installer stage (optional - only amd64)
# The installer creates placeholders for non-amd64 architectures
COPY --from=crowdsec-installer /crowdsec-out/bin/* /usr/local/bin/
COPY --from=crowdsec-installer /crowdsec-out/config /etc/crowdsec.dist
# Clean up placeholder files and verify CrowdSec (if available)
RUN rm -f /usr/local/bin/.placeholder /etc/crowdsec.dist/.placeholder 2>/dev/null || true; \
if [ -x /usr/local/bin/cscli ]; then \
echo "CrowdSec installed:"; \
cscli version || echo "CrowdSec version check failed"; \
else \
echo "CrowdSec not available for this architecture - skipping verification"; \
fi
# Create required CrowdSec directories in runtime image
RUN mkdir -p /etc/crowdsec /etc/crowdsec/acquis.d /etc/crowdsec/bouncers \
/etc/crowdsec/hub /etc/crowdsec/notifications \
/var/lib/crowdsec/data /var/log/crowdsec /var/log/caddy
# Copy CrowdSec configuration templates from source
COPY configs/crowdsec/acquis.yaml /etc/crowdsec.dist/acquis.yaml
COPY configs/crowdsec/install_hub_items.sh /usr/local/bin/install_hub_items.sh
COPY configs/crowdsec/register_bouncer.sh /usr/local/bin/register_bouncer.sh
# Make CrowdSec scripts executable
RUN chmod +x /usr/local/bin/install_hub_items.sh /usr/local/bin/register_bouncer.sh
# Copy Go binary from backend builder
COPY --from=backend-builder /app/backend/charon /app/charon
RUN ln -s /app/charon /app/cpmp || true
# Copy Delve debugger (xx-go install places it in /go/bin)
COPY --from=backend-builder /go/bin/dlv /usr/local/bin/dlv
# Copy frontend build from frontend builder
COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
# Copy startup script
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# Set default environment variables
ENV CHARON_ENV=production \
CHARON_DB_PATH=/app/data/charon.db \
CHARON_FRONTEND_DIR=/app/frontend/dist \
CHARON_CADDY_ADMIN_API=http://localhost:2019 \
CHARON_CADDY_CONFIG_DIR=/app/data/caddy \
CHARON_GEOIP_DB_PATH=/app/data/geoip/GeoLite2-Country.mmdb \
CHARON_HTTP_PORT=8080 \
CHARON_CROWDSEC_CONFIG_DIR=/app/data/crowdsec
# Create necessary directories
RUN mkdir -p /app/data /app/data/caddy /config /app/data/crowdsec
# Re-declare build args for LABEL usage
ARG VERSION=dev
ARG BUILD_DATE
ARG VCS_REF
# OCI image labels for version metadata
LABEL org.opencontainers.image.title="Charon (CPMP legacy)" \
org.opencontainers.image.description="Web UI for managing Caddy reverse proxy configurations" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${VCS_REF}" \
org.opencontainers.image.source="https://github.com/Wikid82/charon" \
org.opencontainers.image.url="https://github.com/Wikid82/charon" \
org.opencontainers.image.vendor="charon" \
org.opencontainers.image.licenses="MIT"
# Expose ports
EXPOSE 80 443 443/udp 2019 8080
# Use custom entrypoint to start both Caddy and Charon
ENTRYPOINT ["/docker-entrypoint.sh"]