Run a FrogTalk Lilypad (node)
A Lilypad is a FrogTalk node. Self-host one β a federated chat server β on a fresh Linux VPS: CLI setup wizard, nginx, systemd, and sync with the public mesh anchored at frogtalk.app.
master branch for production nodes. See
security & disclosure before exposing a node to the public internet.
.env, SSH passwords, or federation tokens. Use SSH keys for VPS login. Placeholders: <YOUR_VPS_IP>, <YOUR_DOMAIN>.
Quick install (copy-paste)
Run on a fresh Ubuntu/Debian VPS as a user with sudo. Replace <YOUR_VPS_IP> (or use https://chat.<YOUR_DOMAIN> once DNS works).
sudo apt update
sudo apt install -y git python3 python3-venv python3-pip curl nginx php-fpm php-curl ufw sqlite3
sudo adduser --disabled-password --gecos "" deploy
sudo mkdir -p /opt/frogtalk && sudo chown deploy:deploy /opt/frogtalk
sudo -u deploy git clone https://github.com/deadinternetfox/frogtalk.git /opt/frogtalk
cd /opt/frogtalk
export PUBLIC_URL="http://<YOUR_VPS_IP>"
export FROGTALK_SERVER_NAME="My FrogTalk Node"
# Same token as on FrogTalk Main (frogtalk.app) β generate once on Main:
export FROGTALK_FEDERATION_TOKEN="<openssl-rand-hex-32-on-main>"
sudo bash node/scripts/install.sh setup -y --install-dir /opt/frogtalk --public-url "$PUBLIC_URL"
sudo bash node/scripts/install.sh systemd -y --install-dir /opt/frogtalk
set -a && source /opt/frogtalk/.env && set +a
curl -sk "$PUBLIC_URL/api/ping" | python3 -m json.tool
curl -sS http://127.0.0.1/board/api/info | python3 -m json.tool
bash node/scripts/install.sh federation -y --install-dir /opt/frogtalk --public-url "$PUBLIC_URL"
sudo ufw allow OpenSSH && sudo ufw allow 80/tcp && sudo ufw allow 443/tcp
sudo ufw --force enable
curl -sS https://frogtalk.app/api/network/servers | python3 -m json.tool
grep ADMIN_PASSWORD /opt/frogtalk/.env
setup -y (root) installs board nginx, auto HTTPS for http:// IPs (self-signed), then updates PUBLIC_URL to https://. See HTTPS.
Open $PUBLIC_URL/app, log in as admin (password in .env), rotate the password.
π³ Run with Docker (fastest)
The backend node ships as a container. This runs the FrogTalk app + imageboard on :8080; put nginx or a Cloudflare Tunnel in front for TLS and your domain. Data persists in named volumes.
git clone https://github.com/deadinternetfox/frogtalk.git
cd frogtalk
# optional: your public URL once TLS/DNS is set
export PUBLIC_URL="https://chat.<YOUR_DOMAIN>"
docker compose up -d # builds the image + starts the node
docker compose logs -f # watch it boot
curl -sS http://127.0.0.1:8080/healthz # {"ok":true}
Then point nginx/Cloudflare at 127.0.0.1:8080, open $PUBLIC_URL/app, log in as admin and rotate the password. Join the mesh with federation. The image is defined in node/Dockerfile and docker-compose.yml; a multi-arch image can also be published to ghcr.io/deadinternetfox/frogtalk (run the GHCR workflow).
Persistence: volumes ft-data (DB + federation identity), ft-secrets, ft-uploads, ft-board. Update with git pull && docker compose up -d --build.
Prerequisites
| Item | Recommendation |
|---|---|
| VPS | 1β2 vCPU, 2β4 GB RAM, 20+ GB disk |
| OS | Debian 12 / Ubuntu 22.04+ with systemd |
| Python | 3.10+ (3.11+ recommended) |
| DNS | Optional β A/AAAA for chat.<YOUR_DOMAIN>, or IP-only http://<YOUR_VPS_IP> |
| Ports | 22 (SSH), 80/443 (nginx) β app stays on 127.0.0.1:8080 |
Install wizard = bash CLI (node/scripts/install.sh), not a browser UI.
1) Bootstrap the VPS
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@<YOUR_VPS_IP>
ssh root@<YOUR_VPS_IP>
sudo apt update
sudo apt install -y git python3 python3-venv python3-pip curl nginx \
certbot python3-certbot-nginx ufw sqlite3
sudo adduser --disabled-password --gecos "" deploy
sudo mkdir -p /opt/frogtalk && sudo chown deploy:deploy /opt/frogtalk
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
2) Clone repo + run setup wizard
sudo -u deploy git clone https://github.com/deadinternetfox/frogtalk.git /opt/frogtalk
cd /opt/frogtalk
export PUBLIC_URL="https://chat.<YOUR_DOMAIN>"
export FROGTALK_SERVER_NAME="My FrogTalk Node"
# Interactive menu
bash node/scripts/install.sh
# Non-interactive (recommended)
bash node/scripts/install.sh setup -y --install-dir /opt/frogtalk --public-url "$PUBLIC_URL"
bash node/scripts/install.sh federation -y --install-dir /opt/frogtalk --public-url "$PUBLIC_URL"
sudo bash node/scripts/install.sh systemd -y --install-dir /opt/frogtalk
The wizard creates venv/, writes .env (HOST=127.0.0.1, PORT=8080), and symlinks node/data, node/.env, node/secrets. A real node/data directory (not a symlink) causes an empty DB β re-run setup or federation join to repair.
Admin: user admin on first boot. Password is in /opt/frogtalk/.env as ADMIN_PASSWORD (auto-generated if left blank).
Account sync (visiting another node)
Users who log in on a community node (not their home) can import joined channel settings (themes, slowmode, forwarding lock), DM thread prefs, friends/following, profile theme/CSS, client prefs (Tor, sounds), and FrogSocial posts from their home server under Settings β Network β Re-sync from home. Sync is automatic on login when incomplete. Exports are Ed25519-signed by the home node and verified on import. Federated directory channels can be joined with Join without a prior sync row (metadata from the index only).
# Optional tuning in /opt/frogtalk/.env (defaults keep features on)
FROGTALK_SYNC_PERSIST=1
FROGTALK_SYNC_BIND_HOME=1
FROGTALK_SYNC_VERIFY_EXPORT=1
FROGTALK_SYNC_SIGN_EXPORT=1
FROGTALK_SYNC_REQUIRE_EXPORT_SIG=1
FROGTALK_SYNC_LOGIN_RESUME=1
FROGTALK_SYNC_PAGINATION=1
# FROGTALK_SYNC_STALE_HOURS=24
Calls: DM voice/video between nodes uses home-server signaling. Travelers currently connected to your node are rung locally even when homed elsewhere. Enable FROGTALK_FEDERATION_CALLS_ENABLED=1 and publish TURN URLs. See API docs (Federation).
Preferred node (app): users save their network choice with
PATCH /api/auth/client-prefs (preferred_node_url, optional
prefer_onion) β included when they re-sync from home. See
API β Auth.
3) Federation β join the mesh
bash node/scripts/install.sh federation -y --install-dir /opt/frogtalk \
--public-url "$PUBLIC_URL"
# Or directly:
bash node/scripts/node_federation_join.sh --install-dir /opt/frogtalk -y \
--public-url "$PUBLIC_URL"
Pulls the official directory, announces this node on the hub (POST β¦/servers/register), imports peers, and pins Ed25519 keys from each peerβs /api/network/status (run after frogtalk is up).
Token: set the same FROGTALK_FEDERATION_TOKEN on FrogTalk Main and on this node (openssl rand -hex 32 on Main; never commit). Without it, join warns and you wonβt appear on frogtalk.app until you set the token and re-run federation.
# Verify global listing (authoritative feed on Main):
curl -sS https://frogtalk.app/api/network/servers | python3 -m json.tool
# Your own node's /api/network/servers always includes local identity β
# that is NOT proof you are listed on frogtalk.app.
| Name | Role |
|---|---|
| FrogTalk Main | https://frogtalk.app β production hub / official directory |
| FrogTalk Tor Mirror | Onion mirror β listed in network picker when configured |
Mesh config (operators): federation seed peers and URL repairs are
defined in FROGTALK_FEDERATION_MESH_FILE (copy
node/deploy/federation-mesh.example.json). Hub VPS:
FROGTALK_FEDERATION_DIRECTORY_HUB=1.
FROGTALK_FEDERATION_ENABLED=1
FROGTALK_FEDERATION_REQUIRE_SIGS=1
FROGTALK_OFFICIAL_DIRECTORY_URL=https://frogtalk.app/api/network/servers
FROGTALK_FEDERATION_TOKEN=
# Hub only (optional):
# FROGTALK_FEDERATION_DIRECTORY_HUB=1
# FROGTALK_FEDERATION_MESH_FILE=/opt/frogtalk/federation-mesh.local.json
FROGTALK_TOR_ENABLED=0
PUBLIC_URL=https://chat.<YOUR_DOMAIN>
FROGTALK_SERVER_NAME=My FrogTalk Node
FROGTALK_HOME_PAGE=main
ALLOWED_ORIGINS=https://chat.<YOUR_DOMAIN>
FROGTALK_AUTO_UPDATE_ENABLED=0
# Clearnet + /board/ behind Cloudflare tunnel (typical):
PORT=8000
FROGTALK_NGINX_TUNNEL_LISTEN=1
4) HTTPS (stop βNot secureβ on HTTP)
http://<IP> is always labeled not secure. After setup, run:
sudo bash node/scripts/install.sh ssl -y --install-dir /opt/frogtalk
The setup wizard now offers a recommended HTTPS option: free trusted cert (Let's Encrypt via certbot, for domains), plus self-signed fallback. IP-only nodes still use self-signed with HTTPβHTTPS redirect. Use https://<YOUR_VPS_IP>/ (accept trust prompt once), or map a domain to get a clean padlock. If certbot fails, the SSL helper prints free CLI alternatives (ZeroSSL/Buypass via acme.sh).
nginx terminates TLS and proxies to 127.0.0.1:8080.
server {
listen 80;
server_name chat.<YOUR_DOMAIN>;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
sudo ln -sf /etc/nginx/sites-available/frogtalk /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d chat.<YOUR_DOMAIN>
Reference: node/deploy/nginx.conf (wizard: install_board_nginx.sh). Match PORT in .env with proxy_pass.
Frog Channel (/board/)
PHP imageboard on the same origin. sudo β¦ setup -y installs nginx + php-fpm for /board/ and sets title from FROGTALK_SERVER_NAME.
sudo bash node/scripts/install.sh board-nginx --install-dir /opt/frogtalk
curl -sS http://127.0.0.1/board/api/info | python3 -m json.tool
bash node/scripts/install.sh federation -y --public-url "$PUBLIC_URL"
Peer board links appear after federation when php-curl is installed. Operators edit identity in Server Admin or /board/admin.
6) Verify
bash node/scripts/install.sh status --install-dir /opt/frogtalk
curl -sS "$PUBLIC_URL/api/ping"
curl -sS "$PUBLIC_URL/api/network/status" | python3 -m json.tool
journalctl -u frogtalk -n 40 --no-pager
Open $PUBLIC_URL/app, register a test user. In Settings β Network, probe peers.
Server Admin shows a warning banner when PUBLIC_URL has no domain or no HTTPS, with fix steps. Use βHide until fixedβ to dismiss for the current page session; it returns on next load until corrected. Raw-IP PUBLIC_URL still exposes the node IP to users and federation peers until a domain + HTTPS is configured.
Troubleshooting
| Symptom | Fix |
|---|---|
no such table / empty app | Fix node/data symlink β /opt/frogtalk/data |
| Works on :8080, not on domain | Align PORT and nginx proxy_pass |
federation -y ignored flags | Use current install.sh β flags must follow the subcommand: federation -y --public-url β¦ |
| Peers listed, no sync | Re-run federation -y after service is running |
| Traveler empty feed on foreign node | Settings β Network β Re-sync; check sync status for errors or partial import |
| Call does not ring on foreign node | Callee may be homed elsewhere β they must be online here or at home; friends-only gate default |
| Channel name clash after sync | Local channel kept; home channel listed in federated directory only β join via directory may return name_collision |
| Friend theme missing on travel node | Re-sync from home; profiles merge federation_user_profiles cache |
| Not on frogtalk.app directory | Match FROGTALK_FEDERATION_TOKEN on Main + node; re-run federation -y |
| CORS errors | Add your URL to ALLOWED_ORIGINS |
/board/ OK but /app 502 | nginx must not listen 8080 when app uses PORT=8080 β re-run board-nginx (wizard strips tunnel port by default) |
/board/ 404 | Re-run sudo bash node/scripts/install.sh board-nginx; check php-fpm + nginx test |
Board pills / curl_init | sudo apt install php-cli php-curl; re-run federation |
systemd permission denied | sudo chown -R deploy:deploy /opt/frogtalk; sudo bash node/scripts/install.sh systemd -y |
Security reports: frogtalk.app/security
Tor / onion (optional)
FROGTALK_TOR_ENABLED=1
FROGTALK_ONION_URL=http://yourhiddenservice.onion
FROGTALK_TOR_SOCKS_PROXY=socks5h://127.0.0.1:9050
bash node/scripts/install.sh federation -y \
--onion-url http://yourhiddenservice.onion --public-url ""
Clients open http://β¦.onion/app. Clearnet hubs need Tor SOCKS for onion peers.
7) Secure your node (free, do all of these)
On a public Linux server, hardening, firewalling, patching and intrusion detection matter far more than antivirus. None of this costs anything. Full reference: docs/NODE_SECURITY.md in the repo.
Firewall β default-deny inbound
apt-get install -y ufw
ufw allow 22/tcp # your SSH port
ufw allow 80/tcp; ufw allow 443/tcp
ufw allow 8080/tcp # nginx app/board upstream (skip if Cloudflare-tunnelled)
# coturn (only if this node runs calls):
ufw allow 3478/tcp; ufw allow 3478/udp; ufw allow 49152:65535/udp
ufw default deny incoming; ufw default allow outgoing
ufw --force enable
Don't lock yourself out: allow your SSH port before enable, and arm an auto-revert while you test β
systemd-run --on-active=300 --unit=ufw-safety --collect ufw --force disable, then open a fresh SSH session and systemctl stop ufw-safety.timer once it works.
Brute-force defence + intrusion detection
apt-get install -y fail2ban && systemctl enable --now fail2ban
# CrowdSec: behavioural detection + crowd-sourced blocklists + a bouncer that bans
curl -s https://install.crowdsec.net | sh
apt-get install -y crowdsec crowdsec-firewall-bouncer-nftables
cscli collections install crowdsecurity/linux crowdsecurity/sshd crowdsecurity/nginx
systemctl enable --now crowdsec crowdsec-firewall-bouncer
Gotcha: CrowdSec's local API defaults to 127.0.0.1:8080, which clashes with FrogTalk's nginx. Move it to 127.0.0.1:8083 in /etc/crowdsec/config.yaml, local_api_credentials.yaml and the bouncer yaml, then restart CrowdSec.
App-layer bouncers β block at nginx and the board (free)
The firewall bouncer drops attackers at the network layer. Add the nginx bouncer to block or CAPTCHA bad IPs at the proxy β this covers /app and the /board/ imageboard β and (optionally) a PHP bouncer for defence-in-depth inside the board:
# nginx bouncer β registers itself with the local API
apt-get install -y crowdsec-nginx-bouncer
# if you relocated the LAPI off :8080 (see gotcha above), point the bouncer at it:
sed -i 's#API_URL.*#API_URL = http://127.0.0.1:8083#' /etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf
nginx -t && systemctl reload nginx # ALWAYS test before reload
# make CrowdSec read the board/app access log so it can detect abuse there
cscli collections install crowdsecurity/nginx
cscli bouncers list # confirm the bouncer is registered + valid
Cloudflare: if your zone is on Cloudflare, the Cloudflare Workers bouncer blocks at the edge before traffic reaches the box (needs a Workers-scoped API token). CTI enrichment: add a free CrowdSec console CTI key to score IP reputation. Verify everything with cscli bouncers list and cscli metrics.
Patch automatically + run least-privilege
apt-get install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
Run FrogTalk as a non-root user (the installer uses deploy; the systemd unit should never be User=root). Prefer SSH keys and, once your key works, PasswordAuthentication no.
Strip the box to "just a FrogTalk node"
Fewer packages = smaller attack surface. Remove desktop/bloat (apt purge snapd xserver-common), disable hardware daemons useless on a VPS (systemctl disable --now ModemManager fwupd multipathd udisks2 upower), and remove anything that isn't FrogTalk. If you find Docker containers or VPN/proxy software you didn't install, treat it as a possible compromise.
Danger Zone β node self-destruct
The /server admin panel has a Danger Zone β Nuke this node for decommission / seizure / confirmed-compromise. It securely shreds the database, secrets, keys, uploads and Tor identity, removes the install itself, best-effort wipes free RAM, then powers off. It's gated by your admin session + a fresh PIN (or password) + typing the node's exact name. Install the helper for the full wipe:
install -o root -g root -m 0500 node/deploy/frogtalk-nuke.sh /usr/local/sbin/frogtalk-nuke.sh
install -o root -g root -m 0440 node/deploy/frogtalk-nuke.sudoers /etc/sudoers.d/frogtalk-nuke
visudo -cf /etc/sudoers.d/frogtalk-nuke
apt-get install -y secure-delete # sdmem / sfill for RAM + free-space wipe
Honest limits: on SSD/NVMe an in-place overwrite isn't guaranteed (wear-levelling) β pair it with a provider disk-destroy; "shred RAM" only covers free RAM + cache, and power-off clears the rest.
Updates
bash node/scripts/install.sh update
bash node/scripts/install.sh update-apply -y
bash node/scripts/install.sh federation -y --public-url "$PUBLIC_URL"
Set FROGTALK_RELEASE_SIGNERS before FROGTALK_AUTO_UPDATE_ENABLED=1.
What your node serves (recent app features)
The web client at $PUBLIC_URL/app includes more than classic chat β operators should know what users will see after federation sync.
| Area | Summary |
|---|---|
| Encryption | Signal Protocol DMs; AES-256-GCM private channels with AAD binding and key rotation on ban/kick |
| Frog Social | Posts, Reels, stories, reactions, friend wall |
| Frog Channel | PHP imageboard at /board/ β threads, greentext, mod tools |
| Bridges | Discord & Telegram two-way sync (public channels only; blocked for E2EE private rooms) |
| Music rooms | Synced YouTube / Spotify / SoundCloud listening |
| Calls | WebRTC voice/video; cross-node DM calls with FROGTALK_FEDERATION_CALLS_ENABLED=1 + TURN |
| Account sync | Travelers re-sync signed exports from home (channels, DMs prefs, friends, themes, FrogSocial) |
| 18+ labels | Voluntary public-channel content warnings with session age gate (not automated scanning) |
| Federation UI | Release feed SHA-256 checks, trust badges, onion handoffs to /app |
API reference: frogtalk.app/docs/api Β· Encryption detail: docs/SECURITY_MODEL.md
Project layout
/opt/frogtalk/
βββ .env
βββ data/
βββ secrets/
βββ venv/
βββ node/
βββ main.py
βββ static/ # /app Β· /docs/node
βββ board/
βββ scripts/ # install.sh Β· federation join