# Software & packages

# Image manipulation

### Compression

Install `sudo apt-get install imagemagick`

Compress command `mogrify -quality 80% *.jpg`

note: this will overwrite images

# yt-dlp

### Download video as mp3

```bash
yt-dlp -x --audio-format mp3 URL_HERE
```

### Convert webm to mp4

```bash
 ffmpeg -i input.webm -c:v libx264 -preset slow -crf 22 -c:a aac -b:a 128k output.mp4
```

### Download video as mp4

```bash
 yt-dlp -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" SOURCE_URL
```

### Convert video codec to x264

```bash
 ffmpeg -i input.mp4 -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 192k output.mp4
```

<div id="bkmrk-">  
</div>

# Python packages and commands

### Managing Virtual Envrionments

Create a new venv `python3 -m venv /path/to/new/virtual/environment`

Activate the new venv Linux / Mac `source myenv/bin/activate`

Activate an envrionment on windows `myenv\Scripts\activate`

Exit the venv `deactivate`

### Installing and uninstalling packages

Installing a package `pip install xyz`

Uninstalling a package `pip uninstall xyz`

### Useful packages

- aqlalchemy - SQL Alchemy
- snowflake-sqlalchemy - SQL Alchemy for snowflake

# spotdl

### Install &amp; Setup

<p class="callout warning">Best to load a venv first</p>

Installing spotdl `pip install spotdl`

 Open the config file at `.spotdl/config.json` and update "output" to be `"{album}/{title}.{output-ext}"`

### Updating

`pip install --upgrade spotdl`

# ffmpeg

### Installing

```bash
sudo apt install ffmpeg
```

### Compress a video to h265

```bash
 ffmpeg -i example_h264.mp4 -c:v libx265 -crf 24 -preset fast -c:a aac -b:a 128k example_h265.mp4
```

<div id="bkmrk-">  
</div>### CRF Info

For H.264 (x264):

The typical CRF range is 18-28.

CRF 18: Near lossless quality (very high quality, larger file size).

CRF 23: Default value, good balance between quality and file size.

CRF 28: Noticeable quality loss, much smaller file size.

<div id="bkmrk--1">  
</div>### Overwright a videos audio with a new audio track

```bash
 ffmpeg -i video_to_overwright.mkv -i audio_input.mp3 -c:v copy -map 0:v:0 -map 1:a:0 output.mp4
```


### MP4 to MP3

```bash
 ffmpeg -i filename.mp4 filename.mp3
```

### Change video format

```bash
ffmpeg -i input.m4v -c copy output.mp4
```

# IPFS

### Installation for Linux

1 - Download the package

```bash
 wget https://dist.ipfs.tech/kubo/v0.31.0/kubo_v0.31.0_linux-amd64.tar.gz
```

2 - Unzip the file

```bash
 tar -xvzf kubo_v0.39.0_linux-amd64.tar.gz
```

3 - Move into the kubo folder

```bash
 cd kubo
```

4 - Run the install script

```bash
 sudo bash install.sh
```

5 - Test the installation worked

```bash
 ipfs --version
```

### Editing the config

Show Config:

```bash
 ipfs config show
```

Change connection count

```bash
ipfs config Swarm.ConnMgr.HighWater 1000 --json
ipfs config Swarm.ConnMgr.LowWater 500 --json
```

Enable filestore (Allow import with no copy)

```bash
ipfs config --json Experimental.FilestoreEnabled true
sudo systemctl restart ipfs
ipfs config Experimental.FilestoreEnabled
```

### IPFS as a service

Open a new file at `/etc/systemd/system/ipfs.service`

```service
[Unit]
Description=IPFS daemon
After=network.target

[Service]
ExecStart=/usr/local/bin/ipfs daemon
Restart=on-failure
User=conor
Group=conor
Environment=IPFS_PATH=/home/conor/.ipfs

[Install]
WantedBy=default.target
```

`WantedBy=default.target` means the service will start up at boot

get it to start at boot

```bash
 sudo systemctl enable ipfs
```

and start it

```bash
 sudo systemctl start ipfs
```

### Usage

Check filestore is enabled and working

```bash
ipfs config Experimental.FilestoreEnabled
```

List files in filestore

```bash
ipfs filestore ls
```

Import files to filestore

```bash
ipfs add --nocopy --recursive --cid-version=1 /path/to/your/folder
```

# Wireguard

### Setup Script

