Skip to content

henrywang/carbon-atomic

Repository files navigation

Carbon Atomic

A custom Fedora 43 bootc image optimized for ThinkPad X1 Carbon with a modern Wayland desktop environment.

Features

  • Base: Fedora 43 bootc (immutable, atomic updates with auto-update)
  • Window Manager: niri (scrollable tiling Wayland compositor)
  • Shell: Noctalia (modern status bar)
  • Terminal: Ghostty with Hack Nerd Font
  • Login Shell: bash (auto-starts niri on tty1)
  • Terminal Shell: zsh with spaceship prompt, syntax highlighting, and autosuggestions (via ghostty)
  • Editors: Vim (with plugins + coc.nvim LSP), Zed
  • Dev Environments: Rust (cargo, rustfmt, clippy), Go, Python (pip, virtualenv), Node.js
  • Fedora Tools: koji, mock, fedpkg, tmt, testing-farm
  • Container Tools: podman, skopeo, buildah
  • Browsers: Chromium, Firefox (with Kerberos SSO)
  • AI Tools: Claude Code, Gemini CLI, OpenCode (installed via install.sh)
  • Cloud CLIs: AWS CLI, Google Cloud CLI, Azure CLI
  • Apps: Slack (Flatpak), Obsidian (Flatpak), LibreOffice, mpv, Evince (PDF), imv (images)
  • Virtualization: libvirt, QEMU/KVM, virt-viewer

Desktop Utilities

  • Launcher: Noctalia launcher (Mod+D)
  • Screenshots: grim + slurp
  • Screen recording: wf-recorder
  • Clipboard manager: Noctalia clipboard (Mod+V)
  • Color picker: grim + slurp + ImageMagick (Mod+Shift+C)

Native Wayland

All GUI applications run native Wayland (no XWayland needed) via ~/.config/environment.d/15-wayland.conf:

Variable Value Purpose
XDG_SESSION_TYPE wayland Session type
XDG_CURRENT_DESKTOP niri Desktop identifier
QT_QPA_PLATFORM wayland Qt apps
GDK_BACKEND wayland GTK apps
ELECTRON_OZONE_PLATFORM_HINT wayland Electron apps (Slack, VS Code)
MOZ_ENABLE_WAYLAND 1 Firefox
LIBVA_DRIVER_NAME iHD Intel VA-API (RPM Fusion, non-free shaders)

Additional app-specific configs:

  • Chromium: /etc/chromium/chromium.conf with --ozone-platform=wayland
  • Slack: Flatpak (installed via install.sh)

Noctalia Shell (Built-in)

  • Lock screen: Integrated lock screen
  • WiFi management: Network configuration UI
  • Bluetooth: Device pairing and management
  • Wallpaper: Background management with effects
  • Power profiles: Battery/performance switching
  • Notifications: Notification center
  • System tray: App indicators
  • Themes: Tokyo Night, Gruvbox, Catppuccin, Nord, etc.

ThinkPad X1 Carbon Optimizations

  • Automatic power profile switching (Performance when docked, Power Saver on battery)
  • Thunderbolt 4 dock support with clamshell mode (TPM2 auto-unlock)
  • TLP and thermald for battery and thermal management
  • Intel iHD VA-API driver (RPM Fusion) for full H.265/HEVC hardware acceleration
  • TPM2 LUKS auto-unlock for closed-lid boot on dock
  • Firmware updates via fwupd/LVFS
  • CAPSLOCK remapped to ESC (tap) / CTRL (hold) via keyd

Corporate SSO / VPN (optional)

If your organization uses Kerberos authentication and VPN, this setup supports:

  • Kerberos authentication (edit configs/krb5/krb5.conf for your realm)
  • Browser SSO via SPNEGO (edit configs/chromium/policies/managed/ and configs/firefox/policies/policies.json for your domain)
  • HTTP/HTTPS proxy via ~/.config/environment.d/25-proxy.conf

Proxy Configuration:

