Machine state management for Arch Linux with Sway (Wayland) desktop environment.
Ansible-driven provisioning and configuration for multiple workstations from a single repository.
sudo pacman -Syu --needed sudo vim git ansible just base-devel
git clone https://github.com/vix-4800/Workstation ~/Code/Workstation
cd ~/Code/Workstation
cp inventory/host_vars/localhost.yml.example inventory/host_vars/localhost.yml
cp vault/secrets.yml.example vault/secrets.yml
just vault-init
# edit inventory/host_vars/localhost.yml and vault/secrets.yml
just vault-encrypt
just deps
just applyjust apply # Full state: packages + configs + services
just sync # Only configs (fast, no package installs)
just plan # Dry-run: show what would change
just role shell # Apply specific role
just role desktop network # Multiple rolesjust vault-init # Create vault password file (first time)
just vault-encrypt # Encrypt vault/secrets.yml (after adding secrets in plain YAML)
just vault-edit # Edit encrypted secretsUse this order on a clean Arch install. just apply is not enough by itself on a fresh machine.
The target user must exist before running this repo and must be allowed to use sudo, because just apply runs
ansible-playbook --ask-become-pass.
Example:
useradd -m -G wheel -s /bin/bash <username>
passwd <username>
EDITOR=vim visudoEnable sudo for the wheel group in /etc/sudoers:
%wheel ALL=(ALL:ALL) ALL
Then log in as that user.
Install the base tools that are needed before this repository can manage the rest of the system:
sudo pacman -Syu --needed sudo vim git ansible just base-develThese are needed for:
sudo— privilege escalation for Ansible tasksvim— editinghost_vars, vault files, andvisudogit— cloning and updating the repositoryansible— running the playbookjust— command runner used by this repobase-devel— required for AUR builds duringyay/AUR tasks
git clone https://github.com/vix-4800/Workstation ~/Code/Workstation
cd ~/Code/WorkstationThis file is intentionally untracked and must be created locally on every machine:
cp inventory/host_vars/localhost.yml.example inventory/host_vars/localhost.yml
vim inventory/host_vars/localhost.ymlAt minimum, fill in the machine-specific values such as:
usernamehostnamecpu_vendorgpu_vendorhas_battery
site.yml always loads vault/secrets.yml, so the file must exist before you run the playbook.
cp vault/secrets.yml.example vault/secrets.yml
vim vault/secrets.yml
just vault-init
just vault-encryptAfter encryption, the file must start with $ANSIBLE_VAULT;.
This repository depends on Galaxy collections declared in requirements.yml,
including kewlfft.aur. On a clean system install them before the first apply:
just depsEquivalent command:
ansible-galaxy collection install -r requirements.ymljust applyThe servers.yml playbook bootstraps and configures remote Ubuntu servers using the server-init role.
It is fully separate from the workstation playbook and uses its own inventory under inventory/servers/.
- Python 3 on the control machine
- SSH key pair (
~/.ssh/id_ed25519/id_ed25519.pub) on the control machine - Root SSH access to the target server already configured with your public key
- Ansible collections installed:
ansible-galaxy collection install -r requirements.yml
Before running the playbook, copy your public key to the server manually:
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@<SERVER_IP>The playbook itself will then disable password authentication on the server permanently.
Edit inventory/servers/hosts.yml and add your servers. No passwords — key only:
all:
children:
servers:
hosts:
myserver:
ansible_host: 1.2.3.4
ansible_user: root
ansible_port: 22
ansible_ssh_private_key_file: ~/.ssh/id_ed25519Each host must declare its own ansible_ssh_private_key_file.
# Full initial setup (ssh hardening + update + docker + dev user + ufw)
ansible-playbook servers.yml -i inventory/servers/hosts.yml
# Enable optional services at runtime
ansible-playbook servers.yml -i inventory/servers/hosts.yml \
-e "server_enable_mtproxy=true"
ansible-playbook servers.yml -i inventory/servers/hosts.yml \
-e "server_enable_3xui=true"
# Install monitoring script with Telegram notifications
ansible-playbook servers.yml -i inventory/servers/hosts.yml \
-e "server_enable_monitor=true" \
-e "server_telegram_bot_token=YOUR_TOKEN" \
-e "server_telegram_chat_id=YOUR_CHAT_ID"
# Run only a specific step by tag
ansible-playbook servers.yml -i inventory/servers/hosts.yml \
-t mtproxy -e "server_enable_mtproxy=true"
# Dry-run
ansible-playbook servers.yml -i inventory/servers/hosts.yml --checkAll variables are defined in roles/server-init/defaults/main.yml and can be overridden
via -e or in inventory/servers/group_vars/.
| Variable | Default | Description |
|---|---|---|
server_run_update |
true |
Run apt update && apt upgrade |
server_install_docker |
true |
Install docker.io and enable the service |
server_create_dev_user |
true |
Create dev user in sudo and docker groups |
server_setup_ufw |
true |
Install and enable ufw, allow SSH |
server_enable_mtproxy |
false |
Deploy MTProxy Telegram proxy container |
server_enable_3xui |
false |
Install 3x-ui panel via official install script |
server_enable_monitor |
false |
Deploy daily monitoring script and cron job |
server_dev_username |
dev |
Name of the user to create |
server_ssh_public_key_file |
~/.ssh/id_ed25519.pub |
Public key to deploy to the dev user |
server_mtproxy_port |
443 |
Port for MTProxy |
server_mtproxy_fake_domain |
google.com |
Domain used for secret generation |
server_mtproxy_ip_prefer |
prefer-ipv4 |
IP preference for MTProxy |
server_telegram_bot_token |
'' |
Telegram bot token for monitoring |
server_telegram_chat_id |
'' |
Telegram chat ID to send reports to |
SSH hardening — runs on every apply (no bool guard). Sets PasswordAuthentication no
and PermitRootLogin prohibit-password in /etc/ssh/sshd_config, then restarts sshd.
MTProxy — Telegram proxy via Docker. After deployment the playbook prints the ready-to-use connection link:
https://t.me/proxy?server=<IP>&port=443&secret=<SECRET>
3x-ui — XRay-based proxy panel. Installed via the official script. The installer output is printed at the end of the task.
Monitoring — Python script deployed to /usr/local/bin/server_monitor.
Sends a daily HTML report to a Telegram chat at 09:00 via cron. The report includes:
- Uptime and load average (1m/5m/15m)
- CPU usage (sampled over 1 second from
/proc/stat) - Memory usage (used/total/available)
- Disk usage for
/ - Network RX/TX totals per interface since boot
- Top 5 processes by CPU and by memory
To get a Telegram bot token, create a bot via @BotFather.
To get your chat ID, send a message to the bot and call getUpdates on the bot API.
Dev user — created without a password. SSH access for the dev user is configured
via ansible.posix.authorized_key using the key from server_ssh_public_key_file.
Everything is managed through Ansible roles — packages, configs, services, and secrets. just provides a thin UX
layer on top of ansible-playbook.
The checked-in inventory targets localhost by default. Machine-specific hardware values live in
inventory/host_vars/localhost.yml, which is intentionally untracked; copy the example file on each machine and adjust
hostname, hardware flags, and any local overrides there.
inventory/
├── hosts.yml # Tracked inventory (defaults to localhost)
├── group_vars/all.yml # Shared: theme, font, shell, editor...
└── host_vars/
├── localhost.yml.example # Template for per-machine hardware values
└── localhost.yml # Local untracked copy
| Role | What it manages |
|---|---|
base |
Locale, hostname, timezone, essential packages (btop, dust, lm_sensors, yq, xdg-utils), env vars, fontconfig, vm.swappiness, paccache.timer |
yay |
AUR helper installation |
cpu |
CPU microcode (AMD/Intel); thermald thermal management service (Intel only) |
gpu |
GPU drivers + fan control (NVIDIA); DRM KMS modprobe and Wayland env config |
shell |
Fish, Bash, Alacritty, Tmux, aliases |
editor |
Neovim (with Lua + LuaRocks), VSCode flags, Git config |
audio |
PipeWire, WirePlumber, EasyEffects, multimedia apps |
network |
Firewall (nftables), Bluetooth (nm-applet + blueman tray flow), WireGuard (vault), V2RayA, USBGuard |
desktop |
SwayFX, Waybar, Wofi, SwayNC, GTKLock, Wlogout, polkit-gnome, XDG portals, swappy, wdisplays, qt5/6-wayland |
display-manager |
Greetd + ReGreet |
development |
PHP, Python, Go, Docker, linter configs |
appearance |
GTK themes (nwg-look), fonts, cursors, icons, wallpapers, plymouth |
apps |
Obsidian, Bitwarden, file-roller, gvfs-goa, Flatpak + Flathub, waypaper, auto-cpufreq (battery only) |
services |
All systemd user services and timers (xdg-user-dirs, etc.) |
ai-tools |
Claude Code, Codex, OpenCode, Qwen, MCP/Serena, agent skills |
Tasks are tagged for granular execution. The common tags are role tags (desktop, network, development, etc.), type
tags (packages, config, services, system), and narrower subgroup tags such as wireguard, gtklock, or php.
| Tag | Scope |
|---|---|
config |
Deploy user-space config files (symlinks, no sudo) |
system |
System-level configs requiring sudo (/etc/, /boot/) |
packages |
Install packages |
services |
Manage systemd units |
| Role tags | shell, desktop, network, etc.; some tasks use only the role tag |
| Subgroup tags | wireguard, gtklock, php, docker, etc. |
WireGuard keys are stored in vault/secrets.yml, encrypted with ansible-vault. Per-host variables in
host_vars/localhost.yml and vault variables render the final configs.
vault/secrets.yml ──> host_vars/localhost.yml ──> templates/wg0.conf.j2 ──> /etc/wireguard/wg0.conf
(encrypted) (local vars) (Jinja2 template) (deployed config)
Create the vault file like this:
just vault-init
# create vault/secrets.yml from vault/secrets.yml.example and fill in your values
just vault-encrypt
just vault-editBefore running just vault-encrypt, vault/secrets.yml may exist temporarily as plain YAML copied from the example.
After that step, site.yml loads it through vars_files, so the file must remain actual ansible-vault ciphertext.
If you already created vault/secrets.yml as plain YAML, re-encrypt it:
ansible-vault encrypt vault/secrets.ymlYou can verify the file is encrypted if the first line starts with:
$ANSIBLE_VAULT;
├── Justfile # Command interface
├── ansible.cfg # Ansible settings
├── requirements.yml # Galaxy collections
├── site.yml # Master playbook
├── inventory/ # Machine definitions + variables
├── vault/ # Encrypted secrets
└── roles/
└── <role>/
├── tasks/main.yml # What to install and deploy
├── defaults/main.yml # Default variables
├── files/ # Config files (symlinked to ~/)
├── templates/ # Jinja2 templates
├── handlers/main.yml # Event handlers (daemon-reload, etc.)
└── scripts/ # Executable scripts (→ ~/.local/bin/)
# On machine A: edit the file (it's a symlink into the repo)
vim roles/desktop/files/waybar/style.css
git add -A && git commit -m "feat: update waybar style" && git push
# On machine B:
git pull && just syncAdd it to the appropriate role's tasks/main.yml, commit, push, and run just apply on both machines.
Host-specific state lives in the local untracked inventory/host_vars/localhost.yml. For example, base sets
hostname, GPU setup depends on gpu_vendor, battery services depend on has_battery, and WireGuard gets its secrets
from vault.
- SwayFX — Sway fork with blur, shadows, and rounded corners
- Waybar — status bar with custom scripts (weather, Docker, resources, network, audio)
- Wofi — launcher and selection menus used by Waybar scripts
- SwayNC — notification daemon
- Greetd + ReGreet — display manager
- GTKLock — screen locker with media and background modules
- Wlogout — session logout menu
- polkit-gnome — authentication agent (autostarted with Sway)
- xdg-desktop-portal-wlr — screensharing and file picker portals
- PipeWire + WirePlumber — audio server
- EasyEffects — per-application equaliser and audio effects
- NetworkManager — connection management
- network-manager-applet — tray-based GUI for joining and managing Wi-Fi networks
- Blueman — Bluetooth manager plus tray applet for paired devices
- WireGuard / V2RayA — VPN
- Swappy — screenshot annotation
- wdisplays — display configuration GUI
- nwg-look — GTK theme manager for Wayland
- Catppuccin Mocha — consistent colour theme across all components
- Obsidian — notes
- Bitwarden — password manager
- File Roller — archive manager
- Flatpak + Flathub — additional applications
- Arch Linux
- A local user account already created
- The user is in
wheel(or otherwise configured insudoers) sudo,vim,git,ansible,just, andbase-develinstalled- Internet connection
| Command | Description |
|---|---|
just apply |
Full state apply |
just sync |
Deploy configs only |
just plan |
Dry-run (check mode) |
just role <tags> |
Apply specific role(s) |
just deps |
Install Galaxy collections |
just vault-edit |
Edit encrypted secrets |
just vault-init |
Create vault password file |
just vault-encrypt |
Encrypt vault/secrets.yml |
just services |
Show running user services |
just molecule |
Run molecule tests |
just lint |
Lint playbooks and roles |
just check |
Syntax check only |