Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
122 changes: 122 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: Docker
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

on:
push:
branches: [main]
tags: ['v*']
Comment thread
jlucaso1 marked this conversation as resolved.
workflow_dispatch:

# Native per-arch runners instead of QEMU: build-std + fat LTO is far too slow
# under emulation. Each arch builds in parallel, pushed by digest, then merged
# into one multi-arch manifest.
concurrency:
group: docker-${{ github.ref }}
cancel-in-progress: true

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- name: Prepare platform pair
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV"

- uses: actions/checkout@v6

- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

- uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- id: build
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
# Per-arch cache scope so the two matrix legs never clobber each other.
cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }}
cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }}
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true

- name: Export digest
run: |
mkdir -p "${{ runner.temp }}/digests"
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"

- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1

merge:
runs-on: ubuntu-latest
needs: [build]
permissions:
contents: read
packages: write
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true

- uses: docker/setup-buildx-action@v3

- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=semver,pattern={{version}}
type=sha
type=raw,value=latest,enable={{is_default_branch}}

- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
env:
DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }}

- name: Inspect
run: docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
36 changes: 28 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
# Build: docker build -t whatsapp-rust .
# Run: docker run -v whatsapp-data:/data whatsapp-rust
#
# The /data volume persists the SQLite database across restarts.
# The /data volume persists the SQLite database across restarts. The image runs
# unprivileged (uid 65532); use a named volume (as below) so it inherits that
# ownership and stays writable — a host bind mount would need matching ownership.
# Pass --phone <number> for pair code auth:
# docker run -v whatsapp-data:/data whatsapp-rust --phone 15551234567

Expand All @@ -33,7 +35,11 @@ FROM chef AS builder
# (measured: -666 KiB, -5.6% .text). Nightly-only, which this image already
# pins via rust-toolchain.toml; with fat LTO the historical inlining downside
# does not apply since LTO sees all bitcode anyway.
ENV RUSTFLAGS="-C target-cpu=native -Zshare-generics=y"
# No -C target-cpu: a published image must run on any host of its arch, so we
# keep the portable musl baseline instead of pinning the build machine's CPU
# (target-cpu=native would risk SIGILL on older/different hosts and is
# meaningless under emulated cross-arch builds).
ENV RUSTFLAGS="-Zshare-generics=y"

# build-std recompiles std with the release profile so it participates in fat
# LTO and dead-code elimination instead of linking the prebuilt rustup std
Expand All @@ -47,19 +53,33 @@ ENV CARGO_UNSTABLE_BUILD_STD="std,panic_abort"
# chef stage's copy at /; the nightly-only RUSTFLAGS above depends on it.
COPY rust-toolchain.toml .
COPY --from=planner /app/recipe.json recipe.json
# build-std demands an explicit --target; use the image's own host triple so
# multi-arch builds (e.g. buildx linux/arm64) keep producing native binaries
# exactly like the implicit-target build did.
RUN rustc -vV | sed -n 's/^host: //p' > /rust-target && test -s /rust-target
RUN cargo chef cook --release --recipe-path recipe.json --target "$(cat /rust-target)"
# build-std demands an explicit --target; derive the image's own host triple so
# buildx per-arch builds (linux/amd64, linux/arm64) each target their own arch.
# No pipe, so a rustc failure isn't masked by sed's exit status (Hadolint DL4006).
RUN rustc -vV > /rustc-version \
&& sed -n 's/^host: //p' /rustc-version > /rust-target \
&& test -s /rust-target \
&& rm /rustc-version
# Cook with the same --example as the final build so the example's dev-deps
# (env_logger, …) are cached here instead of recompiling after the source COPY.
RUN cargo chef cook --release --recipe-path recipe.json --target "$(cat /rust-target)" --example demo
Comment thread
jlucaso1 marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
COPY . .
# The client lives in examples/demo.rs (the package no longer ships a bin); the
# example artifact lands under release/examples/. Default features cover it.
RUN cargo build --release --example demo --target "$(cat /rust-target)" \
&& cp "target/$(cat /rust-target)/release/examples/demo" /app/whatsapp-rust-bin

# --- Runtime: static binary on empty image ---
# Empty dirs to stage into the scratch image; the COPY --chown below grants them
# to the unprivileged uid so /data (DB; a fresh named volume inherits the
# ownership) and /tmp (SQLite temp files, absent on scratch) stay writable.
RUN mkdir -p /newroot/data /newroot/tmp

# --- Runtime: static binary on empty image, unprivileged ---
FROM scratch
COPY --from=builder --chown=65532:65532 /newroot/tmp /tmp
COPY --from=builder --chown=65532:65532 /newroot/data /data
COPY --from=builder /app/whatsapp-rust-bin /whatsapp-rust
ENV TMPDIR=/tmp
WORKDIR /data
USER 65532:65532
Comment thread
jlucaso1 marked this conversation as resolved.
ENTRYPOINT ["/whatsapp-rust"]
Loading