Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 7 additions & 30 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,30 +1,7 @@
# Git
.git
.gitignore

# GitHub
.github/

# Docker
Dockerfile
.dockerignore

# Build artifacts
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out

# IDE files
.idea/
.vscode/
*.swp
*.swo

# Misc
README.md
assets/
LICENSE
# Only application source and module metadata belong in the build context.
**
!Dockerfile
!go.mod
!go.sum
!*.go
*_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: goreleaser
name: publish-image

# Builds the release artifacts for an existing tag and pushes the container
# image. Called by release-pipeline.yml right after release-please cuts a tag,
Expand All @@ -22,21 +22,20 @@ permissions:
packages: write

jobs:
goreleaser:
publish-image:
runs-on: ubuntu-latest
steps:
- name: Checkout tag
uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}
# GoReleaser derives the version from the tag, so a shallow checkout
# without tags would make it fall back to a snapshot version.
fetch-depth: 0
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
# The release image has RUN steps (it creates the runtime user and stamps
# the port-binding capability on the binary), so building the arm64 image
# on an amd64 runner needs emulation for those layers.
- name: Set up QEMU
uses: docker/setup-qemu-action@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
Expand All @@ -48,14 +47,31 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
- name: Image metadata
id: meta
uses: docker/metadata-action@v6
with:
distribution: goreleaser
version: "~> v2.18"
args: release --config .goreleaser.release.yml --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
context: git
images: ghcr.io/darksworm/doormouse
tags: |
type=semver,pattern={{version}},value=${{ inputs.tag }}
type=semver,pattern={{major}}.{{minor}},value=${{ inputs.tag }}
type=semver,pattern={{major}},value=${{ inputs.tag }}
labels: |
org.opencontainers.image.title=doormouse
org.opencontainers.image.description=A reverse proxy that wakes your servers when someone knocks
org.opencontainers.image.licenses=GPL-3.0-or-later

- name: Build and push images
uses: docker/bake-action@v7
with:
# Use the checked-out tag, including during manual retries.
source: .
files: |
./docker-bake.hcl
cwd://${{ steps.meta.outputs.bake-file }}
targets: release
push: true

# release-please creates the release as a draft so it only becomes visible
# once the image it describes is actually pullable.
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: release-pipeline

# On every push to main, release-please keeps a release PR up to date from the
# conventional-commit history. Merging that PR is what cuts a release: it tags
# the commit, drafts the release notes, and hands the tag to goreleaser, which
# the commit, drafts the release notes, and hands the tag to publish-image, which
# builds and pushes the versioned image before the release goes public.
on:
push:
Expand Down Expand Up @@ -45,11 +45,11 @@ jobs:
#
# If this job fails, the tag and the draft release already exist, so a later
# push to main will not retry it — release-please only reports
# release_created once. Re-run the goreleaser workflow directly instead; it
# release_created once. Re-run the publish-image workflow directly instead; it
# takes the tag as a workflow_dispatch input for exactly this case.
goreleaser:
publish-image:
needs: [release-please, test]
if: ${{ needs.release-please.outputs.release_created == 'true' }}
uses: ./.github/workflows/goreleaser.yml
uses: ./.github/workflows/publish-image.yml
with:
tag: ${{ needs.release-please.outputs.tag_name }}
35 changes: 35 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
# Nothing here needs git credentials, and pull_request runs code from
# the branch, so keep the token out of .git/config.
persist-credentials: false

# setup-go caches the module and build caches by default, keyed on go.sum.
- name: Set up Go
Expand Down Expand Up @@ -47,3 +51,34 @@ jobs:
# -shuffle=on stops tests depending on declaration order.
- name: Test
run: go test -race -shuffle=on ./...

# The release image sets a file capability in a RUN step, which on arm64 runs
# under emulation on an amd64 runner. Build it here so a release is not the
# first place that breaks. Go end-to-end tests exercise the native release
# image's runtime permissions and the wake/proxy/shutdown lifecycle.
image:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
# Nothing here needs git credentials, and pull_request runs code from
# the branch, so keep the token out of .git/config.
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod

- name: Set up QEMU
uses: docker/setup-qemu-action@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Build both release architectures
run: docker buildx bake release --set release.output=type=cacheonly

- name: Test container end to end
run: go -C e2e test -race -count=1 -timeout=5m -v ./...
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ doormouse
go-wol-proxy
*.migrated.toml

# goreleaser output
# release build output
dist/
60 changes: 0 additions & 60 deletions .goreleaser.release.yml

This file was deleted.

68 changes: 47 additions & 21 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,30 +1,56 @@
FROM golang:1.23-alpine AS builder

WORKDIR /app

# Copy go.mod and go.sum files
# Compile on the builder's native architecture, even for cross-platform images.
ARG BUILDPLATFORM
FROM --platform=$BUILDPLATFORM golang:1.24.3-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download
COPY *.go ./
ARG TARGETOS=linux
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -ldflags="-s -w" -o /out/doormouse .

# Copy the source code
COPY . .

# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -o doormouse .

# Create a minimal runtime image
FROM alpine:3.22

WORKDIR /app
# Everything that does not need the binary happens before it is copied in, so a
# new release busts as little cache as possible. Only setcap has to come after.
#
# The account is dedicated and unprivileged. UID and GID are pinned to 1000, the
# first user on most Linux hosts, so an SSH key that is 0600 and owned by the
# host user stays readable through a bind mount with no chown.
#
# libcap-setcap rather than libcap: it is the only piece needed here and pulls
# two packages instead of five. It stays in the image, which costs about 60 kB.
# Removing it after the COPY would save nothing, since the files would still sit
# in this layer with only a whiteout on top, and it would put an apk fetch back
# on the path every release rebuilds.
RUN addgroup -g 1000 doormouse \
&& adduser -D -u 1000 -G doormouse doormouse \
&& apk add --no-cache libcap-setcap

# Copy the binary from the builder stage
COPY --from=builder /app/doormouse /app/
WORKDIR /app

# Expose the default port. TCP routes listen on their own ports; with
# network_mode: host they are reachable directly, otherwise publish each one.
# The binary lives outside /app so that /app can be bind-mounted as a whole
# writable config directory without handing the runtime user its own binary.
COPY --from=build /out/doormouse /usr/local/bin/doormouse

# doormouse runs as a non-root user, and the kernel would otherwise stop it from
# binding ports below 1024. This file capability grants that one bind permission,
# so `port = ":443"` works with neither root nor a host sysctl.
# CAP_NET_BIND_SERVICE is in Docker's default set, so no cap_add is needed. Drop
# it and the container will not start at all: the kernel refuses to exec a file
# whose capability it cannot grant.
#
# The one step that cannot move above the COPY: setcap stamps the binary, so the
# binary has to be there. It is a single local syscall, with nothing to fetch.
RUN setcap cap_net_bind_service=+ep /usr/local/bin/doormouse

# Numeric, not the name: Kubernetes cannot verify runAsNonRoot against a
# username and refuses to start the pod, so the number has to be on the image.
USER 1000:1000

# Same contract as before: the default port, and a config mounted at
# /app/config.toml. TCP routes listen on their own ports; with network_mode:
# host they are reachable directly, otherwise publish each one.
EXPOSE 8080

# Run the application
ENTRYPOINT ["/app/doormouse", "/app/config.toml"]
ENTRYPOINT ["/usr/local/bin/doormouse", "/app/config.toml"]
18 changes: 0 additions & 18 deletions Dockerfile.release

This file was deleted.

Loading