The proxy is configured per-user via configs/environment.d/25-proxy.conf (symlinked by install.sh). Uncomment and set your proxy server address if needed.

  • Shell/Apps: Automatically configured via environment.d (sourced in .bash_profile)
  • Firefox: Configure manually in about:preferences → Network Settings → Manual proxy

Disk Partitioning

Single LUKS2 container with btrfs subvolumes (recommended for bootc compatibility):

Partition Size Type Mount Purpose
/dev/nvme0n1p1 1 GB FAT32 /boot/efi UEFI boot
/dev/nvme0n1p2 1 GB ext4 /boot Kernels, initramfs
/dev/nvme0n1p3 remaining LUKS2+btrfs - Encrypted container

Btrfs Subvolumes (inside LUKS)

Subvolume Mount Purpose
root / OS (bootc A/B deployments)
home /var/home User data, projects (survives reinstall)

Why This Layout

  • Single LUKS container: Avoids Anaconda metadata conflicts during bootc install
  • Btrfs subvolumes: Share physical space dynamically - no partition resizing needed
  • TPM2 auto-unlock: Press power on dock, laptop stays closed, boots to login screen
  • Separate home subvolume: User data survives OS reinstalls
  • Compression: zstd:1 saves space on NVMe

Mount Options

Configured automatically during install:

LABEL=fedora  /          btrfs  subvol=root,compress=zstd:1,noatime  0 0
LABEL=fedora  /var/home  btrfs  subvol=home,compress=zstd:1,noatime  0 0

Note: discard=async is omitted by default to avoid potential I/O issues on newer NVMe controllers. You can add it later in /etc/fstab once the system is stable.

TPM2 Auto-Unlock (Clamshell Mode)

TPM2 enrollment happens automatically on first boot:

  1. First boot: Enter LUKS passphrase (foobar) when prompted
  2. Automatic enrollment: tpm2-first-boot-enroll.service enrolls TPM2 for auto-unlock
  3. Subsequent boots: TPM2 provides key automatically (Secure Boot must be enabled)
  4. Dock workflow: Press dock power button → laptop stays closed → boots to login

After changing LUKS passphrase, re-enroll TPM2:

# Wipe existing TPM slot (skip if no TPM2 slot exists)
sudo systemd-cryptenroll --wipe-slot=tpm2 /dev/nvme0n1p3 2>/dev/null || true

# Enroll TPM2 with new passphrase
sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 /dev/nvme0n1p3

Note: No dracut -f needed - TPM2 modules are already in the initramfs from the image build.

TPM2 and bootc Upgrades

The tpm2-first-boot-enroll.service is a one-time service that self-disables after running. It will not run again during bootc upgrade because:

  1. Self-disabling: The enrollment script runs systemctl disable tpm2-first-boot-enroll.service after completing, removing the symlink from multi-user.target.wants/

  2. 3-way merge preserves your changes: bootc/ostree uses a 3-way merge for /etc:

    Base (original image) Local (your /etc) New (upgrade image) Result
    enabled disabled (you changed it) enabled disabled (your change wins)

    Since the base and new images are identical (both have the service enabled), but your local /etc differs (service disabled), the merge logic treats this as "user made a local modification" and preserves your change.

  3. LUKS password is never touched: The LUKS password is stored in the disk's LUKS header, not in the OS image. bootc upgrade only updates /usr and related OS files—it never modifies your disk's LUKS header or partition structure.

Quick Start

Step 1: Build the ISO

Build a bootable installer ISO using bootc-image-builder:

# Build container image and ISO
just build-iso

# Or build ISO from remote registry (skip local build)
just build-iso-remote

Available commands (run just to see all):

Command Description
just build Build container image locally
just build-iso Build image + anaconda-iso installer
just build-qcow2 Build image + qcow2 for VM testing
just push Build and push to registry
just lint Lint Containerfile with hadolint
just clean Remove output directory

Step 2: Install on ThinkPad

Boot the ISO on your ThinkPad X1 Carbon. The Anaconda installer will automatically:

  • Create LUKS2 encrypted partition with btrfs subvolumes
  • Install the bootc image
  • Set root password to foobar

