feat: MyPointVPN landing on mypointvpn.sokolab.xyz and post about Serverus

Landings get per-project themes (params.theme, params.fonts) and optional
chat/start/note/engine sections, so they don't look like clones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
protokeyandClaude Opus 5.5 committed 2026-10-13 13:22:41 +04:00
1 parent 6958d0a975
commit 672467a699
6 files changed
+538 -4

No files matched your search

+124
View File
@@ -0,0 +1,124 @@
/* MyPoint VPN theme: light paper, emerald accent, split hero with a chat, dark "under the hood" block. */
.theme-mypointvpn {
--bg: #f6f4ee;
--bg-alt: #eeebe2;
--surface: #fffdf8;
--border: #ddd8cb;
--text: #16201c;
--muted: #5c6862;
--accent: #0d8a5f;
--accent-strong: #0a6e4c;
--lime: #c8f169;
--ink: #0f1a16;
--radius: 1.1rem;
font-family: 'Manrope', system-ui, sans-serif;
}
.theme-mypointvpn h1,
.theme-mypointvpn h2 { font-family: 'Unbounded', 'Manrope', sans-serif; font-weight: 700; letter-spacing: -.01em; }
.theme-mypointvpn h3 { font-weight: 800; }
.theme-mypointvpn code { background: rgba(13, 138, 95, .1); padding: .05em .35em; border-radius: .35em; font-size: .9em; }
/* nav */
.theme-mypointvpn .nav { background: rgba(246, 244, 238, .85); }
.theme-mypointvpn .nav__logo { font-family: 'Unbounded', sans-serif; font-weight: 700; }
.theme-mypointvpn .nav__logo::before {
content: ""; display: inline-block; width: .6rem; height: .6rem; margin-right: .5rem;
border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 .25rem rgba(13, 138, 95, .2);
vertical-align: middle;
}
/* hero: text left, chat right */
.theme-mypointvpn .hero {
text-align: left;
background:
radial-gradient(40rem 26rem at 85% 20%, rgba(200, 241, 105, .45), transparent 70%),
var(--bg);
}
.theme-mypointvpn .hero--split .wrap {
display: grid; grid-template-columns: 1.15fr 1fr; gap: 3rem; align-items: center;
}
.theme-mypointvpn .hero__title { margin-inline: 0; max-width: 16ch; font-size: clamp(2rem, 5vw, 3.1rem); line-height: 1.12; }
.theme-mypointvpn .hero__lead { margin-inline: 0; }
.theme-mypointvpn .hero__cta { justify-content: flex-start; }
.theme-mypointvpn .badge { border-color: var(--accent); color: var(--accent-strong); background: var(--surface); }
.theme-mypointvpn .btn { background: var(--surface); border-color: var(--border); color: var(--text); border-radius: 999px; }
.theme-mypointvpn .btn:hover { background: #fff; }
.theme-mypointvpn .btn--primary { background: var(--ink); border-color: var(--ink); color: var(--lime); }
.theme-mypointvpn .btn--primary:hover { background: var(--accent-strong); color: #fff; }
@media (max-width: 820px) {
.theme-mypointvpn .hero--split .wrap { grid-template-columns: 1fr; gap: 2.5rem; }
}
.chat {
display: flex; flex-direction: column; gap: .55rem;
padding: 1.1rem; border-radius: 1.4rem;
background: var(--ink); color: #e9f1ec;
box-shadow: 0 1.5rem 3rem -1.5rem rgba(15, 26, 22, .5);
transform: rotate(1.2deg);
}
.chat__title { margin: 0 0 .3rem; color: #8fa39a; font-size: .8rem; text-align: center; }
.chat__msg {
margin: 0; max-width: 85%; padding: .55rem .85rem;
border-radius: 1rem; font-size: .92rem; line-height: 1.45;
}
.chat__msg--bot { align-self: flex-start; background: #1d2b25; border-bottom-left-radius: .3rem; }
.chat__msg--me { align-self: flex-end; background: var(--lime); color: var(--ink); border-bottom-right-radius: .3rem; font-weight: 600; }
.theme-mypointvpn .chat code { background: none; padding: 0; font-size: .95em; }
@media (max-width: 820px) { .chat { transform: none; } }
/* features */
.theme-mypointvpn .section--alt { background: var(--bg-alt); }
.theme-mypointvpn .card { background: var(--surface); box-shadow: 0 1px 0 var(--border); }
.theme-mypointvpn .card h3 { color: var(--text); }
.theme-mypointvpn .card code { background: rgba(13, 138, 95, .1); }
.theme-mypointvpn .card--link:hover { border-color: var(--accent); }
.theme-mypointvpn .card--link h3 { color: var(--accent-strong); }
/* get started: big numbered steps */
.start {
list-style: none; margin: 2rem 0 0; padding: 0; counter-reset: s;
display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 1rem;
}
.start li { counter-increment: s; position: relative; padding: 4.2rem 1.25rem 1.25rem; border-top: 3px solid var(--ink); }
.start li::before {
content: counter(s, decimal-leading-zero); position: absolute; top: .9rem; left: 1.25rem;
font: 700 2.2rem/1 'Unbounded', sans-serif; color: var(--accent);
}
.start p { margin: .5rem 0 0; color: var(--muted); }
/* note */
#note { padding-top: 0; }
.note {
padding: 1.5rem 1.75rem; border-radius: var(--radius);
background: var(--lime); color: var(--ink);
}
.note h2 { font-size: 1.25rem; }
.note p { margin: .6rem 0 0; max-width: 52rem; }
.theme-mypointvpn .note code { background: rgba(15, 26, 22, .1); }
/* under the hood */
.engine { background: var(--ink); color: #e9f1ec; }
.engine .section__lead { color: #a9bab2; }
.engine code { background: rgba(200, 241, 105, .12) !important; color: var(--lime); }
.engine__kicker {
margin: 0 0 .5rem; color: var(--lime);
font: 600 .8rem/1 'JetBrains Mono', monospace; text-transform: uppercase; letter-spacing: .12em;
}
.engine__list {
list-style: none; margin: 2rem 0 0; padding: 0;
display: grid; grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); gap: .75rem 2rem;
}
.engine__list li { position: relative; padding-left: 1.4rem; color: #d3ddd8; }
.engine__list li::before { content: "›"; position: absolute; left: 0; color: var(--lime); font-weight: 800; }
.engine__callout {
margin-top: 2.5rem; padding: 1.25rem 1.5rem;
border: 1px solid rgba(200, 241, 105, .35); border-radius: var(--radius);
background: rgba(200, 241, 105, .06);
}
.engine__callout h3 { color: var(--lime); font-size: 1.1rem; }
.engine__callout p { margin: .5rem 0 0; color: #d3ddd8; }
/* community + footer */
.theme-mypointvpn .grid--4 { grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); }
.theme-mypointvpn .footer { background: var(--bg-alt); }
@@ -0,0 +1,83 @@
---
title: "MyPointVPN: how a VPN bot learned to manage servers"
date: 2026-10-13T12:00:00+04:00
draft: false
tags:
- telegram
- vpn
- server
- ansible
- docker
summary: "It started as a portfolio bot that installs 3x-ui. Along the way I split out Serverus, a package that manages the whole server. Now the bot lives on a server it manages itself"
---
On September 23 I made the first commit to a portfolio project: a Telegram bot that installs and configures the 3x-ui panel by itself, so a person gets their own VPN without any manual fiddling. Three weeks and about 40 commits later, the VPN turned out not to be the most interesting part.
## How it looks
You rent a server with Debian 12 or 13 from any hosting provider, get an IP and a root password, and send the bot one line: `ip:password`. The bot moves ssh to a random port with key-only login, turns on a firewall, updates the system and from then on manages the server itself.
You can take the server back at any time with the "Return" button. The bot removes its access, rolls back ssh and the firewall and sends you a new root password. Your data on the server stays.
## VPN
This is what it all started for. The bot installs 3x-ui and sets up:
- a VLESS + XHTTP + Reality connection on port 443, with the Reality target picked by the bot
- an optional camouflage server: clients connect to it, it forwards traffic to the panel, and the panel's address is never exposed
- a domain with a Let's Encrypt certificate for the panel and the subscription, and without a domain a short-lived IP certificate that renews automatically
- an outbound proxy: your own or Cloudflare WARP
- encrypted DNS on the server: DNS-over-TLS, with DNS-over-HTTPS as a fallback if TLS gets blocked
The bot issues and revokes client keys and hands out subscription links for v2rayN, Hiddify, Happ and v2rayNG. Keys have no limits: no traffic cap, no expiry, no device limit. For every key you can see the traffic and when the client was last online.
Once a day the bot checks the panel, Xray and the subscription certificate and tells you if something broke or got fixed.
## Why no Cloudflare
At first the bot could work through Cloudflare, and I had to drop it. Cloudflare terminates TLS on its side and opens a new connection to the server. Reality lives right inside the client's TLS handshake, and the server has to see it untouched. Reality can't work behind Cloudflare, so I removed the support.
## Where the real work is
When I looked at the code, almost all of the bot's work came down to one thing: take a bare Debian, lock it down and then manage it by key. Roughly what I described in [initial Debian server setup](/en/blog/zero-setup-debian-server/), just without a human. 3x-ui is only one of the tasks on top of that.
So I moved the server setup code into a separate package, `serverus`. It's an ansible-based library with no Django and no Telegram and a clear programmatic API. The Telegram part also became a separate package. The Django app stayed as the glue with the business logic.
## Second scenario: a docker server
With that foundation in place, the next scenario comes together quickly. I built a docker server:
- the bot installs Docker, Caddy and automatic system updates, and Caddy issues certificates for domains
- an app is a folder with `docker-compose.yaml`, `.env` and `caddy.conf`, sent to the bot as an archive or file by file
- before applying, the bot shows a diff and per-file warnings, and warns you if a domain doesn't point to the server
- while applying, it checks the compose file, busy ports and the Caddy config, and if something is wrong it leaves the app untouched
- only the changed services are recreated
- the "Roll back" button restores the previous version of the files
- there are start, stop, restart and logs per service, login to a private docker registry and guest access to volumes for uploading data
The example in the repo is a self-hosted RustDesk server.
## One codebase, different bots
What the bot can do is decided by the interface selected in the admin panel: "MyPoint VPN", "Serverus" or both. It switches on the fly, without a restart. That's how one codebase makes different bots.
## The bot hosts itself
My favorite part. Production MyPointVPN is deployed as a Serverus app on a docker server managed by this very bot. To the bot, its own new version is just another app, like RustDesk.
## Stack
Python, Django, PostgreSQL, RabbitMQ + dramatiq (background jobs with a supervisor), ansible, a self-hosted Telegram Bot API server, Docker, Caddy, 3x-ui v3.8.5.
## Try it and talk about it
The project has its own page: **[mypointvpn.sokolab.xyz](https://mypointvpn.sokolab.xyz/en/)**.
- [The bot](https://t.me/barbarisiska_bot)
- [Code](https://git.sokolab.xyz/s0k0l/mypointvpn), with a wiki: deployment, data migration and how a Serverus app is structured
- [Issues in Redmine](https://redmine.sokolab.xyz/projects/mypointvpn)
- [Forum for questions and ideas](https://redmine.sokolab.xyz/boards/2/topics/3), with a guest login, no sign-up needed
## What's next
Serverus will most likely become a separate project. I'll write about it in a separate post, and it will get its own landing page. To be continued.
+83
View File
@@ -0,0 +1,83 @@
---
title: "MyPointVPN: как бот для VPN научился управлять серверами"
date: 2026-10-13T12:00:00+04:00
draft: false
tags:
- telegram
- vpn
- server
- ansible
- docker
summary: "Начинал с бота для портфолио, который ставит 3x-ui. По дороге выделил пакет Serverus, который управляет сервером целиком. Теперь бот живет на сервере, которым сам же и управляет"
---
23 сентября я сделал первый коммит в проект для портфолио: Телеграм-бот, который сам ставит и настраивает панель 3x-ui, чтобы у человека был свой VPN без ручной возни. Прошло три недели и около 40 коммитов, и VPN оказался не самой интересной частью.
## Как это выглядит
Вы арендуете сервер с Debian 12 или 13 у любого хостера, получаете IP и пароль root и присылаете боту одну строку `ip:пароль`. Бот переводит ssh на случайный порт со входом только по ключу, включает фаервол, обновляет систему и дальше управляет сервером сам.
Сервер можно забрать в любой момент кнопкой «Вернуть». Бот уберет свои доступы, откатит ssh и фаервол и пришлет новый пароль root. Данные на сервере останутся.
## VPN
Ради этого все и затевалось. Бот ставит 3x-ui и настраивает:
- подключение VLESS + XHTTP + Reality на 443 порту, цель для Reality бот подбирает сам
- по желанию сервер маскировки: клиенты подключаются к нему, он пересылает трафик на панель, и адрес панели нигде не светится
- домен с сертификатом Let's Encrypt для панели и подписки, а без домена короткий сертификат на IP, который продлевается автоматически
- прокси на выходе: свой или Cloudflare WARP
- шифрованный DNS на сервере: DNS-over-TLS, а если его режут, запасной DNS-over-HTTPS
Ключи клиентов бот выпускает и отзывает сам и выдает ссылки на подписку для v2rayN, Hiddify, Happ и v2rayNG. Ограничений на ключи нет: ни по трафику, ни по сроку, ни по устройствам. По каждому ключу видно трафик и когда клиент был онлайн.
Раз в сутки бот проверяет панель, Xray и сертификат подписки и пишет, если что-то сломалось или починилось.
## Почему без Cloudflare
Сначала бот умел работать через Cloudflare, и от этого пришлось отказаться. Cloudflare сам завершает TLS у себя и открывает до сервера новое соединение. А Reality живет как раз внутри TLS-рукопожатия клиента, и сервер должен увидеть его как есть. За Cloudflare Reality работать не может, так что поддержку я убрал.
## Где на самом деле работа
Когда я посмотрел на код, оказалось, что почти вся работа бота сводится к одному: взять голый Debian, закрыть его и дальше управлять им по ключу. Примерно то, что я описывал в [первичной настройке сервера](/blog/zero-setup-debian-server/), только без человека. 3x-ui лишь одна из задач поверх этого.
Поэтому код настройки серверов я выделил в отдельный пакет `serverus`. Это библиотека на ansible без Django и без Telegram с понятным программным API. Телеграм-часть тоже стала отдельным пакетом. Django-приложение осталось связующим звеном с бизнес-логикой.
## Второй сценарий: docker-сервер
Когда такая основа есть, следующий сценарий собирается быстро. Я сделал docker-сервер:
- бот ставит Docker, Caddy и автообновления системы, сертификаты для доменов выпускает Caddy
- приложение это папка с `docker-compose.yaml`, `.env` и `caddy.conf`, ее можно прислать боту архивом или по файлам
- перед применением бот показывает разницу и предупреждения по файлам, а если домен не указывает на сервер, тоже предупредит
- при применении бот проверяет compose, занятость портов и конфиг Caddy, и если что-то не так, приложение не трогает
- пересоздаются только изменившиеся сервисы
- кнопка «откатить» возвращает предыдущую версию файлов
- есть старт, стоп, рестарт и логи по сервисам, логин в приватный docker registry и гостевой доступ к volumes, чтобы залить данные
В примере к проекту лежит свой сервер RustDesk.
## Один код, разные боты
Что умеет бот, решает интерфейс, выбранный в админке: «MyPoint VPN», «Serverus» или оба сразу. Переключается на ходу, без перезапуска. Так из одного кода получаются разные боты.
## Бот держит сам себя
Моя любимая часть. Прод MyPointVPN развернут как приложение Serverus на docker-сервере, которым управляет этот же бот. Новая версия бота для него такое же приложение, как RustDesk.
## Стек
Python, Django, PostgreSQL, RabbitMQ + dramatiq (фоновые задачи с супервизором), ansible, собственный сервер Telegram Bot API, Docker, Caddy, 3x-ui v3.8.5.
## Попробовать и обсудить
У проекта есть своя страница: **[mypointvpn.sokolab.xyz](https://mypointvpn.sokolab.xyz/)**.
- [Бот](https://t.me/barbarisiska_bot)
- [Код](https://git.sokolab.xyz/s0k0l/mypointvpn), там же вики: деплой, перенос данных и как устроено приложение для Serverus
- [Задачи в Redmine](https://redmine.sokolab.xyz/projects/mypointvpn)
- [Форум для вопросов и идей](https://redmine.sokolab.xyz/boards/2/topics/3), там же гостевой вход, регистрироваться не обязательно
## Что дальше
Serverus, скорее всего, станет отдельным проектом. Расскажу о нем отдельной записью, и у него будет свой лендинг. Продолжение следует.
+91
View File
@@ -0,0 +1,91 @@
---
title: "MyPoint VPN"
tagline: "Your own VPN on your own server in one message"
description: "Rent a server, send the Telegram bot its IP and password, and it locks the server down, installs 3x-ui and sets up VLESS + Reality. Client keys and subscriptions come from the bot too."
domain: "https://mypointvpn.sokolab.xyz/"
badge: "Telegram bot · Debian 12 and 13"
theme: mypointvpn
fonts: "family=Unbounded:wght@500;700&family=Manrope:wght@400;600;800&family=JetBrains+Mono:wght@400;600"
post: "/blog/mypointvpn-serverus"
postLabel: "How a VPN bot learned to manage servers"
build:
list: never
sitemap:
disable: true
nav:
- { label: "Features", href: "#features" }
- { label: "Get started", href: "#start" }
- { label: "Serverus", href: "#engine" }
- { label: "Community", href: "#community" }
cta:
- { label: "Open the bot", href: "https://t.me/barbarisiska_bot", primary: true }
- { label: "Get started", href: "#start" }
chat:
title: "What it looks like in the bot"
messages:
- { from: "me", text: "`203.0.113.7:root-password`" }
- { from: "bot", text: "ssh moved to a random port, key-only login. Firewall on, system updated." }
- { from: "bot", text: "3x-ui installed: VLESS + XHTTP + Reality on port 443." }
- { from: "me", text: "Issue a key" }
- { from: "bot", text: "Done. Here is the subscription link for your client." }
features:
title: "What you get"
items:
- title: "VLESS + XHTTP + Reality"
text: "A connection on port 443. The bot picks the Reality target itself."
- title: "Camouflage server"
text: "Optional. Clients connect to it, it forwards traffic to the panel, and the panel's address is never exposed."
- title: "Certificates"
text: "A domain with Let's Encrypt for the panel and the subscription. Without a domain the bot issues a short-lived IP certificate and renews it itself."
- title: "Keys without limits"
text: "No traffic cap, no expiry, no device limit. Subscriptions for v2rayN, Hiddify, Happ and v2rayNG. For every key you see the traffic and when the client was last online."
- title: "Proxy and DNS"
text: "Outbound through your own proxy or Cloudflare WARP. DNS on the server is encrypted: DNS-over-TLS, with DNS-over-HTTPS as a fallback if TLS gets blocked."
- title: "Health checks"
text: "Once a day the bot checks the panel, Xray and the subscription certificate and tells you if something broke or got fixed."
start:
title: "Get started"
steps:
- title: "Rent a server"
text: "Any hosting provider, Debian 12 or 13. All you need is the IP and the root password."
- title: "Send the bot `ip:password`"
text: "The bot moves ssh to a random port with key-only login, turns on a firewall, updates the system and installs 3x-ui."
- title: "Issue a key"
text: "Add the subscription link to your client, and the VPN works."
note:
title: "The server stays yours"
text: "The \"Return\" button removes the bot's access, rolls back ssh and the firewall and sends you a new root password. Your data on the server stays. And the bot deliberately doesn't work through Cloudflare: Cloudflare terminates TLS itself, and Reality can't work behind it."
engine:
kicker: "Under the hood"
title: "Serverus"
lead: "Almost all of the bot's work comes down to one thing: take a bare Debian, lock it down and manage it by key. That's the job of `serverus`, an ansible-based package inside the project. The VPN is just one task on top of it, the second one is a docker server."
items:
- "Docker, Caddy and automatic system updates, with Caddy issuing certificates for domains"
- "An app is a folder with `docker-compose.yaml`, `.env` and `caddy.conf`, sent as an archive or file by file"
- "A diff and per-file warnings before applying, including when a domain doesn't point to the server"
- "Checks of the compose file, ports and Caddy config. On error the app is left untouched, and only changed services are recreated"
- "Rollback to the previous version of the files, start, stop, restart and logs per service"
- "Login to a private docker registry and guest access to volumes for uploading data"
callout:
title: "The bot hosts itself"
text: "Production MyPoint VPN runs as a Serverus app on a docker server managed by this very bot. What the bot can do is set by the interface in the admin panel: \"MyPoint VPN\", \"Serverus\" or both."
community:
title: "Community"
lead: "Questions, ideas and bugs go to Redmine. No sign-up needed: the guest login is in the [forum topic](https://redmine.sokolab.xyz/boards/2/topics/3)."
links:
- { title: "Bot", text: "Start using it in Telegram.", href: "https://t.me/barbarisiska_bot" }
- { title: "Code", text: "Sources and wiki: deployment, data migration, Serverus apps.", href: "https://git.sokolab.xyz/s0k0l/mypointvpn" }
- { title: "Issues", text: "Bugs and wishes in Redmine.", href: "https://redmine.sokolab.xyz/projects/mypointvpn" }
- { title: "Forum", text: "Questions and ideas, plus the guest login.", href: "https://redmine.sokolab.xyz/boards/2/topics/3" }
- { title: "Blog post", text: "How a VPN bot learned to manage servers.", href: "https://sokolab.xyz/en/blog/mypointvpn-serverus/" }
footer: "Use a VPN within the laws of your country."
---
+91
View File
@@ -0,0 +1,91 @@
---
title: "MyPoint VPN"
tagline: "Свой VPN на своем сервере за одно сообщение"
description: "Арендуйте сервер, пришлите Телеграм-боту IP и пароль, и он сам закроет сервер, поставит 3x-ui и настроит VLESS + Reality. Ключи и подписки для клиентов тоже выдает бот."
domain: "https://mypointvpn.sokolab.xyz/"
badge: "Телеграм-бот · Debian 12 и 13"
theme: mypointvpn
fonts: "family=Unbounded:wght@500;700&family=Manrope:wght@400;600;800&family=JetBrains+Mono:wght@400;600"
post: "/blog/mypointvpn-serverus"
postLabel: "Как бот для VPN научился управлять серверами"
build:
list: never
sitemap:
disable: true
nav:
- { label: "Возможности", href: "#features" }
- { label: "Как начать", href: "#start" }
- { label: "Serverus", href: "#engine" }
- { label: "Сообщество", href: "#community" }
cta:
- { label: "Открыть бота", href: "https://t.me/barbarisiska_bot", primary: true }
- { label: "Как начать", href: "#start" }
chat:
title: "Так это выглядит в боте"
messages:
- { from: "me", text: "`203.0.113.7:пароль-root`" }
- { from: "bot", text: "ssh перенесен на случайный порт, вход только по ключу. Фаервол включен, система обновлена." }
- { from: "bot", text: "3x-ui установлена: VLESS + XHTTP + Reality на 443 порту." }
- { from: "me", text: "Выпустить ключ" }
- { from: "bot", text: "Готово. Вот ссылка на подписку для клиента." }
features:
title: "Что вы получаете"
items:
- title: "VLESS + XHTTP + Reality"
text: "Подключение на 443 порту. Цель для Reality бот подбирает сам."
- title: "Сервер маскировки"
text: "По желанию. Клиенты подключаются к нему, он пересылает трафик на панель, и адрес панели нигде не светится."
- title: "Сертификаты"
text: "Домен с Let's Encrypt для панели и подписки. Без домена бот выпустит короткий сертификат на IP и будет продлевать его сам."
- title: "Ключи без ограничений"
text: "Ни по трафику, ни по сроку, ни по устройствам. Подписки для v2rayN, Hiddify, Happ и v2rayNG. По каждому ключу видно трафик и когда клиент был онлайн."
- title: "Прокси и DNS"
text: "На выходе свой прокси или Cloudflare WARP. DNS на сервере шифруется: DNS-over-TLS, а если его режут, запасной DNS-over-HTTPS."
- title: "Присмотр"
text: "Раз в сутки бот проверяет панель, Xray и сертификат подписки и пишет, если что-то сломалось или починилось."
start:
title: "Как начать"
steps:
- title: "Арендуйте сервер"
text: "Любой хостер, Debian 12 или 13. Нужны только IP и пароль root."
- title: "Пришлите боту `ip:пароль`"
text: "Бот переведет ssh на случайный порт со входом по ключу, включит фаервол, обновит систему и поставит 3x-ui."
- title: "Выпустите ключ"
text: "Добавьте ссылку на подписку в клиент, и VPN работает."
note:
title: "Сервер остается вашим"
text: "Кнопка «Вернуть» убирает доступы бота, откатывает ssh и фаервол и присылает новый пароль root. Данные на сервере остаются. А через Cloudflare бот не работает специально: Cloudflare сам завершает TLS, и Reality за ним работать не может."
engine:
kicker: "Под капотом"
title: "Serverus"
lead: "Почти вся работа бота сводится к одному: взять голый Debian, закрыть его и управлять им по ключу. Этим занимается `serverus`, пакет на ansible внутри проекта. VPN лишь одна из задач поверх него, вторая это docker-сервер."
items:
- "Docker, Caddy и автообновления системы, сертификаты для доменов выпускает Caddy"
- "Приложение это папка с `docker-compose.yaml`, `.env` и `caddy.conf`, архивом или по файлам"
- "Перед применением разница и предупреждения по файлам, в том числе если домен не указывает на сервер"
- "Проверка compose, портов и конфига Caddy. При ошибке приложение не трогается, пересоздаются только изменившиеся сервисы"
- "Откат на предыдущую версию файлов, старт, стоп, рестарт и логи по сервисам"
- "Логин в приватный docker registry и гостевой доступ к volumes для заливки данных"
callout:
title: "Бот держит сам себя"
text: "Прод MyPoint VPN развернут как приложение Serverus на docker-сервере, которым управляет этот же бот. Что умеет бот, решает интерфейс в админке: «MyPoint VPN», «Serverus» или оба сразу."
community:
title: "Сообщество"
lead: "Вопросы, идеи и баги принимаются в Redmine. Регистрироваться не обязательно: гостевой вход описан в [теме на форуме](https://redmine.sokolab.xyz/boards/2/topics/3)."
links:
- { title: "Бот", text: "Начать пользоваться в Телеграме.", href: "https://t.me/barbarisiska_bot" }
- { title: "Код", text: "Исходники и вики: деплой, перенос данных, приложения Serverus.", href: "https://git.sokolab.xyz/s0k0l/mypointvpn" }
- { title: "Задачи", text: "Баги и пожелания в Redmine.", href: "https://redmine.sokolab.xyz/projects/mypointvpn" }
- { title: "Форум", text: "Вопросы и идеи, там же гостевой вход.", href: "https://redmine.sokolab.xyz/boards/2/topics/3" }
- { title: "Запись в блоге", text: "Как бот для VPN научился управлять серверами.", href: "https://sokolab.xyz/blog/mypointvpn-serverus/" }
footer: "Используйте VPN в рамках законов вашей страны."
---
+66 -4
View File
@@ -6,7 +6,13 @@
*/ -}}
{{- $p := .Params -}}
{{- $home := cond (eq .Language.Lang hugo.Sites.Default.Language.Lang) "/" (printf "/%s/" .Language.Lang) -}}
{{- $css := resources.Get "css/landing.css" | minify | fingerprint -}}
{{- $css := resources.Get "css/landing.css" -}}
{{- /* params.theme adds css/landing-<theme>.css on top of the base styles */ -}}
{{- with $p.theme }}{{ with resources.Get (printf "css/landing-%s.css" .) }}
{{- $css = slice $css . | resources.Concat (printf "css/landing-%s.bundle.css" $p.theme) }}
{{- end }}{{ end -}}
{{- $css = $css | minify | fingerprint -}}
{{- $fonts := $p.fonts | default "family=Inter:wght@400;600;800&family=JetBrains+Mono:wght@400;600" -}}
<!doctype html>
<html lang="{{ .Language.Lang }}">
<head>
@@ -26,10 +32,10 @@
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?{{ $fonts | safeURL }}&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ $css.RelPermalink }}" integrity="{{ $css.Data.Integrity }}">
</head>
<body>
<body{{ with $p.theme }} class="theme-{{ . }}"{{ end }}>
<header class="nav">
<div class="wrap nav__row">
<a class="nav__logo" href="{{ $home }}">{{ .Title }}</a>
@@ -44,14 +50,24 @@
</header>
<main>
<section class="hero">
<section class="hero{{ if $p.chat }} hero--split{{ end }}">
<div class="wrap">
<div class="hero__text">
{{ with $p.badge }}<p class="badge">{{ . }}</p>{{ end }}
<h1 class="hero__title">{{ $p.tagline }}</h1>
<p class="hero__lead">{{ .Description }}</p>
<div class="hero__cta">
{{ range $p.cta }}<a class="btn{{ if .primary }} btn--primary{{ end }}" href="{{ .href }}"{{ if hasPrefix .href "http" }} target="_blank" rel="noopener"{{ end }}>{{ .label }}</a>{{ end }}
</div>
</div>
{{ with $p.chat }}
<div class="chat" aria-label="{{ .title }}">
<p class="chat__title">{{ .title }}</p>
{{ range .messages }}
<p class="chat__msg chat__msg--{{ .from }}">{{ .text | markdownify }}</p>
{{ end }}
</div>
{{ end }}
</div>
</section>
@@ -103,6 +119,52 @@
</section>
{{ end }}
{{ with $p.start }}
<section class="section" id="start">
<div class="wrap">
<h2>{{ .title }}</h2>
<ol class="start">
{{ range .steps }}
<li>
<h3>{{ .title | markdownify }}</h3>
<p>{{ .text | markdownify }}</p>
</li>
{{ end }}
</ol>
</div>
</section>
{{ end }}
{{ with $p.note }}
<section class="section" id="note">
<div class="wrap">
<aside class="note">
<h2>{{ .title }}</h2>
<p>{{ .text | markdownify }}</p>
</aside>
</div>
</section>
{{ end }}
{{ with $p.engine }}
<section class="section engine" id="engine">
<div class="wrap">
{{ with .kicker }}<p class="engine__kicker">{{ . }}</p>{{ end }}
<h2>{{ .title }}</h2>
<p class="section__lead">{{ .lead | markdownify }}</p>
<ul class="engine__list">
{{ range .items }}<li>{{ . | markdownify }}</li>{{ end }}
</ul>
{{ with .callout }}
<div class="engine__callout">
<h3>{{ .title }}</h3>
<p>{{ .text | markdownify }}</p>
</div>
{{ end }}
</div>
</section>
{{ end }}
{{ with $p.limits }}
<section class="section section--alt" id="limits">
<div class="wrap">