Deployment

Docker

Quick Start

docker run -d \
  --name featherci \
  -p 8080:8080 \
  -v featherci-data:/data \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -e FEATHERCI_SECRET_KEY=$(openssl rand -base64 32) \
  -e FEATHERCI_BASE_URL=https://ci.example.com \
  -e FEATHERCI_ADMINS=yourusername \
  -e FEATHERCI_GITHUB_CLIENT_ID=your_client_id \
  -e FEATHERCI_GITHUB_CLIENT_SECRET=your_client_secret \
  ghcr.io/featherci/featherci:latest

Docker Socket Mapping

FeatherCI executes build steps inside Docker containers. When running FeatherCI itself in Docker, mount the host's Docker socket for sibling containers:

-v /var/run/docker.sock:/var/run/docker.sock

Build containers run alongside FeatherCI on the host's Docker daemon, not nested inside it. This is simpler and more performant than Docker-in-Docker (dind).

Permissions

The FeatherCI process must have permission to access the Docker socket:

  1. Run as root (default in the official image)
  2. Match the docker group GID from the host:
    docker run --group-add $(getent group docker | cut -d: -f3) ...

DOCKER_HOST

By default, FeatherCI connects via the unix socket at /var/run/docker.sock. For a remote Docker daemon:

-e DOCKER_HOST=tcp://docker-host:2375

Docker Compose

services:
  featherci:
    image: ghcr.io/featherci/featherci:latest
    ports:
      - "8080:8080"
    volumes:
      - featherci-data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      FEATHERCI_SECRET_KEY: "${FEATHERCI_SECRET_KEY}"
      FEATHERCI_BASE_URL: "https://ci.example.com"
      FEATHERCI_ADMINS: "yourusername"
      FEATHERCI_DATABASE_PATH: "/data/featherci.db"
      FEATHERCI_CACHE_PATH: "/data/cache"
      FEATHERCI_WORKSPACE_PATH: "/data/workspaces"
      FEATHERCI_GITHUB_CLIENT_ID: "${FEATHERCI_GITHUB_CLIENT_ID}"
      FEATHERCI_GITHUB_CLIENT_SECRET: "${FEATHERCI_GITHUB_CLIENT_SECRET}"
    restart: unless-stopped

volumes:
  featherci-data:

Create a .env file alongside your docker-compose.yml with the secret values:

FEATHERCI_SECRET_KEY=your-base64-key
FEATHERCI_GITHUB_CLIENT_ID=your_id
FEATHERCI_GITHUB_CLIENT_SECRET=your_secret

Data Persistence

Mount a volume at /data and configure paths:

VariableRecommended Value
FEATHERCI_DATABASE_PATH/data/featherci.db
FEATHERCI_CACHE_PATH/data/cache
FEATHERCI_WORKSPACE_PATH/data/workspaces

Reverse Proxy

Place FeatherCI behind a reverse proxy for TLS termination. Set FEATHERCI_BASE_URL to the public HTTPS URL.

Caddy:

ci.example.com {
    reverse_proxy featherci:8080
}

nginx:

server {
    listen 443 ssl;
    server_name ci.example.com;

    ssl_certificate /etc/ssl/certs/ci.example.com.pem;
    ssl_certificate_key /etc/ssl/private/ci.example.com.key;

    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Distributed Setup with Docker

# Master
docker run -d \
  --name featherci-master \
  -p 8080:8080 \
  -v featherci-data:/data \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -e FEATHERCI_MODE=master \
  -e FEATHERCI_WORKER_SECRET=your-shared-secret \
  -e FEATHERCI_SECRET_KEY=... \
  -e FEATHERCI_BASE_URL=https://ci.example.com \
  ghcr.io/featherci/featherci:latest

# Worker (on another host)
docker run -d \
  --name featherci-worker \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -e FEATHERCI_MODE=worker \
  -e FEATHERCI_MASTER_URL=https://ci.example.com \
  -e FEATHERCI_WORKER_SECRET=your-shared-secret \
  ghcr.io/featherci/featherci:latest

Systemd

Run FeatherCI as a systemd service on Linux for automatic startup and process management.

Prerequisites

  • FeatherCI binary installed (e.g. at /usr/local/bin/featherci)
  • Docker installed and running
  • A dedicated system user (optional but recommended)

Create a System User

sudo useradd --system --shell /usr/sbin/nologin --create-home --home-dir /var/lib/featherci featherci
sudo usermod -aG docker featherci

Configure

Create a YAML config file at /etc/featherci/config.yaml:

sudo mkdir -p /etc/featherci
sudo tee /etc/featherci/config.yaml > /dev/null <<'EOF'
bind_addr: ":8080"
base_url: "https://ci.example.com"
mode: standalone
database_path: /var/lib/featherci/featherci.db
secret_key: "your-base64-encoded-key"
admins:
  - yourusername
max_concurrent: 2
cache_path: /var/lib/featherci/cache
workspace_path: /var/lib/featherci/workspaces

github:
  client_id: "your_client_id"
  client_secret: "your_client_secret"
EOF
sudo chmod 600 /etc/featherci/config.yaml

Generate the secret key with featherci --generate-key and paste it into the config file.

FeatherCI automatically checks /etc/featherci/config.yaml on startup. You can also specify a custom path with featherci --config /path/to/config.yaml or the FEATHERCI_CONFIG environment variable.

Alternative: Environment file

If you prefer environment variables, create /etc/featherci/featherci.env instead and reference it from the service file with EnvironmentFile=/etc/featherci/featherci.env:

FEATHERCI_SECRET_KEY=your-base64-encoded-key
FEATHERCI_BASE_URL=https://ci.example.com
FEATHERCI_ADMINS=yourusername
FEATHERCI_GITHUB_CLIENT_ID=your_client_id
FEATHERCI_GITHUB_CLIENT_SECRET=your_client_secret
FEATHERCI_DATABASE_PATH=/var/lib/featherci/featherci.db
FEATHERCI_CACHE_PATH=/var/lib/featherci/cache
FEATHERCI_WORKSPACE_PATH=/var/lib/featherci/workspaces

Environment variables always take precedence over the YAML config file.

Create the Service File

Create /etc/systemd/system/featherci.service:

[Unit]
Description=FeatherCI CI/CD Server
Documentation=https://featherci.github.io
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service

[Service]
Type=simple
User=featherci
Group=featherci
EnvironmentFile=/etc/featherci/featherci.env
ExecStart=/usr/local/bin/featherci
Restart=on-failure
RestartSec=5
WorkingDirectory=/var/lib/featherci

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/featherci
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Enable and Start

sudo systemctl daemon-reload
sudo systemctl enable featherci
sudo systemctl start featherci

Check Status

sudo systemctl status featherci
sudo journalctl -u featherci -f

Updating

# Download new binary
sudo curl -Lo /usr/local/bin/featherci https://github.com/featherci/featherci/releases/latest/download/featherci-linux-amd64
sudo chmod +x /usr/local/bin/featherci

# Restart
sudo systemctl restart featherci

Kubernetes

Experimental. Kubernetes deployment is functional but not yet battle-tested in production. Feedback is welcome.

FeatherCI can run in Kubernetes using the Docker socket from the host node. Build steps run as sibling containers on the same node — they are Docker containers, not Kubernetes pods.

Namespace

apiVersion: v1
kind: Namespace
metadata:
  name: featherci

Secret

apiVersion: v1
kind: Secret
metadata:
  name: featherci-config
  namespace: featherci
type: Opaque
stringData:
  FEATHERCI_SECRET_KEY: "your-base64-encoded-key"
  FEATHERCI_GITHUB_CLIENT_ID: "your_client_id"
  FEATHERCI_GITHUB_CLIENT_SECRET: "your_client_secret"

Persistent Volume Claim

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: featherci-data
  namespace: featherci
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: featherci
  namespace: featherci
spec:
  replicas: 1
  selector:
    matchLabels:
      app: featherci
  template:
    metadata:
      labels:
        app: featherci
    spec:
      containers:
        - name: featherci
          image: ghcr.io/featherci/featherci:latest
          ports:
            - containerPort: 8080
          env:
            - name: FEATHERCI_BASE_URL
              value: "https://ci.example.com"
            - name: FEATHERCI_ADMINS
              value: "yourusername"
            - name: FEATHERCI_DATABASE_PATH
              value: "/data/featherci.db"
            - name: FEATHERCI_CACHE_PATH
              value: "/data/cache"
            - name: FEATHERCI_WORKSPACE_PATH
              value: "/data/workspaces"
          envFrom:
            - secretRef:
                name: featherci-config
          volumeMounts:
            - name: data
              mountPath: /data
            - name: docker-socket
              mountPath: /var/run/docker.sock
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 30
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            initialDelaySeconds: 3
            periodSeconds: 10
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: featherci-data
        - name: docker-socket
          hostPath:
            path: /var/run/docker.sock
            type: Socket

Service

apiVersion: v1
kind: Service
metadata:
  name: featherci
  namespace: featherci
spec:
  selector:
    app: featherci
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP

Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: featherci
  namespace: featherci
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts:
        - ci.example.com
      secretName: featherci-tls
  rules:
    - host: ci.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: featherci
                port:
                  number: 80

Important Notes

  • Docker socket: The deployment mounts the host's Docker socket. Build containers run on the host's Docker daemon, not as Kubernetes pods.
  • Single replica: FeatherCI uses SQLite, which does not support concurrent writes. Run a single replica in standalone mode.
  • Node affinity: Ensure the pod always schedules on the same node (or use a ReadWriteOnce PVC that pins it).
  • Security: Mounting the Docker socket gives the container root-equivalent access to the host.