```bash
#!/bin/bash

# Checks to see if script is being run as root
if [ "$EUID" -ne 0 ]; then
  echo "Please run as root"
  exit
fi

sudo apt install wireguard

# Deletes keys in case this script is being run again
rm -f /etc/wireguard/* 

# Create necessary directories, including parents
mkdir -p /etc/wireguard/client


# Create and store server private and public key
wg genkey | tee /etc/wireguard/server_priv.key | wg pubkey | tee /etc/wireguard/server_pub.key

# Get the server public and private key as well as the network interface
server_priv_key=$(cat /etc/wireguard/server_priv.key)
server_pub_key=$(cat /etc/wireguard/server_pub.key)
network_interface=$(ip -o -4 route show to default | awk '{print $5}')


# Client
# Create client public and private key and store it in vars for later
wg genkey | tee /etc/wireguard/client/client_priv.key | wg pubkey | tee /etc/wireguard/client/client_pub.key
client_priv_key=$(cat /etc/wireguard/client/client_priv.key)
client_pub_key=$(cat /etc/wireguard/client/client_pub.key)

# Create initial client config
echo "[Interface]
PrivateKey = $client_priv_key
Address = 10.0.0.2/24

[Peer]
PublicKey = $server_pub_key
Endpoint = servername_or_ip:51820
AllowedIPs = 0.0.0.0/0
" > /etc/wireguard/client/wg0.conf


# Create the config file for wireguard
echo "[Interface]
Address = 10.0.0.1/24
SaveConfig = true
ListenPort = 51820
PrivateKey = $server_priv_key
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o $network_interface -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o $network_interface -j MASQUERADE

[Peer]
PublicKey = $client_pub_key
AllowedIPs = 10.0.0.2/24
" > /etc/wireguard/wg0.conf

# Change permissions so only root can access the files
chmod 600 /etc/wireguard/server_priv.key
chmod 600 /etc/wireguard/wg0.conf

# Allow 51820 through ufw
ufw allow 51820/udp

# Start wireguard and set it to auto start on boot
wg-quick up wg0
systemctl enable wg-quick@wg0




```

### Setup - Manual

1 - Update system and install wireguard

```bash
 apt update && apt upgrade && apt install wireguard
```

2 - Make the necessary folders

```bash
 mkdir -p /etc/wireguard/client
```

3 - Create the server's public and private keys

```bash
 wg genkey | tee /etc/wireguard/server_priv.key | wg pubkey | tee /etc/wireguard/server_pub.key
```

4 - Find the servers network interface

```bash
 ip -o -4 route show to default | awk '{print $5}
```

5 - Nano into

```bash
 /etc/wireguard/wg0.conf 
```

(Make sure to add network interface and server private key)

```bash
 [Interface] Address = 10.0.0.1/24 
SaveConfig = true 
ListenPort = 51820 
PrivateKey = SERVER_PRIVATE_KEY 
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o NETWORK_INTERFACE_HERE -j MASQUERADE 
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o NETWORK_INTERFACE_HERE -j MASQUERADE
```

 6 - Update file permissions so only root can read the config file and private key

```bash
 chmod 600 /etc/wireguard/server_priv.key chmod 600 /etc/wireguard/wg0.conf
```

 7 - Allow the vpn port through UFW

```bash
 ufw allow 51820/udp
```

 8 - Start wireguard and set it to auto start at boot

```bash
 wg-quick up wg0 systemctl enable wg-quick@wg0
```

 9 - Create the client keys

```bash
 wg genkey | tee /etc/wireguard/client/priv.key | wg pubkey | tee /etc/wireguard/client/pub.key
```

 10 - Create the client config file (copy to client device)

```bash
[Interface] PrivateKey = CLIENT_PRIVATE_KEY 
Address = 10.0.0.2/24 [Peer] 
PublicKey = SERVER_PUBLIC_KEY 
Endpoint = SERVER_IP_OR_DOMAIN:51820 
AllowedIPs = 0.0.0.0/0
```

11 - Add peer info to the server config

```
[Interface] Address = 10.0.0.1/24 
SaveConfig = true 
ListenPort = 51820 
PrivateKey = SERVER_PRIVATE_KEY 
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o NETWORK_INTERFACE_HERE -j MASQUERADE 
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o NETWORK_INTERFACE_HERE -j MASQUERADE

# =================
[Peer]
PublicKey = PUBLIC_KEY
AllowedIPs = 10.0.0.2/24

```

### Setup with SeaBee's setup script

```bash
wget https://raw.githubusercontent.com/seabee33/wireguard_helper/refs/heads/main/wg_helper.py && chmod +x wg_helper.py && sudo python3 wg_helper.py
```

