Files
homebox/Dockerfile.hardened
Matt 7980e8e90a Create hardened docker image (#955)
* Create hardened docker image

* Remove healthcheck that can't work

* Pin action dependencies

* Further cleanup and hardening

* Fix broken hardened build

* Enhance Dockerfile with healthcheck and optimizations

Added healthcheck helper using a small Go file module and improved Dockerfile structure for readability.

---------

Co-authored-by: Katos <7927609+katosdev@users.noreply.github.com>
2025-08-23 12:57:51 -04:00

137 lines
4.2 KiB
Docker

# ---------------------------------------
# Node dependencies stage
# ---------------------------------------
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
WORKDIR /app
# Install pnpm globally (caching layer)
RUN npm install -g pnpm
# Copy package.json and lockfile to leverage caching
COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# ---------------------------------------
# Build Nuxt (frontend) stage
# ---------------------------------------
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
WORKDIR /app
# Install pnpm globally again (it can reuse the cache if not changed)
RUN npm install -g pnpm
# Copy over source files and node_modules from dependencies stage
COPY frontend .
COPY --from=frontend-dependencies /app/node_modules ./node_modules
RUN pnpm build
# ---------------------------------------
# Go dependencies stage
# ---------------------------------------
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
WORKDIR /go/src/app
# Copy go.mod and go.sum for better caching
COPY ./backend/go.mod ./backend/go.sum ./
RUN go mod download
# ---------------------------------------
# Build API + healthcheck stage
# ---------------------------------------
FROM public.ecr.aws/docker/library/golang:alpine AS builder
ARG TARGETOS
ARG TARGETARCH
ARG BUILD_TIME
ARG COMMIT
ARG VERSION
# Install necessary build tools
RUN apk update && \
apk upgrade && \
apk add --no-cache git build-base gcc g++
WORKDIR /go/src/app
# Copy Go modules (from dependencies stage) and source code
COPY --from=builder-dependencies /go/pkg/mod /go/pkg/mod
COPY ./backend .
# Clear old public files and copy new ones from frontend build
RUN rm -rf ./app/api/public
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
# Use cache for Go build artifacts to build Homebox API
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-tags nodynamic -o /go/bin/api -v ./app/api/*.go
RUN chmod +x /go/bin/api
RUN mkdir /app
RUN mkdir /data
# ---------- Build static healthcheck helper ----------
# A small Go program that GETs the status URL and exits 0 on 2xx.
RUN cat > /tmp/healthcheck.go <<'EOF'
package main
import (
"fmt"
"net/http"
"os"
"time"
)
func main() {
url := "http://127.0.0.1:7745/api/v1/status"
if len(os.Args) > 1 { url = os.Args[1] }
c := &http.Client{ Timeout: 3 * time.Second }
resp, err := c.Get(url)
if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
resp.Body.Close()
if resp.StatusCode/100 != 2 {
fmt.Fprintln(os.Stderr, "unexpected status:", resp.StatusCode)
os.Exit(1)
}
}
EOF
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -ldflags "-s -w" -o /go/bin/hc /tmp/healthcheck.go
# ---------------------------------------
# Production stage
# ---------------------------------------
FROM gcr.io/distroless/static:nonroot
ENV HBOX_MODE=production
ENV HBOX_STORAGE_CONN_STRING=file:///?no_tmp_dir=true
ENV HBOX_STORAGE_PREFIX_PATH=data
ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite
# Create application directory and copy over built Go binary and assets
COPY --from=builder --chown=65532:65532 /app /app
COPY --from=builder --chown=65532:65532 --chmod=755 /go/bin/api /app
COPY --from=builder --chown=65532:65532 /data /data
# Copy the healthcheck helper
COPY --from=builder --chown=65532:65532 --chmod=755 /go/bin/hc /app/healthcheck
# Labels and configuration for the final image
LABEL Name=homebox Version=0.0.1
LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox"
# Expose necessary ports for Homebox
EXPOSE 7745
WORKDIR /app
# Persist volume for data
VOLUME [ "/data" ]
# Entrypoint and CMD
USER 65532
ENTRYPOINT [ "/app/api" ]
CMD [ "/data/config.yml" ]
# JSON exec-form healthcheck (no shell, no wget)
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD ["/app/healthcheck", "http://127.0.0.1:7745/api/v1/status"]