Operar el servidor
Instalar el servidor
Cómo instalar y operar el servidor Pabal con Docker Compose en un único servidor accesible desde internet.
Cómo instalar el servidor Pabal en un único servidor accesible desde internet. Con Docker Compose se levantan tres contenedores, servidor Pabal · PostgreSQL · Caddy (HTTPS), y todos los datos que no deben perderse se montan (bind mount) en directorios del host. Basta con seguir los pasos en orden, de arriba abajo.
Dominio pabal.me, IP pública del servidor 203.0.113.10 (una dirección de ejemplo: sustitúyela por tu IP real al leer), código fuente en /opt/pabal, datos en /srv/pabal, sistema operativo Ubuntu 24.04 LTS. Los archivos necesarios están en la carpeta deploy/ del repositorio.
Visión general
Explicación del diagrama
- Tres zonas: usuarios en internet → los tres contenedores del servidor de producción → los directorios montados en el disco del host. Los contenedores se pueden borrar y volver a crear en cualquier momento; todo lo que debe perdurar está en
/srv/pabal. - Solo hay dos entradas: la aplicación se conecta directamente al servidor por el puerto MTProto (MTProto se cifra por sí mismo, así que no necesita HTTPS), y el sitio web, la documentación y la Bot API pasan por el 443 de Caddy. Caddy bloquea las rutas de administración (
/admin,/health…) y deja pasar el resto. - La línea discontinua roja es el camino reservado al operador. El panel de administración solo está abierto en
127.0.0.1:8080del servidor, así que solo se entra por un túnel SSH. PostgreSQL no tiene ningún puerto abierto hacia fuera. - Las dos casillas rojas son los datos más importantes. La clave RSA de
pabal_server/keysva incorporada en la aplicación al compilarla, así que si la pierdes tendrás que redistribuir todas las aplicaciones; y enpostgres_data/están todas las cuentas y conversaciones. Son la prioridad número uno de las copias de seguridad. - Las flechas grises son escrituras: el servidor escribe claves, fotos, ajustes y logs; PostgreSQL, los eventos; y Caddy, los certificados, cada uno en su propio directorio.
Requisitos
| Concepto | Detalle |
|---|---|
| Servidor | Ubuntu 24.04 LTS; para empezar, 2 vCPU · 4 GB de RAM · 40 GB de SSD. Cuando crezcan los usuarios y los mensajes, amplía primero la memoria |
| IPv4 pública | IP fija. La aplicación se compila con la dirección IP del servidor, y el servidor también comunica su IP a la aplicación |
| Dominio | Un dominio cuyo DNS puedas modificar (registro A) |
| Medio de entrega de los códigos de registro | Una cuenta de SMS (Twilio · Solapi · webhook) o de correo electrónico (SMTP) |
| Mac | Un Mac en el que compilar la aplicación (Pabal.app) para este servidor |
Puertos
| Puerto | Quién | Abrir | Descripción |
|---|---|---|---|
| 22/tcp | Operador | Se recomienda solo la IP del operador | SSH |
| 80/tcp | Caddy | A todos | Emisión de certificados · redirección a HTTPS |
| 443/tcp, 443/udp | Caddy | A todos | Sitio web · documentación · Bot API (udp es HTTP/3) |
| 8443/tcp | Servidor | A todos | Conexión de la aplicación (MTProto) |
| 8080/tcp | Servidor | No abrir | Panel de administración · comprobación de estado: solo en 127.0.0.1 del servidor |
| 5432/tcp | PostgreSQL | No abrir | Solo en la red interna de los contenedores |
El 443 lo usa el sitio web (HTTPS). MTProto no es ni HTTP ni TLS, así que no se puede repartir el tráfico de un mismo puerto entre ambos. Si, por las redes de empresas o centros educativos que bloquean los puertos poco habituales, quieres que la aplicación también se conecte por el 443, consigue una IP más y asigna el 443 de esa IP al servidor (en .env, pon en PUBLIC_IP la segunda IP y MTPROTO_PORT=443, y reparte los puertos del archivo compose añadiéndoles la IP). El servidor le dice a la aplicación «conéctate a este puerto de esta IP», así que mantén igual el puerto de la aplicación dentro y fuera del contenedor.
1. Preparar el servidor
sudo apt update && sudo apt -y upgrade
timedatectl # comprueba que diga "System clock synchronized: yes"
La sincronización de la hora es imprescindible. Los números de los mensajes MTProto se derivan de la hora, así que si el reloj del servidor se desvía, la aplicación reconecta una y otra vez. Si aparece no, ejecuta sudo timedatectl set-ntp true.
# Cortafuegos
sudo ufw allow OpenSSH # si es posible: sudo ufw allow from <IP del operador> to any port 22
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw allow 8443/tcp
sudo ufw enable
# Docker (script de instalación oficial)
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER # al volver a iniciar sesión, docker funciona sin sudo
docker compose version # v2 o posterior
Los puertos que abre Docker (ports:) se abren independientemente de las reglas de ufw. Por eso el archivo compose solo abre los puertos públicos (80, 443 y 8443), abre el puerto de administración únicamente como 127.0.0.1:8080 y no abre PostgreSQL. Tenlo presente cuando modifiques ports:.
2. Obtener el código fuente
sudo mkdir -p /opt/pabal && sudo chown $USER: /opt/pabal
git clone <dirección del repositorio> /opt/pabal
Si lo copias desde tu ordenador de desarrollo, no envíes las claves, los datos ni los logs (rsync --exclude 'keys/' --exclude 'data/' --exclude 'logs/' --exclude '**/target/'). La clave RSA del servidor de producción la genera el propio servidor de producción la primera vez que arranca: si usas en producción la clave de desarrollo, ese ordenador de desarrollo se convierte en un lugar desde el que se puede descifrar el tráfico de producción.
3. Directorios de datos (bind mount)
sudo mkdir -p /srv/pabal/{postgres_data,pabal_server/keys,pabal_server/data,pabal_server/logs,caddy/data,caddy/config,backups}
sudo chown -R 1000:1000 /srv/pabal/pabal_server # el contenedor del servidor se ejecuta con uid 1000
sudo chown -R $USER: /srv/pabal/backups
sudo chmod 700 /srv/pabal/pabal_server/keys /srv/pabal/backups
| Directorio | Dentro del contenedor | Contenido | Si se pierde |
|---|---|---|---|
pabal_server/keys | /app/keys | private.pem (clave privada RSA del servidor), private.pem.pub | Hay que recompilar y redistribuir todas las aplicaciones |
pabal_server/data | /app/data | Fotos (media/), admin-token, operations.json (ajustes de códigos de registro, SMS y SMTP, secretos incluidos) | Fotos y ajustes de operación |
pabal_server/logs | /app/logs | Logs del servidor (comprimidos a diario) | Solo el historial |
postgres_data | /var/lib/postgresql/data | Todas las cuentas, conversaciones, mensajes, inicios de sesión, bots y ajustes de webhooks | Todo el servicio |
caddy/data, caddy/config | /data, /config | Certificados HTTPS | Se vuelven a emitir |
backups | (solo en el host) | Resultados de backup.sh | — |
4. Archivo de configuración .env
cd /opt/pabal/deploy
cp .env.example .env
chmod 600 .env
openssl rand -base64 30 | tr -d '/+=' | cut -c1-32 # pon el valor resultante en POSTGRES_PASSWORD
nano .env
DOMAIN=pabal.me
PUBLIC_IP=203.0.113.10
POSTGRES_PASSWORD=(el valor generado arriba)
PABAL_HOME=/srv/pabal
PABAL_SOURCE=..
PABAL_NETWORK=ssemiya-net
MTPROTO_PORT=8443
HTTP_PORT=80
HTTPS_PORT=443
ADMIN_PORT=8080
ADMIN_TOKEN=
JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=100"
| Valor | Qué poner | Notas |
|---|---|---|
DOMAIN | El dominio del sitio web | Los enlaces que crea la aplicación (pabal.me/nombre_de_usuario), las direcciones de la documentación y las vistas previas de enlaces también usan esta dirección |
PUBLIC_IP | La IPv4 pública del servidor | En la nube, la IP pública que aparece en la consola (no la IP privada interna del servidor) |
POSTGRES_PASSWORD | Un valor aleatorio | Fíjalo antes del primer arranque. PostgreSQL solo usa este valor cuando crea el directorio de datos por primera vez |
PABAL_HOME | /srv/pabal | El directorio del paso 3 |
PABAL_SOURCE | .. | El repositorio desde el que se construye la imagen del servidor: .. si el archivo compose está en deploy/ del repositorio; si no, la ruta del código fuente |
PABAL_NETWORK | ssemiya-net | La red de Docker a la que se unen los tres contenedores. Créala antes (paso 6) |
MTPROTO_PORT | 8443 | Si lo cambias, cambia también el cortafuegos y la compilación de la aplicación |
ADMIN_TOKEN | Déjalo vacío | Si se deja vacío, se crea en pabal_server/data/admin-token en el primer arranque. Si quieres fijarlo tú, al menos 16 caracteres |
JAVA_OPTS | El valor por defecto | Si falta memoria, sube -Xmx (hasta aproximadamente la mitad de la memoria del servidor) |
El archivo compose de producción arranca el servidor con los números de prueba desactivados (TELEGRAM_TEST_NUMBERS=false). La lista completa de valores de configuración que lee el servidor está en Administración y ajustes — variables de entorno.
5. DNS
| Nombre | Tipo | Valor |
|---|---|---|
pabal.me | A | 203.0.113.10 |
www.pabal.me | A | 203.0.113.10 |
dig +short pabal.me # debe devolver 203.0.113.10 (el cambio tarda de unos minutos a unas horas en propagarse)
Puedes arrancar antes de que el DNS apunte al servidor. Caddy seguirá reintentándolo solo hasta obtener el certificado.
6. Compilar y arrancar
cd /opt/pabal/deploy
docker network inspect ssemiya-net >/dev/null 2>&1 || docker network create ssemiya-net # solo una vez
docker compose up -d --build # la primera vez tarda unos minutos (Maven descarga las bibliotecas)
docker compose ps # pabal-server, pabal-postgres y pabal-caddy en running (healthy)
docker compose logs -f pabal-server # sal con Ctrl+C
En el primer arranque, el log muestra líneas como estas.
WARN ServerKeys - Generated a new RSA key at /app/keys/private.pem (fingerprint -3898654385185406269). Clients must embed /app/keys/private.pem.pub
INFO TelegramServer - JDBC persistence active (jdbc:postgresql://pabal-postgres:5432/pabal)
INFO BotFather - BotFather is user 100000
INFO AdminToken - Created the admin token in /app/data/admin-token
INFO TelegramServer - Website: http://0.0.0.0:8080/ (published at https://pabal.me/)
- La primera línea es un WARN, pero es normal (avisa de que la clave se genera, solo esta primera vez). A partir de entonces aparece
Loaded RSA key … (fingerprint …). El fingerprint es distinto en cada servidor. - El servidor crea las tablas por sí solo al arrancar.
7. Comprobación
# Desde fuera (tu ordenador)
curl -sI https://pabal.me/ | head -1 # HTTP/2 200
curl -s -o /dev/null -w '%{http_code}\n' https://pabal.me/docs/ # 200 — esta documentación
curl -s -o /dev/null -w '%{http_code}\n' https://pabal.me/admin/ # 404 — el panel de administración no es público
curl -s https://pabal.me/docs/server-key.pem | head -1 # -----BEGIN RSA PUBLIC KEY-----
nc -vz 203.0.113.10 8443 # succeeded — puerto de la aplicación
# En el servidor
curl -s http://127.0.0.1:8080/health # {"status":"UP",…}
ls -l /srv/pabal/pabal_server/keys # private.pem(600), private.pem.pub
8. Panel de administración y primeros ajustes
# En tu ordenador (déjalo abierto)
ssh -N -L 8080:127.0.0.1:8080 <usuario>@203.0.113.10
# El token, en el servidor
sudo cat /srv/pabal/pabal_server/data/admin-token
Abre http://localhost:8080/admin/ en el navegador e introduce el token. Ajustes imprescindibles antes de entrar en producción (los detalles están en Administración y ajustes):
- Pon el método de entrega del código (코드 전달 방식) en SMS o correo electrónico. Con el método «panel de administración» (관리 화면), el operador tiene que comunicar los códigos uno a uno.
- Introduce los datos de SMS o de correo electrónico (SMTP), guarda y comprueba con un envío de prueba (테스트 발송) que llegan de verdad.
- Comprueba que los números de prueba (테스트 번호) están desactivados. Si están activados, cualquiera puede iniciar sesión con un número
+99966…. - Si lo necesitas, desactiva Permitir registros nuevos (새 가입 허용) al empezar y da de alta primero solo a las personas que invites.
9. Conectar la aplicación a este servidor
La aplicación se compila con la IP, el puerto y la clave pública del servidor incorporados. Para que se conecte a este servidor, hay que volver a compilarla con la clave pública de este servidor. En el Mac:
# 1. La clave pública de este servidor (la pública, no la privada: también puedes descargarla del sitio de documentación)
curl -s -o keys/production.pem.pub https://pabal.me/docs/server-key.pem
# 2. Apuntar el código fuente de la aplicación a este servidor + aplicar la marca
python3 scripts/tdesktop/point_to_server.py ~/Developer/tdesktop \
--host 203.0.113.10 --port 8443 --key keys/production.pem.pub
python3 scripts/tdesktop/apply_branding.py ~/Developer/tdesktop
Después, sigue los pasos 5 (configure) y 6 (compilación) de docs/tdesktop-build-guide.md del repositorio. Copia el resultado, out/Debug/Pabal.app, con otro nombre: si vuelves a compilar el mismo código fuente para otro servidor, se sobrescribe.
La compilación actual es una compilación de depuración sin firma ni notarización, así que quien la reciba tendrá que hacer clic derecho → Abrir la primera vez que la abra. Una compilación de depuración guarda sus datos en una carpeta junto a la aplicación (tdata/) y, si no puede escribir allí, usa la misma carpeta de datos que la auténtica Telegram Desktop. Indica a los usuarios que coloquen la aplicación en una carpeta de su usuario (por ejemplo, ~/Applications/Pabal/). Una compilación release para distribución general (carpeta de datos separada, firma y notarización de Apple) es un trabajo aparte.
Operación
Estado y logs
cd /opt/pabal/deploy
docker compose ps
docker compose logs --since 1h pabal-server
tail -f /srv/pabal/pabal_server/logs/telegram-server.log
Actualizar
cd /opt/pabal && git pull
cd deploy
./backup.sh # primero, copia de seguridad
docker compose up -d --build pabal-server # sustituye solo el servidor por la imagen nueva
docker compose logs --since 5m pabal-server | grep -E "Website|ERROR"
Durante las decenas de segundos que tarda el servidor en volver a arrancar, la aplicación se desconecta y después se reconecta sola sin volver a iniciar sesión. El servidor aplica solo los cambios de tablas al arrancar. Los directorios montados no dependen de la imagen, así que docker compose down o borrar la imagen no borra los datos: simplemente, no borres /srv/pabal.
Copias de seguridad
./backup.sh # → /srv/pabal/backups/<fecha-hora>/{pabal.dump, server-keys-data.tar.gz}
crontab -e # todos los días a las 03:00:
# 0 3 * * * /opt/pabal/deploy/backup.sh >> /srv/pabal/backups/backup.log 2>&1
- Las copias de más de 14 días se borran solas (se puede cambiar, por ejemplo con
KEEP_DAYS=30 ./backup.sh). - Cópialas también a otro sitio. Una copia en el mismo disco no sobrevive a una avería del disco.
- Las copias contienen la clave privada RSA y los secretos de SMS y SMTP. Protege el lugar donde las guardes tanto como el servidor.
Restaurar
cd /opt/pabal/deploy
B=/srv/pabal/backups/20260920-030000 # copia que se va a restaurar
docker compose stop pabal-server pabal-caddy
docker compose exec -T pabal-postgres dropdb -U pabal pabal
docker compose exec -T pabal-postgres createdb -U pabal pabal
docker compose exec -T pabal-postgres pg_restore -U pabal -d pabal --no-owner < $B/pabal.dump
sudo tar -C /srv/pabal/pabal_server -xzf $B/server-keys-data.tar.gz
sudo chown -R 1000:1000 /srv/pabal/pabal_server
docker compose up -d
Para migrar a un servidor nuevo se hace lo mismo: sigue los pasos 1 a 5 y, en lugar del paso 6, haz la restauración de arriba y arranca. Si restauras la misma clave, no hace falta volver a compilar la aplicación (si cambió la IP del servidor, sí hay que recompilarla).
Arrancar y detener
docker compose restart pabal-server # reinicia solo el servidor
docker compose stop # detiene todo (los datos se conservan)
docker compose up -d # vuelve a arrancar
Aunque reinicies la máquina, los contenedores vuelven a arrancar solos porque tienen restart: unless-stopped.
Lista de comprobación de seguridad
.envcon permisos 600 yPOSTGRES_PASSWORDcon un valor aleatorio/srv/pabal/pabal_server/keyscon permisos 700, y la clave privada no está en ningún otro sitio salvo las copias de seguridad- Cortafuegos: solo 22 (si es posible, solo desde la IP del operador), 80, 443 y 8443
- Desde fuera,
https://dominio/admin/devuelve 404 - Panel de administración: números de prueba (테스트 번호) desactivados, método de entrega del código (코드 전달 방식) en SMS o correo electrónico, envío de prueba (테스트 발송) correcto
TELEGRAM_WEBHOOK_ALLOW_LOCALsin activar (para que los webhooks de los bots no puedan ir a la red interna)- SSH: inicio de sesión con contraseña desactivado, solo con clave
- Cron de copias de seguridad y copia externa comprobados; restauración ensayada al menos una vez
- Actualizaciones de seguridad automáticas del sistema operativo del servidor (
unattended-upgrades)
Solución de problemas
| Síntoma | Causa → solución |
|---|---|
| La aplicación no pasa de «Conectando…» | ① El 8443 está bloqueado → nc -vz IP 8443, revisa ufw y el grupo de seguridad de la nube ② La aplicación se compiló con otra clave pública → repite el paso 9 con la clave de este servidor ③ PUBLIC_IP es incorrecta → corrige .env y ejecuta docker compose up -d pabal-server |
| Al principio se conecta, pero se corta enseguida | La aplicación pasa a la dirección que le indica el servidor (PUBLIC_IP:MTPROTO_PORT) y esa dirección es incorrecta → revisa PUBLIC_IP |
| Error de certificado HTTPS | El DNS todavía no apunta a este servidor o el puerto 80 está bloqueado → dig +short dominio, docker compose logs pabal-caddy |
server se reinicia una y otra vez, password authentication failed | Se cambió POSTGRES_PASSWORD después de crear los datos → vuelve al valor original o, dentro de la base de datos, ALTER USER pabal PASSWORD '…' |
AccessDeniedException: /app/keys/… | Propietario de los directorios montados → sudo chown -R 1000:1000 /srv/pabal/pabal_server |
OutOfMemoryError, lentitud | Sube -Xmx en JAVA_OPTS y ejecuta docker compose up -d pabal-server |
| No llega el código de registro | Estado de envío y motivo del fallo en la pestaña Códigos de registro (가입 코드) del panel de administración → valores de SMS y SMTP en Ajustes (설정), envío de prueba (테스트 발송) |
| No puedo entrar al panel de administración | Comprueba que el túnel SSH está abierto y si otro programa usa el 8080 de tu ordenador (cámbialo por -L 18080:127.0.0.1:8080 y usa localhost:18080) |
network ssemiya-net declared as external, but could not be found | La red todavía no existe → docker network create ssemiya-net (u otro nombre con PABAL_NETWORK en .env) |
Limitaciones conocidas
- Es una configuración de un solo servidor. Al arrancar, carga en memoria todas las cuentas, conversaciones y mensajes. Todavía no admite escalado horizontal repartido entre varias máquinas, y a medida que crecen los datos hay que ampliar la memoria.
- Los códigos de registro pendientes y las actualizaciones que los bots no han recogido están en memoria, así que se pierden al reiniciar el servidor.
- Las peticiones de códigos de registro tienen un intervalo mínimo y un límite diario, pero todavía no hay límite de frecuencia para el resto de las peticiones. Justo después de abrir el servicio al público, revisa a menudo los logs y la pestaña Resumen (대시보드) del panel de administración.
- No hay aplicaciones móviles ni notificaciones push. Los mensajes solo admiten fotos como adjuntos. Canales, supergrupos y verificación en dos pasos todavía no están disponibles.
- Las claves de autorización están en texto plano en la base de datos (el servidor las necesita para descifrar). Protege la base de datos y las copias de seguridad tanto como la clave RSA.
Archivos
| Archivo | Función |
|---|---|
deploy/docker-compose.yml | Configuración de producción (pabal-server · pabal-postgres · pabal-caddy, bind mounts) |
deploy/Caddyfile | HTTPS, bloqueo de las rutas de administración, reenvío del sitio, la documentación y la Bot API |
deploy/.env.example | Plantilla de configuración → deploy/.env |
deploy/backup.sh | Volcado de la base de datos + archivo de claves y datos, limpieza de copias antiguas |
Dockerfile | Imagen del servidor (compilación → JRE 21, uid 1000) |