### Auto start at boot

1 - ensure the client config is at `/etc/wireguard/wg0.conf`  
2 - enable it to start at boot with `sudo systemctl enable wg-quick@wg0`

# Node & NVM

1 - install node version manager (NVM)

```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
```

2 - install node

```bash
nvm install node
```

---

General steps

1 - install components

```bash
npm create vite@latest my-charting-site --template react
cd my-charting-site
npm install

npm install -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p
```

2 - test

```bash
npm run dev

```

build

```bash
npm run build
```

# Syncthing



# Screen

Screen lets you make virtual terminals with

```bash
screen
```

### New session with name

```
screen -S session_name
```

# Tailscale

### Client setup

1\. Install Tailscale client

```bash
curl -fsSL https://tailscale.com/install.sh | sh
```

2\. Create a pre-auth key on your Headscale server

```bash
docker exec headscale headscale preauthkeys create --user 1 --reusable --expiration 24h
```

Copy the key that's generated.

3\. Connect to your Headscale server

```
sudo tailscale up --login-server=https://headscale.seabee.me --authkey=YOUR_PREAUTH_KEY --hostname=rpi3
```

Replace YOUR\_PREAUTH\_KEY with the key from step 2.

4\. Verify connection

```bash
# On Raspberry Pi:
sudo tailscale status

# On your Headscale server:
docker exec headscale headscale nodes list
```

You should see your Raspberry Pi listed with the hostname "raspberry-pi" and an IP like 100.64.0.2.

# smb / samba

# Samba Setup on Linux (Home Server)

A practical guide to installing and configuring Samba for a home network, covering a shared folder setup suitable for a home wiki or general file server.

---

## 1. Install Samba

```bash
sudo apt update && sudo apt install samba samba-common-bin -y
```

Verify it's running:

```bash
sudo systemctl status smbd nmbd
```

Both should be active. If not:

```bash
sudo systemctl enable smbd nmbd --now
```

---

## 2. Create the Share Directory

```bash
sudo mkdir -p /srv/samba/wiki
sudo chown -R conor:conor /srv/samba/wiki
sudo chmod 755 /srv/samba/wiki
```

Adjust owner to your actual Linux username.

---

## 3. Configure Samba

Back up the default config first:

```bash
sudo cp /etc/samba/smb.conf /etc/samba/smb.conf.bak
```

Edit the config:

```bash
sudo nano /etc/samba/smb.conf
```

### Global section — replace or update:

```ini
[global]
   workgroup = WORKGROUP
   server string = Home Server
   server role = standalone server
   log file = /var/log/samba/log.%m
   max log size = 1000
   logging = file
   panic action = /usr/share/samba/panic-action %d

   # Security
   security = user
   map to guest = never
   encrypt passwords = yes

   # Performance
   socket options = TCP_NODELAY IPTOS_LOWDELAY
   read raw = yes
   write raw = yes
   use sendfile = yes
```

### Share definition — add to the bottom:

```ini
[wiki]
   comment = Home Wiki
   path = /srv/samba/wiki
   browseable = yes
   read only = no
   valid users = conor
   create mask = 0664
   directory mask = 0775
   force group = conor
```

---

## 4. Create Samba User

Samba uses its own password store separate from Linux system passwords. The Linux user must already exist.

```bash
sudo smbpasswd -a conor
```

You'll be prompted to set a Samba-specific password. Enable the user:

```bash
sudo smbpasswd -e conor
```

---

## 5. Validate Config and Restart

Test the config for syntax errors:

```bash
testparm
```

If clean:

```bash
sudo systemctl restart smbd nmbd
```

---

## 6. Firewall

If UFW is active:

```bash
sudo ufw allow samba
```

This opens ports 137, 138 (UDP) and 139, 445 (TCP).

---

## 7. Connect From Clients

### Linux (Files / Nautilus):
```
smb://SERVER_IP/wiki
```

Or mount via CLI:

```bash
sudo mount -t cifs //SERVER_IP/wiki /mnt/wiki -o username=conor,password=yourpass,uid=1000,gid=1000
```

Permanent via `/etc/fstab`:

```
//SERVER_IP/wiki  /mnt/wiki  cifs  credentials=/home/conor/.smbcredentials,uid=1000,gid=1000,_netdev,x-systemd.automount  0  0
```

`/home/conor/.smbcredentials`:

```
username=conor
password=yourpass
```

```bash
chmod 600 /home/conor/.smbcredentials
```

