Quick Start

Get Town OS running on a spare computer in minutes. All you need is a USB-C drive (4 GB or larger) and a Linux machine to write it from.

1. Write the USB Drive

Run the installer script on any Linux machine with podman, curl, bzip2, dd, lsblk, and tar installed — Podman is required, since the script pulls the installer image with it (and must be working, not just installed). It downloads the latest Town OS image and writes it directly to a USB drive.

curl -sSLO https://town-os.github.io/install.sh && bash install.sh

The script will scan for connected USB devices, show you a list with device names and sizes, and ask you to pick one. After you confirm, it streams the compressed image and writes it to the drive in a single pass. The whole process takes a few minutes depending on your internet connection.

Raspberry Pi (4 / 400 / CM4, 5 / CM5): pass RPI=1 to write the native-boot Raspberry Pi image to an SD card, USB drive, or NVMe instead of the standard PC image:

curl -sSLO https://town-os.github.io/install.sh && RPI=1 bash install.sh

The Pi image is always 64-bit Arm and boots without UEFI, so it works even when you run the installer from an x86_64 machine. A Raspberry Pi boots straight from the inserted card/drive with no boot menu; for Pi 5 NVMe boot, make sure the bootloader EEPROM boot order includes NVMe (set it with rpi-eeprom-config).

DATA DESTRUCTION WARNING

Town OS will format and use all detected local storage devices (NVMe, SATA, SAS, SD cards). Booting Town OS on a machine will permanently destroy all existing data on every internal drive.

Only boot Town OS on a dedicated machine with no data you want to keep, or use the VM instructions to try it safely first.

2. Boot from USB

Plug the USB drive into the target computer and boot from it. You may need to press a key during startup to reach the boot menu — common keys are F12, F2, Esc, or Del, depending on your hardware.

3. First Boot: Use Sledgehammer

If the target machine has been used before for something else, select the sledgehammer boot option from the boot menu on your first boot. Sledgehammer wipes all detected storage devices and ensures Town OS starts with a completely clean slate. This avoids problems caused by leftover partition tables, filesystems, or RAID metadata from a previous operating system.

After sledgehammer finishes, the machine reboots automatically and Town OS performs a fresh storage setup. See Sledgehammer Boot Option for full details.

4. What Happens on Boot

Town OS loads entirely into RAM from the USB drive, then launches ttyforce, an interactive TUI installer that guides you through setup directly on the machine's console.

  • Network configuration — ttyforce detects available interfaces. If a wired connection with link is present, it advances automatically. Otherwise it presents WiFi network selection with signal strength, security info, and WPA2/WPA3 password entry.
  • Disk provisioning — ttyforce groups detected disks by type and size, then automatically selects the appropriate RAID level: single drive for 1 disk, RAID1 (mirror) for 2, or RAID5 (striped with parity) for 3 or more. All storage uses btrfs.
  • SSH key import — you can enter GitHub usernames to import public SSH keys for secure remote access.

After provisioning completes, the system reboots and ttyforce switches to getty mode — a live status display showing service health, system metrics, and journal output on the console. From here you can log in, reconfigure networking, or trigger a sledgehammer wipe.

5. Create Your Account and Start Using It

From any device on the same network, open a browser and go to http://town-os.local. If that doesn't work, check your router's DHCP client list for a device named "town-os" and use its IP address directly. You will be prompted to create an administrator account. Once logged in, you can install packages, manage storage, and configure services from the dashboard.

6. Point Your Router's DNS at Town OS

Town OS ships with rolodex, a DNS server that resolves package hostnames and forwards everything else upstream. To make every device on your network use it automatically, give the Town OS machine a static IP (or DHCP reservation) and set your router's Primary DNS Server to that address. See Setting Up DNS on Your Router for per-brand instructions and verification steps.

Building the USB Image from Source

The Town OS USB image is built from the install repository. The build process produces a bootable image with a squashfs root filesystem and a GPT partition layout.

Prerequisites

You need a Linux host and a USB drive (4 GB or larger). The build uses Arch Linux tooling (pacstrap, mkinitcpio, arch-chroot), but you do not need to be running Arch — on any other distro make image automatically runs the build inside a same-architecture Arch container. Install the host dependencies with the bundled target for your distro:

  • make deps — Arch (and derivatives) or Fedora/RHEL
  • make deps-debian — Debian or Ubuntu

These pull in everything the build and VM tooling need — make, podman, arch-install-scripts, squashfs-tools, parted, e2fsprogs, dosfstools, qemu, and libvirt.

Cloning and Building

