Guía Completa: Configurar Entorno Full Stack con Angular, Django y Tailscale
Stack: Angular (frontend), Django (API) y Tailscale como reverse proxy seguro. A continuación, además de “qué” configurar, se explica el “por qué”, para que no sea magia negra.
1) Arquitectura del sistema
┌─────────────────┐ HTTPS ┌──────────────────┐
│ Frontend │─────────────│ Tailscale │
│ Angular │ │ Reverse Proxy │
│ Puerto 4200 │◄───────────►│ dominio.ts.net │
└─────────────────┘ └──────────────────┘
│ HTTPS
▼
┌─────────────────┐ ┌──────────────────┐
│ Backend │─────────────│ API Routes │
│ Django │ │ /api/* │
│ Puerto 8000 │ └──────────────────┘
└─────────────────┘
Por qué así: Tailscale emite certificados y publica
https://<algo>.ts.net, evitando montar y mantener Nginx/Let’s Encrypt para desarrollo y entornos internos. Con rutas / y /api unificás origen (evitás CORS en prod) y simplificás el front.2) Prerrequisitos
- Node.js 18+, Angular CLI
- Python 3.8+, Django, DRF,
django-cors-headers - Tailscale (cuenta + cliente)
3) Backend (Django)
3.1 Dependencias
# requirements.txt
Django==4.2.26
djangorestframework==3.14.0
django-cors-headers==4.3.1
Por qué: DRF acelera el CRUD y serialización;
django-cors-headers maneja preflights CORS correctamente para clientes SPA.3.2 Configuración base en settings.py
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "0.0.0.0", ".ts.net"]
INSTALLED_APPS = [
"corsheaders",
"rest_framework",
# ...
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware", # Debe ir de los primeros
"django.middleware.security.SecurityMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
# ...
]
# --- CORS (solo desarrollo) ---
CORS_ALLOW_ALL_ORIGINS = True
# Producción: usar listas/regex, no wildcard:
# CORS_ALLOWED_ORIGINS = ["[https://tu-dominio.ts.net](https://tu-dominio.ts.net)"]
# CORS_ALLOWED_ORIGIN_REGEXES = [r"^[https://.*.ts.net$](https://.*.ts.net$)"]
CORS_ALLOW_METHODS = ["GET","POST","PUT","PATCH","DELETE","OPTIONS"]
CORS_ALLOW_HEADERS = [
"accept","accept-encoding","authorization","content-type","dnt",
"origin","user-agent","x-csrftoken","x-requested-with"
]
CORS_ALLOW_CREDENTIALS = True # si usás cookies/sesiones
# --- CSRF (requerido si usás cookies) ---
CSRF_TRUSTED_ORIGINS = ["https://*.ts.net"]
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
# --- Reverse proxy/HTTPS ---
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
- ALLOWED_HOSTS=".ts.net": Django no servirá peticiones si el host no está permitido; esencial tras el proxy.
- CorsMiddleware primero: para que capte las peticiones
OPTIONS(preflight) antes que otros middlewares. - CORS en dev vs prod:
ALLOW_ALLacelera dev; en prod restringí orígenes para mitigar abuso desde sitios externos. - CSRF_TRUSTED_ORIGINS: con HTTPS tras proxy, Django valida el origen; sin esto, verás 403 CSRF.
- SECURE_PROXY_SSL_HEADER / USE_X_FORWARDED_HOST: Django entiende que “delante” hay TLS y arma URLs correctas; evita redirecciones erróneas y cookies “no seguras”.
3.3 URLs DRF
# server/urls.py
from django.urls import path, include
from rest_framework import routers
from app.views import ClienteViewSet
router = routers.DefaultRouter()
router.register(r'clientes', ClienteViewSet, basename='cliente')
urlpatterns = [
path("api/", include(router.urls)),
]
Por qué: versionar y agrupar bajo
/api/ permite que el reverse proxy de Tailscale enrute limpio con --set-path=/api y deja espacio para servir el frontend en /.4) Frontend (Angular)
4.1 Variables de entorno
// environment.ts
export const environment = {
production: false,
apiUrl: 'https://TU_DOMINIO.ts.net/api'
};
// environment.prod.ts
export const environment = {
production: true,
apiUrl: '[https://TU_DOMINIO.ts.net/api](https://TU_DOMINIO.ts.net/api)'
};
Por qué: el origen de API cambia por entorno; centralizar la base URL evita “string hunting” y reduce errores de Mixed Content.
4.2 Interceptor (base URL + credenciales)
// api.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';
import { environment } from '../../environments/environment';
@Injectable()
export class ApiInterceptor implements HttpInterceptor {
intercept(req: HttpRequest, next: HttpHandler) {
const isApi = req.url.startsWith('/api/');
const url = isApi ? `${environment.apiUrl}${req.url.replace('/api', '')}` : req.url;
return next.handle(req.clone({ url, withCredentials: true }));
}
}
Por qué:
withCredentials es necesario si usás cookies/sesiones en Django; sin esto, el navegador no envía/recibe cookies y falla la auth.4.3 Servicio de ejemplo
// clientes.service.ts
listar() { return this.http.get<any[]>('/api/clientes/'); }
crear(payload: any) { return this.http.post('/api/clientes/', payload); }
4.4 Dev sin CORS con proxy local
{
"/api": {
"target": "http://localhost:8000",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
}
}
ng serve --proxy-config proxy.conf.json
Por qué: servir Angular y Django desde localhost distintos origina CORS. El proxy de Angular hace de “puente” y te permite pedir
/api al mismo origen del Front en dev, sin tocar CORS del backend.5) Tailscale
tailscale up
tailscale status
# Publicar servicios bajo [https://TU_DOMINIO.ts.net](https://TU_DOMINIO.ts.net)
tailscale serve --bg --set-path=/api 8000 # Django
tailscale serve --bg --set-path=/ 4200 # Angular
tailscale serve status
Por qué:
tailscale serve oficia de reverse proxy TLS (certs automáticos), mapea rutas a puertos locales y te da un único dominio seguro para el navegador.6) Integración, CORS y CSRF
- Dev:
CORS_ALLOW_ALL_ORIGINS=Truepara rapidez o usá proxy Angular (mejor). - Prod: restringí con
CORS_ALLOWED_ORIGINSo regex y habilitáCORS_ALLOW_CREDENTIALSsi usás cookies. - CSRF: agregá
CSRF_TRUSTED_ORIGINSahttps://*.ts.net; sin eso, el middleware bloqueará POST/PUT/PATCH/DELETE con 403.
7) Casos de configuración
- Caso A (Dev rápido): Angular +
proxy.conf.json, Django enlocalhost:8000. Cero CORS. - Caso B (Un dominio):
tailscale servecon/→ 4200 y/api→ 8000. Ideal para demos internas. - Caso C (Hosts separados): frontend.ts.net y backend.ts.net; o proxiar
/apien el host del front para mantener un solo origen. - Caso D (Prod estático): construir Angular y servir assets con Django + WhiteNoise.
INSTALLED_APPS = ["whitenoise.runserver_nostatic","django.contrib.staticfiles", ...] MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware") STATIC_URL = "/static/" STATIC_ROOT = BASE_DIR / "staticfiles"Por qué: WhiteNoise sirve estáticos eficientemente sin Nginx para entornos simples; cache y compresión incluidos. - Caso E (Autenticación):
- JWT en header: simple en CORS; cuidado dónde guardás el token.
- Cookies HttpOnly + CSRF: más seguro contra XSS; requiere
withCredentialsy configuración CSRF correcta.
8) Solución de problemas
- CORS bloqueado: en dev usar
ALLOW_ALLo proxy; en prod declarar orígenes explícitos. - 403 CSRF: agregar
CSRF_TRUSTED_ORIGINS, mandar cookie y/o headerX-CSRFToken, ywithCredentialsdel lado de Angular. - Mixed Content: siempre
https://hacia*.ts.net. - SSL/puertos solapados:
tailscale serve reset tailscale serve drain svc:8000 tailscale serve drain svc:4200
9) Comandos de referencia
# Tailscale
tailscale status
tailscale serve status
tailscale serve --bg --set-path=/ruta [puerto]
tailscale serve drain svc:[puerto]
tailscale serve reset
# Django
pip install -r requirements.txt
python manage.py migrate
python manage.py runserver 0.0.0.0:8000
# Angular
npm i
ng serve --host 0.0.0.0 --port 4200
ng serve --proxy-config proxy.conf.json
ng build --configuration production
# Verificación
curl -i [https://TU_DOMINIO.ts.net/api/clientes/](https://TU_DOMINIO.ts.net/api/clientes/)
curl -I [https://TU_DOMINIO.ts.net/](https://TU_DOMINIO.ts.net/)
tailscale ping TU_DOMINIO.ts.net
10) Checklist rápido
CSRF_TRUSTED_ORIGINSincluyehttps://*.ts.net- En prod:
CORS_ALLOW_ALL_ORIGINS = Falsey orígenes definidos - Angular usa
/api/*y, si hay cookies,withCredentials tailscale serve statusmuestra/→ 4200 y/api→ 8000curla/api/devuelve 200/401 esperado
Reemplazá
TU_DOMINIO por tu subdominio real de Tailscale en todas las configuraciones.