### macOS:
Finder → Go → Connect to Server → `smb://SERVER_IP/wiki`

### Windows:
`\\SERVER_IP\wiki` in Explorer address bar, or map as a network drive.

---

## 8. Tailscale Consideration

Since you're on Tailscale, use the Tailscale IP (`100.x.x.x`) instead of the LAN IP for consistent access across all your machines regardless of which network you're on. This also avoids exposing Samba to the public internet — Samba should **never** be exposed publicly, it has a long CVE history (EternalBlue, etc.).

Bind Samba only to your LAN + Tailscale interfaces to be safe:

```ini
[global]
   interfaces = lo tailscale0 eth0
   bind interfaces only = yes
```

Replace `eth0` with your actual LAN interface (`ip a` to check).

---

## 9. Verify the Share is Visible

From the server itself:

```bash
smbclient -L localhost -U conor
```

From another machine:

```bash
smbclient -L //SERVER_IP -U conor
```

You should see `wiki` listed under shares.

---

## 10. Troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| "Permission denied" on connect | Wrong Samba password or user not enabled | `smbpasswd -e username` |
| Share not visible | `browseable = no` or firewall | Check config + `ufw status` |
| Can connect but can't write | Directory permissions | `chmod 775 /srv/samba/wiki` |
| Works on LAN, not Tailscale | Interfaces binding | Add `tailscale0` to `interfaces` |
| `testparm` errors | Config syntax | Read the output carefully, it's specific |

---

## Notes

- Samba passwords and Linux system passwords are **independent** — changing one doesn't change the other.
- For a multi-user setup, create a dedicated group (e.g., `sambashare`) and use `valid users = @sambashare` in the share definition.
- If you're running this on the same machine as your wiki app (e.g., WikiJS, Obsidian vault server), Samba is a good way to access the underlying vault files directly from other machines for editing.

# autofs

# autofs Setup and Configuration Guide

`autofs` mounts filesystems on-demand when accessed and unmounts them after a configurable idle timeout. This makes it significantly more robust than static `/etc/fstab` mounts for network shares — a missing or offline host won't hang your boot, and stale mounts are cleaned up automatically.

---

## 1. Install

```bash
sudo apt update && sudo apt install autofs sshfs -y
```

For NFS shares also install:

```bash
sudo apt install nfs-common -y
```

---

## 2. How autofs Works

autofs has two config layers:

| File | Purpose |
|---|---|
| `/etc/auto.master` | Master map — defines mount point directories and which map file handles them |
| `/etc/auto.<name>` | Detail map — defines individual mounts under that directory |

When you access `/mnt/remote/myshare`, autofs intercepts it, reads the map, and mounts the share on the fly. After the timeout with no activity, it unmounts automatically.

---

## 3. Master Map — `/etc/auto.master`

Each line in `auto.master` follows this format:

```
<base_mount_dir>  <map_file>  [options]
```

Example:

```
/mnt/remote  /etc/auto.sshfs  --timeout=60,--ghost
/mnt/nfs     /etc/auto.nfs    --timeout=120,--ghost
```

**Key options:**

| Option | Effect |
|---|---|
| `--timeout=N` | Unmount after N seconds of inactivity |
| `--ghost` | Creates empty placeholder directories so the mount point is visible even when unmounted. Without this, `ls /mnt/remote` shows nothing until a mount is active |
| `--verbose` | Useful during setup/debugging |

### Important: auto.master entry placement

The default `/etc/auto.master` contains a `+auto.master` line which is a legacy NIS include directive. Your custom entries must go **above** this line, otherwise they may be ignored on some systems. The `+auto.master` line is safe to remove entirely on a standalone home server with no NIS infrastructure.

Clean minimal auto.master:

```
+dir:/etc/auto.master.d

/mnt/remote  /etc/auto.sshfs  --timeout=60,--ghost
```

The `+dir:/etc/auto.master.d` line is worth keeping — it allows dropping extra `.autofs` map files into that directory without editing auto.master directly.

---

## 4. Detail Map Files

### SSHFS (SSH/SFTP) — Modern Syntax

> **Important:** Many guides online show legacy syntax using `fstype=fuse` and `:sshfs#user@host` format. This does **not** work on modern Linux kernels. Always use `fstype=fuse.sshfs` with plain `user@host:/path` syntax.

Create `/etc/auto.sshfs`:

```
<mount_name>  -fstype=fuse.sshfs,rw,allow_other,IdentityFile=/home/conor/.ssh/id_ed25519,uid=1000,gid=1000,StrictHostKeyChecking=no,ServerAliveInterval=15,ServerAliveCountMax=3  conor@100.x.x.x:/home/conor
```