Default credentials (change after setup):

  • Root password: foobar
  • LUKS passphrase: foobar

Step 3: First Boot - Create User

After installation, on first boot:

  1. LUKS unlock - Enter passphrase foobar
  2. TTY login - Login as root with password foobar
  3. Create your user:
useradd -u 1000 -G wheel,libvirt,mock -c "Xiaofeng Wang" -d /var/home/xiaofwan -s /bin/bash -m xiaofwan
echo 'xiaofwan:YOUR_PASSWORD' | chpasswd
  1. Logout and login as xiaofwan

Note: Niri does NOT auto-start yet because ~/.bash_profile doesn't exist (install.sh hasn't run).

Connect to WiFi (if needed):

# Use text UI (recommended)
nmtui

# Or command line
nmcli device wifi list
nmcli device wifi connect "YourSSID" password "YourPassword"

Step 4: Run install.sh (as xiaofwan)

Clone this repo and run the setup script:

git clone --recurse-submodules https://github.com/henrywang/carbon-atomic.git ~/carbon-atomic
cd ~/carbon-atomic
./install.sh

This creates symlinks for:

  • ~/.bash_profile (enables niri auto-start)
  • ~/.config/niri/, ~/.config/ghostty/, ~/.config/zsh/
  • Systemd user services (noctalia, swayidle)
  • Scripts in ~/.local/bin/

Step 5: Reboot into Niri

# Log out (or reboot)
exit

Now when you log in on tty1:

  1. Enter username/password
  2. Niri starts automatically
  3. Noctalia shell bar appears
  4. Press Mod+Return for terminal (ghostty with zsh)

Step 6: Post-Install Setup

Open a terminal (Mod+Return) and complete setup:

# Pull system groups into /etc/group (bootc requirement)
getent group video | sudo tee -a /etc/group
getent group render | sudo tee -a /etc/group
getent group input | sudo tee -a /etc/group

# Add GPU/input groups (needed for graphics acceleration)
sudo usermod -aG video,render,input xiaofwan

# Change root password (default is 'foobar')
sudo passwd root

# Change LUKS passphrase (recommended)
sudo cryptsetup luksChangeKey /dev/nvme0n1p3

# Re-enroll TPM2 for auto-unlock (optional)
sudo systemd-cryptenroll --wipe-slot=tpm2 /dev/nvme0n1p3 2>/dev/null || true
sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 /dev/nvme0n1p3

# Install vim plugins
vim +PlugInstall +qall

# Install coc.nvim language servers (in vim)
# :CocInstall coc-pyright coc-toml coc-yaml coc-json coc-clangd coc-rust-analyzer coc-go coc-sh

# Store API keys in GNOME Keyring (prompts for each)
secrets-store

# Authenticate services
gh auth login
gcloud auth login
claude

# Kerberos authentication (if applicable)
kinit your-username@EXAMPLE.COM

Alternative: Switch from Existing Bootc System

If already on a Fedora bootc system:

sudo bootc switch ghcr.io/henrywang/carbon-atomic:latest

Then follow Steps 3-6 above.

Building Locally

# Build the image
podman build -t localhost/carbon-atomic:latest .

# Switch to the local image
sudo bootc switch localhost/carbon-atomic:latest

Build Notes

  • DNF releasever: The $releasever variable is not set in bootc containers. The Containerfile creates /etc/dnf/vars/releasever with value 43 to enable proper variable expansion for all repos.

  • Non-ASCII filenames: Some packages (just, niri) include documentation files with non-ASCII characters in filenames (e.g., README.中文.md). These break Anaconda's ostree deploy with "Pathname can't be converted from UTF-8" errors. The Containerfile removes these files in the same layer as package installation to prevent them from being stored in layer history.

  • Locale packages: glibc-langpack-en and glibc-langpack-zh are installed for English default locale and Chinese Fcitx5 input support.

  • User creation: User xiaofwan is created manually on first boot (login as root, run useradd). This avoids complexity with kickstart/bootc user creation and allows setting a real password.

  • TPM2 enrollment: TPM2 cannot be enrolled during Anaconda install (chroot environment). The tpm2-first-boot-enroll.service runs on first boot to enroll TPM2 for LUKS auto-unlock using the default passphrase.

