Your phone is already a server. It just doesn’t know it yet.

A modern mid-range Android phone has 4–8 GB RAM, 64–256 GB storage, an ARM64 CPU, and always-on connectivity. 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, without a VPS.

The total cost: a domain (~10€/year) and a Cloudflare account (free tier).


The stack#

SoftwareRole
cloudflaredCloudflare Tunnel — public HTTPS access
HugoStatic website server
CopypartyFile storage / cloud drive
GoToSocialFediverse (ActivityPub) instance
MoxSelf-hosted mail server
ListmonkNewsletter manager
PostgreSQLDatabase
sshdSSH access

All services bind to 127.0.0.1. The phone never needs an open port — public access goes exclusively through the Cloudflare Tunnel.


1. Hardware requirements#

ComponentMinimumNotes
RAM3 GB5+ GB recommended
Internal storage32 GB freemore is better
External SDoptionaluseful for backups
OSAndroid 10+LineageOS works well

Keep the phone plugged in. It acts as a 24/7 server.


2. Required apps#

Install from F-Droid — not Google Play, which ships outdated Termux builds:

  • Termux — Linux shell environment
  • Termux:Boot — auto-start scripts on reboot
  • Termux:API — Android API access from shell

Install F-Droid first: https://f-droid.org

Then install base packages:

pkg update && pkg upgrade
pkg install git curl wget python openssh nano termux-api

3. 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. No open ports, no static IP needed.

Install cloudflared#

# Download the ARM64 binary from:
# 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

Create a tunnel#

cloudflared tunnel login
cloudflared tunnel create my-tunnel

Configure ~/.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

Create DNS records#

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#

nohup cloudflared tunnel run my-tunnel > ~/logs/cloudflared.log 2>&1 &

4. Hugo — static website#

# https://github.com/gohugoio/hugo/releases
# Look for: hugo_extended_X.X.X_linux-arm64.tar.gz
tar -xzf hugo_extended_*_linux-arm64.tar.gz
mv hugo $PREFIX/bin/ && chmod +x $PREFIX/bin/hugo

hugo new site ~/my-site
cd ~/my-site
git init
git submodule add https://github.com/panr/hugo-theme-terminal themes/terminal

hugo.toml:

baseURL = "https://www.yourdomain.com/"
languageCode = "en-us"
title = "My Site"
theme = "terminal"
minify = true

Start the 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 critical — Hugo must only be reachable through the tunnel, not directly.


5. Copyparty — file storage#

Copyparty turns a folder into a web-accessible file manager with upload, streaming, and search.

pip install copyparty

Create ~/config/copyparty.conf:

[/]
/home/YOUR_USERNAME/drive  r  *

[/private]
/home/YOUR_USERNAME/drive/private  rw  myuser  mypassword
nohup copyparty -c ~/config/copyparty.conf > ~/logs/copyparty.log 2>&1 &

6. PostgreSQL — database#

Required by GoToSocial and Listmonk.

pkg install postgresql
initdb $PREFIX/var/lib/postgresql
pg_ctl -D $PREFIX/var/lib/postgresql start

Optimized config for Android#

Edit $PREFIX/var/lib/postgresql/postgresql.conf. The defaults are tuned for desktops — 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

Do not use the standard “15% of RAM” formula for shared_buffers — it’s too high on Android. synchronous_commit = off improves performance; worst case is losing 1 transaction on a crash.

createdb gotosocial
createdb listmonk

7. GoToSocial — fediverse instance#

GoToSocial is a lightweight ActivityPub server (Mastodon-compatible). It lets you run your own fediverse account at @[email protected].

# 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

Key settings in ~/gotosocial/config.yaml:

host: "social.yourdomain.com"
db-type: "postgres"
db-address: "127.0.0.1"
db-port: POSTGRES_PORT
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 because it tries to read cgroup memory limits that don’t exist on Android (automemlimit behavior). This flag disables it. GODEBUG=netdns=go forces Go’s built-in DNS resolver — the system one can hang on Android.

Create your admin account:

cd ~/gotosocial
./gotosocial-cgo --config-path config.yaml admin account create \
  --username yourusername \
  --email [email protected] \
  --password "your-strong-password"

./gotosocial-cgo --config-path config.yaml admin account promote \
  --username yourusername

8. Mox — mail server#

Mox is a modern all-in-one mail server. It works for low-volume transactional email, with one hard limitation: mobile IPs have no PTR record, so Gmail blocks direct delivery. Use a free SMTP relay (Brevo — 300 emails/day) as a workaround.

Important: Mox must be compiled from source with Termux-specific patches. Pre-built binaries won’t work on Android.