git clone https://gitea.com/town-os/install.git
cd install
make image

Running make or make image builds the complete USB image. The process downloads the base system, installs Town OS components, compresses everything into a squashfs filesystem, and assembles the final disk image with a GPT partition table.

Partitioning and Squashfs

The resulting image uses a GPT layout with:

  • A BIOS boot partition (1 MiB) for legacy BIOS booting
  • An EFI system partition (64 MiB, FAT32) for UEFI booting
  • A data partition (ext4) containing the squashfs root filesystem and GRUB

At boot time, Town OS mounts the squashfs as a read-only lower layer, with a tmpfs overlay on top. This means the OS runs entirely in RAM — the USB drive itself is only read at boot. All runtime changes happen in memory and are discarded on reboot, giving you a clean slate every time.

Writing the Image

The simplest way is make flash, which builds the image if it is stale and writes it to a USB device. To do it by hand, note that the build produces a dated, arch-suffixed file (e.g. town-os-2026-07-25-x86_64.img) — write it with dd:

# Find your USB device (e.g. /dev/sdb)
lsblk

# Write the image (replace the filename and /dev/sdX with your device)
sudo dd if=town-os-YYYY-MM-DD-x86_64.img of=/dev/sdX bs=4M status=progress conv=fsync

Double-check the target device — dd will overwrite whatever you point it at without confirmation.

Boot Process
USB Drive
GPT Partition Table
EFI System squashfs Root
boot
Running System
tmpfs overlay read-write
squashfs read-only
RAM
OS runs entirely in memory

Sledgehammer Boot Option

The Town OS USB drive includes a sledgehammer boot option that wipes all detected storage devices and resets the system to a clean state. This is useful when you want to start fresh — for example, after testing, before redeploying to new hardware, or when storage has become corrupted.

What It Does

When you select the sledgehammer option at boot, Town OS will:

  • Detect all local storage devices (NVMe, SATA/SAS, SD cards) — the same detection used during normal boot
  • Wipe the partition tables and filesystems on every detected device
  • Reboot the system, which then performs a fresh storage setup as if it were the first boot

Sledgehammer destroys all data on all detected storage devices. The USB drive itself is not affected — only the internal disks. Make sure you have backups of anything you need before using this option.

How to Use It

  1. Plug the Town OS USB drive into the target machine and boot from it
  2. At the boot menu, select the sledgehammer entry instead of the default boot option
  3. The system will wipe all detected storage and reboot automatically
  4. On the next boot, Town OS will set up storage from scratch using your town-os.yaml configuration

When to Use It

  • Factory reset — return the system to a clean state as if it were freshly deployed
  • Storage backend change — switching from btrfs to ZFS (or vice versa) requires clearing existing storage first
  • Corrupted storage — if the filesystem is damaged beyond repair, sledgehammer gives you a clean starting point
  • Redeployment — repurposing hardware for a different Town OS configuration
Sledgehammer Reset Flow
Boot Menu
Select sledgehammer
sledgehammer
wipe
Clear Storage
All detected disks wiped
NVMe SATA SD
reboot
Fresh Setup
Storage configured from scratch
town-os.yaml

Setting Up DNS on Your Router

Town OS includes rolodex, a DNS server that manages authoritative zones for your packages and forwards upstream queries. To use it as your network's DNS server, you need to tell your router to hand out the Town OS machine's IP address as the DNS server for your network. This way every device on your network automatically uses rolodex for DNS — no per-device configuration needed.

Find Your Town OS IP Address

You'll need the local IP address of your Town OS machine. You can find it by:

  • Logging in to the dashboard at http://town-os.local after first-boot setup and selecting the internal IP shown on the dashboard — this is the address to use as your DNS server
  • Running make vm-ip if you're using a VM
  • Checking your router's DHCP client list for a device named "town-os"
Town OS dashboard showing External IP and Internal IP with copy buttons

For DNS to work reliably, your Town OS machine should have a static IP address or a DHCP reservation on your router. If the IP changes, DNS will break for your whole network.

Assign a Static IP or DHCP Reservation

Most routers let you reserve an IP address for a specific device based on its MAC address. This is usually found under LAN Settings, DHCP, or Address Reservation. Find the Town OS machine in the client list and reserve its current IP.

If your router doesn't support DHCP reservations, you can configure a static IP on the Town OS machine itself by adding it to town-os.yaml.

Change the DNS Server in Your Router

