Tested on Ubuntu Server LTS. Most packages work on other Debian-based distros.
bash setup.sh --profile server # core packages + zsh setup
bash setup.sh --profile server --optional # also install samba, smartmontools, nmap, ffmpeg, etc.
bash setup.sh --profile server --dry-run # preview without installing(Run from the repo root; linux-server/setup.sh is a thin shim onto it.)
After running, log out and back in to start using zsh.
After setup.sh completes, authenticate Tailscale:
sudo tailscale upsetup.sh handles the Tailscale web UI service, Docker services, and .env scaffolding automatically. See post-install.md for the remaining manual steps.
setup.sh— thin shim onto the rootsetup.sh(server platform)post-install.md— step-by-step checklist to follow after setup.shapt_packages.md— full package list with descriptions and linksbenchmark-nas.md— how to measure LAN vs. real-world NAS throughput (ddover Samba + OpenSpeedTest)../dotfiles/zshrc.example— shared zsh config (incl. Powerlevel10k), deployed to~/.zshrcbysetup.sh; the desktop-only bits self-disable headless, so the server uses the same base as every other platform../dotfiles/zsh_plugins.txt— shared antidote plugin list, deployed to~/.zsh_plugins.txtand pre-cloned bysetup.sh../dotfiles/tmux.conf— tmux config with mouse support, vi copy mode, and a status bar (shared across all platforms); copied to~/.tmux.confbysetup.sh../packages.json— machine-readable package manifest (shared across all platforms)
setup.sh handles packages, zsh, tmux, Tailscale, and Docker. Everything below is manual and should be done in order after the script finishes.
Log out and back in. This activates zsh as your default shell and adds you to the docker group (required to run Docker without sudo).
sudo tailscale up
sudo tailscale set --operator=$USERThe Tailscale web UI service is installed and started by setup.sh. The web UI is available at localhost:8088 and tailscale_ip:5252.
bash SSH_and_GPG/create_ssh_key.sh
bash SSH_and_GPG/create_gpg_key.shcurl -fsSL https://claude.ai/install.sh | bashStart services in this order. Most just need docker compose up -d; exceptions are noted.
# Homepage — update .env with your server details first (see post-install.md)
cd linux-server/homepage && cp .env.example .env
# Existing .env: add new .env.example keys, including HOMEPAGE_VAR_PI_HOSTNAME.
# edit .env, then:
docker compose up -d
# Access at http://<server-ip>:3000 (or https://<tailscale-hostname> after HTTPS setup)
# Portainer
cd linux-server/portainer && docker compose up -d
# Access at http://<server-ip>:9000 — create admin account within 5 minutes
# Glances
cd linux-server/glances && docker compose up -d
# Access at http://<server-ip>:61208
# Speedtest Tracker — requires APP_KEY before first run
cd linux-server/speedtest-tracker && cp .env.example .env
echo "base64:$(openssl rand -base64 32)" # paste as APP_KEY in .env
docker compose up -d
# Access at http://<server-ip>:8765
# Filebrowser
cd linux-server/filebrowser && cp .env.example .env
# Optionally set FB_ROOT in .env to limit the browsable path (defaults to /)
docker compose up -d
# Access at http://<server-ip>:8080 — default login: admin / admin (change immediately)
# AdGuard Home — fix systemd-resolved conflict first
sudo sed -i 's/#DNSStubListener=yes/DNSStubListener=no/' /etc/systemd/resolved.conf
sudo systemctl restart systemd-resolved
cd linux-server/adguard && docker compose up -d
# Open http://<server-ip>:3001 for setup wizard
# Set web UI port → 80, DNS port → 53, create admin account
# After setup: http://<server-ip>:8083
# qBittorrent — needs TS_AUTHKEY for its HTTPS sidecar (see HTTPS.md), like forgejo
cd linux-server/qbittorrent && cp .env.example .env
# edit .env: set TS_AUTHKEY (optionally QBITTORRENT_DOWNLOADS_PATH for an external drive), then:
docker compose up -d
# Access at https://qbittorrent.<tailnet>.ts.net/
# First run: get the temporary admin password from the logs and change it:
# docker compose logs qbittorrent | grep -i password
# OpenSpeedTest — needs TS_AUTHKEY for its HTTPS sidecar (see HTTPS.md), like qbittorrent
cd linux-server/openspeedtest && cp .env.example .env
# edit .env: set TS_AUTHKEY, then:
docker compose up -d
# Tailnet UI at https://openspeedtest.<tailnet>.ts.net/ — but run LAN tests via
# http://<server-ip>:3030 so you measure the local network, not the tailnetThe Tailscale widget uses a local OAuth proxy (linux-server/tailscale-proxy) to avoid 90-day API key rotation.
- Create an OAuth client at
tailscale.com/admin/settings/oauthwith scope Core - Read - Get your device ID:
tailscale status --json | jq -r '.Self.ID' - Fill in
linux-server/tailscale-proxy/.envwith the client ID, client secret, and device ID - Start the proxy:
cd linux-server/tailscale-proxy && docker compose up -d
Get a TLS cert from Tailscale and configure NPM to terminate HTTPS:
tailscale cert <your-tailscale-hostname> # e.g. <hostname>.<tailnet>.ts.netIn NPM admin (http://<server-ip>:81):
- SSL Certificates → Add Custom Certificate — paste the
.crtand.keyfile contents - Proxy Hosts → Add — domain: your Tailscale hostname, forward to
http://<server-ip>:3000, enable SSL with the custom cert - Redirection Hosts → Add — domain:
<hostname>.local, redirect tohttps://<tailscale-hostname>(301)
Requires enabling HTTPS certificates in the Tailscale admin console:
login.tailscale.com/admin/dns
-
- Lightweight server dashboard — system stats, running service cards with live Docker status
- Deploy:
cd linux-server/homepage cp .env.example .env # update HOMEPAGE_VAR_SERVER_IP docker compose up -d
- Access at
http://<server-ip>:3000 - Add new services in
config/services.yaml— each card supportsserver: my-docker+container: <name>for live status
-
- Web UI for managing Docker containers, images, volumes, and networks
- Deploy:
cd linux-server/portainer docker compose up -d - Access at
http://<server-ip>:9000(HTTP) orhttps://<server-ip>:9443(HTTPS, self-signed cert) - On first launch, set up an admin account within 5 minutes or the setup will time out
-
atvloadly | GitHub
- Self-hosted web app for sideloading IPA files onto Apple TV — a self-deployable alternative to AltStore/Sideloadly
- Deploy:
docker run -d --name atvloadly --restart=unless-stopped -p 5533:80 bitxeno/atvloadly
-
- Web-based file manager — browse, upload, download, edit, and share files on the server from any browser
- Deploy:
cd linux-server/filebrowser cp .env.example .env # optionally set FB_ROOT to limit the browsable path (defaults to /) docker compose up -d
- Access at
http://<server-ip>:8080; default login isadmin/admin— change on first login
-
- Web-based server admin UI — system metrics, journal logs, network config, storage, and service management
- Installed as a system package by
setup.sh(not Docker); enabled automatically via systemd socket activation - Access at
https://<server-ip>:9090; log in with your Linux username and password
-
- Real-time system monitor — CPU, memory, disk I/O, network, processes, temperatures, and Docker containers in one view
- Deploy:
cd linux-server/glances docker compose up -d - Access at
http://<server-ip>:61208; the Homepage widget shows live CPU stats on the service card
-
speedtest tracker | GitHub | Docs
- Scheduled ISP speed tests with history graphs — tracks ping, download, and upload over time
- Deploy:
cd linux-server/speedtest-tracker cp .env.example .env echo "base64:$(openssl rand -base64 32)" # paste output as APP_KEY in .env docker compose up -d
- Access at
http://<server-ip>:8765; runs a test every 6 hours by default (configurable viaSPEEDTEST_SCHEDULEin.env)
-
- Automatically pulls updated Docker images and restarts containers — runs daily at 3am by default
- Deploy:
cd linux-server/watchtower && docker compose up -d
-
- Self-hosted uptime monitor with status pages and alerts (email, ntfy, Discord, etc.)
- Deploy:
cd linux-server/uptime-kuma && docker compose up -d
- Access at
http://<server-ip>:3001; create an admin account on first visit - Add monitors for each service, then create a status page with slug
defaultfor the Homepage widget
-
tailscale-proxy
- Tiny local HTTP server that exchanges Tailscale OAuth credentials for an access token and proxies device API requests — used by the Homepage Tailscale widget to avoid 90-day key rotation
- Deploy:
cd linux-server/tailscale-proxy cp .env.example .env # fill in TAILSCALE_CLIENT_ID, TAILSCALE_CLIENT_SECRET, TAILSCALE_DEVICE_ID docker compose up -d # builds the image on first run
- Listens on
localhost:8089; Homepage queries it viacustomapiwidget
-
nginx proxy manager | GitHub | Docs
- Reverse proxy with a web UI — the non-tailnet HTTPS edge: trusted certs for LAN/public clients that can't join the tailnet (the tailnet sidecars cover on-tailnet HTTPS). See HTTPS.md → "NPM — trusted HTTPS for non-tailnet clients"
- Deploy:
cd linux-server/nginx-proxy-manager && docker compose up -d
- Access admin UI at
http://<server-ip>:81; default login:admin@example.com/changeme— update immediately - See step 7 above for HTTPS configuration
-
- Self-hosted push notification service — send alerts from any service to your phone or desktop via the ntfy app
- Deploy:
cd linux-server/ntfy && docker compose up -d
- Access at
http://<server-ip>:5080; subscribe to topics in the ntfy app using your server URL
-
- Decentralized file sync across devices — no cloud required; syncs directly between your server and other devices
- Deploy:
cd linux-server/syncthing && docker compose up -d
- Access at
http://<server-ip>:8384 - Add volume mounts to
docker-compose.ymlfor each folder you want to sync, then configure them in the web UI - Get your API key from Actions → Settings → API Key and add it to
linux-server/homepage/.envfor the Homepage widget
-
Network-wide DNS ad and tracker blocker — modern UI, DNS-over-HTTPS/TLS, and per-client rules
-
Prerequisites — Ubuntu's
systemd-resolvedbinds to port 53 and must be told to stop using it:sudo sed -i 's/#DNSStubListener=yes/DNSStubListener=no/' /etc/systemd/resolved.conf sudo systemctl restart systemd-resolved -
Deploy:
cd linux-server/adguard docker compose up -d -
Open
http://<server-ip>:3003for the first-run setup wizard- Set the web UI port to 80 (maps to host port 8083)
- Set the DNS port to 53
- Create your admin username and password
-
After setup, access the web UI at
http://<server-ip>:8083 -
Point your router's DNS (or individual devices) to
<server-ip>to start filtering -
Add credentials to
linux-server/homepage/.envto enable the stats widget on Homepage -
- Lightweight self-hosted git service — GitHub-like web UI, SSH push/pull, repo mirroring; LAN/Tailscale only, no public exposure
- Deploy:
cd linux-server/forgejo cp .env.example .env # set FORGEJO_DOMAIN (forgejo.<tailnet>.ts.net) and TS_AUTHKEY; optionally FORGEJO_DATA_PATH for external drive docker compose up -d
- Runs behind a Tailscale sidecar (its own tailnet device, HTTPS via
tailscale serve), so it's reachable only on the tailnet — no host port. Web UI athttps://forgejo.<tailnet>.ts.net/— complete the setup wizard on first visit, create admin account - Git over SSH on port
22(the sidecar's own tailnet device, no conflict with the host's sshd):git clone ssh://git@forgejo.<tailnet>.ts.net:22/<username>/<repo>.git
- Add your SSH public key in Settings → SSH / GPG Keys after creating your account
- To migrate from GitHub: use Forgejo's built-in migration (+ → New Migration → GitHub), then optionally configure a push mirror back to GitHub under repo Settings → Push Mirrors while validating the setup
-
- BitTorrent client with a full web UI — currently used for academic torrents. Fronted by its own Tailscale HTTPS sidecar (see
HTTPS.md) - Deploy:
cd linux-server/qbittorrent cp .env.example .env # set TS_AUTHKEY; optionally set QBITTORRENT_DOWNLOADS_PATH for an external drive docker compose up -d docker compose logs -f qbittorrent-ts # watch the node join + cert provision
- Access at
https://qbittorrent.<tailnet>.ts.net/ - On first run, the LinuxServer image generates a temporary admin password — find it with
docker compose logs qbittorrent | grep -i password, log in asadmin, then change it in Options → Web UI (and add the new login toHOMEPAGE_VAR_QBITTORRENT_PASSWORDinlinux-server/homepage/.envfor the widget) - In Options → Web UI, set the "IP address" to
127.0.0.1so the UI is reachable only through the HTTPS sidecar, not the raw tailnet port - BitTorrent
:6881(tcp/udp) is published on the sidecar. Not routed through a VPN — fine for academic/legal torrents; see../docs/TODO.mdfor the planned Gluetun VPN sidecar before any other use
- BitTorrent client with a full web UI — currently used for academic torrents. Fronted by its own Tailscale HTTPS sidecar (see
-
- Self-hosted browser speed test for the LAN (the local-network counterpart to speedtest-tracker's ISP test) — any device opens it in a browser and measures its throughput to the server. Fronted by its own Tailscale HTTPS sidecar (see
HTTPS.md), like the other services - Deploy:
cd linux-server/openspeedtest cp .env.example .env # set TS_AUTHKEY docker compose up -d
- Tailnet UI at
https://openspeedtest.<tailnet>.ts.net/. For an accurate LAN result, test against the LAN IP instead —http://<server-ip>:3030— or the result reflects the tailnet path rather than the local network - LAN ports
:3030/:3031are published on the sidecar (not the default:3000/:3001, which collide with homepage and uptime-kuma)
- Self-hosted browser speed test for the LAN (the local-network counterpart to speedtest-tracker's ISP test) — any device opens it in a browser and measures its throughput to the server. Fronted by its own Tailscale HTTPS sidecar (see
-
- Nightly encrypted, deduplicated backup of all persistent service state (SQLite DBs, Forgejo git repos, certs, configs,
.envs) to the 1TB drive, with an optional second copy to the 14TB drive. systemd timer + ntfy alerts + a homepage status card. Full setup and restore runbook inbackup/README.md - Deploy:
sudo apt install restic sqlite3 jq cd linux-server/backup cp .env.example .env # set RESTIC_PASSWORD (save it in Bitwarden!) + ntfy sudo touch /mnt/wd1tb/.backup-target-ok docker compose up -d # status-card server (loopback :8099) bash setup.sh --dry-run sudo bash setup.sh
- The status card on Homepage shows last-run time, status, and repo size; failures push to ntfy
- Nightly encrypted, deduplicated backup of all persistent service state (SQLite DBs, Forgejo git repos, certs, configs,
-
- Battery-backup monitoring for the CyberPower CST135UC2 over USB — ntfy alerts on power events, clean shutdown on low battery, auto-restart when wall power returns. Full runbook in
ups/README.md - Deploy:
sudo apt install nut # or rerun the root setup.sh --profile server cd linux-server/ups cp .env.example .env # set UPSMON_PASSWORD (openssl rand -hex 16) + ntfy sudo bash setup.sh upsc cyberpower ups.status # expect: OL docker compose up -d # PeaNUT dashboard + homepage widget
- Dashboard (charge/load/runtime graphs) at
https://peanut.<tailnet>.ts.net/(setTS_AUTHKEYinups/.envfor the sidecar); the homepage ups card reads it via thepeanutwidget on localhost - Set BIOS Restore on AC Power Loss → Power On so the server boots unattended after an outage
- Battery-backup monitoring for the CyberPower CST135UC2 over USB — ntfy alerts on power events, clean shutdown on low battery, auto-restart when wall power returns. Full runbook in
-
- Network-wide DNS ad blocker — alternative to AdGuard Home
- Not yet configured
-
- Self-hosted photo and video backup — Google Photos alternative with mobile apps, face recognition, and timeline view
- Not yet configured
-
- Self-hosted media server — stream your own movies, TV shows, and music to any device
- Not yet configured
-
Home Assistant | GitHub | Docs
- Open source smart home hub — integrates with thousands of devices and services
- Not yet configured