pkg install golang
git clone https://github.com/mjl-/mox ~/mox-src
cd ~/mox-src
# Apply Termux patches (setgid/setuid, chroot, 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 [email protected]

⚠️ Mox config files use TAB indentation. Always edit with printf, never with a text editor or heredoc. Verify with cat -A.

DNS records needed:

RecordTypeValue
mail.yourdomain.comAyour current public IP
yourdomain.comMXmail.yourdomain.com
yourdomain.comTXTv=spf1 a:mail.yourdomain.com ~all
_dmarc.yourdomain.comTXTv=DMARC1; p=none; rua=mailto:[email protected]
DKIM selectorsTXTgenerated by mox — copy from mox-data/data/dkim/

9. Listmonk — newsletter#

# https://github.com/knadh/listmonk/releases
mkdir ~/listmonk-app
# extract binary to ~/listmonk-app/
cd ~/listmonk-app && ./listmonk --install

SMTP settings (connect to Mox locally):

FieldValue
Host127.0.0.1
PortSMTP_LOCAL_PORT
Auth ProtocolPlain
Username[email protected]
TLSOff
HELO hostnamemail.yourdomain.com

Subscription form for Hugo:

<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 type="checkbox" name="l" checked value="YOUR_LIST_UUID" style="display:none" />
  <input type="submit" value="subscribe" />
</form>

10. Auto-start, watchdog, and backup#

Auto-start on boot#

Install Termux:Boot from F-Droid and open it once to register the boot receiver.

Create ~/.termux/boot/start.sh:

#!/data/data/com.termux/files/usr/bin/bash
sleep 10
termux-wake-lock
crond
~/gestione/start.sh

termux-wake-lock prevents Android from killing Termux when the screen turns off.

Watchdog (cron every 5 minutes)#

A watchdog script checks whether services are running and restarts them. Key design decisions that matter:

  • Lockfile — prevents parallel instances, which caused PostgreSQL cascade crashes
  • psql timeout — avoids infinite hang when GoToSocial saturates DB connections
  • Stale PID cleanup — removes postmaster.pid if the PostgreSQL process is gone
  • Network-aware — skips network-dependent services when offline; always checks sshd
crontab -e
# Add:
*/5 * * * * ~/gestione/watchdog.sh
0 3 * * 0 ~/gestione/logrotate.sh

Backup#

tar -czf backup.tar.gz \
  --exclude="$HOME/.cache" \
  --exclude="$HOME/my-site/public" \
  --exclude="$HOME/my-site/resources" \
  -C /data/data/com.termux/files home

Back up these separately to external storage — they cannot be regenerated:

  • ~/mox-data/data/dkim/ — DKIM private keys
  • ~/.cf-credentials — Cloudflare API credentials

11. SSH access#

Termux’s SSH runs on a non-standard port (configurable in $PREFIX/etc/ssh/sshd_config).

pkg install openssh && passwd && sshd

Connect from local network:

ssh -p SSH_PORT $(whoami)@PHONE_LOCAL_IP

Connect from anywhere via Cloudflare Tunnel:

ssh -o ProxyCommand="cloudflared access ssh --hostname ssh.yourdomain.com" \
  $(whoami)@ssh.yourdomain.com

12. Dynamic DNS for the mail server#

Your phone’s public IP changes when you switch networks. The mail server’s A record must stay updated for SPF to pass.

~/update-dns.sh calls the Cloudflare API to update the record on every startup. Credentials are stored in ~/.cf-credentials (chmod 600).

CURRENT_IP=$(curl -s ifconfig.me)
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}"

13. Security checklist#

  • ~/.cf-credentialschmod 600
  • Cloudflare API token with DNS-edit-only permissions
  • ~/mox-data/data/dkim/ with restricted permissions
  • DKIM keys backed up to external storage
  • All services bind to 127.0.0.1, not 0.0.0.0
  • Disable SSH password auth — use keys only ($PREFIX/etc/ssh/sshd_config)
  • Phone locked with strong PIN

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/cloudflared.log
tail -f ~/logs/gotosocial.log
tail -f ~/logs/watchdog.log

# Current public IP
curl -s ifconfig.me && echo

# Disk space
df -h | grep -E "data|storage"

# Cloudflare Tunnel status
cloudflared tunnel info my-tunnel

Appendix: tested versions#

SoftwareVersionNotes
cloudflared2025-10-19ARM64 binary
Hugov0.156.0+extendedandroid/arm64
Copypartylatest via pip
Moxv0.0.15+compiled from source with Termux patches
Listmonkv6.0.0
GoToSociallatest ARM64-cgo variant required
PostgreSQL18.xvia pkg install postgresql

This stack runs entirely on an Android phone. It is radical, fragile in some ways, and completely yours.