**Breakdown:**

| Part | Meaning |
|---|---|
| `<mount_name>` | The subdirectory created under the base — e.g. `farmdesktop` becomes `/mnt/remote/farmdesktop` |
| `-fstype=fuse.sshfs` | Correct modern fstype for SSHFS mounts |
| `rw` | Read/write |
| `allow_other` | Allows users other than root to access the mount |
| `IdentityFile=` | Path to SSH private key — **must use key auth, not password** |
| `uid=1000,gid=1000` | Map remote files to your local user. Check yours with `id conor` |
| `StrictHostKeyChecking=no` | Required because autofs runs as root which has an empty known_hosts — without this the mount hangs waiting for an interactive host key confirmation that never comes |
| `ServerAliveInterval=15` | Send keepalive every 15s to prevent silent disconnects |
| `ServerAliveCountMax=3` | Drop connection after 3 missed keepalives |

Full working example:

```
farm-server  -fstype=fuse.sshfs,rw,allow_other,IdentityFile=/home/conor/.ssh/cbcore-key,uid=1000,gid=1000,StrictHostKeyChecking=no,ServerAliveInterval=15,ServerAliveCountMax=3  conor@100.64.0.12:/home/conor
```

Access via: `/mnt/remote/farm-server`

### Why legacy syntax fails

Old guides show:
```
farmdesktop  -fstype=fuse,rw,allow_other,...  :sshfs#conor@100.64.0.5:/home/conor
```

This tells autofs to call `mount -t fuse` with `sshfs#` as the device — an approach that relied on the generic FUSE kernel module handling the mount. Modern kernels expect `mount -t fuse.sshfs` with a plain remote path. Using the old syntax results in `signal 22 (SIGABRT)` and a failed mount with no useful error message.

---

### NFS

Create `/etc/auto.nfs`:

```
nas  -fstype=nfs4,rw,soft,intr  100.64.0.10:/exports/data
```

| Option | Meaning |
|---|---|
| `nfs4` | Use NFSv4 (preferred) |
| `soft` | Return error on timeout instead of hanging indefinitely |
| `intr` | Allow interrupting a hung NFS call with Ctrl+C |

---

### Samba / CIFS

Create `/etc/auto.smb`:

```
wiki  -fstype=cifs,rw,credentials=/home/conor/.smbcredentials,uid=1000,gid=1000,iocharset=utf8  ://100.64.0.10/wiki
```

`/home/conor/.smbcredentials`:

```
username=conor
password=yourpassword
```

```bash
chmod 600 /home/conor/.smbcredentials
```

---

## 5. FUSE Prerequisite for SSHFS

`allow_other` requires this line to be uncommented in `/etc/fuse.conf`:

```bash
sudo nano /etc/fuse.conf
```

Uncomment:

```
user_allow_other
```

Without this, SSHFS mounts via autofs (which runs as root) will be inaccessible to your user account.

---

## 6. SSH Key Setup

autofs runs as root, so two things must be true:

1. The key file must be readable by root
2. Root must have the remote host in its known_hosts — **or** `StrictHostKeyChecking=no` must be set

**Seed root's known_hosts before first use:**

```bash
sudo ssh -i /home/conor/.ssh/your-key conor@100.x.x.x echo "ok"
# Type "yes" when prompted to accept the host key
```

Alternatively, use `StrictHostKeyChecking=no` in the map file options (acceptable on a private Tailscale network).

**Option A** — use your existing user key (works if permissions allow root to read it):

```
IdentityFile=/home/conor/.ssh/id_ed25519
```

**Option B** — create a dedicated root-owned key for autofs:

```bash
sudo ssh-keygen -t ed25519 -f /root/.ssh/autofs_id_ed25519 -N ""
sudo ssh-copy-id -i /root/.ssh/autofs_id_ed25519.pub conor@100.x.x.x
```

Then use `IdentityFile=/root/.ssh/autofs_id_ed25519` in the map file. Cleaner — keeps autofs auth separate from your interactive SSH key.

---

## 7. Enable and Start autofs

```bash
sudo systemctl enable autofs --now
```

Reload after map file changes:

```bash
sudo systemctl reload autofs
```

Full restart required after auto.master changes:

```bash
sudo systemctl restart autofs
```

---

## 8. Testing

Trigger a mount by accessing the path directly:

```bash
ls /mnt/remote/farm-server
```

Check what's currently mounted:

