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:
1 parent
6958d0a975
commit
672467a699
6 files changed
+538
-4
No files matched your search
@@ -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.
|
||||
@@ -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, скорее всего, станет отдельным проектом. Расскажу о нем отдельной записью, и у него будет свой лендинг. Продолжение следует.
|
||||
@@ -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."
|
||||
---
|
||||
@@ -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 в рамках законов вашей страны."
|
||||
---
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user