**Auto mutual pairing** When Cell A imports Cell B's invite (POST /api/cells on A), A now immediately pushes its own invite to Cell B over the LAN (using the endpoint IP, before the WG tunnel exists) via the new endpoint: POST /api/cells/peer-sync/accept-invite Cell B auto-adds Cell A as a WireGuard peer and DNS forward, completing the bidirectional tunnel without any manual action on Cell B's UI. The endpoint is idempotent and unauthenticated (runs before WG tunnel). Previously, the pairing was one-sided: Cell A had Cell B as a WG peer but Cell B never had Cell A — the tunnel never established and all cross-cell operations silently failed. **Conflict detection (add_connection + accept-invite)** _check_invite_conflicts() now validates before connecting: - VPN subnet must not overlap own subnet or any already-connected cell's subnet - Domain must not match own domain or any already-connected cell's domain Returns clear error messages so the admin knows which cell to reconfigure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Personal Internet Cell (PIC)
PIC is a self-hosted digital infrastructure platform. It manages DNS, DHCP, NTP, WireGuard VPN, email, calendar/contacts (CalDAV), file storage (WebDAV), a reverse proxy, and a certificate authority — all controlled from a single REST API and React web UI. No manual config file editing is required for normal operations.
Architecture
Browser
└── React SPA (cell-webui :8081)
└── Flask REST API (cell-api :3000, bound to 127.0.0.1)
└── Docker SDK / config files
├── cell-caddy :80/:443 reverse proxy
├── cell-dns :53 CoreDNS
├── cell-dhcp :67/udp dnsmasq
├── cell-ntp :123/udp chrony
├── cell-wireguard :51820/udp WireGuard VPN
├── cell-mail :25/:587/:993 Postfix + Dovecot
├── cell-radicale 127.0.0.1:5232 CalDAV/CardDAV
├── cell-webdav 127.0.0.1:8080 WebDAV
├── cell-rainloop :8888 webmail (RainLoop)
├── cell-filegator :8082 file manager UI
└── cell-webui :8081 React UI (Nginx)
All containers run on a custom Docker bridge network (cell-network, default 172.20.0.0/16). Static IPs per container are set in docker-compose.yml and overridden via .env.
The Flask API (api/app.py, ~2800 lines) contains all REST endpoints, runs a background health-monitoring thread, and manages the entire lifecycle of generated config artefacts: Caddyfile, Corefile, wg0.conf, and cell_config.json (the single source of truth at config/api/cell_config.json).
The React frontend (webui/) is built with Vite + Tailwind CSS. All API calls go through src/services/api.js (Axios). Pages: Dashboard, Peers, Network Services, WireGuard, Email, Calendar, Files, Routing, Vault, Containers, Cell Network, Logs, Settings.
Requirements
- Linux host with the WireGuard kernel module loaded
- Docker Engine and Docker Compose (v2 plugin or v1 standalone)
- Python 3.10+ (for
make setupand local dev only; not needed at runtime) - 2 GB+ RAM, 10 GB+ disk
- Ports available: 53, 67/udp, 80, 443, 51820/udp, 25, 587, 993
Quick Start
See QUICKSTART.md for step-by-step setup.
Configuration
Runtime configuration is controlled by .env in the project root. Copy .env.example to .env before first run.
| Variable | Default | Description |
|---|---|---|
CELL_NETWORK |
172.20.0.0/16 |
Docker bridge subnet for all containers |
CADDY_IP through FILEGATOR_IP |
172.20.0.2–.13 |
Static IP for each container |
DNS_PORT |
53 |
DNS (UDP+TCP) |
DHCP_PORT |
67 |
DHCP (UDP) |
NTP_PORT |
123 |
NTP (UDP) |
WG_PORT |
51820 |
WireGuard listen port (UDP) |
API_PORT |
3000 |
Flask API (bound to 127.0.0.1) |
WEBUI_PORT |
8081 |
React UI |
MAIL_SMTP_PORT |
25 |
SMTP |
MAIL_SUBMISSION_PORT |
587 |
SMTP submission |
MAIL_IMAP_PORT |
993 |
IMAP |
RADICALE_PORT |
5232 |
CalDAV (bound to 127.0.0.1) |
WEBDAV_PORT |
8080 |
WebDAV (bound to 127.0.0.1) |
RAINLOOP_PORT |
8888 |
Webmail |
FILEGATOR_PORT |
8082 |
File manager UI |
WEBDAV_USER |
admin |
WebDAV basic-auth username |
WEBDAV_PASS |
(required) | WebDAV basic-auth password — must be set before make start |
FLASK_DEBUG |
(unset) | Set to 1 to enable Flask debug mode; do not use in production |
PUID / PGID |
current user | UID/GID passed to the WireGuard container |
Cell identity (cell name, domain, VPN IP range) is configured via make setup or the Settings → Identity page in the UI after startup. The VPN IP range must be an RFC-1918 CIDR (10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16); the API and UI both enforce this.
Security Notes
Ports exposed to the network:
80/443— Caddy (HTTP/HTTPS reverse proxy)51820/udp— WireGuard25/587/993— Mail (SMTP, submission, IMAP)53— DNS (UDP + TCP)67/udp— DHCP8081— Web UI8888— Webmail (RainLoop)8082— File manager (Filegator)
Ports bound to 127.0.0.1 only (not directly reachable from the network):
3000— Flask API5232— Radicale (CalDAV)8080— WebDAV
The API has no authentication layer. It relies on is_local_request() to restrict sensitive endpoints (containers, vault) to requests originating from loopback or the cell's Docker network. The Docker socket is mounted into cell-api; treat access to port 3000 as equivalent to root access on the host.
For internet-facing deployments, place the host behind a firewall or VPN and restrict access to the API and UI ports.
Development
# Start the full stack (builds api and webui images)
make start
# Rebuild a single image after code changes
make build-api
make build-webui
# Run Flask API locally without Docker (port 3000)
pip install -r api/requirements.txt
python api/app.py
# Run React UI dev server locally (port 5173, proxies /api to :3000)
cd webui && npm install && npm run dev
# Follow all container logs
make logs
# Follow logs for one service (e.g. api, dns, caddy, wireguard, mail)
make logs-api
# Open a shell inside a container
make shell-api
Testing
make test # run the full pytest suite
make test-coverage # run with coverage; HTML report in htmlcov/
Tests live in tests/ (34 files, 642 test functions). Coverage includes:
- All service managers (network, WireGuard, email, calendar, file, routing, vault, container)
- API endpoint tests for each service area
- Config manager (CRUD, validation, backup/restore)
- IP utilities and Caddyfile generation
- Peer registry and WireGuard peer lifecycle
- Service bus pub/sub
- Firewall manager
- Pending-restart logic
Integration tests (tests/integration/) require a running PIC stack:
make test-integration # full suite (creates peers)
make test-integration-readonly # read-only checks, safe to run anytime
Management Commands
make setup # generate WireGuard keys, write configs, create data dirs
make start # docker compose up -d --build
make stop # docker compose down
make restart # docker compose restart
make status # container status + API health check
make logs # follow all service logs
make logs-<svc> # follow logs for one service
make shell-<svc> # shell inside a container
make update # git pull + rebuild + restart
make reinstall # full wipe of config/ and data/, then setup + start
make uninstall # stop containers; prompts whether to also delete config/ and data/
make backup # tar config/ + data/ into backups/
make restore # list available backups
make list-peers # show WireGuard peers via API
make show-routes # wg show inside the wireguard container
make add-peer PEER_NAME=foo PEER_IP=10.0.0.5 PEER_KEY=<pubkey>
License
MIT — see LICENSE.