```bash
mount | grep autofs
```

Verify autofs has loaded your maps correctly:

```bash
sudo automount --dumpmaps
```

Check status and logs:

```bash
sudo systemctl status autofs
sudo journalctl -u autofs -f
```

Force unmount manually:

```bash
sudo umount /mnt/remote/farm-server
```

---

## 9. Debugging a Failed Mount

If the mount silently fails, run autofs in foreground debug mode:

```bash
sudo systemctl stop autofs
sudo automount -f --verbose --debug 2>&1 | tee /tmp/autofs-debug.log &
sleep 2
ls /mnt/remote/farm-server
sleep 2
cat /tmp/autofs-debug.log
```

Then kill the debug instance and restart the service:

```bash
sudo pkill -9 -f automount
sudo rm -f /run/autofs.pid
sudo systemctl start autofs
```

If the mount command itself hangs, test it manually first:

```bash
# Test SSH connectivity as root
sudo ssh -i /path/to/key user@host echo "ok"

# Test sshfs as your normal user
mkdir /tmp/testmount
sshfs -o IdentityFile=/path/to/key user@host:/remote/path /tmp/testmount
ls /tmp/testmount
```

If sshfs works as your user but hangs as root, the issue is root's SSH environment (known_hosts, key permissions). Add `StrictHostKeyChecking=no` to the map options and ensure the key is readable by root.

---

## 10. Troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| `ls` on mount point returns "No such file or directory" | autofs not reading map, or entry placement in auto.master | Run `sudo automount --dumpmaps` to verify map is loaded; check entry is above `+auto.master` |
| Mount hangs on access | Root's known_hosts empty — SSH waiting for interactive host key confirmation | Add `StrictHostKeyChecking=no` to map options, or pre-seed with `sudo ssh user@host echo ok` |
| `signal 22` / SIGABRT in debug output | Legacy `fstype=fuse` + `sshfs#` syntax | Change to `fstype=fuse.sshfs` and plain `user@host:/path` |
| "Permission denied" accessing mounted path | `allow_other` not set or `user_allow_other` not in fuse.conf | Check `/etc/fuse.conf` |
| Mount works as root but not as user | `uid=` / `gid=` not set in map file | Add `uid=1000,gid=1000` (check with `id username`) |
| autofs fails to start after manual debug session | Stale PID file from debug automount process | `sudo pkill -9 -f automount && sudo rm -f /run/autofs.pid` |
| Changes to map file have no effect | autofs cached old config | `sudo systemctl reload autofs` |
| `ls /mnt/remote` shows nothing despite ghost mode | Ghost mode requires autofs to be fully running and map parsed | Run `sudo automount --dumpmaps` to confirm map loaded |

---

## 11. Full Example: Tailscale Homelab

`/etc/auto.master`:

```
+dir:/etc/auto.master.d

/mnt/remote  /etc/auto.sshfs  --timeout=60,--ghost
```

`/etc/auto.sshfs`:

```
farm-pi      -fstype=fuse.sshfs,rw,allow_other,IdentityFile=/root/.ssh/autofs_id_ed25519,uid=1000,gid=1000,StrictHostKeyChecking=no,ServerAliveInterval=15,ServerAliveCountMax=3  conor@100.64.0.2:/home/conor
farmdesktop  -fstype=fuse.sshfs,rw,allow_other,IdentityFile=/root/.ssh/autofs_id_ed25519,uid=1000,gid=1000,StrictHostKeyChecking=no,ServerAliveInterval=15,ServerAliveCountMax=3  conor@100.64.0.3:/home/conor
homeserver   -fstype=fuse.sshfs,rw,allow_other,IdentityFile=/root/.ssh/autofs_id_ed25519,uid=1000,gid=1000,StrictHostKeyChecking=no,ServerAliveInterval=15,ServerAliveCountMax=3  conor@100.64.0.4:/home/conor
```

Access:

```
/mnt/remote/farm-pi/
/mnt/remote/farmdesktop/
/mnt/remote/homeserver/
```

All mount on first access, unmount after 60 seconds idle. If a farm machine is offline, accessing its path returns an error immediately rather than hanging the system.

---

## Notes

- autofs mounts do **not** appear in `df` or `mount` output unless currently active — this is normal.
- `/etc/auto.master` changes require a full `systemctl restart autofs`; map file changes only need `systemctl reload autofs`.
- For Ansible management across your fleet, the map files and master config are straightforward to template — a single playbook can deploy consistent autofs config to all five machines.

# Headscale

