Docs para desarrolladores
Español

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.

Valores de ejemplo de este documento

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

Internet docker compose · ssemiya-net /srv/pabal (bind mount) Aplicación PabalPabal.app Navegador · botsSitio · Bot API OperadorSSH pabal-server MTProto :8443 Web·Bot API·admin:8080 uid 1000 · JRE 21 Webhooks → bots pabal-caddy:80 · :443 · TLS auto pabal-postgres 16Red interna · sin puerto pabal_server/keys · clave RSA pabal_server/data · fotos pabal_server/logs postgres_data/ · cuentas·chats caddy/ · certificados backups/ · backup.sh MTProto :8443 (cifrado propio) HTTPS :443 Túnel SSH → 127.0.0.1:8080/admin/
Un servidor, tres contenedores y todo lo que debe perdurar, en /srv/pabal

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:8080 del 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/keys va incorporada en la aplicación al compilarla, así que si la pierdes tendrás que redistribuir todas las aplicaciones; y en postgres_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

ConceptoDetalle
ServidorUbuntu 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úblicaIP 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
DominioUn dominio cuyo DNS puedas modificar (registro A)
Medio de entrega de los códigos de registroUna cuenta de SMS (Twilio · Solapi · webhook) o de correo electrónico (SMTP)
MacUn Mac en el que compilar la aplicación (Pabal.app) para este servidor

Puertos

PuertoQuiénAbrirDescripción
22/tcpOperadorSe recomienda solo la IP del operadorSSH
80/tcpCaddyA todosEmisión de certificados · redirección a HTTPS
443/tcp, 443/udpCaddyA todosSitio web · documentación · Bot API (udp es HTTP/3)
8443/tcpServidorA todosConexión de la aplicación (MTProto)
8080/tcpServidorNo abrirPanel de administración · comprobación de estado: solo en 127.0.0.1 del servidor
5432/tcpPostgreSQLNo abrirSolo en la red interna de los contenedores
Por qué el puerto de la aplicación no es el 443

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
Docker y ufw

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
DirectorioDentro del contenedorContenidoSi se pierde
pabal_server/keys/app/keysprivate.pem (clave privada RSA del servidor), private.pem.pubHay que recompilar y redistribuir todas las aplicaciones
pabal_server/data/app/dataFotos (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/logsLogs del servidor (comprimidos a diario)Solo el historial
postgres_data/var/lib/postgresql/dataTodas las cuentas, conversaciones, mensajes, inicios de sesión, bots y ajustes de webhooksTodo el servicio
caddy/data, caddy/config/data, /configCertificados HTTPSSe 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"
ValorQué ponerNotas
DOMAINEl dominio del sitio webLos 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_IPLa IPv4 pública del servidorEn la nube, la IP pública que aparece en la consola (no la IP privada interna del servidor)
POSTGRES_PASSWORDUn valor aleatorioFíjalo antes del primer arranque. PostgreSQL solo usa este valor cuando crea el directorio de datos por primera vez
PABAL_HOME/srv/pabalEl 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_NETWORKssemiya-netLa red de Docker a la que se unen los tres contenedores. Créala antes (paso 6)
MTPROTO_PORT8443Si lo cambias, cambia también el cortafuegos y la compilación de la aplicación
ADMIN_TOKENDéjalo vacíoSi 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_OPTSEl valor por defectoSi 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

NombreTipoValor
pabal.meA203.0.113.10
www.pabal.meA203.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):

  1. 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.
  2. Introduce los datos de SMS o de correo electrónico (SMTP), guarda y comprueba con un envío de prueba (테스트 발송) que llegan de verdad.
  3. Comprueba que los números de prueba (테스트 번호) están desactivados. Si están activados, cualquiera puede iniciar sesión con un número +99966….
  4. 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.

Antes de repartir la aplicación a otras personas

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

  • .env con permisos 600 y POSTGRES_PASSWORD con un valor aleatorio
  • /srv/pabal/pabal_server/keys con 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_LOCAL sin 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íntomaCausa → 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 enseguidaLa 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 HTTPSEl 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 failedSe 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, lentitudSube -Xmx en JAVA_OPTS y ejecuta docker compose up -d pabal-server
No llega el código de registroEstado 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ónComprueba 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 foundLa 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

ArchivoFunción
deploy/docker-compose.ymlConfiguración de producción (pabal-server · pabal-postgres · pabal-caddy, bind mounts)
deploy/CaddyfileHTTPS, bloqueo de las rutas de administración, reenvío del sitio, la documentación y la Bot API
deploy/.env.examplePlantilla de configuración → deploy/.env
deploy/backup.shVolcado de la base de datos + archivo de claves y datos, limpieza de copias antiguas
DockerfileImagen del servidor (compilación → JRE 21, uid 1000)