This is the setup guide for my personal website: https://knowyourdoc.org
Join, share, and help me grow. Testing the website and sharing best practices to improve its aesthetics, usability, and performance will be appreciated.
Ready?
Run a full web stack from an Android phone
self-hosting a website, file storage, newsletter, mail server, and fediverse instance — all from Android device running Termux.
Table of Contents
- Why this is possible
- Hardware requirements
- Required apps
- Software stack overview
- Folder structure
- Cloudflare Tunnel — public access without port forwarding
- Hugo — static website
- Copyparty — file storage
- PostgreSQL — database
- GoToSocial — fediverse instance
- Mox — mail server
- Listmonk — newsletter
- SSH access
- Automatic DNS update
- Auto-start on boot
- Watchdog — service monitor
- Backup
- Security checklist
- Publishing content with Hugo
- Quick diagnostics
1. Why this is possible
Modern mid-range Android phones have:
- 4–8 GB RAM
- 64–256 GB internal storage
- Always-on connectivity (mobile data / Wi-Fi)
- A powerful ARM64 CPU
With Termux (a full Linux environment on Android) and a Cloudflare Tunnel (bypasses NAT and port restrictions), you can run a complete web stack that is publicly accessible — without a static IP, without opening router ports, and without a VPS.
The total cost is: a domain (10€/year) and a Cloudflare account (free tier).
2. Hardware requirements
| Component | Minimum | Tested on |
|-----------|---------|-----------|
| RAM | 3 GB | 5.4 GB (Pocophone X3 NFC) |
| Internal storage | 32 GB free | 80 GB free |
| External SD | optional but recommended | 230 GB |
| OS | Android 10+ | LineageOS 22.2 / Android 15 |
| Connectivity | Mobile data or Wi-Fi | — |
The phone acts as a 24/7 server. Keep it plugged in or manage battery carefully.
3. Required apps
Install from F-Droid (not Google Play — Play Store versions of Termux are outdated):
| App | Purpose | Source |
|-----|---------|--------|
| Termux | Linux shell environment | F-Droid |
| Termux:Boot | Auto-start scripts on device reboot | F-Droid |
| Termux:API | Android API access from shell | F-Droid |
Install F-Droid first: https://f-droid.org
4. Software stack overview
| Software | Role | Port |
|----------|------|------|
| cloudflared | Cloudflare Tunnel — public HTTPS access | — |
| Hugo | Static website server | your choice |
| Copyparty | File storage / cloud drive | your choice |
| GoToSocial | Fediverse (ActivityPub) instance | your choice |
| Mox | Self-hosted mail server | your choice |
| Listmonk | Newsletter manager | your choice |
| PostgreSQL | Database (for GoToSocial + Listmonk) | your choice |
| sshd | SSH access | your choice |
All services bind to 127.0.0.1 (localhost). Public access goes through the Cloudflare Tunnel only — the phone never needs an open port.
Install base packages
pkg update && pkg upgrade
pkg install git curl wget python openssh nano termux-api
5. Folder structure
~/
├── .cloudflared/ # Cloudflare Tunnel config (keep private)
├── .cf-credentials # Cloudflare API token + Zone/Record IDs (chmod 600)
├── .ssh/ # SSH keys
├── drive/ # Shared folders via Copyparty
│ └── [your-folders]/
├── config/
│ └── copyparty.conf # Copyparty configuration
├── gestione/
│ ├── start.sh # Start all services
│ ├── stop.sh # Stop all services
│ ├── watchdog.sh # Service monitor (run via cron)
│ ├── logrotate.sh # Weekly log rotation
│ └── backup-dkim/ # DKIM key backup (copy to external storage)
├── my-site/ # Hugo site
│ ├── hugo.toml
│ ├── content/
│ ├── layouts/
│ ├── static/
│ ├── themes/
│ └── public/ # Hugo build output
├── logs/
│ ├── copyparty.log
│ ├── cloudflared.log
│ ├── hugo/hugo.log
│ ├── mox.log
│ ├── gotosocial.log
│ ├── dns-update.log
│ └── watchdog.lock
├── mox # Mox binary (compiled from source)
├── mox-data/
│ ├── config/
│ │ ├── mox.conf
│ │ └── domains.conf
│ └── data/
│ └── dkim/ # DKIM keys (chmod 600 — never share)
├── gotosocial/
│ ├── gotosocial-cgo
│ └── config.yaml
├── listmonk-app/
│ └── listmonk
├── update-dns.sh # Dynamic DNS updater for mail record
└── storage -> /storage/emulated/0
Create the log directory:
mkdir -p ~/logs/hugo
6. Cloudflare Tunnel — public access without port forwarding
A Cloudflare Tunnel creates an encrypted outbound connection from your phone to Cloudflare's edge. Visitors hit Cloudflare's servers, which forward traffic to your phone via the tunnel. No open ports, no static IP needed.
Prerequisites
- A domain registered with or transferred to Cloudflare (or any registrar with Cloudflare DNS)
- A free Cloudflare account
cloudflared installed on your phone
Install cloudflared
# Download the latest ARM64 binary from Cloudflare's GitHub releases
# https://github.com/cloudflare/cloudflared/releases
# Look for: cloudflared-linux-arm64
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64 \
-O $PREFIX/bin/cloudflared
chmod +x $PREFIX/bin/cloudflared
Authenticate and create a tunnel
# Authenticate with your Cloudflare account (opens a browser link)
cloudflared tunnel login
# Create the tunnel
cloudflared tunnel create my-tunnel
# This outputs a Tunnel ID — save it
# Credentials JSON is saved to ~/.cloudflared/
Configure the tunnel
Create ~/.cloudflared/config.yml:
tunnel: YOUR_TUNNEL_ID
credentials-file: /data/data/com.termux/files/home/.cloudflared/YOUR_TUNNEL_ID.json
ingress:
- hostname: files.yourdomain.com
service: http://localhost:COPYPARTY_PORT
- hostname: newsletter.yourdomain.com
service: http://localhost:LISTMONK_PORT
- hostname: www.yourdomain.com
service: http://localhost:HUGO_PORT
- hostname: social.yourdomain.com
service: http://localhost:GTS_PORT
- hostname: ssh.yourdomain.com
service: ssh://localhost:SSH_PORT
- service: http_status:404
Remove any hostname entries for services you are not running.
Create DNS records
# Create a CNAME for each hostname pointing to the tunnel
cloudflared tunnel route dns my-tunnel www.yourdomain.com
cloudflared tunnel route dns my-tunnel files.yourdomain.com
cloudflared tunnel route dns my-tunnel newsletter.yourdomain.com
cloudflared tunnel route dns my-tunnel social.yourdomain.com
cloudflared tunnel route dns my-tunnel ssh.yourdomain.com
Start the tunnel
nohup cloudflared tunnel run my-tunnel > ~/logs/cloudflared.log 2>&1 &
The tunnel name in the run command can be the tunnel name or ID.
7. Hugo — static website
Hugo is a fast static site generator. It runs a local server; Cloudflare Tunnel makes it public.
Install
# Download the ARM64 extended binary from Hugo releases
# https://github.com/gohugoio/hugo/releases
# Look for: hugo_extended_X.X.X_linux-arm64.tar.gz
# Extract and move to $PREFIX/bin/
tar -xzf hugo_extended_*_linux-arm64.tar.gz
mv hugo $PREFIX/bin/
chmod +x $PREFIX/bin/hugo
hugo version
Create a new site
hugo new site ~/my-site
cd ~/my-site
# Install a theme (example: terminal theme)
git init
git submodule add https://github.com/panr/hugo-theme-terminal themes/terminal
Configure hugo.toml
baseURL = "https://www.yourdomain.com/"
languageCode = "en-us"
title = "My Site"
theme = "terminal"
minify = true
[params]
contentTypeName = "posts"
Start the server
pkill -f "hugo server"
cd ~/my-site && nohup hugo server \
--bind 127.0.0.1 \
--baseURL "https://www.yourdomain.com/" \
--appendPort=false \
> ~/logs/hugo/hugo.log 2>&1 &
--bind 127.0.0.1 is important: Hugo must only be reachable via the Cloudflare Tunnel, not directly from the network.
Build static output (optional)
cd ~/my-site && hugo
8. Copyparty — file storage
Copyparty turns a folder into a web-accessible file manager with upload, streaming, and search.
Install
pip install copyparty
Configure
Create ~/config/copyparty.conf:
[global]
# Optional: require password for write access
[/]
# Public read-only root
/home/YOUR_USERNAME/drive r *
[/private]
# Password-protected folder
/home/YOUR_USERNAME/drive/private rw myuser mypassword
Replace YOUR_USERNAME with your Termux username. Find it with:
echo $HOME
# Output: /data/data/com.termux/files/home
# Or use: whoami
Start
nohup copyparty -c ~/config/copyparty.conf > ~/logs/copyparty.log 2>&1 &
9. PostgreSQL — database
Required by GoToSocial and Listmonk.
Install
pkg install postgresql
Initialize and start
initdb $PREFIX/var/lib/postgresql
pg_ctl -D $PREFIX/var/lib/postgresql start
Optimized config for Android
Edit $PREFIX/var/lib/postgresql/postgresql.conf and add/override these values. The defaults are tuned for desktop machines; these are safer for a phone:
shared_buffers = 128MB
effective_cache_size = 512MB
work_mem = 8MB
maintenance_work_mem = 32MB
max_connections = 50
checkpoint_completion_target = 0.9
wal_buffers = 8MB
min_wal_size = 80MB
max_wal_size = 256MB
synchronous_commit = off
shared_buffers = 128MB: do not use the standard "15% of RAM" formula — it is too high on Android.
synchronous_commit = off: improves performance; risk is losing at most 1 transaction on a crash.
Create databases
createdb gotosocial
createdb listmonk
Commands
pg_ctl -D $PREFIX/var/lib/postgresql start
pg_ctl -D $PREFIX/var/lib/postgresql stop
pg_ctl -D $PREFIX/var/lib/postgresql status
10. GoToSocial — fediverse instance
GoToSocial is a lightweight ActivityPub server (Mastodon-compatible). It lets you run your own fediverse account at @you@yourdomain.com.
Install
Download the ARM64 binary from the releases page:
https://github.com/superseriousbusiness/gotosocial/releases
Look for: gotosocial_X.X.X_linux_armv8.tar.gz
mkdir ~/gotosocial
tar -xzf gotosocial_*_linux_armv8.tar.gz -C ~/gotosocial
Configure
Edit ~/gotosocial/config.yaml — key settings:
host: "social.yourdomain.com"
db-type: "postgres"
db-address: "127.0.0.1"
db-port: POSTGRES_PORT
db-user: "YOUR_POSTGRES_USER"
db-password: "YOUR_POSTGRES_PASSWORD"
db-database: "gotosocial"
port: GTS_PORT
bind-address: "127.0.0.1"
# Memory optimization for Android
advanced-sender-multiplier: 1
db-max-open-conns-multiplier: 4
Start
cd ~/gotosocial && GODEBUG=netdns=go GOMEMLIMIT=384MiB \
nohup ./gotosocial-cgo \
--config-path config.yaml \
--web-template-base-dir ~/gotosocial/web/template/ \
--web-asset-base-dir ~/gotosocial/web/assets/ \
server start \
>> ~/logs/gotosocial.log 2>&1 &
GOMEMLIMIT=384MiB is required. Without it, GoToSocial crashes every 20 minutes on Android because it tries to read cgroup memory limits which don't exist (automemlimit). This flag disables that behavior.
GODEBUG=netdns=go forces Go's built-in DNS resolver instead of the system one, which can hang on Android.
Create your admin account
cd ~/gotosocial
./gotosocial-cgo --config-path config.yaml admin account create \
--username yourusername \
--email you@yourdomain.com \
--password "your-strong-password"
./gotosocial-cgo --config-path config.yaml admin account promote \
--username yourusername
11. Mox — mail server
Mox is a modern, all-in-one mail server. Running a mail server on a mobile IP has limitations (see risks below), but it works for low-volume transactional email.
Important: Mox must be compiled from source with Termux-specific patches. Pre-built binaries will not work on Android/Termux.
Build from source
pkg install golang
git clone https://github.com/mjl-/mox ~/mox-src
cd ~/mox-src
# Apply Termux compatibility patches:
# 1. Replace setgid/setuid calls (not available without root)
# 2. Remove chroot calls
# 3. Fix file descriptor limits
# (Search the mox issue tracker or Termux community for current patches)
go build -o ~/mox .
Initialize
mkdir ~/mox-data
cd ~/mox-data
../mox quickstart newsletter@yourdomain.com
This generates mox-data/config/mox.conf and domains.conf. Review and adjust.
Key config values (mox.conf)
⚠️ Mox config files use TAB indentation. Always use printf to edit them, never a text editor or heredoc. Verify with cat -A.
DataDir: data
User: YOUR_TERMUX_USERNAME
LogLevel: info
Hostname: mail.yourdomain.com
Listeners:
public:
IPs:
- 0.0.0.0
Submission:
Enabled: true
Port: SMTP_SUBMISSION_PORT
NoRequireSTARTTLS: true
Postmaster:
Account: newsletter
Mailbox: Postmaster
NoRequireSTARTTLS: true — acceptable because the local submission port never accepts external connections; all mail is submitted locally by Listmonk.
Start
cd ~/mox-data
nohup ../mox serve >> ~/logs/mox.log 2>&1 &
cd ~
DNS records required
| Record | Type | Value |
|--------|------|-------|
| mail.yourdomain.com | A | your current public IP |
| yourdomain.com | MX | mail.yourdomain.com |
| yourdomain.com | TXT | SPF: v=spf1 a:mail.yourdomain.com ~all |
| _dmarc.yourdomain.com | TXT | v=DMARC1; p=none; rua=mailto:postmaster@yourdomain.com |
| DKIM selectors | TXT | Generated by mox quickstart — copy from mox-data/data/dkim/ |
Email authentication status
| Requirement | Achievable on mobile | Notes |
|-------------|---------------------|-------|
| SPF | ✅ | Update via dynamic DNS script |
| DMARC | ✅ | Set to p=none initially |
| DKIM Ed25519 | ✅ | Generated by mox |
| DKIM RSA | ✅ | Generated by mox |
| PTR record | ❌ | Not configurable on mobile IPs |
| Domain reputation | ⚠️ | Takes time to build |
Known limitation: PTR record
Mobile IPs don't have PTR (reverse DNS) records, which Gmail uses for spam filtering. Gmail responds 550 5.7.1 to your emails.
Solutions:
- Use a transactional email relay (e.g. Brevo — 300 emails/day free) as a smart relay
- Rent a cheap VPS (4€/month, Hetzner) just as an SMTP relay with a static IP
Verify DKIM
cd ~/mox-data
../mox dkim lookup ed25519-1 yourdomain.com
../mox dkim lookup rsa-1 yourdomain.com
Manage suppression list
cd ~/mox-data
../mox queue suppress list -account newsletter
../mox queue suppress remove newsletter address@example.com
12. Listmonk — newsletter
Listmonk is a self-hosted newsletter and mailing list manager.
Install
Download the ARM64 binary from:
https://github.com/knadh/listmonk/releases
mkdir ~/listmonk-app
# Extract binary and config template to ~/listmonk-app/
Initialize database
cd ~/listmonk-app
./listmonk --install
Configure SMTP (connect to Mox)
In Listmonk's Settings → SMTP:
| Field | Value |
|-------|-------|
| Host | 127.0.0.1 |
| Port | SMTP_LOCAL_PORT |
| Auth Protocol | Plain |
| Username | newsletter@yourdomain.com |
| Password | (your mox account password) |
| TLS | Off |
| HELO hostname | mail.yourdomain.com |
In Settings → General, From Email should be just newsletter@yourdomain.com with no display name.
Start
# PostgreSQL must be running first
pg_ctl -D $PREFIX/var/lib/postgresql start
cd ~/listmonk-app && nohup ./listmonk >> ~/logs/listmonk.log 2>&1 &
Subscription form
Add this to any Hugo page:
<form method="post" action="https://newsletter.yourdomain.com/subscription/form?page=done">
<input type="hidden" name="nonce" />
<input type="hidden" name="redirect" value="https://www.yourdomain.com/welcome/" />
<p><input type="email" name="email" required placeholder="email" /></p>
<p><input type="text" name="name" placeholder="name (optional)" /></p>
<input id="LIST_ID_SHORT" type="checkbox" name="l" checked
value="YOUR_LIST_UUID" style="display:none" />
<input type="submit" value="subscribe" />
</form>
Replace YOUR_LIST_UUID with the UUID from Listmonk → Lists.
Redirect after subscription (Settings → Appearance → Custom JS)
if (new URLSearchParams(window.location.search).get('page') === 'done') {
window.location.href = 'https://www.yourdomain.com/welcome/';
}
⚠️ Known issue: custom JS may not apply on mobile browsers (Brave). Test on Firefox mobile.
13. SSH access
Termux's SSH runs on a non-standard port (not 22 — requires no root). The default is configurable in $PREFIX/etc/ssh/sshd_config via the Port directive.
Enable SSH
pkg install openssh
# Set a password
passwd
# Start SSH
sshd
Connect from local network
# Find the phone's local IP first:
ifconfig 2>/dev/null | grep inet
ssh -p SSH_PORT $(whoami)@PHONE_LOCAL_IP
The local IP changes when you switch networks or hotspots. Check it each time.
Connect from anywhere (via Cloudflare Tunnel)
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh.yourdomain.com" \
$(whoami)@ssh.yourdomain.com
Laptop SSH config (~/.ssh/config on your laptop)
Host myphone
HostName PHONE_LOCAL_IP
Port SSH_PORT
User YOUR_TERMUX_USERNAME
Host myphone-tunnel
HostName ssh.yourdomain.com
User YOUR_TERMUX_USERNAME
ProxyCommand cloudflared access ssh --hostname %h
Security: disable password authentication (recommended)
Generate a key pair on your laptop and copy the public key:
# On laptop:
ssh-keygen -t ed25519
# Copy public key to phone (while password auth is still on):
ssh-copy-id -p SSH_PORT YOUR_USERNAME@PHONE_IP
# Then on the phone, edit Termux sshd config:
# $PREFIX/etc/ssh/sshd_config
# Set: PasswordAuthentication no
# Restart sshd
14. Automatic DNS update
Your phone's public IP changes when you switch networks. For the mail server's A record (required for SPF), you need to keep DNS updated automatically.
Create ~/update-dns.sh
#!/data/data/com.termux/files/usr/bin/bash
# Dynamic DNS updater for Cloudflare
# Updates an A record and SPF TXT record with the current public IP
source ~/.cf-credentials
# Expected variables: CF_TOKEN, ZONE_ID, RECORD_ID_A, RECORD_ID_SPF, MAIL_HOSTNAME
CURRENT_IP=$(curl -s ifconfig.me)
STORED_IP=$(cat ~/.last-public-ip 2>/dev/null)
if [ "$CURRENT_IP" = "$STORED_IP" ]; then
echo "$(date) IP unchanged ($CURRENT_IP)" >> ~/logs/dns-update.log
exit 0
fi
echo "$(date) Updating DNS: $STORED_IP -> $CURRENT_IP" >> ~/logs/dns-update.log
# Update A record
curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID_A" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"$MAIL_HOSTNAME\",\"content\":\"$CURRENT_IP\",\"ttl\":60,\"proxied\":false}" \
>> ~/logs/dns-update.log
echo "$CURRENT_IP" > ~/.last-public-ip
chmod +x ~/update-dns.sh
Create ~/.cf-credentials (chmod 600)
# ~/.cf-credentials
CF_TOKEN="your-cloudflare-api-token"
ZONE_ID="your-zone-id"
RECORD_ID_A="dns-record-id-for-mail-A-record"
RECORD_ID_SPF="dns-record-id-for-spf-txt-record"
MAIL_HOSTNAME="mail.yourdomain.com"
chmod 600 ~/.cf-credentials
To find your Zone ID and Record IDs: Cloudflare Dashboard → your domain → Overview (Zone ID in the sidebar) and use the Cloudflare API to list DNS records.
15. Auto-start on boot
Install Termux:Boot
Install from F-Droid. Open the app once to register it as a boot receiver.
Create ~/.termux/boot/start.sh
mkdir -p ~/.termux/boot
#!/data/data/com.termux/files/usr/bin/bash
sleep 10
termux-wake-lock
crond
~/gestione/start.sh
chmod +x ~/.termux/boot/start.sh
sleep 10 — gives Android time to finish booting before starting services.
termux-wake-lock — prevents Android from killing Termux when the screen turns off.
crond — starts the cron daemon (needed for watchdog and log rotation).
Create ~/gestione/start.sh
#!/data/data/com.termux/files/usr/bin/bash
# Update DNS first (in case IP changed while offline)
~/update-dns.sh
# Start SSH (usually managed by runit, but ensure it's running)
sshd
# Start PostgreSQL
pg_ctl -D $PREFIX/var/lib/postgresql start
sleep 3
# Start services
nohup cloudflared tunnel run my-tunnel > ~/logs/cloudflared.log 2>&1 &
sleep 2
cd ~/my-site && nohup hugo server \
--bind 127.0.0.1 \
--baseURL "https://www.yourdomain.com/" \
--appendPort=false \
> ~/logs/hugo/hugo.log 2>&1 &
nohup copyparty -c ~/config/copyparty.conf > ~/logs/copyparty.log 2>&1 &
cd ~/mox-data && nohup ../mox serve >> ~/logs/mox.log 2>&1 & && cd ~
cd ~/listmonk-app && nohup ./listmonk >> ~/logs/listmonk.log 2>&1 &
cd ~/gotosocial && GODEBUG=netdns=go GOMEMLIMIT=384MiB \
nohup ./gotosocial-cgo \
--config-path config.yaml \
--web-template-base-dir ~/gotosocial/web/template/ \
--web-asset-base-dir ~/gotosocial/web/assets/ \
server start \
>> ~/logs/gotosocial.log 2>&1 &
echo "$(date) All services started" >> ~/logs/start.log
chmod +x ~/gestione/start.sh
16. Watchdog — service monitor
The watchdog script checks every 5 minutes whether services are still running and restarts them if they've crashed.
Create ~/gestione/watchdog.sh
Key design decisions:
- Lockfile — prevents parallel watchdog instances (which caused cascade crashes of PostgreSQL)
- psql timeout — avoids infinite hang if GoToSocial saturates DB connections
- Stale PID cleanup — removes PostgreSQL's
postmaster.pid if the process is gone
- Network-aware — skips network-dependent services when offline, but always checks sshd
#!/data/data/com.termux/files/usr/bin/bash
LOCKFILE=~/logs/watchdog.lock
LOGFILE=~/logs/watchdog.log
# Prevent parallel execution
if [ -f "$LOCKFILE" ]; then
PID=$(cat "$LOCKFILE")
if kill -0 "$PID" 2>/dev/null; then
exit 0
fi
fi
echo $$ > "$LOCKFILE"
trap "rm -f $LOCKFILE" EXIT
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOGFILE"; }
# Check network
ONLINE=true
curl -s --max-time 5 https://cloudflare.com > /dev/null 2>&1 || ONLINE=false
# Always check SSH
if ! pgrep -x sshd > /dev/null; then
log "sshd down — restarting"
sshd
fi
if [ "$ONLINE" = false ]; then
log "Network down — skipping network services"
exit 0
fi
# PostgreSQL — clean stale PID if needed
PGDATA="$PREFIX/var/lib/postgresql"
PIDFILE="$PGDATA/postmaster.pid"
if [ -f "$PIDFILE" ]; then
PGPID=$(head -1 "$PIDFILE")
if ! kill -0 "$PGPID" 2>/dev/null; then
log "Stale PostgreSQL PID — removing"
rm -f "$PIDFILE"
fi
fi
if ! psql -U "$(whoami)" --connect-timeout=5 -c "SELECT 1" postgres > /dev/null 2>&1; then
log "PostgreSQL down — restarting"
pg_ctl -D "$PGDATA" start
sleep 3
fi
# Cloudflare Tunnel
if ! pgrep -f "cloudflared tunnel" > /dev/null; then
log "cloudflared down — restarting"
nohup cloudflared tunnel run my-tunnel >> ~/logs/cloudflared.log 2>&1 &
fi
# Hugo
if ! pgrep -f "hugo server" > /dev/null; then
log "Hugo down — restarting"
cd ~/my-site && nohup hugo server \
--bind 127.0.0.1 \
--baseURL "https://www.yourdomain.com/" \
--appendPort=false \
>> ~/logs/hugo/hugo.log 2>&1 &
fi
# Copyparty
if ! pgrep -f "copyparty" > /dev/null; then
log "Copyparty down — restarting"
nohup copyparty -c ~/config/copyparty.conf >> ~/logs/copyparty.log 2>&1 &
fi
# GoToSocial
if ! pgrep -f "gotosocial" > /dev/null; then
log "GoToSocial down — restarting"
cd ~/gotosocial && GODEBUG=netdns=go GOMEMLIMIT=384MiB \
nohup ./gotosocial-cgo \
--config-path config.yaml \
--web-template-base-dir ~/gotosocial/web/template/ \
--web-asset-base-dir ~/gotosocial/web/assets/ \
server start \
>> ~/logs/gotosocial.log 2>&1 &
fi
# Mox
if ! pgrep -f "mox serve" > /dev/null; then
log "Mox down — restarting"
cd ~/mox-data && nohup ../mox serve >> ~/logs/mox.log 2>&1 &
cd ~
fi
# Listmonk
if ! pgrep -f "listmonk" > /dev/null; then
log "Listmonk down — restarting"
cd ~/listmonk-app && nohup ./listmonk >> ~/logs/listmonk.log 2>&1 &
fi
chmod +x ~/gestione/watchdog.sh
Create ~/gestione/logrotate.sh
#!/data/data/com.termux/files/usr/bin/bash
for log in ~/logs/*.log ~/logs/hugo/*.log; do
[ -f "$log" ] && mv "$log" "${log}.$(date +%Y%m%d)" && gzip "${log}.$(date +%Y%m%d)" 2>/dev/null
done
find ~/logs -name "*.gz" -mtime +30 -delete
chmod +x ~/gestione/logrotate.sh
Set up cron
crontab -e
Add:
*/5 * * * * ~/gestione/watchdog.sh
0 3 * * 0 ~/gestione/logrotate.sh
17. Backup
Full Termux backup script
Create ~/gestione/backup-completo.sh:
#!/data/data/com.termux/files/usr/bin/bash
DATE=$(date +%Y%m%d_%H%M)
DEST_INT=~/storage/backup
DEST_EXT=~/storage/external-1/backup
mkdir -p "$DEST_INT" "$DEST_EXT"
BACKUP_FILE="termux_backup_${DATE}.tar.gz"
tar -czf "$DEST_INT/$BACKUP_FILE" \
--exclude="$HOME/.cache" \
--exclude="$HOME/storage/backup" \
--exclude="$HOME/storage/external-1" \
--exclude="$HOME/my-site/public" \
--exclude="$HOME/my-site/resources" \
-C /data/data/com.termux/files home \
2>&1 | grep -v "file changed as we read it"
# Copy to SD card if available
cp "$DEST_INT/$BACKUP_FILE" "$DEST_EXT/" 2>/dev/null
# Keep only last 7 backups
ls -t "$DEST_INT"/termux_backup_*.tar.gz | tail -n +8 | xargs rm -f
ls -t "$DEST_EXT"/termux_backup_*.tar.gz | tail -n +8 | xargs rm -f
echo "$(date) Backup complete: $BACKUP_FILE"
chmod +x ~/gestione/backup-completo.sh
bash ~/gestione/backup-completo.sh
Exit code 1 with file changed as we read it is a warning, not an error. The backup is valid.
Backup duration: 30–40 minutes for a full system.
Pull backup from laptop via rsync
Run this on your laptop, not the phone:
rsync -avz --progress \
-e "ssh -p SSH_PORT" \
YOUR_USERNAME@PHONE_IP:~/storage/backup/ \
~/backups/phone/
Critical files to back up separately
These must be backed up to external storage (USB drive, encrypted cloud, etc.):
~/mox-data/data/dkim/ — DKIM private keys. If lost, you must regenerate keys and update all DNS records.
~/.cf-credentials — Cloudflare API credentials
~/.cloudflared/ — Tunnel credentials
18. Security checklist
Completed at setup
- [ ]
~/.cf-credentials has chmod 600
- [ ] Cloudflare API token has minimal permissions (DNS edit only for your zone)
- [ ]
~/mox-data/data/dkim/ has restricted permissions
- [ ] DKIM keys backed up to external storage
- [ ] Phone locked with strong PIN / biometrics
- [ ] All services bind to
127.0.0.1, not 0.0.0.0 (except sshd)
Recommended hardening
- [ ] Disable SSH password authentication (use keys only) — edit
$PREFIX/etc/ssh/sshd_config
- [ ] Audit Copyparty: verify which folders are publicly readable vs. password-protected
- [ ] Regular updates:
pkg upgrade and pip install --upgrade copyparty
If credentials are compromised
- Cloudflare API token: Cloudflare Dashboard → Profile → API Tokens → Revoke → Create new
- DKIM keys: Regenerate with mox, update DNS records, re-verify
19. Publishing content with Hugo
Content structure
Use page bundles: a folder per article with index.md and media files inside.
content/
├── _index.md ← site homepage
├── section-name/
│ ├── _index.md ← section page
│ └── article-slug/
│ ├── index.md ← article
│ ├── banner.jpg ← required to appear in list view
│ └── photo.jpg
Every folder needs an index.md, otherwise Hugo ignores it.
Article frontmatter (TOML format)
+++
title = "Article Title"
date = 2026-01-15
draft = false
cover = "banner.jpg"
description = "One-line preview shown in the list."
tags = ["tag1", "tag2"]
showDate = true
+++
Article content here...
Hugo is case-sensitive: showDate works, showdate does not.
Useful frontmatter parameters
| Parameter | Values | Effect |
|-----------|--------|--------|
| draft | true / false | true = not published |
| cover | "banner.jpg" | cover image in list (file must exist in folder) |
| description | text | list preview text |
| tags | ["tag1", "tag2"] | article tags |
| showDate | true / false | show/hide date |
| showFullContent | true / false | show full text in list view |
Images
Banner:
- Filename: exactly
banner.jpg
- Recommended size: 1200×480px (5:2 ratio)
- Keep under 300 KB
Images in article body — always use absolute paths from site root:

