ovpn operates self-hosted Xray (VLESS + REALITY) VPN servers from a local Go CLI over SSH.
The CLI keeps server and user state in ~/.ovpn, renders Docker Compose runtime files, and pushes them to Linux hosts. Optional HAProxy support can route traffic to a VPN backend pool.
It is intended for operators who prefer a local CLI and SSH workflow over a web panel. It is not a hosted VPN service; it gives you reproducible files and commands for your own servers.
- Project site
- X / updates
- Architecture
- Design points
- Security and quota defaults
- Quick start
- Production flow
- HA proxy rollout
- User operations
- CLI guide
- Transport profiles
- Monitoring
- Documentation map
- Ansible prepares the host baseline: packages, Docker, SSH, firewall, unattended upgrades, and hardening.
ovpnowns the VPN runtime: Xray config, users, quotas, monitoring, backups, and cleanup.- Desired state stays local under
~/.ovpn; deploys render runtime files and push them over SSH/SCP. - Remote services run as Docker Compose under
/opt/ovpn, which keeps maintenance and recovery predictable. - Xray REALITY listens on
443/tcp; internal agent and monitoring endpoints stay private by default. - Real inventory, hostnames, IPs, tokens, and private keys stay outside the public repository.
flowchart LR
classDef operator fill:#e8f5ef,stroke:#167054,stroke-width:2px,color:#17231d
classDef host fill:#eef4fb,stroke:#225b8d,stroke-width:2px,color:#17231d
classDef runtime fill:#fff7e8,stroke:#8f4f23,stroke-width:2px,color:#17231d
classDef optional fill:#f7f0ff,stroke:#7050a8,stroke-width:2px,color:#17231d
classDef client fill:#f2f2ec,stroke:#6a6f68,stroke-width:1.5px,color:#17231d
subgraph local["Operator machine"]
cli["ovpn CLI"]
state["~/.ovpn\nservers, users, quotas"]
ansible["Ansible inventory\nhost hardening"]
ci["GitHub Actions\nCI, security, release"]
end
subgraph vpn["VPN host(s)"]
hostA["VPN host A\n/opt/ovpn\nXray 443 + agent"]
hostN["VPN host B..N\nsame runtime model"]
monitoring["optional monitoring\nPrometheus/Grafana\nTelegram bot"]
backups["backup + cleanup\nsnapshots"]
end
subgraph ha["Optional HA entrypoint"]
proxy["HAProxy proxy\ncountry preset routing"]
backends["VPN backend pool\nhost A..N"]
end
clients["Clients\nmobile / desktop\nVLESS link or QR"]
state --> cli
ansible -->|bootstrap| hostA
ansible -->|bootstrap| hostN
ci -->|validated release binary| cli
cli -->|SSH/SCP deploy| hostA
cli -->|SSH/SCP deploy| hostN
hostA --> monitoring
hostN --> monitoring
hostA --> backups
hostN --> backups
proxy -->|foreign relay| backends
backends --> hostA
backends --> hostN
clients -->|443/tcp| proxy
clients -->|direct 443/tcp| hostA
clients -->|direct 443/tcp| hostN
class cli,state,ansible,ci operator
class hostA,hostN host
class backups runtime
class monitoring,proxy,backends optional
class clients client
- Local state:
~/.ovpnstores servers, users, quotas, and metadata used for rendering. - SSH/SCP deploy path: commands render files locally and apply them through normal server access.
- Docker runtime: Xray,
ovpn-agent, optional proxy, and monitoring run under/opt/ovpn. - Multi-host user operations: user add/remove/enable/disable/quota commands apply to all enabled VPN servers by default.
- Transport profiles: keep deprecated TCP/REALITY links for compatibility and use opt-in fallback profiles for degraded networks; plain XHTTP is available when operators accept its lack of transport security.
- Optional proxy role: HAProxy can front a backend pool and use country presets for split routing.
- Security defaults: Xray routing blocks BitTorrent and public tracker domains; Ansible can add host-level Tor exit filtering. The rendered Xray config (REALITY private key + client UUIDs) is stored
0640 root:<xray-gid>, andovpn-agentmutating endpoints require a bearer token (OVPN_AGENT_TOKEN, auto-provisioned on deploy). - Maintenance commands:
doctor,status, logs, backups, restore, cleanup, monitoring, and release checks are part of the CLI.
- Local desired state in
~/.ovpn - Remote Docker Compose runtime in
/opt/ovpn - User lifecycle commands and VLESS link generation
- Global-by-default user provisioning across enabled servers
- Profile-aware VLESS link, QR, and export commands
- Rolling
30dtraffic quota enforcement - Lightweight per-user connection diagnostics and short targeted debug windows (
basicmode is on by default; seedocs/cli.md) - Optional Prometheus, Alertmanager, Grafana, and Telegram bot monitoring
- Optional HA proxy entrypoint with backend failover
- Backup, restore, and decommission commands
- Current pinned version:
1.7.2 - Check locally:
./ovpn version - Release source of truth:
VERSION- top entry in
CHANGELOG.md
- Runtime image defaults source of truth:
- Versioning policy:
- major: breaking CLI, API, or operator workflow changes
- minor: new commands, monitoring surfaces, or operator features
- patch: fixes and small UX improvements
- X / updates: https://x.com/OVPN_project
- Issues: confirmed bugs and concrete feature requests
- Discussions: questions, usage help, design ideas, and announcements
- Security: use private vulnerability reporting, not public issues
- Release binary, or Go
1.26.2+when building from source - SSH key access to the target host
- Target host running Debian
12+or Ubuntu22.04+ - Clean Linux host recommended
- Root SSH access is the simplest supported setup
- Docker/Compose on the host, usually installed by the Ansible bootstrap
- Default security profile:
OVPN_SECURITY_PROFILE=minimal - Minimal profile blocks BitTorrent protocol traffic and public tracker domains in Xray routing.
- Threat DNS defaults to
9.9.9.9,149.112.112.112. - If the selected Xray image cannot validate geosite resources, temporarily roll back with:
export OVPN_SECURITY_PROFILE=off
./ovpn deploy <server>- Default quota: rolling
30d,300 GBper user when no per-user limit is set. - Optional host-level Tor exit-node blocking exists in Ansible and is off by default.
- Ansible tunes host conntrack for VPN/NAT workloads and monitoring alerts on missing/high/critical conntrack usage.
See docs/security.md for the full security model.
- Remote server backups: keep latest
7 - Local backups: keep latest
7 - Remote pre-deploy snapshots: keep latest
7 - Monitoring defaults are sized for small hosts:
- Prometheus scrape/evaluation interval:
60s - Prometheus retention:
10dwith a512MBsize cap - Prometheus WAL compression: enabled
- cAdvisor housekeeping:
60s(max5m) - Critical host memory guard:
MemAvailable < 64MiBfor2m - Container OOM events: alerted from cAdvisor
- Grafana background reporting and update checks: disabled
- Memory and transient collector warnings use longer windows before firing, but still send resolved Telegram notifications.
- Prometheus scrape/evaluation interval:
Use this flow for a first small server. Replace placeholders with your own values.
# Use the release archive for your workstation OS/arch, or build from source for development.
# Release archives contain one executable: ovpn.
# Source build:
# go build -o ovpn ./cmd/ovpn
./ovpn version
./ovpn server add \
--name <server> \
--host <server-ip> \
--domain <domain> \
--ssh-user root \
--ssh-port 22 \
--xray-version 26.3.27
./ovpn server init <server>
./ovpn doctor <server>
./ovpn user add --username <user>
./ovpn user link --server <server> --username <user>user link prints the client link and a terminal QR code by default for mobile onboarding. Use --qr=false when you need link-only output for scripts.
See docs/transports.md before enabling fallback profiles or switching the default generated link profile.
server init performs the first bootstrap/deploy for the VPN runtime. Use deploy later when you change runtime settings:
./ovpn deploy <server>Recommended order:
- Prepare the host with Ansible.
- Register the server in local
ovpnstate. - Initialize and validate the runtime.
- Add users and share client links.
- Enable monitoring if you need it.
- Back up before risky changes.
Before changing a host, confirm SSH, listening ports, and Docker state:
ssh root@<server-ip> 'hostnamectl'
ssh root@<server-ip> 'sudo ss -lntup'
ssh root@<server-ip> 'sudo docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'Start from the example inventory and keep your real inventory private.
cd ansible
mkdir -p inventories/production
cp -R inventories/example/. inventories/production/Edit inventories/production/hosts.yml, then validate and apply:
ANSIBLE_CONFIG=ansible.cfg ansible-playbook -i inventories/production/hosts.yml playbooks/bootstrap.yml --syntax-check
ANSIBLE_CONFIG=ansible.cfg ansible-playbook -i inventories/production/hosts.yml playbooks/bootstrap.yml --limit <host> --check --diff
ANSIBLE_CONFIG=ansible.cfg ansible-playbook -i inventories/production/hosts.yml playbooks/bootstrap.yml --limit <host>For already deployed hosts, use the maintenance playbook instead:
ANSIBLE_CONFIG=ansible.cfg ansible-playbook -i inventories/production/hosts.yml playbooks/host-maintenance.yml --limit <host> --check --diff
ANSIBLE_CONFIG=ansible.cfg ansible-playbook -i inventories/production/hosts.yml playbooks/host-maintenance.yml --limit <host>See README.ansible.md for inventory and hardening details.
./ovpn server add \
--name <server> \
--host <server-ip> \
--domain <domain> \
--ssh-user root \
--ssh-port 22 \
--xray-version 26.3.27./ovpn server init <server>
./ovpn doctor <server>
./ovpn server status <server>Use deploy after config or code changes:
./ovpn deploy <server>
./ovpn doctor <server>Official release binaries embed the Linux ovpn-agent and ovpn-telegram-bot runtime binaries used by deploys, so release archives contain only the ovpn executable. Normal installed usage does not require Go, a source checkout, or keeping sidecar binaries in the same directory. Use OVPN_REPO_ROOT only for development builds without embedded runtime assets, or when you intentionally deploy an unsupported runtime architecture through source fallback:
export OVPN_REPO_ROOT=/absolute/path/to/ovpn
ovpn deploy <server>Use a proxy only when you want one entrypoint in front of existing VPN backends.
Available proxy presets are ru and cn (china is accepted as an alias for cn).
./ovpn server add \
--name <proxy> \
--role proxy \
--proxy-preset ru \
--host <proxy-ip> \
--domain <proxy-domain> \
--ssh-user root \
--ssh-port 22
./ovpn server backend attach --proxy <proxy> --backend <vpn-backend>
./ovpn deploy <vpn-backend>
./ovpn server init <proxy>
./ovpn config validate --server <proxy>
./ovpn deploy <proxy>
./ovpn doctor <proxy>Attach and deploy the backend before initializing the proxy. The proxy render requires at least one backend, and the backend deploy makes the relay identity live before proxy traffic arrives.
See docs/ha.md for the full HA design and troubleshooting guide.
./ovpn doctor <server>
./ovpn server status <server>
./ovpn server logs <server> --service xray --tail 200
./ovpn server logs <server> --service ovpn-agent --tail 200./ovpn user add --username <user>
./ovpn user add --username <user> --expiry 2026-05-01
./ovpn user quota-set --username <user> --monthly-gb 400
./ovpn user quota-set --username <user> --monthly-bytes <bytes>
./ovpn user enable --username <user>
./ovpn user rm --username <user>
./ovpn user link --server <server> --username <user>
./ovpn user link --server <server> --username <user> --qr=false
./ovpn user link --server <server> --username <user> --qr-file ~/Desktop/<user>.png
./ovpn user show --server <server> --username <user>
./ovpn user list --server <server>Mutating user commands apply to all enabled servers by default. Link and read commands stay server-scoped. QR output is enabled by default and contains the same full client credential as the vless:// link; treat saved QR files as secrets.
Use --monthly-gb for human-sized quota changes. ovpn stores quota internally as bytes; --monthly-gb 400 means 400 * 1024^3 bytes. --monthly-bytes remains available for exact automation.
When --email is omitted, new users get stable identity username@global. User expiry is cluster-wide and uses UTC end-of-day semantics.
For per-user support, start with:
./ovpn user diagnose --server <server> --username <user> --since 24h
./ovpn user debug start --server <server> --username <user> --duration 15m
./ovpn user debug list --server <server>
./ovpn user debug show --server <server> --username <user> --since 15m
./ovpn user debug stop --server <server> --username <user>See docs/cli.md for the full CLI command map and targeted debug workflow.
Start monitoring:
./ovpn deploy <server>
./ovpn server monitor up <server>
./ovpn server monitor status <server>Telegram setup can be done in one command:
OVPN_TELEGRAM_BOT_TOKEN=<token> \
./ovpn server monitor telegram-setup <server> \
--owner-user-id <your-numeric-user-id>- Get
<token>from @BotFather →/newbot(one bot per server). - Get
<your-numeric-user-id>by sending any message to @RawDataBot — your ID ismessage.from.idin the reply.
The Telegram bot is read-only by default. Owner-only recovery actions require OVPN_TELEGRAM_ADMIN_TOKEN.
See docs/monitoring.md for full setup instructions, dashboards, alerts, and Telegram behavior.
./ovpn server backup <server>
ls -lah ~/.ovpn/backupsPreview first:
./ovpn --dry-run server cleanup <server>Then run cleanup with explicit confirmation:
./ovpn server cleanup <server> --confirm CLEANUPBy default, cleanup removes remote runtime files and disables local server metadata. Add --remove-local only when you also want to remove local metadata.
./ovpn version
./ovpn deploy <server>
./ovpn restart <server>
./ovpn doctor <server>
./ovpn server status <server>
./ovpn server logs <server> --service xray --tail 200
./ovpn stats --server <server>
./ovpn user list --server <server>
./ovpn user add --username <user>
./ovpn user quota-set --username <user> --monthly-gb 400
./ovpn user link --server <server> --username <user>
./ovpn server monitor up <server>
./ovpn server backup <server>
./ovpn server restore <server> --remote-path /opt/ovpn-backups/<archive>.tgz
./ovpn server cleanup <server> --confirm CLEANUPEnd-user setup instructions cover iOS, Android, Windows, and macOS:
docs/clients.md: Englishdocs/clients-ru.md: Russian
Generate the optional PDF guides locally when you need them for Telegram /guide:
make docs-pdfREADME.md: main operator entrypointREADME.ansible.md: host bootstrap and hardeningDEVELOPMENT.md: contributor and architecture guidedocs/cli.md: CLI command map and practical operator workflowsdocs/transports.md: transport profiles, fallback links, and rollout notesdocs/clients.md: English client guidedocs/clients-ru.md: Russian client guidedocs/security.md: security and hardening modeldocs/ha.md: HA proxy topology and rolloutdocs/monitoring.md: monitoring operationsdocs/ci.md: GitHub Actions workflowsdocs/testing.md: testing strategydocs/upgrades.md: upgrades, rollback, and cleanup
- Update
VERSION. - Prepend the matching version entry to
CHANGELOG.md. - Merge to
main. - GitHub Actions validates both files, creates the semver tag if needed, and publishes the release.
Recommended required checks:
Go QualityAnsible QualityGitleaks Tree ScanTrivy FS Scan
Release tags matching *.*.* should be protected from update and deletion.
This repository includes an optional sponsor button and public donation page:
- X / updates:
https://x.com/OVPN_project - sponsor button config:
.github/FUNDING.yml - donation page source:
docs/donate/index.html - project page URL:
https://agentram.github.io/ovpn/donate/
This repository is source-available under the Attribution-NonCommercial Source License 1.0 (ovpn).
You may use, modify, and share it for non-commercial purposes with attribution. Commercial use requires separate permission.