The exact steps vary by router brand, but the general process is the same:

  1. Log in to your router's admin interface (usually 192.168.1.1 or 192.168.0.1)
  2. Find the DHCP Settings, LAN Settings, or DNS Settings section
  3. Change the Primary DNS Server to your Town OS machine's IP address
  4. Optionally set a Secondary DNS Server as a fallback (e.g. 1.1.1.1 or 8.8.8.8) — this will be used if Town OS is unreachable
  5. Save and apply the settings

After saving, devices on your network will pick up the new DNS server the next time they renew their DHCP lease. You can force this by disconnecting and reconnecting to the network, or by rebooting the device.

Common Router Interfaces

Router BrandWhere to Find DNS Settings
ASUSLAN → DHCP Server → DNS Server
TP-LinkDHCP → DHCP Settings → Primary DNS
NetgearInternet → Domain Name Server (DNS) Address
LinksysConnectivity → Local Network → DHCP Server → Static DNS
UniFiSettings → Networks → (your network) → DHCP Name Server
pfSense / OPNsenseServices → DHCP Server → DNS Servers
OpenWrtNetwork → Interfaces → LAN → DHCP Server → Advanced → DHCP-Options: 6,<town-os-ip>

Verify It Works

After your device picks up the new DNS settings, verify that rolodex is handling your DNS queries:

# Check which DNS server your machine is using
nslookup example.com

# Or query Town OS directly
dig @<town-os-ip> example.com

# Query a package domain (if you have packages installed).
# Names follow <name>.<repo>.<tld> — .home is the default network's TLD
dig @<town-os-ip> gitea.default.home

If example.com resolves correctly, rolodex is working and handling DNS for your network.

Choosing a Resolution Mode

Names that don't belong to one of your networks are resolved according to the resolution mode, which you set under Settings → DNS Resolution in the dashboard. Three modes are available:

  • Automatic (recommended) — the default. Tries to resolve iteratively from the root servers first, then falls back through DoH/DoT, a local forwarder, and finally a public resolver, sticking with whichever tier last worked. This keeps recursion's privacy where the network allows it, and degrades gracefully on networks that filter outbound DNS.
  • Recursive only — resolves everything iteratively from the root servers with no fallback. Guarantees no query is ever handed to a third-party resolver, but every external name fails on networks that block or hijack outbound port 53 (hotels, captive portals, some ISPs).
  • Forward — always sends unmatched queries to upstream resolvers (Google Public DNS by default). This is the classic forwarding behavior.

Blocking Ads and Malicious Domains

rolodex can screen queries against DNSBL (domain blocklists) and RBL (IP blocklists) providers, giving you network-wide ad and threat blocking without a separate Pi-hole. Manage these under DNS → Blocklists. Town OS never downloads or caches blocklist feeds — providers are queried on demand as regular DNS zones.

  • Provider zones — enable a category (DNSBL or RBL), then add provider zones. One-click suggestions include well-known lists such as Spamhaus (dbl.spamhaus.org, zen.spamhaus.org), SURBL, URIBL, SpamCop, and Barracuda; you can also add any custom zone. Each provider has its own on/off switch.
  • Local entries — block a specific domain or IP by hand. Blocking a domain returns NXDOMAIN for forward lookups; you can note a reason for each entry.

Publishing Services in DNS

By default every installed package service is published in DNS under its network's TLD so you can reach it by name. Under DNS → Services you can toggle publishing per service — an unpublished service keeps running but is no longer resolvable by name. Each service's fully-qualified name follows the pattern <name>.<repo>.<tld> (for the default network, that TLD is .home).

DNS Flow
Device
Phone, laptop, etc.
DNS query
DHCP
Router
Hands out Town OS as DNS
DNS = Town OS IP
forward
Town OS
rolodex DNS server
Authoritative zones Upstream forwarding

Trusting the Certificate Authority

Town OS issues TLS certificates for your internal services through the rolodex built-in certificate authority. For your browser to show a padlock instead of a warning, each device needs to trust the Rolodex root CA once. There are three ways to fetch it — pick whichever fits the device.

Option 1: From DNS (works anywhere DNS does)

Rolodex publishes the CA chain in DNS itself, so any device that can resolve your zone can fetch the CA — no access to the enrollment portal required. The root and per-zone intermediate are served as CERT records (RFC 4398) at _ca.<zone>, with a chunked TXT fallback at _rolodex-ca.<zone>:

# See the published CA records
dig @<town-os-ip> CERT _ca.example.home

# Extract the root CA to a PEM file (root = the self-signed one)
dig @<town-os-ip> +short CERT _ca.example.home

