From 34fd1a5a4dd1aba794ab64ec3a221dbbf6ad10be Mon Sep 17 00:00:00 2001 From: Justin Harms Date: Mon, 1 Jun 2026 02:03:17 -0500 Subject: [PATCH] Add Docker support with Dockerfile, entrypoint script, and CI workflows - Introduced a Dockerfile for building the application with Go and Node.js dependencies. - Added a docker-entrypoint.sh script to manage container execution. - Created a .dockerignore file to exclude unnecessary files from the Docker context. - Updated Makefile to include a new target for building the Docker image. - Added CI workflows for testing, formatting, and releasing artifacts in Gitea. --- .dockerignore | 7 +++ .gitea/workflows/ci.yaml | 53 ++++++++++++++++++++ .gitea/workflows/fmt.yaml | 31 ++++++++++++ .gitea/workflows/release.yaml | 92 +++++++++++++++++++++++++++++++++++ Dockerfile | 39 +++++++++++++++ Makefile | 9 +++- README.md | 54 ++++++++++++++++++++ docker-entrypoint.sh | 12 +++++ 8 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/ci.yaml create mode 100644 .gitea/workflows/fmt.yaml create mode 100644 .gitea/workflows/release.yaml create mode 100644 Dockerfile create mode 100644 docker-entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..34fec17 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitea +agent-transcripts +bin +data +node_modules +web/ui/node_modules diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..2669c3a --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: + - "**" + pull_request: + +jobs: + test: + name: Go Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Run tests + run: make test + + - name: Run race tests + run: make test-race + + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: web/ui/package-lock.json + + - name: Install UI deps + run: npm --prefix ./web/ui ci + + - name: Build + run: make build diff --git a/.gitea/workflows/fmt.yaml b/.gitea/workflows/fmt.yaml new file mode 100644 index 0000000..7681f2b --- /dev/null +++ b/.gitea/workflows/fmt.yaml @@ -0,0 +1,31 @@ +name: Format + +on: + push: + branches: + - "**" + pull_request: + +jobs: + gofmt: + name: gofmt + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Verify formatting + shell: bash + run: | + files="$(gofmt -l .)" + if [[ -n "${files}" ]]; then + echo "These files need formatting:" + echo "${files}" + exit 1 + fi diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml new file mode 100644 index 0000000..5ecd49c --- /dev/null +++ b/.gitea/workflows/release.yaml @@ -0,0 +1,92 @@ +name: Release Artifacts + +on: + push: + tags: + - "*" + workflow_dispatch: + +jobs: + build: + name: Build ${{ matrix.goos }}/${{ matrix.goarch }} (${{ matrix.variant }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - goos: linux + goarch: amd64 + variant: default + static: false + - goos: linux + goarch: amd64 + variant: alpine-static + static: true + - goos: linux + goarch: arm64 + variant: default + static: false + - goos: linux + goarch: arm64 + variant: alpine-static + static: true + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: web/ui/package-lock.json + + - name: Install UI deps + run: npm --prefix ./web/ui ci + + - name: Build UI assets + run: npm --prefix ./web/ui run build + + - name: Build binary + shell: bash + run: | + mkdir -p dist + if [[ "${{ matrix.static }}" == "true" ]]; then + CGO_ENABLED=0 GOOS="${{ matrix.goos }}" GOARCH="${{ matrix.goarch }}" \ + go build -trimpath -tags "netgo osusergo" \ + -ldflags "-s -w" \ + -o "dist/scratchbox-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.variant }}" \ + ./cmd/scratchbox + else + CGO_ENABLED=0 GOOS="${{ matrix.goos }}" GOARCH="${{ matrix.goarch }}" \ + go build -trimpath \ + -ldflags "-s -w" \ + -o "dist/scratchbox-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.variant }}" \ + ./cmd/scratchbox + fi + + - name: Verify static linux binary + if: ${{ matrix.static }} + shell: bash + run: | + file "dist/scratchbox-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.variant }}" | grep -q "statically linked" + + - name: Package artifact + shell: bash + run: | + mkdir -p artifacts + cp README.md artifacts/ + cp "dist/scratchbox-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.variant }}" artifacts/ + tar -C artifacts -czf "scratchbox-${{ github.ref_name }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.variant }}.tar.gz" . + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: scratchbox-${{ github.ref_name }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.variant }} + path: scratchbox-${{ github.ref_name }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.variant }}.tar.gz diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3542b82 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +FROM golang:1.25-alpine AS builder + +WORKDIR /src + +# Build-time dependencies for UI and Go compilation. +RUN apk add --no-cache nodejs npm + +COPY go.mod go.sum ./ +RUN go mod download + +COPY web/ui/package.json web/ui/package-lock.json ./web/ui/ +RUN npm --prefix ./web/ui ci + +COPY . . + +RUN npm --prefix ./web/ui run build +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -trimpath -tags "netgo osusergo" -ldflags "-s -w" \ + -o /out/scratchbox ./cmd/scratchbox + +FROM alpine:3.22 + +RUN addgroup -S scratchbox && adduser -S -G scratchbox scratchbox + +WORKDIR /app + +COPY --from=builder /out/scratchbox /usr/local/bin/scratchbox +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh + +RUN chmod +x /usr/local/bin/docker-entrypoint.sh && \ + mkdir -p /data /config && chown -R scratchbox:scratchbox /data /config + +USER scratchbox + +EXPOSE 8080 + +VOLUME ["/data", "/config"] + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/Makefile b/Makefile index ffe470b..9f2a15d 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,9 @@ CMD_DIR := ./cmd/scratchbox BIN_DIR := ./bin BIN := $(BIN_DIR)/$(APP_NAME) CONFIG ?= config.yaml +DOCKER_IMAGE ?= scratchbox:dev -.PHONY: help run dev config build test test-race fmt clean +.PHONY: help run dev config build docker test test-race fmt clean help: @echo "Targets:" @@ -12,6 +13,7 @@ help: @echo " make dev - Build and run server (CONFIG=$(CONFIG))" @echo " make config - Write default config to $(CONFIG) and generate sibling config.key if missing" @echo " make build - Build Svelte UI and binary to $(BIN)" + @echo " make docker - Build Docker image (DOCKER_IMAGE=$(DOCKER_IMAGE))" @echo " make test - Run all Go tests with timeout and shuffle" @echo " make test-race - Run all Go tests with race detector and timeout" @echo " make fmt - Format Go code" @@ -32,7 +34,10 @@ config: build: clean npm --prefix ./web/ui run build mkdir -p $(BIN_DIR) - go build -o $(BIN) $(CMD_DIR) + CGO_ENABLED=0 go build -o $(BIN) $(CMD_DIR) + +docker: + docker build -t $(DOCKER_IMAGE) . test: go test -timeout 30s -shuffle=on ./... diff --git a/README.md b/README.md index e4f09ac..62c5665 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,60 @@ Without `--config`, built-in defaults are used. If you modify the web UI source (`web/ui`), rebuild frontend assets with: - `make build` +## Docker + +Build the container image: + +- `make docker` +- Default image tag: `scratchbox:dev` + +Optionally set a custom image name/tag: + +- `make docker DOCKER_IMAGE=scratchbox:latest` + +Run with persistent data and config directories: + +- `mkdir -p ./docker-data ./docker-config` +- `docker run --rm -p 8080:8080 -v "$(pwd)/docker-data:/data" -v "$(pwd)/docker-config:/config" scratchbox:dev` + +Inside the container, the default command is: + +- `scratchbox server --config /config/config.yaml` + +Create config files with the image (writes to mounted `./docker-config`): + +- `docker run --rm -v "$(pwd)/docker-config:/config" scratchbox:dev generate-config > ./docker-config/config.yaml` +- `docker run --rm -v "$(pwd)/docker-config:/config" scratchbox:dev generate-key > ./docker-config/config.key` + +## Release Artifacts + +Tagged releases provide `.tar.gz` archives with prebuilt Linux binaries. + +Variant guide: + +| Variant | Recommended for | Notes | +| --- | --- | --- | +| `default` | Most Linux distributions (Ubuntu, Debian, Fedora, Arch, etc.) | Smaller, dynamically linked Linux build | +| `alpine-static` | Alpine Linux / musl-based environments | Statically linked for better compatibility | + +To unpack an artifact: + +- `tar -xzf scratchbox--linux-amd64-default.tar.gz` + +To unpack into a specific directory: + +- `mkdir -p ./scratchbox-release` +- `tar -xzf scratchbox--linux-amd64-default.tar.gz -C ./scratchbox-release` + +To run from an unpacked artifact: + +- `./scratchbox--- server --config config.yaml` + +If you do not have a config file yet, generate one first: + +- `go run ./cmd/scratchbox generate-config > config.yaml` +- `go run ./cmd/scratchbox generate-key > config.key` + ## Configuration All configuration is YAML. diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..f2e0207 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu + +# Default to running the server with mounted config. +if [ "$#" -eq 0 ]; then + set -- server --config /config/config.yaml +elif [ "${1#-}" != "$1" ]; then + # Allow passing server flags directly, e.g. `docker run ... --config ...`. + set -- server "$@" +fi + +exec scratchbox "$@"