This post is part of the series about my personal BeagleBone Black project. The project’s CI/CD infrastructure runs on Drone CI — but not with Docker, rather with Podman as the container backend. What sounds simple on paper has a few rough edges in practice.
Why Podman instead of Docker?
The short answer: rootless operation and no daemon.
Docker requires a privileged daemon running permanently — dockerd —
and access to the Docker socket effectively means root privileges on the host.
That’s an acceptable tradeoff for many environments, but not for my private server.
Podman runs without a daemon:
# Docker: client talks to a daemon running as root
docker run ubuntu echo hello
# Podman: no daemon, direct fork/exec as a normal user
podman run ubuntu echo helloConcrete advantages in operation:
No permanently running privileged process
Containers run under the calling user’s own UID
podman-auto-updateinstead of Docker watchersDrop-in compatible with Docker for most cases
Podman is not a perfect Docker replacement. Where there are differences, I describe them further below under the pitfalls. |
Installing the Drone CI server with Podman
The Drone server and runner themselves run as containers — started with Podman.
Drone server
podman run \
--detach \
--name=drone \
--restart=always \
--env=DRONE_GITEA_SERVER=https://gitea.example.com \
--env=DRONE_GITEA_CLIENT_ID=<client-id> \
--env=DRONE_GITEA_CLIENT_SECRET=<secret> \
--env=DRONE_RPC_SECRET=<shared-secret> \
--env=DRONE_SERVER_HOST=drone.example.com \
--env=DRONE_SERVER_PROTO=https \
--publish=80:80 \
--publish=443:443 \
--volume=/var/lib/drone:/data \
docker.io/drone/drone:2Drone runner (Podman backend)
The standard Drone Docker runner talks to the Docker socket. For Podman, there are two approaches:
Option A — expose a Docker-compatible socket:
Podman can provide a Docker-compatible socket:
systemctl --user enable --now podman.socket
# Socket is located at: /run/user/<UID>/podman/podman.sockThen start the runner with the Podman socket:
podman run \
--detach \
--name=drone-runner \
--restart=always \
--env=DRONE_RPC_PROTO=https \
--env=DRONE_RPC_HOST=drone.example.com \
--env=DRONE_RPC_SECRET=<shared-secret> \
--env=DRONE_RUNNER_CAPACITY=2 \
--volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock \
docker.io/drone/drone-runner-docker:1Option B — exec runner (no container socket needed):
The exec runner runs steps directly on the host — simpler, less isolated:
podman run \
--detach \
--name=drone-runner-exec \
--restart=always \
--env=DRONE_RPC_PROTO=https \
--env=DRONE_RPC_HOST=drone.example.com \
--env=DRONE_RPC_SECRET=<shared-secret> \
--volume=/var/run/drone-runner:/data \
docker.io/drone/drone-runner-exec:latestI use Option A because the Docker runner offers better step isolation
and .drone.yml files work without adjustment.
Pitfalls
Volume mounts and file permissions
The biggest problem with rootless operation: file permissions.
Rootless Podman maps UIDs inside the container to subuid ranges of the host user.
A process running as root (UID 0) inside the container runs on the host as
the user’s UID plus an offset.
Problem: A build step writes files as root inside the container —
after the build, these files belong to a UID on the host that doesn’t exist.
Fix: Explicit UID mapping or --userns=keep-id:
# .drone.yml
steps:
- name: build
image: ubuntu:22.04
volumes:
- name: workspace
path: /workspace
environment:
DRONE_WORKSPACE_BASE: /workspace
volumes:
- name: workspace
host:
path: /tmp/drone-workspaceOr start the Drone runner with --userns=keep-id, so the container user
matches the host user.
Network isolation between steps
Docker runner steps in the same pipeline run share a network.
With Podman in a rootless setup, the network backend (slirp4netns vs pasta)
can lead to different results.
Problem: A database container in step A is not reachable from step B under localhost.
Fix: Name service containers explicitly and address them by hostname:
services:
- name: postgres
image: postgres:15
environment:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: test
steps:
- name: test
image: golang:1.21
commands:
# The hostname is the service name, not localhost
- go test -db-host=postgres ./...Secrets and environment variables
Drone secrets work unchanged — that’s not a Podman-specific problem. But: with rootless Podman, secrets must not be written to mounted host directories that are accessible to other users.
Best practice: Secrets only as environment variables, never as files in /tmp.
steps:
- name: deploy
image: alpine
environment:
SSH_KEY:
from_secret: ssh_private_key
commands:
# Keep the key in memory, never on disk
- eval $(ssh-agent -s)
- echo "$SSH_KEY" | ssh-add -
- ssh user@host "systemctl restart myservice"Cross-compilation and privileged operations
My build setup cross-compiles for ARMv7.
The cross-compilation container doesn’t need any special privileges —
but I initially fell into the trap of setting --privileged because
another step needed it.
Rule: Reduce every step to minimal privileges.
Only use --privileged when truly necessary (e.g. loading kernel modules).
Cross-compilation never needs it.
A working .drone.yml configuration
This is my actual pipeline — simplified but functional:
kind: pipeline
type: docker
name: beaglebone-build
steps:
- name: build-c
image: ubuntu:22.04
commands:
- apt-get update -q && apt-get install -y cmake gcc-arm-linux-gnueabihf
- cmake -DCMAKE_TOOLCHAIN_FILE=cmake/armv7-toolchain.cmake -B build-arm
- cmake --build build-arm
- name: build-rust
image: rust:1.75
commands:
- rustup target add armv7-unknown-linux-gnueabihf
- cd rust
- cargo build --release --target armv7-unknown-linux-gnueabihf
- name: build-go
image: golang:1.21
commands:
- cd go-api
- go build ./...
- go test ./...
- name: deploy
image: alpine
environment:
SSH_KEY:
from_secret: bbb_ssh_key
commands:
- apk add --no-cache openssh-client rsync
- eval $(ssh-agent -s)
- echo "$SSH_KEY" | ssh-add -
- rsync -avz build-arm/ pi@beaglebone:/opt/bbb-hal/
- ssh pi@beaglebone "systemctl restart bbb-hal"
when:
branch:
- mainConclusion
Podman as a Drone CI backend works well — with a few adjustments.
When Podman pays off:
No Docker daemon should run permanently
Rootless operation is a requirement
Private server without complex container orchestration
When Docker is simpler:
Large teams with existing Docker workflows
Many images that use the Docker socket directly
When compatibility issues cost more time than running the daemon
For my private project, Podman is the right choice. The pitfalls were solvable, and the result is a CI infrastructure that doesn’t rely on privileged background services.
Next post in the series: Cross-Compilation for ARMv7 with CMake