Android Emulators in Docker Gitea Actions on x86 NixOS
Android emulation in CI requires hardware acceleration. Without KVM, emulators crawl. With proper setup, they run at native speed. Here’s how to configure Gitea Actions runners on NixOS for KVM-accelerated Android testing.
NixOS Host Configuration
The foundation requires KVM kernel modules and proper device permissions. NixOS loads KVM modules automatically on x86_64 systems, but container access needs explicit configuration.
# gitea_actions_runner.nix
{
# Enable KVM for Android emulator hardware acceleration
services.udev.extraRules = ''
KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"
'';
services.gitea-actions-runner.instances = let
labels = [
"ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"
"ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04"
];
in {
"runner0" = {
enable = true;
name = "TeraCI amd64 (0)";
url = "http://git.local.teralink.net:3000/";
tokenFile = config.age.secrets.gitea_actions_runner_env.path;
settings = {
capacity = 2;
container = {
options = "--device /dev/kvm --group-add 302";
};
};
inherit labels;
};
};
}
The critical line: options = "--device /dev/kvm --group-add 302";
. This passes the KVM device into Docker containers and adds the container to the KVM group.
Docker Configuration
Enable Docker with systemd cgroup driver for proper container management:
# configuration.nix
virtualisation.docker = {
enable = true;
daemon.settings = {
exec-opts = ["native.cgroupdriver=systemd"];
};
};
boot.kernelModules = [
"bridge"
"veth"
"overlay"
# KVM modules load automatically on x86_64
];
Android Workflow Configuration
The Gitea Actions workflow needs specific Android SDK setup and managed device configuration:
name: Integration Tests
on:
push:
branches: [master, develop]
pull_request:
branches: [master, develop]
jobs:
integration-tests:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
GRADLE_OPTS: "-Xmx4g -Dorg.gradle.daemon=false"
ANDROID_HOME: "/root/.android/sdk"
ANDROID_SDK_ROOT: "/root/.android/sdk"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "temurin"
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Cache system images
id: system-images-cache
uses: actions/cache@v4
with:
path: |
/root/.android/sdk/system-images
key: android-system-images-atd-30-34-${{ runner.os }}
- name: Install system images for managed devices
if: steps.system-images-cache.outputs.cache-hit != 'true'
run: |
yes | sdkmanager --licenses || true
sdkmanager "system-images;android-30;google_atd;x86_64"
sdkmanager "system-images;android-34;google_atd;x86_64"
- name: Run integration tests with managed devices
run: |
./gradlew cleanManagedDevices --continue
./gradlew pixel2Api30DebugAndroidTest pixel6Api34DebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.notAnnotation=com.two13tec.zeitmeister.categories.ExcludeFromCI \
--continue
Key points:
- Uses
google_atd
(Android Test Device) images for faster CI execution - Caches system images to avoid repeated downloads
- Runs tests on multiple API levels with managed devices
- Excludes CI-incompatible tests with annotations
Gradle Managed Devices
Configure managed devices in your build.gradle
:
android {
testOptions {
managedDevices {
devices {
pixel2Api30(ManagedVirtualDevice) {
device = "Pixel 2"
apiLevel = 30
systemImageSource = "google_atd"
}
pixel6Api34(ManagedVirtualDevice) {
device = "Pixel 6"
apiLevel = 34
systemImageSource = "google_atd"
}
}
}
}
}
Verification
Check KVM availability in the container:
# In the CI environment
ls -la /dev/kvm
# Should show: crw-rw-rw- 1 root kvm 10, 232 ... /dev/kvm
# Test emulator acceleration
emulator -accel-check
# Should report: accel: OK
Performance Notes
- ATD images start ~40% faster than full Google Play images
- System image caching is essential for build time optimization
- KVM acceleration reduces emulator boot time from 5+ minutes to ~30 seconds
- Gradle managed devices handle emulator lifecycle automatically
The setup works because NixOS properly configures KVM permissions, Docker passes the device through to containers, and Gradle managed devices leverage hardware acceleration for efficient Android testing in CI.
Without KVM passthrough, Android emulators timeout or fail entirely. With it, integration tests complete in reasonable time. The configuration is straightforward once the device permissions align correctly.