Best practices for deploying an executable on Linux

Hello,

I'm struggling to find inspiration online about the currently recommended way to distribute a binary on a Linux box. Say I have an SPM package with a few executable targets, and I wish I can deploy and run binaries on a Linux server.

There are so many questions I'm lost. Do you happen to know a good online reference or tutorial?

  • Do I need to cross-compile on a Mac and distribute binaries via some Docker encapsulation that depends on the Swift runtime?
  • Do I need to ship Swift sources embedded in some Docker image that depends on the Swift compiler, and build the executables once sources are deployed?

As you can see, I'm pretty blue and noob - both about Docker and Linux distribution, and Swift integration in this landscape. A tutorial or a general sketch of the recommended pattern would be super helpful.

5 Likes

We have some guides over here, they may be helpful: Swift.org - Deploying to Servers or Public Cloud Have you seen those? Feel free to follow up with questions!

Two popular methods would be just docker on the server, or building the linux binary in docker locally and then distributing that.

Thank you! I'll certainly have a second look, and ask more specific questions if I have any.

1 Like

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...

3 Likes

i would not try to run Docker on the cloud machine, instead i would use Docker locally to replicate the Swift runtime installed on the cloud machine and compile the package inside the Docker container, possibly using -scratch-path to avoid conflicting with your macOS build artifacts.

if you have an Apple Silicon mac, it will be a lot easier to choose an aarch64 cloud machine as opposed to an x86_64 machine.

it will probably be much easier to mount your local clone as a Docker volume. when you've built the executables, i recommend uploading them to some central binary store, like a private S3 bucket, and then upgrade the application by downloading the newest binaries from the S3 bucket, rather than uploading them directly to the cloud machine.

(i would also recommend storing a copy of the Swift runtime in the same bucket and using that to bootstrap a new host)

1 Like