VPS Coolify Bootstrap — Setup de VPS nuevo para alojar apps
Skill de bootstrap paso a paso para armar un VPS Oracle Cloud Always Free desde cero, dedicado 100% a alojar apps web vía Coolify (PaaS self-hosted) con Traefik. Excluye juegos, Minecraft y similares — eso va por otra vía (containers manuales en /srv/games/).
Pensado para producción seria con clientes: Traefik + Let's Encrypt + Cloudflare + Umami + monitoreo a Telegram + backups automáticos.
🎯 Qué terminás teniendo al final del setup
- VM ARM64 en Oracle Cloud Always Free (4 OCPU + 24 GB RAM, al tope del Free Tier)
- Acceso SSH solo vía Tailscale (puerto 22 cerrado en OCI)
- Coolify 4.x corriendo con Traefik incorporado
- DNS en Cloudflare con proxy inteligente (gris para emitir cert LE, después naranja opcional)
- HTTPS automático en cada app que deployes
- Analytics con Umami self-hosted
- Monitoreo edge-triggered a Telegram cada 5 min
- Backups diarios automáticos del boot volume
- Parches de seguridad automáticos
- Headers de seguridad via Cloudflare Transform Rules (score A en securityheaders.com)
- Renovate configurado en repos para alertas de seguridad de deps
0. Pre-requisitos (cuentas y herramientas)
Antes de tocar nada, tené listas estas cuentas:
| Cuenta | Uso |
|---|---|
| Oracle Cloud (siempre Free Tier, no upgradable a paid por accidente) | Hostea la VM |
| Tailscale (cuenta personal, Free Tier 100 devices) | Red privada para SSH |
| Cloudflare (cuenta + dominio agregado) | DNS + proxy + headers de seguridad |
| GitHub | Repos de las apps + GitHub App para Coolify |
| Telegram (bot + chat) | Alertas de monitoreo |
| Resend / SendGrid / similar | Email transaccional (SMTP bloqueado en OCI Always Free) |
En tu máquina local (macOS Apple Silicon):
- Tailscale.app instalado y logueado
- SSH (
~/.ssh/configcon alias para el VPS) - OCI CLI (
brew install oci-cli) - Python 3 (para decodificar JWT del token OCI)
1. Crear la VM en Oracle Cloud (Always Free)
1.1 Lanzar la instancia
OCI Console → Compute → Instances → Create instance
| Campo | Valor |
|---|---|
| Name | vps (o el que quieras) |
| Placement | AD cualquiera de tu home region |
| Image | Ubuntu 20.04 (o 22.04) Minimal aarch64 |
| Shape | VM.Standard.A1.Flex (ARM Ampere) |
| OCPU | 4 (máximo Free) |
| Memory (GB) | 24 (máximo Free) |
| Boot volume | 150 GB (default, dentro del Free) |
| VCN | Crear nueva o usar default |
| Subnet | Pública (con Internet Gateway) |
| Assign public IPv4 | Sí |
| SSH key | Generar nuevo par de claves o subir pública |
⚠️ Si te da error "Out of capacity", reintentar cada 5-10 min. Es el problema #1 de Always Free. Si pasa 24h sin poder, probar otra AD de la misma región.
1.2 Asignar IP pública reservada (recomendado)
Para que la IP no cambie en stop/start:
- Networking → IPs → Reserve public IP (regional)
- Asignar a la VM recién creada
- Anotar el OCID de la IP
Sobrevive stop/start, gratis mientras esté assigned. Si la dejas unattached, empieza a cobrar.
1.3 Anotar OCIDs críticos (los vas a usar mil veces)
# En OCI CLI desde tu Mac, después de instalar y auth:
export INST_OCID="ocid1.instance.oc1.<region>.<...>"
export SEC_LIST_OCID="ocid1.securitylist.oc1.<region>.<...>"
export COMP_OCID="ocid1.tenancy.oc1..<...>"
Setup completo de OCI CLI: ver sección "Auth OCI" más abajo.
2. Reglas OCI Security List (puertos del host)
Estas reglas son PERMANENTES en OCI. El UFW del VPS no sirve de nada si OCI no las permite.
| Protocolo | Puerto | Source | Propósito | Recomendado |
|---|---|---|---|---|
| ICMP | all | 0.0.0.0/0 |
Ping diagnóstico | ✅ Sí |
| ICMP | all | 10.0.0.0/16 |
Intra-VCN | ✅ Sí |
| TCP | 80 | 0.0.0.0/0 |
HTTP → 443, ACME http-01 | ✅ Sí (necesario para Let's Encrypt) |
| TCP | 443 | 0.0.0.0/0 |
HTTPS apps | ✅ Sí |
| TCP | 22 | 0.0.0.0/0 |
SSH | ❌ NO (usar Tailscale) |
| TCP | 8000 | 0.0.0.0/0 |
Coolify UI | ❌ NO (solo Tailscale) |
| TCP | 8080 | 0.0.0.0/0 |
Traefik dashboard | ❌ NO (solo Tailscale) |
SSH por Tailscale, no por OCI. Mantener 22 cerrado en OCI elimina la superficie de ataque más común (bots escaneando SSH).
Cómo aplicar las reglas
# Bajar las reglas actuales a JSON
oci network security-list get \
--security-list-id "$SEC_LIST_OCID" \
--query 'data."ingress-security-rules"' > /tmp/rules.json
# Editar /tmp/rules.json (agregar/quitar las que correspondan)
# Después subir:
oci network security-list update \
--security-list-id "$SEC_LIST_OCID" \
--ingress-security-rules "file:///tmp/rules.json" \
--force
3. Primer SSH + hardening inicial
3.1 Conectar por primera vez (vía IP pública con la clave que generaste)
ssh -i ~/.ssh/oci-vps.key ubuntu@<IP_PUBLICA>
3.2 Hardening base
# Update + upgrade
sudo apt update && sudo apt upgrade -y
# Timezone Chile
sudo timedatectl set-timezone America/Santiago
# UFW (después de instalar Tailscale, sino te quedás afuera)
sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# NO abrir 22 — solo Tailscale entra
sudo ufw enable
# Hostname
sudo hostnamectl set-hostname vps
3.3 Parches automáticos (security updates)
Configurar unattended-upgrades para auto-instalar parches de seguridad y reiniciar a las 03:00 hora Chile si hace falta.
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
# Elegir "Yes"
Editar /etc/apt/apt.conf.d/50unattended-upgrades para asegurar:
Unattended-Upgrade::Allowed-Originsincluye${distro_id}:${distro_codename}-securityUnattended-Upgrade::Automatic-Reboot "true";Unattended-Upgrade::Automatic-Reboot-Time "06:00";(06:00 UTC = 03:00 CLT en winter, 02:00 CLT en summer — ajustar a gusto)
3.4 Configurar alias SSH en tu Mac
cat >> ~/.ssh/config <<'EOF'
Host vps
HostName <IP_PUBLICA>
User ubuntu
IdentityFile ~/.ssh/oci-vps.key
EOF
CRÍTICO: este bloque
Host vpsdebe ir ANTES de cualquierHost *en~/.ssh/config. El bloqueHost *puede desactivarPubkeyAuthpara hosts no específicos.
4. Instalar Tailscale (SSH privado, cerrar 22 en OCI)
Tailscale crea una VPN mesh sin abrir puertos. Después de esto, cerrar 22 en OCI.
4.1 Instalar Tailscale en el VPS
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh --hostname=vps
4.2 Aprobar el VPS en tu tailnet
Tailscale te va a dar una URL para aprobar. Abrirla, aceptar.
4.3 Instalar Tailscale en tu Mac
brew install --cask tailscale
# Abrir Tailscale.app, loguear con misma cuenta
# Después de logueado, debería aparecer tu Mac y el VPS en la lista
4.4 Actualizar SSH config para usar Tailscale
cat >> ~/.ssh/config <<'EOF'
Host vps
HostName 100.x.y.z # la IP Tailscale del VPS (ver con `tailscale status` en el VPS)
User ubuntu
EOF
4.5 Validar que Tailscale SSH funciona
# Desde tu Mac:
ssh vps # debe entrar sin pedir clave, vía Tailscale SSH
# Anotar la IP Tailscale del VPS:
ssh vps "tailscale ip -4"
4.6 CERRAR PUERTO 22 EN OCI (operación crítica)
Una vez validado que Tailscale SSH funciona, eliminar la regla TCP 22 de la Security List.
Mantener como fallback solo si se rompe Tailscale (ver sección "Emergencia SSH" abajo).
5. Instalar Docker
# Instalar Docker oficial
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker ubuntu
# Logout y login de nuevo para que tome el grupo
# Validar
docker --version
docker run hello-world
Nota sobre ARM64
El VPS es ARM64. Cosas a tener en cuenta:
- Imágenes oficiales (
nginx,postgres,redis,node,python) son multi-arch → OK - Imágenes custom: verificar soporte ARM64 en Docker Hub (tag
linux/arm64) - Si una imagen no soporta ARM64, compilar localmente desde Dockerfile
6. Instalar Coolify
Coolify es un PaaS self-hosted que wrappea Docker + Traefik + tu repo Git. Deployas apps con git push.
6.1 Install
ssh vps
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
Tarda 3-5 min. Al final muestra URL + credenciales iniciales.
6.2 Primer acceso
Por Tailscale (puerto 8000):
http://<IP_TAILSCALE>:8000
Por HTTPS público con sslip.io (recomendado para acceder desde cualquier lado):
https://coolify.<IP_PUBLICA>.sslip.io
Coolify emite cert Let's Encrypt automático para *.sslip.io.
⚠️ Para que el cert Let's Encrypt emita OK, el subdominio coolify.*.sslip.io debe resolver a la IP pública. sslip.io lo hace por convención, no hace falta tocar DNS.
6.3 Setup inicial UI
- Cambiar password del admin (no dejar el random)
- Settings → Updates → habilitar auto-updates
- Settings → SSH Keys → agregar tu clave pública para git
- Keys & Tokens → API tokens → generar uno con permisos
read+write all→ guardarlo en password manager
6.4 Obtener UUIDs clave de Coolify
Necesarios para API automation:
# Project "personal" UUID (ya existe, o crear uno nuevo)
# Server "localhost" UUID (ya existe - es el VPS mismo)
# En UI: click en Project / Server → URL tiene el UUID
PROJECT_UUID="<UUID del project 'personal'>"
SERVER_UUID="<UUID del server 'localhost'>"
6.5 Configurar GitHub App (para repos privados sin PAT)
- Settings → Integrations → GitHub App → Create
- Autorizar la App en tu cuenta GitHub (System Wide scope)
- Anotar el
github_app_uuidque devuelve Coolify
Permite a Coolify clonar repos privados y recibir webhooks de push para auto-deploy.
6.6 Backup OBLIGATORIO del .env de Coolify
# En tu Mac:
scp vps:/data/coolify/source/.env ./coolify-env-backup-$(date +%F)
# Subir a password manager, borrar la copia local
Si perdés este .env, no podés desencriptar los secrets de las apps deployadas. Es el archivo más importante del VPS.
7. DNS en Cloudflare
7.1 Agregar el dominio a Cloudflare (si no está)
Si el dominio está en otro registrar, primero cambiar nameservers a los que Cloudflare te asigna. La propagación tarda 24-48h.
7.2 Configurar SSL/TLS
Cloudflare → SSL/TLS → Overview:
- Mode: Full (strict)
- Always Use HTTPS: ON
- Automatic HTTPS Rewrites: ON
- Minimum TLS Version: 1.2 (recomendado)
7.3 Apuntar el dominio a la IP del VPS
DNS → Records → Add:
Type: A
Name: @ (o subdominio)
IPv4: <IP_PUBLICA>
Proxy: Proxied (naranja) ← para producción
Recomendado agregar wildcard:
Type: A
Name: *
IPv4: <IP_PUBLICA>
Proxy: Proxied (naranja)
7.4 Regla de subdominios nuevos
SI vas a emitir cert Let's Encrypt para un subdominio nuevo, empezá con proxy en gris (DNS-only).
Cloudflare proxy reescribe headers HTTP y puede romper el challenge HTTP-01 de Let's Encrypt. Traefik se queda en loop intentando emitir el cert.
Procedimiento:
- Cloudflare → DNS → Records → Add record
- Type A, name
mi-app, IPv4<IP_PUBLICA>, proxy DNS only (gris) - En Coolify configurar la app con FQDN
https://mi-app.midominio.com - Coolify emite cert Let's Encrypt automático
- Validar con
curl -sI https://mi-app.midominio.com→ cert válido - Recién después cambiar a Proxied (naranja) en Cloudflare para DDoS protection
8. Headers de seguridad (Cloudflare Transform Rules)
SIEMPRE en Cloudflare, NUNCA en Traefik labels ni custom_labels de Coolify. Razones:
- Aplica a TODAS las responses (200, 308, 404, 500)
- Cero contacto con el VPS / Coolify / Traefik
- Si rompe algo, 1 click en CF UI desactiva la rule
- Reusable para múltiples dominios desde la misma cuenta
8.1 Setup
Cloudflare → DOMAIN → Rules → Transform Rules → tab Modify Response Header → Create rule (template "Add static header to response"). Crear UNA rule por header.
8.2 Los 6 headers estándar
| Rule name | Header name | Value |
|---|---|---|
add-xframe |
X-Frame-Options |
DENY |
add-xcontent |
X-Content-Type-Options |
nosniff |
add-xss |
X-XSS-Protection |
1; mode=block |
add-referrer |
Referrer-Policy |
strict-origin-when-cross-origin |
add-permissions |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
add-hsts |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
Para cada rule:
If incoming requests match= All incoming requests- Action = Add static
- Deploy
8.3 Validar
curl -sI https://midominio.com | grep -iE 'x-frame|x-content|x-xss|referrer|permissions|strict-transport'
# Debería listar los 6
Score esperado: A en https://securityheaders.com/?q=midominio.com
8.4 NO incluir CSP por default
CSP (Content-Security-Policy) requiere whitelistear cada servicio externo (analytics, ads, pagos, Sentry, fonts). Sin whitelist completa, rompe la app (carga blanca, scripts bloqueados). Solo agregar si necesitás A+ para audit de seguridad B2B.
9. Analytics con Umami (self-hosted)
9.1 Deploy via Coolify
Coolify → Add Resource → One-Click Services → buscar Umami → Deploy.
Coolify te pide dominio. Usar un subdominio dedicado:
analytics.midominio.com
DNS en Cloudflare: agregar A record con DNS only (gris) al principio. Let's Encrypt necesita llegar directo al VPS. Después de emitido el cert, podés dejarlo gris (no necesita DDoS protection - solo recibe el script.js).
9.2 Post-deploy
- Coolify espera ~1-2 min, UMAMI listo
- Entrar a
https://analytics.midominio.comconadmin/umami(cambiar password INMEDIATAMENTE) - Settings → Websites → Add → name + domain
- Copiar el snippet:
<script defer src="https://analytics.midominio.com/script.js" data-website-id="..."></script> - Pegar en el
<head>de cada app que quieras trackear
10. Monitoreo a Telegram (script edge-triggered)
10.1 Crear bot de Telegram
- Hablar con
@BotFather→/newbot→ obtenerBOT_TOKEN - Crear grupo o chat personal, agregar el bot
- Obtener
CHAT_ID(con@userinfoboto viahttps://api.telegram.org/bot<TOKEN>/getUpdates)
10.2 Crear directorio de credenciales en el VPS
ssh vps
sudo mkdir -p /opt/vps-monitor
sudo tee /opt/vps-monitor/monitor.env > /dev/null <<'EOF'
BOT_TOKEN="<TU_BOT_TOKEN>"
CHAT_ID="<TU_CHAT_ID>"
EOF
sudo chmod 600 /opt/vps-monitor/monitor.env
10.3 Instalar el script de monitoring
Crear /opt/vps-monitor/check.sh con un script bash que:
- Use
flockpara evitar corridas concurrentes - Reintente 3 veces los checks HTTP (12s timeout, 4s entre reintentos)
- Separe capa local (Traefik interno, vía
--resolve) de capa pública (vía Cloudflare, como usuario real) - Sea edge-triggered: solo notifica cuando un check cambia de OK↔FAIL
- Persista estado en
/var/lib/vps-monitor/state/<check>.state - Escriba heartbeat a
/var/log/vps-monitor.logpor corrida - Mande mensajes HTML a Telegram con timestamp + emojis 🔴/🟢
Checks sugeridos (editar para tu setup):
- Disco > 85%, RAM > 90%, Load > 8 (2x cores)
- Containers core: coolify, coolify-proxy, coolify-db, coolify-redis, + prefijo UUID de cada app
- HTTP check de cada dominio público (vía Cloudflare, con reintentos)
- HTTP check LOCAL de cada app (sin Cloudflare, para detectar problemas del VPS vs CDN)
10.4 Cron
sudo crontab -e
# Agregar:
*/5 * * * * /opt/vps-monitor/check.sh >> /var/log/vps-monitor.log 2>&1
10.5 UptimeRobot externo (complementario)
Script local puede fallar si el VPS entero está down. UptimeRobot es un check externo que te avisa aunque el VPS esté muerto.
- https://uptimerobot.com (Free: 50 monitors, check cada 5 min, alerta a email)
- Agregar un HTTP monitor por cada dominio público
11. Backups automáticos (Oracle CLI)
11.1 Crear backup policy custom (Always Free friendly)
⚠️ NO usar las policies Gold/Silver/Bronze default — pueden exceder el Free Tier de 5 backups.
Crear policy custom en OCI:
- Block Storage → Backup Policies → Create Custom Policy
- Daily backup schedule a la hora que quieras
- Retention: 4 días (= máximo 4 backups concurrentes, dentro del límite de 5)
- Tipo: Incremental
11.2 Asignar policy al boot volume
oci bv backup-policy-assignment create \
--asset-id "$BOOT_VOL_OCID" \
--policy-id "$POLICY_OCID" \
--query 'data.id'
11.3 Validar que se ejecutan
oci bv boot-volume-backup list --compartment-id "$COMP_OCID" --output table
11.4 Backup adicional OFF-site (recomendado para producción con clientes)
Los backups OCI viven en la misma región. Si la región cae, perdés todo.
Opciones baratas:
- Backblaze B2 (10 GB gratis)
- Cloudflare R2 (10 GB gratis, sin egress)
- rsync nocturno vía cron
12. SRE Checklist (operación continua)
12.1 Reglas duras para cambios en producción
- Snapshot antes de cualquier cambio (curl GET a la app, guardar en
/tmp/backup-*.json) - Validar HTTP 200 × 60s post-cambio. Si 5xx → revert INMEDIATO sin debuggear
- Cambios de routing/proxy/domains: probar en staging (sslip.io) primero
- Cambios riesgosos en horario bajo (madrugada Chile)
- Headers: SIEMPRE via Cloudflare, NUNCA via custom_labels Traefik
- NO mezclar custom_labels con auto-config de Coolify (Coolify genera sus propios labels, los tuyos pueden romper el YAML)
- Limpiar custom_labels requiere
UPDATE applications SET custom_labels=NULL WHERE uuid='...'via SQL directo (la API rechaza string vacío) - Post-deploy: confirmar
curl -sI https://DOMAINantes de declarar éxito (status "finished" ≠ serving)
12.2 Pre-flight checks antes de cualquier health check
# 1) OCI session token vigente (expira 1h, refresh hasta 24h)
oci session refresh --profile DEFAULT 2>&1 | tail -2
# 2) Tailscale up
tailscale status 2>&1 | grep -E "Tailscale is|active" | head -3
Sin ambos, perdés 30+ segundos en timeouts.
12.3 Límites Always Free a no olvidar
| Recurso | Límite |
|---|---|
| Compute ARM A1 | 4 OCPU + 24 GB RAM TOTAL |
| Compute AMD E2.1.Micro | 2 instancias (1/8 OCPU + 1 GB c/u) |
| Block Storage TOTAL | 200 GB |
| Boot Volume Backups | 5 TOTALES |
| VCN | 2 |
| Reserved Public IPs | 1 gratis si assigned |
| Outbound bandwidth | 10 TB/mes |
| TCP 25 (SMTP) | BLOQUEADO (usar Resend/SendGrid) |
Si Oracle reclama la VM por "idle" (>7 días sin uso real), restaurar desde el último backup en una nueva instancia.
12.4 Renovate en cada repo (alertas de seguridad de deps)
- Instalar https://github.com/apps/renovate (1 click por repo)
- Agregar
renovate.jsonal repo:
{
"extends": ["config:recommended"],
"minimumReleaseAge": "7 days",
"vulnerabilityAlerts": { "enabled": true, "minimumReleaseAge": "0 days" },
"schedule": ["before 8am on monday"]
}
13. Agregar una app nueva (playbook)
13.1 DNS primero (si va a tener dominio propio)
Cloudflare → DNS → Add record:
Type: A, Name: mi-app, IPv4: <IP_PUBLICA>, Proxy: DNS only (gris)
13.2 Crear app en Coolify via API
COOLIFY_API="http://100.x.y.z:8000/api/v1" # o https://coolify.<IP>.sslip.io/api/v1
COOLIFY_TOKEN="<tu token>"
curl -X POST -H "Authorization: Bearer $COOLIFY_TOKEN" -H "Content-Type: application/json" \
-d '{
"project_uuid": "'"$PROJECT_UUID"'",
"server_uuid": "'"$SERVER_UUID"'",
"environment_name": "production",
"github_app_uuid": "'"$GITHUB_APP_UUID"'",
"git_repository": "usuario/mi-app",
"git_branch": "main",
"build_pack": "nixpacks",
"ports_exposes": "3000",
"name": "mi-app",
"domains": "https://mi-app.midominio.com,https://mi-app-staging.<IP>.sslip.io",
"instant_deploy": false
}' \
"$COOLIFY_API/applications/private-github-app"
13.3 Setear env vars + deploy + validar
Ver playbook completo en vps-personal skill (Steps 4-8). Lo crítico:
- App de staging (sslip.io) primero, prod después
- Validar con
curl -sI https://mi-app-staging.<IP>.sslip.io→ 200 con cert válido - Después agregar el dominio prod a la config de Coolify
- Actualizar env var
ORIGINal dominio prod - Trigger redeploy
- Una vez validado en prod, opcionalmente pasar el subdominio a Cloudflare Proxied
13.4 Agregar al monitoring
- UptimeRobot: nuevo monitor para el dominio
/opt/vps-monitor/check.sh: agregar check HTTP con reintentos- Umami: Settings → Websites → Add → pegar snippet en
<head>de la app
14. Troubleshooting
"ssh vps" timeout
tailscale status # ver si ambos nodos están "active"
ssh vps-public # fallback (requiere reabrir 22 en OCI)
"Permission denied (publickey)" en ssh vps
El bloque Host * al final de ~/.ssh/config puede desactivar PubkeyAuth para hosts no específicos. El bloque Host vps debe ir antes de Host *.
Docker: "no matching manifest for linux/arm64"
La imagen es solo amd64. Opciones:
- Buscar alternativa con
linux/arm64 docker pull --platform linux/arm64 imagen:tag- Compilar localmente desde Dockerfile
Disco lleno
ssh vps "df -h /; docker system df"
ssh vps "docker system prune -af --volumes" # ⚠️ borra containers parados, imágenes sin usar, volúmenes huérfanos
VPS parece lento / se cae periódicamente
ssh vps "uptime; free -h; iostat 1 5"
ssh vps "journalctl --since '1 hour ago' | grep -iE 'oom|kill|error'"
Normal: reboot a las 3 AM Chile si un parche lo requiere.
Tailscale dejó de funcionar tras reboot del VPS
sudo systemctl restart tailscaled
sudo tailscale up --ssh --hostname=vps
Cert Let's Encrypt no emite
docker logs coolify-proxy --tail 100 | grep -iE 'error|acme|letsencrypt'
Causas comunes:
- Subdominio en Cloudflare con Proxy ON → cambiar a DNS-only
- Puerto 80 cerrado en OCI Security List → abrirlo
- DNS no resuelve a la IP del VPS → validar con
dig mi-app.midominio.com
App responde 503 tras agregar dominio nuevo
PATCH /api/v1/applications/{uuid} con {"domains": "https://nuevo.dominio,..."} y trigger redeploy. Traefik no tiene regla para ese Host header.
App deployó "finished" pero no responde
# Validar que el container está Up
ssh vps "docker ps --filter name=<app>"
# Si está Up, validar local
ssh vps "curl -sI http://localhost:<port>"
# Si local responde pero externo no → problema de proxy/DNS/firewall
15. Emergencia SSH (Tailscale roto)
Si Tailscale queda en loop y no podés entrar:
- Consola OCI → Networking → Security Lists → agregar TCP 22 desde
0.0.0.0/0temporalmente ssh vps-public(vía IP pública con la clave RSA)- Diagnosticar y reparar Tailscale:
sudo systemctl restart tailscaled sudo tailscale up --ssh --hostname=vps - Validar que
tailscale statusmuestra tu Mac y el VPS - Validar
ssh vpsdesde tu Mac sin pedir clave - Cerrar puerto 22 en OCI de nuevo
16. Consideraciones para producción con clientes
El VPS puede alojar apps con clientes pagando, con caveats:
OK para:
- Side projects, MVPs, lanzamientos beta
- Apps de baja-media demanda (< ~100 usuarios concurrentes)
- Servicios internos con clientes B2B sin SLA estricto
- Demos, staging, herramientas de equipo
NO recomendado para:
- Apps con SLA contractual (Oracle no garantiza uptime en Always Free)
- Apps que NO toleren reboot automático a las 3 AM
- Datos críticos sin backups externos (los snapshots OCI están en la misma región)
- Apps con tráfico alto y picos (1 instancia, 1 región, 1 AD = SPOF)
Recomendaciones cuando vayas a producción:
- Cloudflare proxy ON sobre el dominio (DDoS + WAF + oculta IP del VPS)
- HTTPS automático vía Coolify Traefik + Let's Encrypt
- Backup adicional OFF-site (rsync nocturno a R2/B2, ver sección 11.4)
- Monitoreo externo (UptimeRobot gratis 50 monitors)
- Sentry o equivalente para errores de runtime
- Backup del
/data/coolify/source/.enva password manager (ver sección 6.6) - Renovate activado en cada repo
- Si crecen los clientes, considerar tier pagado de OCI o migrar a infra con redundancia
17. Anexo: Auth OCI CLI (para gestión de infra desde tu Mac)
Install
brew install oci-cli
Auth inicial
oci session authenticate --region sa-valparaiso-1 --profile-name DEFAULT
# → abre browser a Oracle SSO
# → loguear con la cuenta del VPS
# → el browser redirige a http://localhost:8181 (puede dar "sitio no alcanzable", IGNORAR — el token ya se guardó)
Bug conocido: "Key user | missing"
OCI CLI 3.84.0 (brew) NO escribe la línea user= al ~/.oci/config. Fix:
USER_OCID=$(python3 -c "
import json, base64
with open('$HOME/.oci/sessions/DEFAULT/token') as f:
parts = f.read().strip().split('.')
payload = parts[1] + '=' * (4 - len(parts[1]) % 4)
print(json.loads(base64.urlsafe_b64decode(payload))['sub'])
")
cp ~/.oci/config ~/.oci/config.bak
sed -i '' "/^tenancy=/a\\
user=${USER_OCID}
" ~/.oci/config
cat ~/.oci/config # debe tener user=... después de tenancy=
Env var permanente
cat >> ~/.zshenv <<'EOF'
export OCI_CLI_AUTH=security_token
export INST_OCID="ocid1.instance.oc1.<region>.<...>"
export SEC_LIST_OCID="ocid1.securitylist.oc1.<region>.<...>"
export COMP_OCID="ocid1.tenancy.oc1..<...>"
EOF
source ~/.zshenv
Sin OCI_CLI_AUTH=security_token, todos los comandos fallan con WARNING: security_token_file which is not being used + 401.
Renovar sesión cuando expira
# Renovar (hasta 24h)
oci session refresh --profile DEFAULT
# Si refresh falla (token >24h viejo):
oci session authenticate --region sa-valparaiso-1 --profile-name DEFAULT
# → después REPETIR el fix del user OCID
Comandos clave
# Estado instancia
oci compute instance get --instance-id "$INST_OCID" \
--query 'data.{State:"lifecycle-state",OCPUs:"shape-config".ocpus,RAM:"shape-config"."memory-in-gbs"}'
# Stop / Start
oci compute instance action --instance-id "$INST_OCID" --action STOP --wait-for-state STOPPED
oci compute instance action --instance-id "$INST_OCID" --action START --wait-for-state RUNNING
# Resize (STOPPED)
echo '{"ocpus": 4.0, "memoryInGBs": 24.0}' > /tmp/shape.json
oci compute instance update --instance-id "$INST_OCID" --shape-config "file:///tmp/shape.json" --force
# Backups
oci bv boot-volume-backup list --compartment-id "$COMP_OCID" --output table
# Security List rules
oci network security-list get --security-list-id "$SEC_LIST_OCID" \
--query 'data."ingress-security-rules"'
18. Resumen rápido — checklist de primer deploy
- Cuenta Oracle Cloud Always Free + tenancy activa
- VM.Standard.A1.Flex creada (4 OCPU, 24 GB, 150 GB disco, Ubuntu ARM64)
- IP pública reservada asignada y anotada
- OCIDs de instance, security list, compartment, boot volume guardados
- Security List con reglas: ICMP, TCP 80, TCP 443 (sin 22)
- SSH inicial via IP pública con clave RSA
- UFW configurado (80, 443 permitidos)
- Timezone America/Santiago
- unattended-upgrades activo
- Tailscale instalado en VPS + Mac, SSH validado
- Puerto 22 CERRADO en OCI
- Docker instalado y testeado
- Coolify instalado, password cambiado, API token guardado
-
/data/coolify/source/.envrespaldado en password manager - GitHub App configurada en Coolify
- Dominio en Cloudflare, NS propagados, SSL/TLS = Full (strict)
- A record del dominio al VPS (con wildcard
*recomendado) - 6 Transform Rules de seguridad configuradas
- Umami deployado y password cambiado
- Script de monitoreo instalado, Telegram bot configurado, cron activo
- UptimeRobot con monitor del primer dominio
- Backup policy custom de OCI creada y asignada al boot volume
- Renovate instalado en cada repo
- SSH config en Mac con
Host vpsapuntando a IP Tailscale (no IP pública) - OCI CLI instalado y auth funcionando con
OCI_CLI_AUTH=security_token - OCIDs exportados en
~/.zshenv
Cuando todo esté ✅, ya tenés un VPS production-ready para alojar apps vía Coolify, con SRE básico, Always Free compliant.
Próximo paso lógico: deployar la primera app siguiendo la sección 13.