Files
compose-farm/docs/getting-started.md
Bas Nijholt 26dea691ca feat(docker): make container user configurable via CF_UID/CF_GID (#118)
* feat(docker): make container user configurable via CF_UID/CF_GID

Add support for running compose-farm containers as a non-root user
to preserve file ownership on mounted volumes. This prevents files
like compose-farm-state.yaml and web UI config edits from being
owned by root on NFS mounts.

Set CF_UID, CF_GID, and CF_HOME environment variables to run as
your user. Defaults to root (0:0) for backwards compatibility.

* docs: document non-root user configuration for Docker

- Add CF_UID/CF_GID/CF_HOME documentation to README and getting-started
- Add XDG config volume mount for backup/log persistence across restarts
- Update SSH volume examples to use CF_HOME variable

* fix(docker): allow non-root user access and add USER env for SSH

- Add `chmod 755 /root` to Dockerfile so non-root users can access
  the installed tool at /root/.local/share/uv/tools/compose-farm
- Add USER environment variable to docker-compose.yml for SSH to work
  when running as non-root (UID not in /etc/passwd)
- Update docs to include CF_USER in the setup instructions
- Support building from local source with SETUPTOOLS_SCM_PRETEND_VERSION

* fix(docker): revert local build changes, keep only chmod 755 /root

Remove the local source build logic that was added during testing.
The only required change is `chmod 755 /root` to allow non-root users
to access the installed tool.

* docs: add .envrc.example for direnv users

* docs: mention direnv option in README and getting-started
2025-12-21 22:19:40 -08:00

6.6 KiB

icon
icon
lucide/rocket

Getting Started

This guide walks you through installing Compose Farm and setting up your first multi-host deployment.

Prerequisites

Before you begin, ensure you have:

  • uv (recommended) or Python 3.11+
  • SSH key-based authentication to your Docker hosts
  • Docker and Docker Compose installed on all target hosts
  • Shared storage for compose files (NFS, Syncthing, etc.)

Installation

curl -fsSL https://compose-farm.nijho.lt/install | sh

This installs uv if needed, then installs compose-farm.

Using uv

If you already have uv installed:

uv tool install compose-farm

Using pip

If you already have Python 3.11+ installed:

pip install compose-farm

Using Docker

docker run --rm \
  -v $SSH_AUTH_SOCK:/ssh-agent -e SSH_AUTH_SOCK=/ssh-agent \
  -v ./compose-farm.yaml:/root/.config/compose-farm/compose-farm.yaml:ro \
  ghcr.io/basnijholt/compose-farm up --all

Running as non-root user (recommended for NFS mounts):

By default, containers run as root. To preserve file ownership on mounted volumes, set these environment variables in your .env file:

# Add to .env file (one-time setup)
echo "CF_UID=$(id -u)" >> .env
echo "CF_GID=$(id -g)" >> .env
echo "CF_HOME=$HOME" >> .env
echo "CF_USER=$USER" >> .env

Or use direnv to auto-set these variables when entering the directory:

cp .envrc.example .envrc && direnv allow

This ensures files like compose-farm-state.yaml and web UI edits are owned by your user instead of root. The CF_USER variable is required for SSH to work when running as a non-root user.

Verify Installation

cf --version
cf --help

SSH Setup

Compose Farm uses SSH to run commands on remote hosts. You need passwordless SSH access.

Option 1: SSH Agent (default)

If you already have SSH keys loaded in your agent:

# Verify keys are loaded
ssh-add -l

# Test connection
ssh user@192.168.1.10 "docker --version"

For persistent access when running in Docker:

# Generate and distribute key to all hosts
cf ssh setup

# Check status
cf ssh status

This creates ~/.ssh/compose-farm/id_ed25519 and copies the public key to each host.

Shared Storage Setup

Compose files must be accessible at the same path on all hosts. Common approaches:

NFS Mount

# On each Docker host
sudo mount nas:/volume1/compose /opt/compose

# Or add to /etc/fstab
nas:/volume1/compose /opt/compose nfs defaults 0 0

Directory Structure

/opt/compose/           # compose_dir in config
├── plex/
│   └── docker-compose.yml
├── grafana/
│   └── docker-compose.yml
├── nextcloud/
│   └── docker-compose.yml
└── jellyfin/
    └── docker-compose.yml

Configuration

Create Config File

Create compose-farm.yaml in the directory where you'll run commands. For example, if your stacks are in /opt/stacks, place the config there too:

cd /opt/stacks
cf config init

Alternatively, use ~/.config/compose-farm/compose-farm.yaml for a global config. You can also symlink a working directory config to the global location:

# Create config in your stacks directory, symlink to ~/.config
cf config symlink /opt/stacks/compose-farm.yaml

This way, cf commands work from anywhere while the config lives with your stacks.

Single host example

# Where compose files are located (one folder per stack)
compose_dir: /opt/stacks

hosts:
  local: localhost

stacks:
  plex: local
  grafana: local
  nextcloud: local

Multi-host example

# Where compose files are located (same path on all hosts)
compose_dir: /opt/compose

# Define your Docker hosts
hosts:
  nuc:
    address: 192.168.1.10
    user: docker           # SSH user
  hp:
    address: 192.168.1.11
    # user defaults to current user

# Map stacks to hosts
stacks:
  plex: nuc
  grafana: nuc
  nextcloud: hp

Each entry in stacks: maps to a folder under compose_dir that contains a compose file.

For cross-host HTTP routing, add Traefik labels and configure traefik_file (see Traefik Integration).

Validate Configuration

cf check --local

This validates syntax without SSH connections. For full validation:

cf check

First Commands

Check Status

cf ps

Shows all configured stacks and their status.

Start All Stacks

cf up --all

Starts all stacks on their assigned hosts.

Start Specific Stacks

cf up plex grafana

Apply Configuration

The most powerful command - reconciles reality with your config:

cf apply --dry-run   # Preview changes
cf apply             # Execute changes

This will:

  1. Start stacks in config but not running
  2. Migrate stacks on wrong host
  3. Stop stacks removed from config

Docker Network Setup

If your stacks use an external Docker network:

# Create network on all hosts
cf init-network

# Or specific hosts
cf init-network nuc hp

Default network: mynetwork with subnet 172.20.0.0/16

Example Workflow

1. Add a New Stack

Create the compose file:

# On any host (shared storage)
mkdir -p /opt/compose/gitea
cat > /opt/compose/gitea/docker-compose.yml << 'EOF'
services:
  gitea:
    image: docker.gitea.com/gitea:latest
    container_name: gitea
    environment:
      - USER_UID=1000
      - USER_GID=1000
    volumes:
      - /opt/config/gitea:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3000:3000"
      - "2222:22"
    restart: unless-stopped
EOF

Add to config:

stacks:
  # ... existing stacks
  gitea: nuc

Start the stack:

cf up gitea

2. Move a Stack to Another Host

Edit compose-farm.yaml:

stacks:
  plex: hp  # Changed from nuc

Apply the change:

cf up plex
# Automatically: down on nuc, up on hp

Or use apply to reconcile everything:

cf apply

3. Update All Stacks

cf update --all
# Runs: pull + build + down + up for each stack

Next Steps