The easiest way to consume these records in a browser is the Rolodex browser extension (in the rolodex repository under extension/, loaded unpacked via chrome://extensions or about:debugging in Firefox). Open the popup’s CA via DNS section, enter your DoH URL (https://<town-os-ip>/dns-query) and zone, and it retrieves the chain over DNS-over-HTTPS, prefers the CERT records with automatic TXT fallback, verifies it against the published DANE TLSA records if you give it a hostname, and offers the root, intermediate, and full chain as PEM downloads.

Option 2: From the Enrollment Portal

On the trusted network, browse to the enrollment portal (https://<town-os-ip>:8500 by default) and click Download root CA (PEM). The same extension and the rolodex-ca-ui local console can do this too.

Option 3: From the Command Line

# Via the management CLI (prints root + intermediate PEM)
rolodex-dns-cli ensure-zone-ca --zone example.home

# Or fetch the portal download directly
curl -k https://<town-os-ip>:8500/api/ca -o rolodex-root-ca.pem

Install the Root CA on Your Device

Once you have rolodex-root-ca.pem, add it to the trust store:

PlatformHow
FirefoxSettings → Privacy & Security → Certificates → View Certificates → Authorities → Import (check “Trust this CA to identify websites”)
Chrome / Edge (desktop)Uses the OS trust store — install per the OS rows below, then restart the browser
macOSDouble-click the PEM to add it to Keychain Access, then set it to “Always Trust” under SSL
Linux (Fedora/RHEL)sudo cp rolodex-root-ca.pem /etc/pki/ca-trust/source/anchors/ && sudo update-ca-trust
Linux (Debian/Ubuntu)sudo cp rolodex-root-ca.pem /usr/local/share/ca-certificates/rolodex.crt && sudo update-ca-certificates
WindowsDouble-click → Install Certificate → Local Machine → “Trusted Root Certification Authorities”
AndroidSettings → Security → Encryption & credentials → Install a certificate → CA certificate
iOSOpen the PEM (AirDrop/mail), install the profile, then enable it under Settings → General → About → Certificate Trust Settings

Servers issued through the Rolodex ACME endpoint present a leaf + intermediate chain that validates against this root. DANE-aware clients can additionally verify the intermediate against the TLSA records rolodex publishes automatically on issuance.

Networks and Remote Access

Town OS groups your services into networks. Every box starts with a built-in network called home — it is LAN-only, resolves names under .home, and has no tunnel. To reach your services securely from outside your house, create additional networks: each one is a WireGuard overlay paired with its own DNS TLD, managed under Dashboard → Networks (admin only).

The Networks page listing the built-in home network with its TLD, subnet, peer count, and a Remote access toggle, plus a Create Network button

The Default Network

  • Always present, can't be removed. The home network is created automatically and is where packages land unless you pick another.
  • Local only. It has no WireGuard transport — .home names resolve on your LAN and are deliberately never exposed to remote peers.
  • Uses the .home TLD, which comes from the dns_tld setting.

Creating a Network for Remote Access

Click Create Network and give it a name (for example office) and, optionally, a TLD (it defaults to the network name). Town OS then:

  • Generates a WireGuard interface with a deterministic overlay subnet drawn from the 10.64.0.0/10 range, with the box itself as the .1 address.
  • Claims the network's TLD (TLDs are unique per box — creating a network with a TLD another network already owns is rejected).
  • Runs a per-network DNS resolver on the overlay so joined devices can resolve that network's names.
The Create Network dialog with Name and TLD fields, noting the TLD defaults to the network name

The Remote access switch on each row brings the WireGuard interface up or down. Turning it off cuts remote access while leaving the containers running and reachable locally.

Enrolling a Device

Open a network's Peers dialog, enter a device name, and click Add Peer. Town OS returns a ready-to-import WireGuard configuration — paste it into the WireGuard app on your phone or laptop.

Copy the config right away. It contains a freshly generated private key that is never stored — you cannot retrieve it later, and will have to enroll the device again if you lose it.

The generated config points the device's DNS at the box's overlay address, so your network's names resolve automatically once the tunnel is up. Leave the Runs rolodex DNS switch off for ordinary devices; enable it only for a peer that itself runs a rolodex DNS server you want to forward to.

WireGuard-Only Accounts

To let someone enroll their own devices without giving them run of the dashboard, create a WireGuard-only account (a checkbox on the Create User screen). Such an account:

  • Is scoped to one or more specific networks — it can enroll peers only on those, and never on the home network.
  • Is fail-closed: it can authenticate, enroll and refresh its own peers, and fetch the CA, but nothing else in the control plane.
  • Enrolls expiring peers. Each enrollment carries a TTL (default 2 hours, set under Settings → WireGuard Peer TTL) and must be refreshed to stay connected; abandoned devices age out automatically. Peers an admin adds are permanent.

Watching and Disconnecting Peers

The Connected Peers panel on the Networks page lists every enrolled peer across all networks with its live handshake status, overlay IP, data transferred, and expiry. To forcibly cut a device off, use Disconnect — this removes the peer, tears its tunnel down immediately, and revokes its key, so the device cannot reconnect until it is enrolled again.

Installing Packages on a Network

The install dialog (and the Pages dialog) includes a Network selector. Whichever network you choose determines the service's DNS name and TLS certificate — a package installed on office resolves under .office and its certificate is issued for that name. Services on a non-default network are dual-homed: they resolve to the overlay address for tunnelled peers and to the LAN address for local clients. Reinstalling a package on a different network moves its DNS and certificates to that network's TLD.

The Install jitsi dialog with a Network selector at the top labeled 'The network this package is served on', above the package's configuration questions

For example, installing Jitsi on a network with the fart TLD publishes it at jitsi.default.fart. Once it's running, the service shows up on your Dashboard as a clickable link at that address, ready to open from any device on the network.

The Dashboard's Installed Services list showing jitsi with a link to https://jitsi.default.fart/
Remote Access over a Network
Your Device
Phone or laptop
WireGuard
tunnel
Town OS
Network overlay + DNS
office subnet .office TLD
resolve
Your Services
Reachable by name
gitea.default.office

Town OS on Android

The Town OS Android client connects your phone to one of your networks over WireGuard and sets up DNS so your services resolve by name — from anywhere. It is a full WireGuard client that enrolls the phone as a peer for you; there is no config file to copy by hand.

Town OS Android client login screen — a Connect to a box card with box address, admin username, and password fields
Logging in to a box
Town OS Android client connection screen — Disconnected status with Connect and Forget this network buttons, above a DNS resolver settings card
Managing the connection and DNS

Installing the App

The app is distributed as an APK from the project's releases page — it is not on the Play Store or F-Droid.

  • Download the debug APK (town-os-client-<version>-debug.apk). The -unsigned.apk on the same release cannot be installed as-is — the project ships no signing key, so the debug build is the one to grab.
  • Requires Android 8.0 (Oreo) or newer.
  • Because it is sideloaded, allow installing unknown apps when your browser or file manager prompts.
  • Prefer to build it yourself? Clone the repo and run make deps && make debug && make install with the phone connected over USB and USB debugging enabled. make help lists every target.

To install the downloaded APK on your phone:

  1. Get the APK onto the phone. Download it directly in the phone's browser, or copy it over from a computer via USB or adb push.
  2. Open the APK from your browser's downloads or a file manager. The first time, Android asks to allow installs from this source — grant it (Settings → Apps → Special access → Install unknown apps), then tap Install.
  3. Or install from a computer with the phone in USB-debugging mode: adb install town-os-client-<version>-debug.apk.
  4. Open Town OS from your app drawer once it finishes installing.

Before You Connect

  • Create a network first. The app can only join networks that carry a WireGuard overlay — the built-in home network is LAN-only and won't appear. See Networks and Remote Access to create one.
  • Have an administrator account ready. Enrolling a device is an admin action, so the app signs in with admin credentials.

Connecting to a Network

  1. Log in to your box. Enter its address — a bare IP, IP:port, or full URL (port 5309 is assumed) — along with your admin username and password.
  2. Join a network. The app lists the networks you can join, each with its TLD, subnet, and peer count. Give the device a name (it defaults to your phone's model) and tap Join. The WireGuard keypair is generated on the phone and only the public half is sent to the box, so your private key never leaves the device.
  3. Connect. Tap Connect and approve Android's Connection request (VPN) prompt. The app brings up the tunnel and installs the network's TLD as a search domain, so both gitea and gitea.default.<tld> resolve.
  4. You are now on the network by name from anywhere. Use Disconnect to drop the tunnel, or Forget this network to remove the saved enrollment.

Troubleshooting

  • Names don't resolve, but the tunnel is up. The usual culprit is Android's strict Private DNS. If it is set to a specific provider hostname, Android sends every lookup there and ignores the tunnel, so Town OS names come back "not found." Switch Settings → Network & internet → Private DNS to Automatic or Off. The app detects this and shows a warning with a shortcut straight to the setting. (Automatic mode is fine and never warns.)
  • Still nothing? In the app's DNS card, set the resolver override to the box's LAN address — the app routes it through the tunnel so split-horizon DNS still works.
  • Logged out after the box reboots. Town OS clears all sessions on restart; just log in again.

Building VMs from USB Images

The install repository includes scripts for launching Town OS in virtual machines, which is useful for testing and development without a physical USB drive.

QEMU

make qemu

Launches a QEMU VM with KVM acceleration, 4 IDE drives for storage testing, and bridge networking. The VM boots from the USB image and behaves identically to a physical installation. Use make qemu-fg to run it in the foreground with the serial console attached.

Booting a Physical USB (qemu-usb)

make qemu-usb USB_DEV=/dev/sdX

Boots QEMU in the foreground straight from a flashed physical USB stick instead of a built image — handy for confirming that a stick you just wrote actually boots. The device is opened read-only (snapshot), so guest writes are discarded and the real USB is never modified; the four virtual data disks are still attached for storage testing. This target does not build anything. On an x86_64 host, TARGET=aarch64 (or rpi) boots the stick under full-system emulation, so a foreign-arch image can be tested without the matching hardware.

VirtualBox

make virtualbox

Creates a VirtualBox VM with bridged networking and converts the raw disk image to VDI format. Bridged mode lets the VM appear as a regular device on your network.

Auto-Detection

make run

Automatically detects your hypervisor — prefers QEMU if available, falls back to VirtualBox.

Environment Variables

VariableDefaultDescription
IMAGE_SIZE12GSize of the USB image
VM_DISK_SIZE50GSize of each virtual disk
VM_MEMORY4GVM memory
VM_CPUS4vCPUs for the VM
VM_BRIDGEvirbr0Host bridge interface for networking

Serial Console

# Attach to the VM serial console
make serial

# Or manually via socat
socat -,rawer,escape=0x1d unix-connect:/tmp/town-os-serial.sock

# Detach with Ctrl-]

Finding the VM

# Get the VM's IP address
make vm-ip

# Or connect via mDNS
ssh root@town-os.local
VM Networking
Host Machine
br0 bridge interface
QEMU / VirtualBox
bridge
Town OS VM
Full Town OS stack
eth0
network
Local Network
LAN devices can reach VM
mDNS: town-os.local

RAID Installation

Disk provisioning is handled interactively by ttyforce during the first boot. All storage uses btrfs.

Automatic RAID Selection

ttyforce detects available disks, groups them by transport type (NVMe, SATA, etc.) and similar size, then automatically selects the RAID level based on disk count:

  • 1 disk — single mode (no redundancy)
  • 2 disks — RAID 1 (btrfs mirror)
  • 3+ disks — RAID 5 (btrfs striped with parity)

The default mount point is /town-os. ttyforce creates @etc and @var subvolumes for overlayfs-compatible persistence.

Disk Detection

ttyforce automatically detects available storage devices. It identifies NVMe drives, SATA/SAS drives, and SD cards, while excluding the boot USB device and any removable media. The detection logic ensures that Town OS never touches the drive it booted from.

Overlay Filesystem

Regardless of storage backend, Town OS uses overlay mounts for /var and /etc persistence. The lower layer comes from the squashfs root, while the upper layer lives on the RAID/ZFS storage. This means system configuration and service data survive reboots while the base OS remains immutable.

Storage Architecture
NVMe
Fast SSD
SATA / SAS
HDD or SSD
SD Card
Removable
detection
btrfs single
1 disk
btrfs RAID 1
2 disks (mirror)
btrfs RAID 5
3+ disks (parity)
Subvolumes
Per-package storage
pkg-a pkg-b
Overlay Mounts
Persistent system dirs
/var /etc

Using the User Interface

Town OS provides a clean web-based dashboard for managing your server. After booting, open a browser and navigate to your Town OS machine's IP address or http://town-os.local.

First Boot: Creating Your Account

On first boot, you'll be prompted to create an administrator account. Choose a username and password — this account has full control over the system.

Create account screen

Dashboard

The dashboard shows an overview of your system — installed packages displayed as service cards with status indicators, quick actions, and system health at a glance.

Dashboard overview

Browsing and Installing Packages

The Packages view lets you search available packages, view details, and install them with guided prompts. Each package's questions are presented as a form — fill in hostnames, ports, and other configuration, then click install.

Package browser
Package installation prompts
Installation details

Managing Services

Installed services can be started, stopped, and restarted from the Services view. Status indicators show whether each service is running, stopped, or in an error state.

Service management

Viewing Logs

The Logs view provides live journal output with filtering and grep capabilities. You can filter by service, priority level, and search for specific text.

Log viewer

Storage Management

View and manage btrfs subvolumes, configure per-package quotas, and monitor disk usage across your storage pool.

Storage management

Monitoring

Town OS includes built-in monitoring with Prometheus and Node Exporter for tracking system metrics, service health, and resource usage over time. A lightweight built-in dashboard is the default, with an optional Grafana upgrade available.

Monitoring dashboards

Settings and Audit Log

The Settings page lets you configure system-wide options. The Audit Log tracks every administrative action — installs, uninstalls, service state changes, and configuration modifications — so you always know what changed and when.

Settings
Audit log

Hosting Static Sites

Beyond containerized packages, Town OS has built-in static site hosting, always on and managed under Dashboard → Pages. Each page is served by a shared web server behind the ingress and gets a DNS name and TLS certificate under its network's TLD, just like a package.

Content Sources

When you create a page you pick a name, a domain (defaults to the name), a network (defaults to home), and one of three content sources:

  • Archive upload — upload a tarball of your site. The page waits in a pending state until you upload the archive.
  • Git repository — clone from a repo URL. You can specify a branch (defaults to main), which is handy for sites published to gh-pages.
  • Container image — extract a directory out of an OCI image.

Git and container pages provision asynchronously — the table shows a Provisioning… badge that resolves to active or error. Use Rebuild to pull the latest content for git and container pages; for an archive page, upload a new tarball instead.

How Pages Are Served

Each page lives on its own storage subvolume and is served directly over HTTP on port 80, while package services redirect port 80 to HTTPS. Because a page carries a network, its name resolves under that network's TLD and is reachable by your LAN and — if the network is a WireGuard overlay — by your enrolled remote devices.

Updating Town OS

Town OS updates its core services in place from the dashboard. On System Management, the Refresh Core Services button pulls the latest container images and restarts every core service — including the system controller itself, which is how the box self-updates.

What Happens During a Refresh

  • Images are pulled and services restarted in dependency order: the system controller first (so its new image is ready before it restarts itself), then DNS, then the rest.
  • Your login session is intentionally reset as the controller restarts, so the dashboard holds off its usual session checks while the refresh runs rather than bouncing you to the login page.
  • Progress is shown as a five-stage stepper: Booting system controllerBooting DNSStarting system servicesRestarting packages (with one row per installed package) → Ready.
  • When the refresh completes, the dashboard shows a Reload button instead of reloading itself out from under you.

These same five stages appear on the provisioning screen during a normal boot, so you can watch startup progress package by package.

Building a Package

Town OS packages are YAML files that describe how to run a containerized service. For the full specification, see the Packaging Format reference. This section covers the practical workflow.

Repository Structure

A package repository is a git repository with a packages/ directory. Each package gets a subdirectory containing versioned YAML definitions:

my-packages/
  packages/
    my-app/
      1.0.yaml
      2.0.yaml

Writing a Package Definition

A minimal package needs only an image field. A typical package includes a description, networking, volumes, and user-facing questions:

image: myapp:latest
description: My custom application
supplies: ["http"]
network:
  external:
    "@port@": "8080"
volumes:
  data:
    mountpoint: /app/data
    quota: 5gb
questions:
  port:
    query: "What external port should this app use?"
    type: port
    default: "9000"
notes:
  URL:
    value: "http://@LOCAL_EXTERNAL_HOST@:@port@"
    type: url

Questions support several types beyond free text and port: secret (auto-generated when left blank), boolean (rendered as a checkbox), oauth (a Connect button that runs a provider's device-flow to obtain a token), and any question can be marked optional: true to allow it to be left blank. See the Packaging Format reference for the full list. At install time the operator also picks which network the package is served on.

Template System

Use @variable@ syntax to reference question answers and built-in variables (@LOCAL_EXTERNAL_HOST@, @LOCAL_INTERNAL_HOST@). Templates work in environment variables, port mappings, quotas, and note values.

Testing Locally

Use the dev environment to test your package. Place your repository on disk, add it through the UI or repositories.json, and install your package. The dev environment provides the full Town OS stack for testing.

Adding a Repository

Add your package repository to Town OS through the UI (Packages → Repositories → Add Repository) or by editing repositories.json directly:

[
  ["default", "https://github.com/town-os/default-packages"],
  ["my-packages", "https://github.com/myuser/my-packages"]
]
Package Lifecycle
YAML Definition
image, volumes, questions
1.0.yaml
install
Config Prompts
User answers questions
hostname port
deploy
Running Container
Service active with volumes
@variable@ → value

Things You Can Self-Host

Town OS is built to run anything that ships as a container image. Here are some ideas to get you started — many of these are available in the default package repository, and any container image can be packaged with a simple YAML definition.

Media & Entertainment

  • Plex / Jellyfin — stream your movie and TV library to any device
  • Navidrome — personal music streaming server
  • Calibre-web — manage and read your ebook collection

Code & Collaboration

  • Gitea / Forgejo — lightweight self-hosted Git
  • GitLab — full DevOps platform
  • Nextcloud — files, calendar, contacts, and more
  • Wiki.js / BookStack — documentation and knowledge bases

Communication

  • Jitsi Meet — private video conferencing
  • Matrix / Synapse — federated encrypted chat
  • Mattermost / Rocket.Chat — team messaging

Game Servers

  • Valheim, Minecraft, Terraria, Satisfactory dedicated servers
  • Proton/Steam support via GloriousEggroll containers
  • Any game server that ships as a Linux binary or container

Home Automation

  • Home Assistant — smart home control hub
  • Node-RED — visual automation workflows
  • Mosquitto — MQTT broker for IoT devices

Privacy & Security

  • Pi-hole / AdGuard — network-wide ad blocking
  • WireGuard / OpenVPN — VPN servers for remote access
  • Vaultwarden — self-hosted password manager

Productivity

  • Paperless-ngx — document management and OCR
  • Immich — self-hosted photo and video management
  • Planka / Wekan — kanban boards and project management

Development Environment and Test Suite

The Town OS development environment runs the full stack locally using Podman containers. This is the fastest way to test changes, develop packages, and run the test suite.

Prerequisites

  • Linux — required for btrfs and Podman rootful containers
  • Podman — container runtime (rootful mode, with sudo)
  • Go 1.25+ — for the backend API server
  • Bun — for the frontend build and dev server
  • btrfs-progs — for storage management
  • QEMU — qemu-system-x86_64 and qemu-img for VM package support
  • libsystemd — development headers for systemd integration
  • golangci-lint — for Go linting
  • Python 3 — for build and test scripts

Starting the Dev Environment

git clone https://gitea.com/town-os/town-os.git
cd town-os
make dev

This starts the full development stack with hot-reloading. Once it's ready, open the URL printed in the terminal to access the Town OS dashboard.

Dev Environment Commands

CommandDescription
make devStart the full dev environment
make dev-stopStop all dev containers
make dev-logsTail logs from dev containers
make dev-cleanRemove dev containers and volumes

Running Tests

CommandDescription
make testRun unit tests
make test-integrationRun integration tests (requires privileged Podman)
make test-ui-integrationRun UI integration tests
make test-fullRun all tests (unit + integration + UI)
make auto-testWatch for changes and re-run tests automatically

Integration Tests

Integration tests run inside a privileged Podman container that provides a real btrfs filesystem, systemd, and Podman-in-Podman. This ensures tests exercise the same code paths as a production installation. The test container is ephemeral — it's created fresh for each test run and cleaned up afterward.

Dev Workflow
Editor
Edit code
Go + Bun
save
auto-test
Tests run on save
make auto-test
reload
Browser
Live preview
localhost:5173
↻ iterate — Backend :5309 · Frontend :5173

Reporting Bugs

Found something broken? Good bug reports help fix issues faster. Here's how to gather the information needed and file an effective report.

Gathering Logs with the API

Town OS exposes journal logs through its REST API. You can use Claude Code to connect to the API and generate a summary of recent errors:

# Fetch error-priority journal entries from Town OS
curl -s http://town-os.local:5309/api/systemd/logs/tail?priority=err | jq .

Or use Claude Code to summarize errors interactively:

# Example Claude Code prompt:
"Connect to the Town OS API at http://town-os.local:5309
 and fetch the last 100 error-priority journal entries from
 /api/systemd/logs/tail. Summarize the errors, group them
 by service, and suggest likely causes."

Filing an Issue

File issues on the Town OS Gitea instance at gitea.com/town-os/town-os/issues.

What to Include

  • Journal summary — error log output or a Claude Code summary of recent errors
  • Reproduction steps — what you did to trigger the problem, step by step
  • Town OS version — the build date or commit hash from the boot screen
  • Storage backend — btrfs, btrfs-mdadm, or ZFS, and how many disks
  • Environment — physical hardware, QEMU, or VirtualBox; RAM and disk sizes
Bug Reporting Flow
Detect Issue
Something broke
⚠ error
curl API
Gather Logs
Fetch journal entries
:5309/api/systemd/logs
summarize
Summarize
Group by service
Claude Code
file
File on Gitea
Create issue with details
✓ reported