This supersedes the original guide. Changes from that version, all based on what you actually have running:

1. Caddy is a **native systemd service on the host**, not a Docker container — the original guide's `caddy` service block in `docker-compose.yml` and the Docker-network Caddyfile no longer apply.
2. `server_url` had the internal port (`:8080`) wrongly appended — fixed.
3. `trusted_proxies` was missing entirely — added, using `127.0.0.1/32` (correct because Caddy reaches Headscale via host loopback, not a Docker bridge network — that CIDR would only have been right if Caddy were a container in the same Compose stack, which it isn't).
4. `dns.base_domain` was left as the placeholder `example.com` — fixed to a subdomain distinct from `server_url`'s domain, per Headscale's own requirement that these must differ.

**Version basis:** this is checked against Headscale's current `main`-branch `config-example.yaml` and the official reverse-proxy docs, consistent with the `v0.28.0` pin from the original compose file. I don't have confirmation of which version is actually running in your container — run `docker exec headscale headscale version` and tell me if it's different, since config field names have moved between sections in past releases.

---

## 0. DNS — what actually needs a real domain record

Only **one** public DNS record is needed: an A (and AAAA, if your VPS has IPv6) record for **`headscale.seabee.me`**, pointing at the VPS's public IP. This is the domain Caddy serves over real HTTPS, the one Let's Encrypt validates via HTTP-01, and the one used in `server_url`.

**`ts.seabee.me` (the `dns.base_domain` value) needs no DNS record at all.** It's never queried against public DNS or your registrar — it's a string Headscale uses internally to construct MagicDNS device names (`hostname.ts.seabee.me`), resolved only by Headscale's own embedded DNS server, which Tailscale clients query via `100.100.100.100`. It doesn't need to exist anywhere except in this config file.

It still has to be a domain you own and aren't using for anything else, though — not because it needs to resolve anywhere, but because if you pick a domain you're actively using for real services, MagicDNS will hijack every query under that domain for any device on the tailnet and return NXDOMAIN instead of the real record, confirmed by a filed upstream Headscale bug report showing exactly this happening to someone's real subdomain. Since `ts.seabee.me` isn't serving anything else, this doesn't affect you — just don't reuse it for a real service later without remembering why.

In the Caddyfile (Section 4 below), only `headscale.seabee.me` appears — that's correct and intentional. `ts.seabee.me` never touches Caddy.

---

## 1. Folder structure

You already created this. The `caddy/` subfolders (`caddy/data`, `caddy/config`) from the original `mkdir` are no longer needed since Caddy isn't running in this stack — leave them (harmless) or `rm -rf caddy/` from `~/Docker/headscale/`, your call.

Structure actually in use now:
```
~/Docker/headscale/
├── docker-compose.yml
├── headscale/
│   ├── config/config.yaml
│   ├── lib/
│   └── run/
└── headplane/
    ├── config/config.yaml
    └── lib/
```

---

## 2. `headscale/config/config.yaml` — corrected

Edit your existing file. The specific lines to change:

```yaml
# WAS: server_url: https://headscale.seabee.me:8080
server_url: https://headscale.seabee.me

listen_addr: 0.0.0.0:8080          # correct as-is — Docker needs this bound inside the container
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false

# ADD THIS — was missing entirely:
trusted_proxies:
  - 127.0.0.1/32
  - ::1/128

noise:
  private_key_path: /var/lib/headscale/noise_private.key
prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48
  allocation: sequential
derp:
  server:
    enabled: false
    region_id: 999
    region_code: "headscale"
    region_name: "Headscale Embedded DERP"
    verify_clients: true
    stun_listen_addr: "0.0.0.0:3478"
    private_key_path: /var/lib/headscale/derp_server_private.key
    automatically_add_embedded_derp_region: true
    ipv4: 198.51.100.1
    ipv6: 2001:db8::1
  urls:
    - https://controlplane.tailscale.com/derpmap/default
  paths: []
  auto_update_enabled: true
  update_frequency: 3h
disable_check_updates: false
ephemeral_node_inactivity_timeout: 30m
database:
  type: sqlite
  debug: false
  gorm:
    prepare_stmt: true
    parameterized_queries: true
    skip_err_record_not_found: true
    slow_threshold: 1000
  sqlite:
    path: /var/lib/headscale/db.sqlite
    write_ahead_log: true
    wal_autocheckpoint: 1000
acme_url: https://acme-v02.api.letsencrypt.org/directory
acme_email: ""
tls_letsencrypt_hostname: ""
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ":http"
tls_cert_path: ""
tls_key_path: ""
log:
  level: info
  format: text
policy:
  mode: file
  path: ""
dns:
  magic_dns: true
  # WAS: base_domain: example.com  — the untouched placeholder, and it must differ from server_url's domain
  base_domain: ts.seabee.me
  override_local_dns: true
  nameservers:
    global:
      - 1.1.1.1
      - 1.0.0.1
      - 2606:4700:4700::1111
      - 2606:4700:4700::1001
    split: {}
  search_domains: []
  extra_records: []
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
logtail:
  enabled: false
randomize_client_port: false
taildrop:
  enabled: true
```

`base_domain: ts.seabee.me` is a placeholder suggestion — any subdomain of `seabee.me` other than `headscale.seabee.me` (which `server_url` already uses) works. Pick whatever you actually want your devices' MagicDNS names to end in.

---

## 3. `docker-compose.yml` — drop the Caddy service

```yaml
services:
  headscale:
    image: headscale/headscale:0.28.0
    container_name: headscale
    restart: unless-stopped
    command: serve
    volumes:
      - ./headscale/config:/etc/headscale
      - ./headscale/lib:/var/lib/headscale
      - ./headscale/run:/var/run/headscale
    ports:
      - "127.0.0.1:8080:8080"
      - "127.0.0.1:9090:9090"
    networks:
      - headscale-net

  headplane:
    image: ghcr.io/tale/headplane:v0.6.1
    container_name: headplane
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"
    volumes:
      - ./headplane/config/config.yaml:/etc/headplane/config.yaml
      - ./headplane/lib:/var/lib/headplane
      - ./headscale/config/config.yaml:/etc/headscale/config.yaml
      - /var/run/docker.sock:/var/run/docker.sock:ro
    depends_on:
      - headscale
    networks:
      - headscale-net

networks:
  headscale-net:
    driver: bridge
```

Pin the Headplane tag to whatever's actually current at https://github.com/tale/headplane/releases — `v0.6.1` here is a placeholder, not a verified-current release.

If your existing compose file still has a `caddy:` block from the original guide, delete it — running two things bound to host port 443/80 (native Caddy plus a would-be Caddy container) will conflict, and you don't want the container one anyway now.

---

## 4. Native Caddy config

I don't have confirmation of your VPS's distro or how Caddy was installed, so I can't state the config path or reload command as fact. **If it's the official Caddy apt repo on Debian/Ubuntu**, the convention is `/etc/caddy/Caddyfile`, reloaded with `sudo systemctl reload caddy` — verify this against your actual install before running it, since a manually-built binary, snap package, or a different distro's package manager may differ.

Caddyfile content (path-independent, this part is safe regardless of how Caddy was installed):

```
headscale.seabee.me {
    handle /admin* {
        reverse_proxy 127.0.0.1:3000
    }
    handle {
        reverse_proxy 127.0.0.1:8080
    }
}
```

Note the target is `127.0.0.1:8080` / `127.0.0.1:3000` — host loopback, not a Docker container hostname like `headscale:8080`. That hostname-based addressing only resolves inside the `headscale-net` Docker bridge network, which your native Caddy process isn't part of.

---

## 5. `headplane/config/config.yaml`

Unchanged from the original guide, provided you generate a real `cookie_secret` and fill in `api_key` after first boot (step 6 below):

```yaml
server:
  host: "0.0.0.0"
  port: 3000
  cookie_secret: "REPLACE_WITH_32_CHAR_RANDOM_STRING"
  base_url: "https://headscale.seabee.me"

headscale:
  url: "http://headscale:8080"
  config_path: "/etc/headscale/config.yaml"
  api_key: "REPLACE_AFTER_HEADSCALE_STARTS"

integration:
  docker:
    enabled: true
    container_name: "headscale"
```

`headscale.url` stays as `http://headscale:8080` here — that's correct, because this is Headplane-to-Headscale traffic, which stays inside the Docker network (both containers are on `headscale-net`), unlike Caddy which is outside it.

Generate the cookie secret:
```bash
openssl rand -hex 32
```

---

## 6. Bring it up

```bash
cd ~/Docker/headscale
docker compose up -d headscale
docker exec headscale headscale users create myuser
docker exec headscale headscale apikeys create --expiration 90d
```

Paste that API key into `headplane/config/config.yaml` → `headscale.api_key`, then:

```bash
docker compose up -d
```

Reload Caddy (command depends on your actual install — see section 4). Then visit `https://headscale.seabee.me/admin`.