Why LXC
I wanted a dedicated runner for my self-hosted GitLab instance. LXC containers are lighter than full VMs and provide enough isolation for a build environment. The runner uses a shell executor, which means jobs run directly on the container with the gitlab-runner user's permissions. This is fine for me (for now), because all my needs are from repos I make myself.
Container Setup
Created an unprivileged Ubuntu container in Proxmox with 2 cores, 4GB RAM, and 20GB disk. The key settings:
- Nesting enabled (required for Docker)
- keyctl enabled
- On local network.
After starting the container, DHCP didn't auto-assign an IP. Had to run dhclient eth0 manually. Set up a DHCP reservation on the router to make it persistent.
LAN Access to GitLab
I figured The runner would be better to talk to GitLab over the local network instead of going out to the internet and back. Added an entry to /etc/hosts:
echo "192.x.x.x gitlab.yourdomain.com" >> /etc/hosts
Installing the Runner
Got this right from gitlab.com
apt update && apt upgrade -y
apt install -y curl git
# Add GitLab runner repo
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | bash
apt install -y gitlab-runner
Docker in LXC
Docker lets CI jobs run in containers without installing every tool on the runner itself. Standard Docker CE install:
apt install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
usermod -aG docker gitlab-runner
The systemctl Problem
LXC containers don't have a full init system, so systemctl start docker doesn't work. Had to start Docker manually:
dockerd &
For auto-start on boot, created /etc/rc.local:
#!/bin/bash
dockerd &
exit 0
Made it executable with chmod +x /etc/rc.local.
Runner Registration
gitlab-runner register \
--non-interactive \
--url "https://gitlab.yourdomain.com" \
--token "glrt-xxxxxxxxxxxx" \
--description "lxc-runner" \
--tag-list "build,test,deploy" \
--executor "shell"
Runner Config
Edited /etc/gitlab-runner/config.toml to allow parallel jobs:
concurrent = 4
check_interval = 3
[[runners]]
name = "lxc-runner"
url = "https://gitlab.yourdomain.com"
token = "YOUR_TOKEN"
executor = "shell"
SSH for Deployments
Generated an SSH key for the gitlab-runner user:
su - gitlab-runner
ssh-keygen -t ed25519 -C "gitlab-runner" -f ~/.ssh/id_ed25519 -N ""
The public key goes on production servers. The private key gets added to GitLab CI/CD variables as SSH_PRIVATE_KEY (type: File, protected, masked).
Things I ran into
- DHCP issues - Had to run
dhclient eth0after first boot - No systemctl - Docker won't start the normal way in LXC
- Nesting required - Container won't run Docker without it enabled in Proxmox
