# 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.5-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.5-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"]