Containerize Apps for ALPON with Dockerfile Templates
Learn how to containerize applications on the ALPON Edge Computers using Docker and optimized Dockerfile templates. Build lightweight ARM64 containers for edge computing and IoT deployments.
Containerize Apps for ALPON with Dockerfile Templates
Package your application as an ARM64 Docker image that runs on both ALPON X5 AI and ALPON X4. This guide walks through preparing your project, writing the Dockerfile, and using two ready-made templates (Debian for development, Alpine for production) so the resulting image deploys cleanly through Sixfab Connect.
Place your source, dependencies, and a .dockerignore in one project folder, then write a
Dockerfile that picks an ARM64 base image, sets a working directory, copies your code,
installs dependencies, and declares the startup command. Build for ARM64 with
docker buildx build --platform linux/arm64, then push the image to the
Sixfab Container Registry at cr.sixfab.io. From there, deploy it to any
ALPON X5 AI or ALPON X4 through Sixfab Connect.
Both ALPON X5 AI (Raspberry Pi CM5 with Cortex-A76) and ALPON X4 (Raspberry Pi CM4 with Cortex-A72) run on
ARM64. Every image you deploy must include a linux/arm64 variant. Most official
images on Docker Hub (nginx, redis, postgres, node, python) already ship multi-arch manifests. Custom
images built on an x86 workstation must use docker buildx build --platform linux/arm64. A
plain docker build on a Mac or PC produces an x86 image that fails to start on ALPON with
exec format error.
Why ALPON for containerization
Every ALPON device ships with a Docker runtime preinstalled and authenticated against the Sixfab Container
Registry out of the box. There is no SSH setup, no per-device credential rotation, no manual
docker pull. The two devices in the line share the same ARM64 build target, so a single image
works on both.
apt install docker, no daemon config.
linux/arm64 image runs on both ALPON X5 AI and ALPON X4 without changes.
Traditional vs. containerized application management
In a traditional setup each application sits on its own OS or virtual machine, brings its own dependencies,
and is updated through manual scp and systemctl rituals. Scaling means provisioning
more hosts. Debugging means dialling into individual devices. On a fleet of edge devices that pattern collapses
fast.
Containers flip the model. The application and every dependency it needs are baked into one immutable image. The same image runs identically on every device, deployments take seconds, and rollbacks are a tag change rather than a hot-fix shell session. For ALPON fleets, that is the only model we recommend.
Prepare the project for containerization
Before writing the Dockerfile, get the project folder in shape. The image build process copies whatever is in this directory into the container, so anything that belongs out should be excluded explicitly.
-
1
List every dependency
Every library, runtime, and system tool the app needs must be declared somewhere the build can read. Use the convention for your language:
- Python:
requirements.txt(orpyproject.tomlfor Poetry or uv). - Node.js:
package.jsonwithpackage-lock.json. - Go:
go.modwithgo.sum. - System packages: list them in the Dockerfile
RUN apt-get install(Debian) orRUN apk add(Alpine) line.
A reference Python project layout:
project layout treemy_app/ ├── app.py # Main application entrypoint ├── requirements.txt # Python dependencies ├── Dockerfile └── .dockerignore # Files excluded from the image
- Python:
-
2
Add a
.dockerignorefileWithout a
.dockerignore, every log file, secret, and IDE folder gets copied into the image. That bloats the size, slows pulls over cellular, and can leak credentials. Treat it like a.gitignorefor the build context:.dockerignore ignore# Logs and runtime artefacts *.log *.pyc __pycache__/ # Tests and dev tooling tests/ .vscode/ .idea/ # Secrets and local config .env .env.* # Version control .git/ .gitignore
-
3
Identify the runtime surface
Decide upfront what the container needs from the host. These three answers shape the deployment form on Sixfab Connect later:
- Environment variables: anything that changes between dev and prod (database URLs, API keys, feature flags). Never hard-code these in the source.
- Ports: which TCP/UDP port the app listens on inside the container. On ALPON, the
host port must sit in the
30000–32767range. The container-side port is free. - Volumes: directories the container needs to read or write on the host (persistent storage, device files, configs). Plan these as Docker volume mounts, not bind mounts to specific host paths.
Write the Dockerfile
The Dockerfile is the recipe for the image. Each instruction creates a new image layer; ordered correctly, it lets Docker cache the slow steps and rebuild only what changed. The five instructions below cover the vast majority of edge applications.
FROM
Picks the base image. Always pin a tag (e.g. python:3.11-slim), never use latest in production.
WORKDIR
Sets the working directory inside the container. Subsequent instructions run here.
COPY
Copies files from the build context into the image. Copy requirements.txt before the rest of the source to maximise cache reuse.
RUN
Executes a shell command at build time. Use it to install system packages and language dependencies.
CMD
Defines the command that runs when the container starts. Always exec-form: CMD ["python", "app.py"].
A minimal Python Dockerfile, walked through
Pick the base image. The -slim variants are far smaller than the default and still include
enough to install most pure-Python packages:
# Pinned, slim, ARM64-compatible Python runtime FROM python:3.11-slim
Set the working directory. Every COPY and RUN that follows resolves relative to
it:
WORKDIR /app
Copy requirements.txt first, install, then copy the rest of the source. This ordering means
Docker only rebuilds the dependency layer when requirements.txt changes, so code-only edits skip
the slow pip install step entirely:
COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . .
Declare the startup command. Use exec-form (the JSON array, not the shell string) so signals like
SIGTERM reach the app cleanly when the container stops:
CMD ["python", "app.py"]
The complete Dockerfile
Combine the pieces above:
# Pinned, slim, ARM64-compatible Python runtime FROM python:3.11-slim # Working directory for the rest of the build WORKDIR /app # Install dependencies first for better cache reuse COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy the rest of the source COPY . . # Start the application CMD ["python", "app.py"]
Build it for ARM64
From the project root on your workstation:
# 1. Enable buildx (one-time, if not already) docker buildx create --use # 2. Build the ARM64 image and load it locally docker buildx build --load --platform linux/arm64 -t my-app:latest .
Dockerfile templates
Two ready-made templates cover the two phases of work. Use the Debian template while iterating; switch to the Alpine template before shipping to a fleet.
Built on debian:bookworm-slim. Includes the build toolchain and git so you can compile
native modules, debug from inside the container, and tweak dependencies without rebuilding the base.
- Includes
build-essentialandgit - Familiar
aptecosystem - Larger image (150 to 300 MB), fine for dev
Built on alpine:latest. Stripped down to the minimum runtime, no build tools, no shell
utilities you don't need. Pulls faster over cellular and presents a smaller attack surface.
- Uses the
apkpackage manager - Tiny image (5 to 50 MB typical)
- Faster pulls, less to patch
Development Dockerfile (Debian-based)
Drop this in the project root during active development:
# Development Dockerfile, Debian-based FROM debian:bookworm-slim WORKDIR /app # Build toolchain and dev utilities RUN apt-get update && apt-get install -y build-essential git && rm -rf /var/lib/apt/lists/* COPY . . # Mark the runtime environment ENV APP_ENV=development # Replace with your dev entrypoint CMD ["./your-dev-app"]
Production Dockerfile (Alpine-based)
Switch to this template once the app is stable. It optimises for image size, pull time, and the smaller attack surface that matters on a fleet of edge devices:
# Production Dockerfile, Alpine-based FROM alpine:latest WORKDIR /app # Runtime-only dependencies, no build tools RUN apk add --no-cache libc6-compat && rm -rf /var/cache/apk/* COPY . . # Mark the runtime environment ENV APP_ENV=production # Replace with your prod entrypoint CMD ["./your-prod-app"]
Alpine uses apk; Debian uses apt. Package names also differ. Debian's
libssl-dev is Alpine's openssl-dev; Debian's libpq-dev is Alpine's
postgresql-dev. When porting from the dev template to prod, look up each apt
package on pkgs.alpinelinux.org
and replace it with the Alpine equivalent. Native C extensions built against glibc may also need
libc6-compat on Alpine (musl).
Best practices for ALPON containers
The same image runs on both ALPON X5 AI and ALPON X4, but small choices in the Dockerfile decide whether it pulls in 10 seconds or 10 minutes over a cellular link, and whether it survives the next dependency update.
Pick small base images
Prefer -slim, -alpine, or distroless variants over the default full-fat tags. A 50 MB image pulls in seconds; a 1.2 GB image can take half an hour over cellular.
Pin every version
Use python:3.11-slim, not python:latest. Use nginx:1.27-alpine, not nginx:alpine. Pinned tags mean today's working build still works in three months.
Always include a .dockerignore
Strip out .git, logs, secrets, IDE folders, and test artefacts. A clean build context is faster to send to the daemon and produces smaller, safer images.
Order layers for cache reuse
Copy dependency manifests (requirements.txt, package.json) and install them before copying the rest of the source. Edits to app.py won't invalidate the dependency layer.
Combine RUN commands
Each RUN creates a layer. Chain related commands with && and clean up in the same line so removed files don't survive in the previous layer.
Use ENV for configuration, not source code
Database URLs, API keys, log levels. Set them as environment variables on the deploy form. Hard-coded values mean a code change for every environment.
Build with buildx for ARM64
A plain docker build on an x86 Mac or PC produces an x86 image that fails to start on ALPON. Always include --platform linux/arm64.
Use exec-form CMD
Exec form passes signals (SIGTERM on stop) directly to your process, so the container shuts down cleanly instead of being killed nine seconds later.
A polished, production-ready example
Applying the practices above to a real Dockerfile:
# Pinned, slim, ARM64 base FROM python:3.11-slim WORKDIR /app # System packages and Python deps in one layer, with cleanup RUN apt-get update && apt-get install -y --no-install-recommends libpq5 && rm -rf /var/lib/apt/lists/* # Dependencies first, cached unless requirements.txt changes COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Then the source COPY . . # Runtime configuration via ENV (override on the deploy form) ENV APP_ENV=production LOG_LEVEL=info # Exec-form CMD for clean signal handling CMD ["python", "app.py"]
Next steps: push and deploy
The image is built and tested locally. Two things remain: push it to the Sixfab Container Registry, then
deploy it from Sixfab Connect. Both are covered end-to-end on the Deploy Applications guide, including the
exact docker login, docker tag, and docker push sequence, plus the
deploy form fields (ports, environment variables, volumes, network mode).
