A custom Fedora 43 bootc image optimized for ThinkPad X1 Carbon with a modern Wayland desktop environment.
- 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
- 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)
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.confwith--ozone-platform=wayland - Slack: Flatpak (installed via install.sh)
- 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.
- 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
If your organization uses Kerberos authentication and VPN, this setup supports:
- Kerberos authentication (edit
configs/krb5/krb5.conffor your realm) - Browser SSO via SPNEGO (edit
configs/chromium/policies/managed/andconfigs/firefox/policies/policies.jsonfor 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
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 |
| Subvolume | Mount | Purpose |
|---|---|---|
root |
/ |
OS (bootc A/B deployments) |
home |
/var/home |
User data, projects (survives reinstall) |
- 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
homesubvolume: User data survives OS reinstalls - Compression:
zstd:1saves space on NVMe
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 enrollment happens automatically on first boot:
- First boot: Enter LUKS passphrase (
foobar) when prompted - Automatic enrollment:
tpm2-first-boot-enroll.serviceenrolls TPM2 for auto-unlock - Subsequent boots: TPM2 provides key automatically (Secure Boot must be enabled)
- 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/nvme0n1p3Note: No dracut -f needed - TPM2 modules are already in the initramfs from the image build.
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:
-
Self-disabling: The enrollment script runs
systemctl disable tpm2-first-boot-enroll.serviceafter completing, removing the symlink frommulti-user.target.wants/ -
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
/etcdiffers (service disabled), the merge logic treats this as "user made a local modification" and preserves your change. -
LUKS password is never touched: The LUKS password is stored in the disk's LUKS header, not in the OS image.
bootc upgradeonly updates/usrand related OS files—it never modifies your disk's LUKS header or partition structure.
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-remoteAvailable 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 |
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
After installation, on first boot:
- LUKS unlock - Enter passphrase
foobar - TTY login - Login as
rootwith passwordfoobar - 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- 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"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.shThis creates symlinks for:
~/.bash_profile(enables niri auto-start)~/.config/niri/,~/.config/ghostty/,~/.config/zsh/- Systemd user services (noctalia, swayidle)
- Scripts in
~/.local/bin/
# Log out (or reboot)
exitNow when you log in on tty1:
- Enter username/password
- Niri starts automatically
- Noctalia shell bar appears
- Press
Mod+Returnfor terminal (ghostty with zsh)
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.COMIf already on a Fedora bootc system:
sudo bootc switch ghcr.io/henrywang/carbon-atomic:latestThen follow Steps 3-6 above.
# Build the image
podman build -t localhost/carbon-atomic:latest .
# Switch to the local image
sudo bootc switch localhost/carbon-atomic:latest-
DNF releasever: The
$releasevervariable is not set in bootc containers. The Containerfile creates/etc/dnf/vars/releaseverwith value43to 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-enandglibc-langpack-zhare installed for English default locale and Chinese Fcitx5 input support. -
User creation: User
xiaofwanis created manually on first boot (login as root, runuseradd). 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.serviceruns on first boot to enroll TPM2 for LUKS auto-unlock using the default passphrase.
.
├── .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 | 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 |
| 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 |
| 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 |
| 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 |
| 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) |
- Plug in dock: Power profile switches to Performance mode automatically
- External monitors: niri moves workspaces to dock displays
- Close lid: Laptop continues running in clamshell mode
- Unplug dock: Power profile switches to Power Saver mode
- Auto-lock: After 5 minutes idle
- Manual lock:
Mod+Escape - Before sleep: Automatically locks
- Theme: Matches Noctalia theme
All configuration files are symlinked from the repository. Edit them in ~/carbon-atomic/configs/ and changes apply immediately (or after reload).
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 --listAfter storing secrets, open a new terminal or run load-secrets to activate them.
See CONTEXT.md for details on how secrets work.
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.
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
}# Set battery to stop charging at 80% (preserves battery health)
sudo tlp setcharge 70 80The 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.timerDisable auto-updates:
sudo systemctl disable --now bootc-fetch-apply-updates.timerRe-enable auto-updates:
sudo systemctl enable --now bootc-fetch-apply-updates.timer# 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- Username: xiaofwan
- Login Shell: bash (auto-starts niri)
- Terminal Shell: zsh (via ghostty)
- Sudo: Passwordless (wheel group)
MIT