@cryptotaxi247 / kubo / commits / c12d24949

feat: optimize docker builds (#10925)

* feat(docker): optimize Dockerfile for faster builds - add BuildKit syntax directive for advanced caching features - implement cache mounts for Go modules and build cache - reduce layers by combining RUN commands (5→2 in final stage) - optimize apt-get with --no-install-recommends flag - use COPY --chmod to avoid separate permission fixing Performance improvements: - incremental builds after code changes: ~8.6x faster (1m51s → 13s) - go module/build cache persists between builds - reduced layer count improves cache efficiency * ci: optimize Docker builds with BuildKit caching - enable BuildKit with GitHub Actions cache backend - add Docker Hub registry cache for cross-workflow sharing - move Docker login earlier to enable registry cache writes - use dual cache strategy (gha + registry) for faster builds expected improvements: - PR builds can reuse main branch cache from Docker Hub - rebuild after code changes ~5-10x faster with persistent cache - cross-PR cache sharing reduces redundant builds

Marcin Rataj committed Aug 21, 2025 at 14:44 UTC c12d24949c4acfcfe53a2984cf6d67968f609132
3 files changed +74 -76
.github/workflows/docker-build.yml
+17 -4
@@ -27,8 +27,21 @@ jobs:
27 shell: bash
28 steps:
29 - uses: actions/checkout@v5
30 - - uses: actions/setup-go@v5
30 +
31 + - name: Set up Docker Buildx
32 + uses: docker/setup-buildx-action@v3
33 +
34 + - name: Build Docker image with BuildKit
35 + uses: docker/build-push-action@v6
36 with:
32 - go-version: 1.25.x
33 - - run: docker build -t $IMAGE_NAME:$WIP_IMAGE_TAG .
34 - - run: docker run --rm $IMAGE_NAME:$WIP_IMAGE_TAG --version
37 + context: .
38 + push: false
39 + load: true
40 + tags: ${{ env.IMAGE_NAME }}:${{ env.WIP_IMAGE_TAG }}
41 + cache-from: |
42 + type=gha
43 + type=registry,ref=${{ env.IMAGE_NAME }}:buildcache
44 + cache-to: type=gha,mode=max
45 +
46 + - name: Test Docker image
47 + run: docker run --rm $IMAGE_NAME:$WIP_IMAGE_TAG --version
.github/workflows/docker-image.yml
+22 -27
@@ -46,13 +46,11 @@ jobs:
46 - name: Set up Docker Buildx
47 uses: docker/setup-buildx-action@v3
48
49 - - name: Cache Docker layers
50 - uses: actions/cache@v4
49 + - name: Log in to Docker Hub
50 + uses: docker/login-action@v3
51 with:
52 - path: /tmp/.buildx-cache
53 - key: ${{ runner.os }}-buildx-${{ github.sha }}
54 - restore-keys: |
55 - ${{ runner.os }}-buildx-
52 + username: ${{ vars.DOCKER_USERNAME }}
53 + password: ${{ secrets.DOCKER_PASSWORD }}
54
55 - name: Get tags
56 id: tags
@@ -63,12 +61,6 @@ jobs:
61 echo "EOF" >> $GITHUB_OUTPUT
62 shell: bash
63
66 - - name: Log in to Docker Hub
67 - uses: docker/login-action@v3
68 - with:
69 - username: ${{ vars.DOCKER_USERNAME }}
70 - password: ${{ secrets.DOCKER_PASSWORD }}
71 -
64 # We have to build each platform separately because when using multi-arch
65 # builds, only one platform is being loaded into the cache. This would
66 # prevent us from testing the other platforms.
@@ -81,8 +73,10 @@ jobs:
73 load: true
74 file: ./Dockerfile
75 tags: ${{ env.IMAGE_NAME }}:linux-amd64
84 - cache-from: type=local,src=/tmp/.buildx-cache
85 - cache-to: type=local,dest=/tmp/.buildx-cache-new
76 + cache-from: |
77 + type=gha
78 + type=registry,ref=${{ env.IMAGE_NAME }}:buildcache
79 + cache-to: type=gha,mode=max
80
81 - name: Build Docker image (linux/arm/v7)
82 uses: docker/build-push-action@v6
@@ -93,8 +87,10 @@ jobs:
87 load: true
88 file: ./Dockerfile
89 tags: ${{ env.IMAGE_NAME }}:linux-arm-v7
96 - cache-from: type=local,src=/tmp/.buildx-cache
97 - cache-to: type=local,dest=/tmp/.buildx-cache-new
90 + cache-from: |
91 + type=gha
92 + type=registry,ref=${{ env.IMAGE_NAME }}:buildcache
93 + cache-to: type=gha,mode=max
94
95 - name: Build Docker image (linux/arm64/v8)
96 uses: docker/build-push-action@v6
@@ -105,8 +101,10 @@ jobs:
101 load: true
102 file: ./Dockerfile
103 tags: ${{ env.IMAGE_NAME }}:linux-arm64-v8
108 - cache-from: type=local,src=/tmp/.buildx-cache
109 - cache-to: type=local,dest=/tmp/.buildx-cache-new
104 + cache-from: |
105 + type=gha
106 + type=registry,ref=${{ env.IMAGE_NAME }}:buildcache
107 + cache-to: type=gha,mode=max
108
109 # We test all the images on amd64 host here. This uses QEMU to emulate
110 # the other platforms.
@@ -132,12 +130,9 @@ jobs:
130 push: true
131 file: ./Dockerfile
132 tags: "${{ github.event.inputs.tags || steps.tags.outputs.value }}"
135 - cache-from: type=local,src=/tmp/.buildx-cache-new
136 - cache-to: type=local,dest=/tmp/.buildx-cache-new
137 -
138 - # https://github.com/docker/build-push-action/issues/252
139 - # https://github.com/moby/buildkit/issues/1896
140 - - name: Move cache to limit growth
141 - run: |
142 - rm -rf /tmp/.buildx-cache
143 - mv /tmp/.buildx-cache-new /tmp/.buildx-cache
133 + cache-from: |
134 + type=gha
135 + type=registry,ref=${{ env.IMAGE_NAME }}:buildcache
136 + cache-to: |
137 + type=gha,mode=max
138 + type=registry,ref=${{ env.IMAGE_NAME }}:buildcache,mode=max
Dockerfile
+35 -45
@@ -1,12 +1,15 @@
1 +# syntax=docker/dockerfile:1
2 +# Enables BuildKit with cache mounts for faster builds
3 FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.25 AS builder
4
5 ARG TARGETOS TARGETARCH
6
7 ENV SRC_DIR=/kubo
8
7 -# Download packages first so they can be cached.
9 +# Cache go module downloads between builds for faster rebuilds
10 COPY go.mod go.sum $SRC_DIR/
9 -RUN cd $SRC_DIR \
11 +RUN --mount=type=cache,target=/go/pkg/mod \
12 + cd $SRC_DIR \
13 && go mod download
14
15 COPY . $SRC_DIR
@@ -18,92 +21,79 @@ ARG IPFS_PLUGINS
21 # Allow for other targets to be built, e.g.: docker build --build-arg MAKE_TARGET="nofuse"
22 ARG MAKE_TARGET=build
23
21 -# Build the thing.
22 -# Also: fix getting HEAD commit hash via git rev-parse.
23 -RUN cd $SRC_DIR \
24 +# Build ipfs binary with cached go modules and build cache.
25 +# mkdir .git/objects allows git rev-parse to read commit hash for version info
26 +RUN --mount=type=cache,target=/go/pkg/mod \
27 + --mount=type=cache,target=/root/.cache/go-build \
28 + cd $SRC_DIR \
29 && mkdir -p .git/objects \
30 && GOOS=$TARGETOS GOARCH=$TARGETARCH GOFLAGS=-buildvcs=false make ${MAKE_TARGET} IPFS_PLUGINS=$IPFS_PLUGINS
31
27 -# Using Debian Buster because the version of busybox we're using is based on it
28 -# and we want to make sure the libraries we're using are compatible. That's also
29 -# why we're running this for the target platform.
32 +# Extract required runtime tools from Debian.
33 +# We use Debian instead of Alpine because we need glibc compatibility
34 +# for the busybox base image we're using.
35 FROM debian:bookworm-slim AS utilities
36 RUN set -eux; \
37 apt-get update; \
33 - apt-get install -y \
38 + apt-get install -y --no-install-recommends \
39 tini \
40 # Using gosu (~2MB) instead of su-exec (~20KB) because it's easier to
41 # install on Debian. Useful links:
42 # - https://github.com/ncopa/su-exec#why-reinvent-gosu
43 # - https://github.com/tianon/gosu/issues/52#issuecomment-441946745
44 gosu \
40 - # This installs fusermount which we later copy over to the target image.
45 + # fusermount enables IPFS mount commands
46 fuse \
47 ca-certificates \
48 ; \
44 - rm -rf /var/lib/apt/lists/*
49 + apt-get clean; \
50 + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
51
46 -# Now comes the actual target image, which aims to be as small as possible.
52 +# Final minimal image with shell for debugging (busybox provides sh)
53 FROM busybox:stable-glibc
54
49 -# Get the ipfs binary, entrypoint script, and TLS CAs from the build container.
55 +# Copy ipfs binary, startup scripts, and runtime dependencies
56 ENV SRC_DIR=/kubo
57 COPY --from=utilities /usr/sbin/gosu /sbin/gosu
58 COPY --from=utilities /usr/bin/tini /sbin/tini
59 COPY --from=utilities /bin/fusermount /usr/local/bin/fusermount
60 COPY --from=utilities /etc/ssl/certs /etc/ssl/certs
61 COPY --from=builder $SRC_DIR/cmd/ipfs/ipfs /usr/local/bin/ipfs
56 -COPY --from=builder $SRC_DIR/bin/container_daemon /usr/local/bin/start_ipfs
62 +COPY --from=builder --chmod=755 $SRC_DIR/bin/container_daemon /usr/local/bin/start_ipfs
63 COPY --from=builder $SRC_DIR/bin/container_init_run /usr/local/bin/container_init_run
64
59 -# Add suid bit on fusermount so it will run properly
65 +# Set SUID for fusermount to enable FUSE mounting by non-root user
66 RUN chmod 4755 /usr/local/bin/fusermount
67
62 -# Fix permissions on start_ipfs (ignore the build machine's permissions)
63 -RUN chmod 0755 /usr/local/bin/start_ipfs
64 -
65 -# Swarm TCP; should be exposed to the public
66 -EXPOSE 4001
67 -# Swarm UDP; should be exposed to the public
68 -EXPOSE 4001/udp
69 -# Daemon API; must not be exposed publicly but to client services under you control
68 +# Swarm P2P port (TCP/UDP) - expose publicly for peer connections
69 +EXPOSE 4001 4001/udp
70 +# API port - keep private, only for trusted clients
71 EXPOSE 5001
71 -# Web Gateway; can be exposed publicly with a proxy, e.g. as https://ipfs.example.org
72 +# Gateway port - can be exposed publicly via reverse proxy
73 EXPOSE 8080
73 -# Swarm Websockets; must be exposed publicly when the node is listening using the websocket transport (/ipX/.../tcp/8081/ws).
74 +# Swarm WebSockets - expose publicly for browser-based peers
75 EXPOSE 8081
76
76 -# Create the fs-repo directory and switch to a non-privileged user.
77 +# Create ipfs user (uid 1000) and required directories with proper ownership
78 ENV IPFS_PATH=/data/ipfs
78 -RUN mkdir -p $IPFS_PATH \
79 +RUN mkdir -p $IPFS_PATH /ipfs /ipns /mfs /container-init.d \
80 && adduser -D -h $IPFS_PATH -u 1000 -G users ipfs \
80 - && chown ipfs:users $IPFS_PATH
81 -
82 -# Create mount points for `ipfs mount` command
83 -RUN mkdir /ipfs /ipns /mfs \
84 - && chown ipfs:users /ipfs /ipns /mfs
85 -
86 -# Create the init scripts directory
87 -RUN mkdir /container-init.d \
88 - && chown ipfs:users /container-init.d
81 + && chown ipfs:users $IPFS_PATH /ipfs /ipns /mfs /container-init.d
82
90 -# Expose the fs-repo as a volume.
91 -# start_ipfs initializes an fs-repo if none is mounted.
92 -# Important this happens after the USER directive so permissions are correct.
83 +# Volume for IPFS repository data persistence
84 VOLUME $IPFS_PATH
85
86 # The default logging level
87 ENV GOLOG_LOG_LEVEL=""
88
98 -# This just makes sure that:
99 -# 1. There's an fs-repo, and initializes one if there isn't.
100 -# 2. The API and Gateway are accessible from outside the container.
89 +# Entrypoint initializes IPFS repo if needed and configures networking.
90 +# tini ensures proper signal handling and zombie process cleanup
91 ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/start_ipfs"]
92
103 -# Healthcheck for the container
104 -# QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn is the CID of empty folder
93 +# Health check verifies IPFS daemon is responsive.
94 +# Uses empty directory CID (QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn) as test
95 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
96 CMD ipfs --api=/ip4/127.0.0.1/tcp/5001 dag stat /ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn || exit 1
97
108 -# Execute the daemon subcommand by default
98 +# Default: run IPFS daemon with auto-migration enabled
99 CMD ["daemon", "--migrate=true", "--agent-version-suffix=docker"]