Repository Structure

.
├── .gitlab-ci.yml                # GitLab CI/CD
├── .gitmodules                   # ZSH plugins as submodules
├── Containerfile                 # OS image definition
├── config.toml                   # bootc-image-builder config for anaconda-iso
├── justfile                      # Build commands (just build-iso, etc.)
├── install.sh                    # Home directory setup script
├── configs/
│   ├── environment.d/
│   │   ├── 10-xdg.conf           # XDG Base Directory
│   │   ├── 15-wayland.conf       # Wayland + Fcitx5 environment
│   │   ├── 20-fzf.conf           # FZF configuration
│   │   ├── 25-proxy.conf         # HTTP/HTTPS proxy (Red Hat internal)
│   │   └── 30-tools.conf         # CLI tool settings
│   ├── zsh/
│   │   ├── .zshrc                # ZSH configuration
│   │   ├── .zshenv               # ZSH environment (sets ZDOTDIR)
│   │   ├── spaceship/            # Spaceship prompt (submodule)
│   │   ├── zsh-autosuggestions/  # Autosuggestions (submodule)
│   │   ├── zsh-syntax-highlighting/
│   │   └── zsh-history-substring-search/
│   ├── vim/
│   │   ├── vimrc                 # VIM configuration
│   │   └── ftplugin/             # Filetype-specific settings
│   ├── git/
│   │   ├── config                # Git configuration
│   │   ├── gitignore_global      # Global gitignore
│   │   └── ignore                # Local ignore patterns
│   ├── niri/config.kdl           # Window manager config
│   ├── swayidle/config           # Idle management (uses Noctalia lock)
│   ├── ghostty/config            # Terminal config
│   ├── lsd/                      # lsd color theme
│   ├── krb5/krb5.conf            # Kerberos config
│   ├── keyd/default.conf         # CAPSLOCK remap
│   ├── chromium/policies/        # Chromium Kerberos SSO
│   ├── firefox/policies/         # Firefox Kerberos SSO
│   └── udev/99-thinkpad-pwr.rules
├── scripts/
│   ├── tp-autopower              # Auto power profile switching
│   ├── tp-performance-toggle     # Manual power toggle (Mod+P)
│   ├── noctalia-dnd-toggle       # Do Not Disturb toggle
│   └── screen-record-toggle      # Screen recording toggle
└── systemd/
    ├── swayidle.service          # Idle daemon service
    └── noctalia.service          # Shell bar service

Key Bindings

Niri (Window Manager)

Key Action
Mod+Return Launch Ghostty
Mod+D Application launcher (Noctalia)
Mod+S Control Center (Noctalia)
Mod+Comma Settings (Noctalia)
Mod+Z Launch Zed
Mod+B Launch Chromium
Mod+O Launch Obsidian
Mod+Q Close window
Mod+H/J/K/L Focus navigation
Mod+Shift+H/J/K/L Move windows
Mod+1-5 Switch workspace
Mod+F Maximize column
Mod+Shift+F Fullscreen
Mod+Shift+Space Toggle floating window
Mod+C Center column
Mod+W Toggle tabbed column
Mod+BracketLeft/Right Consume/expel window
Mod+Ctrl+R Reload niri config
Mod+Shift+Slash Show hotkey overlay
Mod+Shift+E Exit niri

Desktop Utilities

Key Action
Mod+Escape Lock screen (Noctalia)
Mod+V Clipboard history (Noctalia)
Mod+Shift+C Color picker
Print Screenshot selection → clipboard
Mod+Print Screenshot full screen → clipboard
Shift+Print Screenshot selection → file
Mod+Shift+R Toggle screen recording

ThinkPad