With the Terminal theme (and most Hugo themes), relative paths like photo.jpg do not work. Always use the full path from the site root.
Video
<video controls width="100%">
<source src="/section/article-slug/video.mp4" type="video/mp4">
Your browser does not support video.
</video>
Publication checklist
- [ ] Folder created (lowercase, no spaces)
- [ ]
index.md with complete frontmatter
- [ ]
draft = false
- [ ]
banner.jpg present
- [ ] Images use absolute paths
/section/slug/file.jpg
- [ ] Restart Hugo if images don't appear
- [ ] Verify on live URL
Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
| Article doesn't appear | draft = true | Change to draft = false |
| Banner missing | File not named banner.jpg | Rename file |
| Images missing | Relative path | Use /section/slug/image.jpg |
| Site not updated | Hugo cache | Restart Hugo |
| Email not delivered to Gmail | No PTR record on mobile IP | Use SMTP relay (Brevo) |
20. Quick diagnostics
# Check running services
ps aux | grep -E "cloudflared|hugo|copyparty|sshd|mox|listmonk|gotosocial|postgres"
# Live logs
tail -f ~/logs/hugo/hugo.log
tail -f ~/logs/mox.log
tail -f ~/logs/cloudflared.log
tail -f ~/logs/copyparty.log
tail -f ~/logs/gotosocial.log
tail -f ~/logs/dns-update.log
tail -f ~/logs/watchdog.log
# Current public IP
curl -s ifconfig.me && echo
# Current local IP
ifconfig 2>/dev/null | grep inet
# Test SMTP port (Mox)
nc -zv 127.0.0.1 SMTP_LOCAL_PORT && echo 'Mox SMTP OK'
# Verify DKIM
cd ~/mox-data && ../mox dkim lookup ed25519-1 yourdomain.com
# Disk space
df -h | grep -E "data|storage"
# PostgreSQL status
pg_ctl -D $PREFIX/var/lib/postgresql status
# Cloudflare Tunnel status
cloudflared tunnel info my-tunnel
Appendix: Software versions tested
| Software | Version tested | Notes |
|----------|---------------|-------|
| cloudflared | 2025-10-19 | ARM64 binary |
| Hugo | v0.156.0+extended | android/arm64 |
| Python | 3.12.x | via pkg install python |
| Copyparty | latest via pip | |
| Mox | v0.0.15+ | compiled from source with Termux patches |
| Listmonk | v6.0.0 | |
| GoToSocial | latest ARM64 | -cgo variant required |
| PostgreSQL | 18.x | via pkg install postgresql |
This stack runs entirely on an Android phone. It is radical, fragile in some ways, and completely yours.
No platform can ban your instance. The hardware fits in your pocket.*
**the result https://knowyourdoc.org **