Linux on ARM: Running Servers on aarch64

Linux on ARM: Running Servers on aarch64

ARM servers are no longer unusual. The major cloud providers offer them at lower cost per core than x86, Apple made aarch64 laptops ordinary, and a Raspberry Pi runs a serious set of services.

Most of the time it is just Linux. This covers the parts where it is not.

Naming

Three terms, and the confusion is worth clearing up.

armhf is 32-bit ARM with hardware floating point. Older Raspberry Pi models, some embedded boards. Increasingly legacy.

arm64 and aarch64 are the same thing: 64-bit ARM. Debian and Ubuntu call it arm64, Red Hat and Fedora call it aarch64, and the difference is naming convention rather than substance.

uname -m           # aarch64
dpkg --print-architecture   # arm64

If you see both in the same documentation, they are not two things.

What actually differs

Very little, at the operating system level. Debian, Ubuntu, Fedora, Arch, and Alpine all build complete aarch64 archives. apt install nginx works. systemd is systemd.

The differences that surface:

Page size. x86 uses 4K pages. Some server-class ARM systems use 64K, which changes memory accounting and can affect software that assumes 4K.

getconf PAGESIZE

Our hugepages guide covers why page size matters for TLB pressure, and a 64K base page changes those calculations considerably.

Platform variation. x86 machines boot the same way. ARM platforms vary: device trees, vendor bootloaders, and platform-specific firmware. A Raspberry Pi, an AWS Graviton instance, and an Ampere server have little in common below the kernel.

Proprietary software. This is where it actually bites. Commercial software frequently ships amd64 binaries only.

Containers are the real friction

docker run --rm hello-world
# exec format error

That image has no arm64 variant.

docker manifest inspect nginx | grep architecture
docker image inspect myimage | grep Architecture

Most popular images publish multi-architecture manifests now, and docker pull selects the right one automatically. Smaller and older images frequently do not.

Building for both

docker buildx create --name multi --use
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t registry.example.com/myapp:1.2.3 \
  --push .

One command, both architectures, one manifest. Consumers get the right variant without thinking about it.

Building arm64 on an x86 machine uses QEMU emulation and is slow. For anything non-trivial, native builders are better:

docker buildx create --name multi --driver docker-container
docker buildx create --name multi --append --node arm-node ssh://builder@arm-host

A real ARM machine in the build set removes the emulation penalty entirely.

Watch the base image

FROM --platform=$BUILDPLATFORM golang:1.25 AS build
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /app

FROM alpine:3.21
COPY --from=build /app /app

$BUILDPLATFORM keeps the toolchain native while cross-compiling to the target, which is much faster than emulating the whole build.

Our container security guide covers the rest of good image practice, all of which applies unchanged.

Emulation when you need it

sudo apt install qemu-user-static binfmt-support
docker run --rm --platform linux/amd64 amd64-only-image

The kernel’s binfmt support routes foreign binaries through QEMU automatically. It works and it is slow, frequently several times slower than native.

Reasonable for a one-off tool or a build step. Not reasonable for a service.

A Raspberry Pi as a server

Genuinely capable for self-hosting, with one thing that matters more than the rest.

Do not boot from an SD card. SD cards are slow at random I/O and they wear out, and a self-hosted service writing logs and database files will kill one. Boot from an SSD over USB.

# check what you are running from
lsblk
findmnt /

Our Raspberry Pi OS coverage notes the platform tracks an LTS kernel, which is the right choice for fixed hardware.

Beyond storage:

Memory is the real limit. 4GB or 8GB runs a surprising number of containers, and Nextcloud with PostgreSQL and Redis is near the upper end of comfortable.

No hardware transcoding worth the name. A Pi will not serve Jellyfin transcodes well. Direct play is fine.

Power efficiency is the point. A few watts continuously, against thirty or more for a small x86 machine.

Our self-hosting introduction covers choosing a first project, and a Pi is an excellent machine for one.

ARM in the cloud

Graviton on AWS, Ampere on Oracle and Hetzner, Axion on Google. The pitch is cost per core, and it is real: typically 20 to 40 percent cheaper for comparable throughput on suitable workloads.

Suitable means: web servers, application runtimes, databases, anything compiled from source or running in an interpreted language. Which is most things.

Unsuitable means: anything depending on x86-only proprietary software, and anything with hand-written x86 assembly that has no ARM path.

# before migrating, check what you depend on
dpkg --get-selections | awk '{print $1}' | xargs apt-cache policy 2>/dev/null | grep -B2 'amd64'

The realistic migration test is to run your stack on an ARM instance for a week. Most people find it works, and the things that do not are obvious immediately rather than subtly.

Compiling

# native, which is the simple case
make -j$(nproc)

# cross-compiling from x86
sudo apt install gcc-aarch64-linux-gnu
CC=aarch64-linux-gnu-gcc ./configure --host=aarch64-linux-gnu

Our building from source guide covers the general case. Things that break on ARM are usually hand-written assembly, or -march=native flags baked into a build, or assumptions about char signedness, which differs between x86 and ARM and is a genuinely nasty source of bugs.

That last one is worth knowing: char is signed on x86 and unsigned on ARM by default. Code that assumes signed char behaves differently, and it fails in ways that look like data corruption rather than a compilation error.

Frequently Asked Questions

Is ARM ready for production Linux servers?

For most workloads yes. The major distributions build full aarch64 archives, the large cloud providers offer ARM instances at lower cost per core, and mainstream server software is packaged for it. The gaps are in proprietary software and in older container images that were only ever built for amd64.

Why does my Docker image fail on an ARM server?

Because the image was built for amd64 only. Docker will either refuse to run it or silently run it under emulation, which is extremely slow. Check the image manifest for an arm64 variant, and build multi-architecture images for anything you publish yourself.

What is the difference between armhf, arm64, and aarch64?

armhf is the 32-bit ARM architecture with hardware floating point, used on older Raspberry Pi models. arm64 and aarch64 are two names for the same 64-bit architecture, with Debian preferring arm64 and Red Hat preferring aarch64.

Can I run x86 binaries on an ARM server?

Yes, through emulation with qemu-user-static, and it is slow enough that it suits occasional use and builds rather than running a service. Install the binfmt support and the kernel routes foreign binaries through the emulator automatically.

Is a Raspberry Pi good enough to run real services?

A Pi 4 or 5 with adequate storage handles several self-hosted services comfortably. The usual bottleneck is storage rather than CPU, so boot from an SSD over USB rather than an SD card, which wears out and is slow at random I/O.

Do I need different kernel tuning on ARM?

Mostly no, since the kernel abstracts the architecture well. The differences that surface are page size on some server-class ARM systems, which can be 64K rather than 4K, and the fact that hardware-specific features like power management vary considerably between ARM platforms.