I have just set up something like that a few weeks ago (CLI utility that can run in "watch mode", deployment to various "on-premise" servers via docker image, both ARM and x64).
I can't vouch for "best practice" or anything, but it's working just fine for me, so here is what I use:
dockerfile ("watson" is the name of the CLI app/product)
# syntax=docker/dockerfile:1.4
ARG SWIFT_VERSION=5.10
###################################
#####
##### FOR BUILDING SWIFT EXECUTABLES #####
FROM swift:$SWIFT_VERSION AS builder
WORKDIR /build
# Resolve all dependencies if there was a change
COPY --link ./Package.* ./
RUN --mount=type=cache,target=/build/.build swift package resolve --disable-automatic-resolution
# Copy all sources and build all
COPY --link . .
# build with caching and copy app out of cached folder
RUN --mount=type=cache,target=/build/.build \
swift build -c release --product watson --skip-update --disable-automatic-resolution --no-static-swift-stdlib \
&& cp -a .build/release/ /release
###################################
#####
##### RUNTIME IMAGE #####
FROM swift:$SWIFT_VERSION-slim
WORKDIR /app
COPY --link --from=builder /release/watson ./watson
ENTRYPOINT ["./watson"]
You can build this just fine with docker (locally or in CI), but I use github actions and depot.dev to build a multi-arch image whenever I push a version tag, and then host it via github packages.
Here is my workflow file:
name: Push Docker Image
on:
workflow_dispatch:
push:
tags:
- v*
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker Tags
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/watson-agent
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- uses: depot/setup-action@v1
- name: Build and push
uses: depot/build-push-action@v1
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
You can then simply docker pull
the image and run commands.
If you have tight control of the targeted linux distro, (cross-)compiling to a native binary will obviously result in a much smaller deliverable, otherwise the headache of managing distros, runtime-dependencies, CPU-architectures, etc. is very much not fun.
One could definitely tweak the runtime image (instead of using the comfy swift:5.10-slim
) to shave off a few MBs, but that gets fiddly quickly as well...