Sistema de redirección de URLs con panel de administración. Construido con Bun, Hono, SQLite y Docker.
- Redirector: Servicio de redirección rápida con tracking de clicks y cache en memoria
- Admin: Panel de administración protegido con JWT para gestionar links
- Base de datos: SQLite compartida entre ambos servicios mediante volumen Docker
- Despliegue: Soporta desarrollo local con Docker Compose y producción en Coolify
- Bun 1.1+
- Docker y Docker Compose (para despliegue)
- Servidor VPS (para producción)
- Cuenta en Cloudflare (recomendado)
git clone <tu-repo>
cd url-redirectCopia el archivo de ejemplo:
cp .env.example .envEdita .env con tus valores:
# Base de datos
DATABASE_URL=./db/data/links.db
# Redirector
PORT=3000
IP_SALT=tu-salt-secreto-aleatorio
REDIRECT_BASE_URL=https://go.tudominio.com
ADMIN_BASE_URL=https://admin.tudominio.com
# Admin
ADMIN_PORT=3001
JWT_SECRET=tu-jwt-secret-muy-largo-y-aleatorio
ADMIN_USERNAME=admin
ADMIN_PASSWORD=$2a$12$... # Generar con el script (ver abajo)
NODE_ENV=productionbun install
bun run scripts/generate-password.ts tu-contraseña-seguraCopia el hash generado en tu .env como ADMIN_PASSWORD.
docker-compose up -dLos servicios estarán disponibles en:
- Redirector: http://localhost:3000
- Admin: http://localhost:3001
bun run scripts/seed.tsPrueba el redirector:
curl http://localhost:3000/test
# Debería redirigir a https://example.comPara proteger tu aplicación y usar tu propio dominio, sigue estos pasos:
- Ve a Cloudflare Dashboard
- Click en "Add a Site"
- Ingresa tu dominio (ej:
tudominio.com) - Selecciona el plan gratuito y continúa
- Cloudflare te dará nameservers para configurar en tu registrador de dominio
- Espera a que el estado cambie a "Active" (puede tomar unos minutos)
Ve a DNS > Records y crea:
| Type | Name | Content | Proxy Status | TTL |
|---|---|---|---|---|
| A | go |
IP de tu VPS | 🟠 Proxied | Auto |
| A | admin |
IP de tu VPS | 🟠 Proxied | Auto |
Nota: El "proxy naranja" (🟠) DEBE estar activado para que Cloudflare maneje SSL y protección.
Ve a SSL/TLS > Overview:
- Cambia el modo a "Full (strict)"
- Esto asegura que la conexión esté cifrada entre Cloudflare → tu servidor
Ve a Security > WAF > Custom rules:
- Click en Create rule
- Nombre: "Block Admin from other countries"
- Expression:
(Ajusta los países según tus necesidades)
(http.host eq "admin.tudominio.com" and not ip.geoip.country in {"US" "MX" "ES"}) - Action: Block
- Click en Deploy
Ve a Zero Trust > Access > Applications:
- Click en Add an application
- Selecciona Self-hosted
- Configura:
- Application name:
URL Admin - Session duration:
24 hours - Application domain:
admin.tudominio.com
- Application name:
- En Identity providers: Selecciona One-time PIN
- En Policies:
- Policy name:
Allow email - Action:
Allow - Include:
Emails→ agrega tu email
- Policy name:
- Click en Next y luego Add application
Ahora el admin requerirá un código OTP enviado a tu email antes de acceder.
Coolify es una alternativa a Heroku/Vercel auto-hosteada que facilita el despliegue.
- Un VPS con Coolify instalado (sigue su guía oficial)
- Acceso SSH al VPS
- Tu código en un repositorio Git
- Accede al panel de Coolify (
https://coolify.tuservidor.com) - Click en Projects → Create project
- Nombre:
url-redirect - Click en el proyecto creado
- En el proyecto, click en + Add resource
- Selecciona Docker Compose
- Configuración:
- Name:
url-redirect-stack - Build pack:
dockercompose
- Name:
Pega el contenido del archivo docker-compose.coolify.yml:
version: '3.9'
volumes:
db_data:
driver: local
services:
redirector:
build:
context: .
dockerfile: apps/redirector/Dockerfile
container_name: url-redirector
restart: unless-stopped
expose:
- "3000"
volumes:
- db_data:/data
environment:
- DATABASE_URL=/data/links.db
- IP_SALT=${IP_SALT}
- REDIRECT_BASE_URL=${REDIRECT_BASE_URL}
- ADMIN_BASE_URL=${ADMIN_BASE_URL}
- PORT=3000
labels:
- "coolify.managed=true"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
- app-network
admin:
build:
context: .
dockerfile: apps/admin/Dockerfile
container_name: url-admin
restart: unless-stopped
expose:
- "3001"
volumes:
- db_data:/data
environment:
- DATABASE_URL=/data/links.db
- JWT_SECRET=${JWT_SECRET}
- ADMIN_USERNAME=${ADMIN_USERNAME}
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
- REDIRECT_BASE_URL=${REDIRECT_BASE_URL}
- ADMIN_BASE_URL=${ADMIN_BASE_URL}
- ADMIN_PORT=3001
- NODE_ENV=production
depends_on:
redirector:
condition: service_healthy
labels:
- "coolify.managed=true"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3001/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
networks:
- app-network
networks:
app-network:
driver: bridgeVe a la pestaña Environment variables y agrega todas las variables del .env:
| Variable | Valor de ejemplo | Descripción |
|---|---|---|
IP_SALT |
abc123xyz... |
Salt para hash de IPs |
REDIRECT_BASE_URL |
https://go.tudominio.com |
URL base del redirector |
ADMIN_BASE_URL |
https://admin.tudominio.com |
URL base del admin |
JWT_SECRET |
tu-secret-jwt... |
Clave secreta para JWT |
ADMIN_USERNAME |
admin |
Usuario del panel |
ADMIN_PASSWORD |
$2a$12$... |
Hash de contraseña |
Nota: Genera el hash de contraseña con
bun run scripts/generate-password.ts
Ve a la pestaña Domains:
- Click en + Add domain
- Para el redirector:
- Domain:
go.tudominio.com - Container port:
3000 - Container:
url-redirector
- Domain:
- Click en + Add domain
- Para el admin:
- Domain:
admin.tudominio.com - Container port:
3001 - Container:
url-admin
- Domain:
- Ve a la pestaña General
- Click en Deploy
- Coolify construirá las imágenes y levantará los contenedores
- Revisa los logs en la pestaña Logs si hay problemas
# Test redirector
curl https://go.tudominio.com/health
# Test admin login (desde navegador)
# https://admin.tudominio.com/| Variable | Requerida | Descripción | Ejemplo |
|---|---|---|---|
DATABASE_URL |
Sí | Ruta a la base de datos SQLite | /data/links.db |
PORT |
Sí | Puerto del servidor | 3000 |
IP_SALT |
Sí | Salt para anonimizar IPs | random-string |
REDIRECT_BASE_URL |
Sí | URL base del redirector | https://go.tudominio.com |
ADMIN_BASE_URL |
Sí | URL base del admin (para CORS) | https://admin.tudominio.com |
| Variable | Requerida | Descripción | Ejemplo |
|---|---|---|---|
DATABASE_URL |
Sí | Ruta a la base de datos SQLite | /data/links.db |
ADMIN_PORT |
Sí | Puerto del servidor admin | 3001 |
JWT_SECRET |
Sí | Clave secreta para firmar JWT | long-random-secret |
ADMIN_USERNAME |
Sí | Usuario para login | admin |
ADMIN_PASSWORD |
Sí | Hash bcrypt de la contraseña | $2a$12$... |
REDIRECT_BASE_URL |
Sí | URL base del redirector | https://go.tudominio.com |
ADMIN_BASE_URL |
Sí | URL base del admin | https://admin.tudominio.com |
NODE_ENV |
No | Entorno de ejecución | production |
# Copiar desde el contenedor a tu máquina
docker cp url-redirector:/data/links.db ./backup-$(date +%Y%m%d).db# Detener contenedores
docker-compose down
# Copiar backup al volumen
docker run --rm -v url-redirect_db_data:/data -v $(pwd):/backup alpine cp /backup/links.db /data/
# O si usas Docker directamente:
docker cp ./backup-20240101.db url-redirector:/data/links.db
# Levantar contenedores
docker-compose up -dEn tu VPS, agrega al crontab:
# Backup diario a las 3 AM
0 3 * * * docker cp url-redirector:/data/links.db /backups/links-$(date +\%Y\%m\%d).db && find /backups -name "links-*.db" -mtime +7 -deleteSi prefieres desarrollar sin Docker:
# Instalar dependencias
bun install
# Configurar .env
# Ajusta DATABASE_URL a una ruta local como ./db/data/links.db
# Ejecutar migraciones
bun run --cwd packages/db migrate
# Iniciar redirector (terminal 1)
bun run --cwd apps/redirector dev
# Iniciar admin (terminal 2)
bun run --cwd apps/admin devurl-redirect/
├── apps/
│ ├── redirector/ # Servicio de redirección
│ │ ├── src/
│ │ │ ├── index.ts # Servidor Hono
│ │ │ └── cache.ts # Cache en memoria
│ │ ├── Dockerfile # Dockerfile del redirector
│ │ └── package.json
│ └── admin/ # Panel de administración
│ ├── src/
│ │ └── index.ts # Servidor Hono + JWT
│ ├── Dockerfile # Dockerfile del admin
│ └── package.json
├── packages/
│ └── db/ # Package compartido
│ ├── src/
│ │ ├── client.ts # Conexión SQLite
│ │ ├── schema.ts # Schema Drizzle
│ │ └── index.ts # Exports
│ └── drizzle/ # Migraciones
├── scripts/
│ ├── generate-password.ts # Generar hash de contraseña
│ └── seed.ts # Crear datos de prueba
├── db/data/ # Datos SQLite (montado como volumen)
├── docker-compose.yml # Desarrollo local
├── docker-compose.coolify.yml # Producción en Coolify
└── README.md
Asegúrate de que el directorio /data existe en el contenedor y tiene permisos correctos. Los Dockerfiles incluyen mkdir -p /data y cambian permisos al usuario appuser.
Verifica que ambos servicios usen el mismo volumen:
volumes:
- db_data:/data # Mismo volumen para ambosAsegúrate de que REDIRECT_BASE_URL y ADMIN_BASE_URL estén configuradas correctamente y coincidan con las URLs que usas.
Verifica que:
- El link existe en la base de datos
- El redirector está accediendo a la misma base de datos que el admin
- Las migraciones se ejecutaron correctamente
Si Coolify reporta el servicio como "unhealthy", verifica:
- Que el contenedor tenga
wgetinstalado (los Dockerfiles lo incluyen víaapk) - Que la ruta del healthcheck sea correcta (
/healthpara redirector,/para admin) - Los logs del contenedor para ver el error específico
MIT
Las contribuciones son bienvenidas. Por favor, abre un issue primero para discutir los cambios que deseas hacer.