Key Action
Mod+N VPN selector (fuzzel menu)
Mod+P Toggle power profile
Mod+Shift+N Toggle Do Not Disturb
CAPSLOCK (tap) Escape
CAPSLOCK (hold) Control

ZSH

Key Action
Ctrl+F Accept autosuggestion
Ctrl+R FZF history search
Ctrl+T FZF file search
Alt+C FZF cd to directory
↑/↓ History substring search

VIM

Key Action
Shift+P FZF file search
Shift+T NERDTree toggle
Shift+F NERDTree find
Tab/Shift+Tab Completion navigation / Buffer navigation
Ctrl+H/J/K/L Window navigation
\W Strip trailing whitespace
\s Toggle spell check
gd Go to definition (coc.nvim)
gy Go to type definition (coc.nvim)
gi Go to implementation (coc.nvim)
gr Find references (coc.nvim)
K Show documentation (coc.nvim)
\rn Rename symbol (coc.nvim)
\f Format selected (coc.nvim)
\ac Code actions (coc.nvim)
\qf Quick fix (coc.nvim)
\d Show diagnostics (coc.nvim)

ThinkPad Docking Workflow

  1. Plug in dock: Power profile switches to Performance mode automatically
  2. External monitors: niri moves workspaces to dock displays
  3. Close lid: Laptop continues running in clamshell mode
  4. Unplug dock: Power profile switches to Power Saver mode

Lock Screen (Noctalia)

  • Auto-lock: After 5 minutes idle
  • Manual lock: Mod+Escape
  • Before sleep: Automatically locks
  • Theme: Matches Noctalia theme

Configuration

All configuration files are symlinked from the repository. Edit them in ~/carbon-atomic/configs/ and changes apply immediately (or after reload).

AI Tools Setup

Secrets are stored in GNOME Keyring and loaded automatically on login:

# Store your API keys (interactive prompts)
secrets-store

# Or store specific services
secrets-store anthropic   # Claude Code credentials
secrets-store gcloud      # Google Cloud project
secrets-store github      # GitHub token

# List stored secrets
secrets-store --list

After storing secrets, open a new terminal or run load-secrets to activate them.

See CONTEXT.md for details on how secrets work.

Wallpaper (Noctalia)

Noctalia has built-in wallpaper management:

  • Open Settings (Mod+Comma) → Wallpaper tab
  • Or use IPC: qs -c noctalia-shell ipc call wallpaper toggle

Place wallpapers in ~/Pictures/Wallpapers/ for Noctalia to find them.

Monitor Configuration

Edit ~/.config/niri/config.kdl and uncomment/modify the output sections for your external monitors:

output "DP-1" {
    mode "3840x2160@60.0"
    scale 1.5
    position x=0 y=0
}

Battery Thresholds

# Set battery to stop charging at 80% (preserves battery health)
sudo tlp setcharge 70 80

Updating

Automatic Updates

The system automatically checks for and applies OS updates via bootc-fetch-apply-updates.timer:

Setting Value
First check 1 hour after boot
Subsequent checks Every 8 hours
Randomized delay Up to 2 hours
Action Downloads update, stages it, reboots automatically

This timer is enabled by default via a vendor symlink in /usr/lib/systemd/system/default.target.wants/.

Check timer status:

systemctl status bootc-fetch-apply-updates.timer
systemctl list-timers bootc-fetch-apply-updates.timer

Disable auto-updates:

sudo systemctl disable --now bootc-fetch-apply-updates.timer

Re-enable auto-updates:

sudo systemctl enable --now bootc-fetch-apply-updates.timer

Manual Updates

# Check for OS updates
sudo bootc upgrade

# Rollback if needed (select previous boot entry in GRUB)
sudo bootc rollback

# Update config repo and submodules
cd ~/carbon-atomic
git pull
git submodule update --recursive

User

  • Username: xiaofwan
  • Login Shell: bash (auto-starts niri)
  • Terminal Shell: zsh (via ghostty)
  • Sudo: Passwordless (wheel group)

License

MIT

About

Fedora 43 bootc image for ThinkPad X1 Carbon with niri + Noctalia desktop.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors