📚 Manual de Ayuda
VPS Contabo - anna.ar

Flask + VPS
Instalación, systemd, HTTPS y subdominios
Dólar Oficial
Cron, SQLite y exportación para PowerCOBOL
PHP + MySQL
nginx, PHP-FPM, MariaDB y Adminer

Tutorial: instalar (y eliminar) una app Flask en tu VPS

Guía de referencia basada en todo lo que armamos con agro. Usá esto como receta para cualquier proyecto nuevo — cambiá agro2 por el nombre que corresponda cada vez.


PARTE A — Instalación desde cero

Paso 0: Elegir un puerto libre (¡el paso que más tiempo nos llevó hoy!)

Antes de escribir una sola línea de código, confirmá qué puertos ya están ocupados en tu VPS:

sudo ss -tlnp
docker ps --format "table {{.Names}}\t{{.Ports}}"

Puertos que ya tenés ocupados en esta VPS (actualizá esta lista si instalás algo nuevo):

Puerto(s) Servicio
22 SSH
80 nginx
21985 Relay VisualDesk
21115-21119 (TCP) / 21116 (UDP) RustDesk
5000-5150 (TCP y UDP) Traccar
8082, 5055 Traccar
9090 agro (este proyecto)

Elegí un número que no esté en esa lista. Fuera del rango 5000-5150 (que se lo come Traccar entero) y fuera de 21000-22000 (RustDesk + relay), vas bastante seguro. Ideas: 9091, 9092, 9100, 3001, etc.

Paso 1: Crear la carpeta del proyecto

sudo mkdir -p /opt/agro2
sudo chown -R $USER:$USER /opt/agro2
cd /opt/agro2

Paso 2: Preparar tus archivos en tu PC

En tu entorno local, tené listos como mínimo: - api_server.py (con el puerto elegido en el paso 0, ej. app.run(host="0.0.0.0", port=9091, debug=False)) - index.html (si tu app sirve una página web) - Cualquier otro archivo que tu proyecto necesite

Importante: debug=False siempre en cualquier servidor expuesto a internet. El modo debug de Flask abre una consola interactiva peligrosa si alguien la encuentra.

Paso 3: Subir los archivos con FileZilla

Conectate por SFTP a la VPS y subí los archivos a /opt/agro2/.

Paso 4: Crear el entorno virtual e instalar dependencias

cd /opt/agro2
python3 -m venv venv
source venv/bin/activate
pip install flask

(Si te falta python3-venv, primero: sudo apt install -y python3.12-venv)

Paso 5: Probar a mano antes de automatizar nada

python api_server.py

Si no tira "Address already in use", vas bien. Probá desde el navegador con http://IP_DE_TU_VPS:PUERTO. Cuando confirmes que anda, cortá con Ctrl+C.

Paso 6: Crear el servicio systemd (para que quede corriendo solo)

En tu PC, creá un archivo agro2.service con este contenido (ajustando nombres y puerto si hace falta):

[Unit]
Description=Agro2 (Flask)
After=network.target

[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/opt/agro2
ExecStart=/opt/agro2/venv/bin/python api_server.py
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

Ojo con la ubicación: este archivo NO va en /opt/agro2 con el resto del proyecto. Tiene que ir en /etc/systemd/system/ — es la carpeta fija donde systemd busca todos los servicios del sistema (ahí conviven el de Traccar, el del relay, y el tuyo).

Subilo por FileZilla a /opt/agro2/ (temporalmente) y después movelo con:

sudo mv /opt/agro2/agro2.service /etc/systemd/system/agro2.service

Paso 7: Activar el servicio

sudo systemctl daemon-reload
sudo systemctl enable agro2.service
sudo systemctl start agro2.service
sudo systemctl status agro2.service

Tiene que decir active (running).

Paso 8: Abrir el puerto (en LOS DOS firewalls)

Firewall interno (ufw):

sudo ufw allow PUERTO/tcp
sudo ufw status

Firewall de la nube (panel de Contabo): Entrá al panel web → sección de firewall → agregá regla: - Protocolo: TCP - Puerto: el que elegiste - Origen: Cualquiera

Los dos son necesarios — si falta uno solo, no entra el tráfico.

Paso 9: La prueba real

Cerrá la sesión SSH del todo (exit) y probá desde el navegador:

http://IP_DE_TU_VPS:PUERTO

Si sigue andando con la sesión cerrada, quedó como servicio permanente de verdad — no depende de que tengas una terminal abierta.


PARTE B — Eliminar un proyecto por completo

Cuando quieras dar de baja agro2 (o cualquier proyecto similar) sin dejar rastros:

1. Parar y deshabilitar el servicio

sudo systemctl stop agro2.service
sudo systemctl disable agro2.service

2. Borrar el archivo de systemd

sudo rm /etc/systemd/system/agro2.service
sudo systemctl daemon-reload

3. Si usaba nginx como intermediario, borrar esa config también

ls /etc/nginx/sites-enabled/          # confirmar el nombre exacto primero
sudo rm /etc/nginx/sites-enabled/NOMBRE_DEL_SITIO
sudo nginx -t                         # debe decir "syntax is ok" / "test is successful"
sudo systemctl reload nginx

4. Borrar la carpeta del proyecto

sudo rm -rf /opt/agro2

5. Si usaba Docker (docker-compose), bajar contenedores y volúmenes

cd /opt/agro2   # antes de borrar la carpeta en el paso 4
docker compose down -v
docker ps -a | grep -i agro2        # debe salir vacío
docker volume ls | grep -i agro2    # debe salir vacío

⚠️ El flag -v borra también los datos guardados en los volúmenes, sin posibilidad de recuperarlos. Confirmá antes de correrlo que no haya nada importante ahí.

6. Cerrar el puerto en ambos firewalls

sudo ufw delete allow PUERTO/tcp
sudo ufw status

Y borrar manualmente la regla correspondiente en el panel de Contabo.

7. Verificación final — confirmar que no queda nada

ls /opt/ | grep agro2                          # vacío
ls /etc/systemd/system/ | grep -i agro2        # vacío
sudo ss -tlnp | grep PUERTO                    # vacío
sudo ufw status | grep PUERTO                  # vacío

Si los cuatro comandos devuelven vacío, el proyecto quedó eliminado sin dejar ningún rastro en el servidor.


Cuándo hace falta reiniciar el servicio después de modificar archivos

Archivo que cambiaste ¿Hace falta systemctl restart?
Cualquier .py (ej. api_server.py, generar_pdf.py) Sí, siempre
.html sueltos (ej. index.html) No — Flask los lee del disco en cada visita
Archivos dentro de templates/ No — se leen de nuevo en cada uso

Razón: Python carga el código de los .py en memoria una sola vez al arrancar el servicio, y se queda trabajando con esa copia. Si cambiás algo ahí, necesita "releerlo" — de ahí el reinicio. El HTML y las plantillas, en cambio, se leen del disco en cada pedido.

Reiniciar es seguro: el corte dura milisegundos, y como el .service tiene Restart=always, si el código nuevo tuviera un error de sintaxis te vas a enterar con systemctl status NOMBRE.service en vez de quedar el servidor "colgado" a medias.

sudo systemctl restart NOMBRE.service
sudo systemctl status NOMBRE.service


PARTE C — Agregar HTTPS con dominio propio (opcional, pero recomendado)

Esta parte transforma http://IP:PUERTO (funcional pero con avisos de "no seguro") en algo como https://agro.tudominio.ar (con candado verde, apto para compartir con terceros). La hicimos real hoy con agro.anna.ar — quedate tranquilo, cada paso ya está probado.

Requisito previo: un dominio (o subdominio) apuntando a tu VPS

Si ya tenés un dominio propio (como anna.ar), no hace falta comprar nada nuevo — un subdominio (agro.anna.ar) es gratis y no afecta en nada al resto de lo que tengas en ese dominio.

Paso 1: confirmar dónde se administra el DNS de tu dominio

nslookup -type=NS tudominio.ar

Esto te dice si el DNS lo maneja NIC.ar directamente, o algún hosting (en nuestro caso, dio Hostinger).

Paso 2: agregar el registro A del subdominio

Entrá al panel correspondiente (en nuestro caso, hPanel de Hostinger → Dominios → tu dominio → Zona DNS) y agregá:

Campo Valor
Tipo A
Nombre agro (o el subdominio que quieras)
Valor la IP de tu VPS
TTL dejar el valor por default

Paso 3: esperar la propagación y confirmar

Puede tardar de minutos a un par de horas. Confirmás así:

nslookup agro.tudominio.ar

Cuando te devuelva la IP de tu VPS, seguís al paso 4.

Paso 4: crear la configuración de nginx para el subdominio

sudo nano /etc/nginx/sites-available/agro.tudominio.ar

⚠️ Ojo con esto: una vez que corras el comando de arriba, la pantalla de la terminal cambia por completo (vas a ver una franja azul/gris abajo con ^O Escribir ^X Salir). Recién ahí, cuando confirmes que estás dentro de esa pantalla de edición, pegás el siguiente bloque. Si lo pegás en la terminal normal (antes de que cambie la pantalla), bash va a tirar errores tipo "command not found" porque intenta ejecutar cada línea como si fuera un comando de Linux.

server {
    listen 80;
    listen [::]:80;
    server_name agro.tudominio.ar;

    location / {
        proxy_pass http://127.0.0.1:PUERTO;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

(reemplazá PUERTO por el puerto real de tu app, ej. 9090)

Guardar y salir: Ctrl+O, Enter, Ctrl+X.

Verificá que quedó bien escrito antes de seguir:

cat /etc/nginx/sites-available/agro.tudominio.ar

Paso 5: activar el sitio

sudo ln -s /etc/nginx/sites-available/agro.tudominio.ar /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

nginx -t tiene que decir syntax is ok / test is successful.

⚠️ Los dos últimos comandos NO son lo mismo, y los dos son necesarios: - nginx -t solo revisa que la sintaxis esté bien — no aplica nada. - systemctl reload nginx es el que efectivamente aplica la configuración nueva. Sin este paso, nginx sigue trabajando con la configuración vieja en memoria, aunque el archivo ya esté bien guardado en disco — por eso a veces "no anda" después de crear un sitio nuevo, aunque todo esté bien escrito: falta este último paso.

Paso 6: abrir el puerto 80 en AMBOS firewalls

sudo ufw allow 80/tcp

Y en el panel de Contabo: regla TCP, puerto 80, origen Anywhere. (Si ya lo tenías abierto de antes para otro proyecto, no hace falta repetirlo — pero confirmalo, porque en limpiezas de otros proyectos es fácil borrarlo sin querer, como nos pasó hoy.)

Paso 7: probar por HTTP antes de pasar a HTTPS

http://agro.tudominio.ar

Si no carga, puede ser que el firewall de Contabo tarde un par de minutos en aplicar la regla nueva — esperá y volvé a probar antes de asumir que algo está mal.

Paso 8: instalar Certbot (una sola vez por servidor, como nginx)

sudo apt install -y certbot python3-certbot-nginx

Paso 9: generar el certificado

sudo certbot --nginx -d agro.tudominio.ar

Te va a pedir: - Un email (para avisos de vencimiento — el certificado se renueva solo, pero por las dudas). - Aceptar términos de servicio (Y). - Compartir el email con la EFF (opcional, Y o N, no afecta nada).

Si sale bien, termina con un mensaje tipo "Congratulations! You have successfully enabled HTTPS". Certbot modifica el archivo de nginx solo, agregando un segundo bloque server con listen 443 ssl y las rutas a los certificados.

Paso 10: abrir el puerto 443 en AMBOS firewalls

Mismo patrón que el puerto 80 — es un puerto nuevo, hay que abrirlo en los dos lados:

sudo ufw allow 443/tcp

Y en el panel de Contabo: regla TCP, puerto 443, origen Anywhere.

Paso 11: la prueba final

https://agro.tudominio.ar

Si no carga al toque, esperá 1-2 minutos (propagación del firewall de Contabo) y probá en una pestaña de incógnito antes de asumir que algo falló — el navegador a veces cachea el error anterior.


Resumen — checklist HTTPS

  1. Confirmar dónde se administra el DNS (nslookup -type=NS)
  2. Agregar registro A del subdominio en ese panel
  3. Esperar propagación, confirmar con nslookup
  4. Crear config de nginx para el subdominio (dentro de nano, no en la terminal)
  5. Activar el sitio (ln -s + nginx -t + reload)
  6. Abrir puerto 80 en ufw Y en Contabo
  7. Probar por HTTP
  8. Instalar Certbot (una vez por servidor)
  9. certbot --nginx -d subdominio
  10. Abrir puerto 443 en ufw Y en Contabo
  11. Probar por HTTPS (con paciencia si tarda unos minutos)
  12. (Opcional) Confirmar que la renovación automática funciona: sudo certbot renew --dry-run

Nota: Certbot se instala una sola vez, el certificado se pide por cada subdominio

  • sudo apt install -y certbot python3-certbot-nginx → una sola vez por servidor (como nginx).
  • sudo certbot --nginx -d nombre.tudominio.ar → una vez por cada subdominio nuevo que armes, porque cada uno necesita su propio certificado.
  • La renovación (cada ~60 días, antes de que venzan los 90 días de validez) queda automática, gracias a una tarea programada (certbot.timer) que Certbot deja configurada solo. Podés verla con: bash sudo systemctl list-timers | grep certbot Y probar que funciona (sin cambiar nada de verdad) con: bash sudo certbot renew --dry-run

Resumen — checklist rápido

Instalar: 1. Elegir puerto libre (ss -tlnp + docker ps) 2. mkdir /opt/NOMBRE + permisos 3. Subir archivos (FileZilla) 4. venv + pip install flask 5. Probar a mano 6. Crear .service → moverlo a /etc/systemd/system/ 7. daemon-reload + enable + start 8. Abrir puerto en ufw Y en Contabo 9. Probar con la sesión SSH cerrada

Eliminar: 1. stop + disable el servicio 2. Borrar el .service 3. Borrar config de nginx (si aplica) 4. Borrar la carpeta del proyecto 5. docker compose down -v (si aplica) 6. Cerrar puerto en ufw Y en Contabo 7. Verificar que no quede nada

Tutorial: proyecto Dólar Oficial (Contabo)

Referencia completa del proyecto dolar: desde la instalación hasta el registro automático diario y la exportación de históricos para PowerCOBOL. Complementa a tutorial_vps_flask.md (el general), pero acá está todo lo específico de este proyecto en un solo lugar.


Archivos del proyecto

Archivo Para qué sirve
api_server.py Sirve la web dolar.anna.ar (cotización actual + histórico consultado en vivo a la API externa)
index.html La página que se ve en el navegador
requirements.txt Dependencias (flask, requests)
instalar.sh Crea el venv e instala dependencias de una sola vez
registrar_cotizacion.py Guarda la cotización del día en SQLite + exporta cotizacion_hoy.txt. Pensado para correr solo, una vez por día, con cron
exportar_historico.py Genera un .txt con un rango de fechas (desde tu base propia, o completando con la API externa)
dolar_historico.db Se crea sola la primera vez que corre registrar_cotizacion.py — tu propio histórico acumulado

PARTE 1 — Instalación inicial (una sola vez)

1. Crear la carpeta y subir los archivos

sudo mkdir -p /opt/dolar
sudo chown -R $USER:$USER /opt/dolar

Subir por FileZilla a /opt/dolar/: api_server.py, index.html, requirements.txt, instalar.sh, registrar_cotizacion.py, exportar_historico.py.

2. Instalar entorno virtual y dependencias

cd /opt/dolar
source instalar.sh

(recordá: con source, no con ./instalar.sh — si no, la activación del venv no queda en la terminal)

3. Instalar sqlite3 a nivel de servidor (una sola vez, para siempre)

Esto es el programa de consola para poder inspeccionar la base a mano. La librería Python sqlite3 ya viene incluida en Python, esto es aparte, solo para vos poder mirar los datos desde la terminal.

sudo apt install -y sqlite3

No hace falta repetir este paso para otros proyectos ni para reinstalaciones — queda instalado en el sistema operativo, mismo criterio que nginx o certbot.

4. Probar el servidor web a mano

python api_server.py

Probar en http://localhost:9091 (o con la IP pública si el puerto ya está abierto). Cortar con Ctrl+C.

5. Armar el servicio systemd (para que la web quede corriendo sola)

sudo nano /etc/systemd/system/dolar.service

Contenido:

[Unit]
Description=Dolar Oficial (Flask)
After=network.target

[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/opt/dolar
ExecStart=/opt/dolar/venv/bin/python api_server.py
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

Activarlo:

sudo systemctl daemon-reload
sudo systemctl enable dolar.service
sudo systemctl start dolar.service
sudo systemctl status dolar.service

6. Subdominio + HTTPS

Mismo proceso que en tutorial_vps_flask.md, Parte C, con dolar.anna.ar y puerto 9091. En resumen:

# DNS: registro A "dolar" -> IP de la VPS, cargado en Hostinger

sudo nano /etc/nginx/sites-available/dolar.anna.ar
server {
    listen 80;
    listen [::]:80;
    server_name dolar.anna.ar;

    location / {
        proxy_pass http://127.0.0.1:9091;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
sudo ln -s /etc/nginx/sites-available/dolar.anna.ar /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

sudo certbot --nginx -d dolar.anna.ar

PARTE 2 — Registro automático diario (cron)

1. Probar el script a mano primero

cd /opt/dolar
source instalar.sh
python registrar_cotizacion.py

Tiene que mostrar algo como:

OK -> Compra: $XXXX.XX  Venta: $XXXX.XX
Guardado en: /opt/dolar/dolar_historico.db
Exportado a: /opt/dolar/cotizacion_hoy.txt

2. Abrir el editor de tareas programadas

crontab -e

(la primera vez puede preguntar qué editor usar — elegir nano)

3. Agregar la tarea al final del archivo

0 9 * * * cd /opt/dolar && /opt/dolar/venv/bin/python /opt/dolar/registrar_cotizacion.py >> /opt/dolar/registro.log 2>&1

Guardar y salir: Ctrl+O, Enter, Ctrl+X.

Qué significa 0 9 * * *: "todos los días a las 9:00 AM" (formato: minuto hora día mes día-semana, con * = "cualquiera").

Por qué la ruta completa al Python del venv: cron no ejecuta source instalar.sh por nosotros, así que hay que decirle explícitamente qué Python usar (/opt/dolar/venv/bin/python), no el del sistema.

4. Confirmar que la tarea quedó cargada

crontab -l

5. Probar sin esperar al horario programado

Corriendo exactamente la misma línea a mano:

cd /opt/dolar && /opt/dolar/venv/bin/python /opt/dolar/registrar_cotizacion.py >> /opt/dolar/registro.log 2>&1
cat /opt/dolar/registro.log

Si el log muestra el mensaje de éxito, la tarea programada va a funcionar sola a partir de mañana.

6. Revisar el histórico acumulado (opcional, para confirmar)

sqlite3 /opt/dolar/dolar_historico.db "SELECT * FROM cotizaciones;"

PARTE 3 — Exportar un rango de fechas para PowerCOBOL

Cómo funciona

  • registrar_cotizacion.py (el del cron) va acumulando un registro por día, a partir de hoy en adelante — no trae retroactivamente los días anteriores a que empezaste a correrlo.
  • exportar_historico.py genera el .txt con el rango que pidas, tomando esos datos acumulados.

Modo 1 — solo lo que ya tenés guardado (rápido)

python exportar_historico.py 2026-08-01 2026-08-30

Si el rango incluye fechas de antes de que arrancara el cron, va a traer menos registros de los esperados (solo los días que ya se guardaron solos). No es un error, es esperable.

Modo 2 — completando huecos con la API externa (para el pasado)

python exportar_historico.py --completar 2026-08-01 2026-08-30

Este modo, además de exportar, completa tu base SQLite con los días que falten (consultándolos a ArgentinaDatos), así la próxima vez que pidas ese mismo rango, ya no hace falta --completar de nuevo.

Resultado

Se genera un archivo historico_DESDE_a_HASTA.txt en /opt/dolar/, con formato:

fecha;compra;venta
2026-08-01;1470.00;1490.00
2026-08-15;1478.00;1498.00
2026-08-29;1485.00;1535.00

Listo para bajarlo por FileZilla y leerlo desde PowerCOBOL.


Resumen — checklist rápido

Instalación (una vez): 1. mkdir /opt/dolar + permisos 2. Subir los 6 archivos 3. source instalar.sh 4. sudo apt install -y sqlite3 (una sola vez por servidor) 5. Probar python api_server.py a mano 6. Armar dolar.service, activar 7. Subdominio + nginx + Certbot

Uso diario (automático, no requiere nada de vos): - cron corre registrar_cotizacion.py todos los días a las 9 AM

Cuando necesites un rango para PowerCOBOL:

cd /opt/dolar && source instalar.sh
python exportar_historico.py [--completar] DESDE HASTA

Comandos de diagnóstico útiles:

crontab -l                                   # ver tareas programadas
cat /opt/dolar/registro.log                  # ver el log del cron
sqlite3 /opt/dolar/dolar_historico.db "SELECT * FROM cotizaciones;"  # ver la base
sudo systemctl status dolar.service          # ver si la web sigue viva

Tutorial: sitio PHP + MySQL/MariaDB en la VPS

Guía de referencia para desplegar un sitio en PHP con base de datos, distinta a la receta de Flask porque acá no hay venv ni systemd por proyecto — PHP-FPM y MariaDB son servicios únicos, compartidos por todos los proyectos PHP que armes.


Diferencia clave con los proyectos Flask (agro, dolar, etc.)

Flask (Python) PHP
¿Necesita venv? Sí, uno por proyecto No
¿Necesita .service propio? Sí, uno por proyecto No — PHP-FPM ya está corriendo para todos
¿Dónde vive el proyecto? /opt/nombre /var/www/nombre
¿Qué hace nginx? Reenvía (proxy_pass) a un puerto Python aparte Sirve los archivos directo, y delega los .php a PHP-FPM
Base de datos La que vos armes (SQLite, etc.) MariaDB, instalado una vez para todo el servidor

PARTE 1 — Instalación de PHP-FPM y MariaDB (una sola vez por servidor)

1. Instalar PHP-FPM y extensiones comunes

sudo apt update
sudo apt install -y php-fpm php-mysql php-mbstring php-xml php-curl

Confirmar la versión real instalada (importante para el paso de nginx más adelante):

sudo systemctl status php8.3-fpm

(el número puede variar; usar el que confirme el status)

2. Instalar MariaDB

sudo apt install -y mariadb-server
sudo systemctl status mariadb

3. Asegurar la instalación

sudo mysql_secure_installation

Preguntas y respuestas: - "Enter current password for root" → Enter en blanco (recién instalado, no tiene contraseña todavía) - "Set root password?" → Y, y ahí sí escribir una contraseña real - Remove anonymous users? → Y - Disallow root login remotely? → Y - Remove test database? → Y - Reload privilege tables now? → Y

⚠️ Guardar la contraseña de root de MariaDB en un lugar seguro (gestor de contraseñas, no en el chat ni en ningún archivo del servidor).


PARTE 2 — Crear un proyecto nuevo (repetible por cada sitio PHP)

1. Crear la base de datos y un usuario propio del proyecto

sudo mysql -u root -p

Dentro de la consola:

CREATE DATABASE nombreproyecto;
CREATE USER 'nombreproyecto_user'@'localhost' IDENTIFIED BY 'CONTRASEÑA_PROPIA_DISTINTA_DE_ROOT';
GRANT ALL PRIVILEGES ON nombreproyecto.* TO 'nombreproyecto_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

⚠️ Nunca usar la misma contraseña que la de root. Si esta contraseña de proyecto se filtra alguna vez (por ejemplo, quedó escrita en un archivo PHP que se compartió sin querer), con contraseñas distintas solo se compromete esa base puntual, no el servidor de bases de datos entero.

Para generar una contraseña segura al azar, se puede pedir en el momento en vez de inventar una de memoria.

2. Crear la carpeta del proyecto

A diferencia de /opt/, PHP usa la convención /var/www/, donde nginx y PHP-FPM ya tienen los permisos correctos para el usuario www-data (el que ejecuta ambos procesos):

sudo mkdir -p /var/www/nombreproyecto
sudo chown -R www-data:www-data /var/www/nombreproyecto

3. Crear el archivo de prueba

sudo nano /var/www/nombreproyecto/index.php
<?php
echo "<h1>PHP funcionando</h1>";
echo "<p>Versión: " . phpversion() . "</p>";

$conexion = new mysqli("localhost", "nombreproyecto_user", "CONTRASEÑA_PROPIA", "nombreproyecto");

if ($conexion->connect_error) {
    echo "<p style='color:red'>Error de conexión a la base: " . $conexion->connect_error . "</p>";
} else {
    echo "<p style='color:green'>Conexión a la base de datos exitosa.</p>";
}

$conexion->close();
?>

Guardar y salir: Ctrl+O, Enter, Ctrl+X.

4. Crear la configuración de nginx

sudo nano /etc/nginx/sites-available/nombreproyecto.anna.ar
server {
    listen 80;
    listen [::]:80;
    server_name nombreproyecto.anna.ar;
    root /var/www/nombreproyecto;
    index index.php index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

(ajustar php8.3-fpm.sock al número de versión real confirmado en el Paso 1)

5. Activar el sitio

sudo ln -s /etc/nginx/sites-available/nombreproyecto.anna.ar /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

6. DNS + HTTPS

Mismo proceso que con los proyectos Flask (ver tutorial_vps_flask.md, Parte C):

  1. Registro A del subdominio en Hostinger, apuntando a la IP de la VPS.
  2. Confirmar propagación con nslookup nombreproyecto.anna.ar.
  3. Probar por HTTP: http://nombreproyecto.anna.ar.
  4. Abrir puerto 80/443 en ufw y en el panel de Contabo (si es la primera vez en esta VPS; si ya se abrieron para otro proyecto, no hace falta repetirlo).
  5. Generar el certificado: bash sudo certbot --nginx -d nombreproyecto.anna.ar
  6. Probar por HTTPS: https://nombreproyecto.anna.ar.

Errores comunes vistos al armar ayuda.anna.ar

"El archivo de configuración quedó con el nombre viejo del proyecto"

Si en algún momento se renombra el proyecto (por ejemplo, de miweb a ayuda), hay que renombrar tres cosas por separado, ninguna se actualiza sola:

sudo mv /etc/nginx/sites-available/NOMBRE_VIEJO /etc/nginx/sites-available/NOMBRE_NUEVO
sudo mv /var/www/NOMBRE_VIEJO /var/www/NOMBRE_NUEVO

Y dentro del archivo de nginx, actualizar a mano server_name y root con el nombre nuevo. También borrar el link viejo de sites-enabled si ya se había activado, y volver a crearlo con el nombre correcto.

"La página de prueba no muestra el mensaje de conexión a la base"

Puede ser que el index.php que quedó en el servidor sea una versión más corta (por ejemplo, si se creó antes de agregar el bloque de mysqli). Revisar con cat /var/www/nombreproyecto/index.php y completar si falta.


PARTE 3 — Administrar la base de datos visualmente (sin panel = incómodo)

Trabajar la base solo por consola SQL es tedioso. Hay opciones para verla y editarla con una interfaz web o de escritorio.

Opción rápida: Adminer (un solo archivo PHP)

cd /var/www/nombreproyecto
sudo wget https://www.adminer.org/latest.php -O adminer.php

Probar en https://nombreproyecto.anna.ar/adminer.php.

Datos de login — ojo con no mezclar usuarios:

Campo Con el usuario del proyecto Con root
Servidor localhost localhost
Usuario nombreproyecto_user root
Contraseña la de nombreproyecto_user la de root (la de mysql_secure_installation)
Base de datos nombreproyecto (dejar vacío, muestra todas)

⚠️ Error común: usar el usuario root con la contraseña del proyecto (o viceversa) — son dos usuarios completamente separados, cada uno con su propia contraseña. No se pueden combinar.

Truco para no olvidar las credenciales: el propio archivo index.php de prueba (Parte 2, Paso 3) ya tiene las credenciales reales escritas en la línea de new mysqli(...) — sirve como referencia rápida de qué usuario/contraseña/base usar en cualquier herramienta.

⚠️ Adminer queda público en internet una vez subido. Para uso esporádico, subirlo cuando se necesita y borrarlo después:

sudo rm /var/www/nombreproyecto/adminer.php

Para dejarlo instalado de forma permanente y segura, agregarle autenticación básica a nivel de nginx (contraseña extra antes de llegar al login de Adminer).

Opción más prolija: cliente de escritorio (HeidiSQL, DBeaver, TablePlus)

Ventaja: no expone nada nuevo a internet, interfaz más cómoda para uso frecuente. Como MariaDB está configurado para escuchar solo en localhost (no accesible directo desde afuera, por seguridad), la conexión desde estos programas se hace mediante un túnel SSH (la mayoría de estos clientes lo tienen integrado como una pestaña más de configuración, no hace falta armarlo aparte a mano).


Una sola vez por servidor: 1. apt install php-fpm php-mysql ... 2. apt install mariadb-server 3. mysql_secure_installation

Por cada proyecto PHP nuevo: 1. Crear base + usuario propio (contraseña distinta a root) 2. mkdir /var/www/nombreproyecto + permisos a www-data 3. Crear index.php de prueba con conexión a la base 4. Archivo de nginx (root + fastcgi_pass al socket de PHP-FPM correcto) 5. Activar (ln -s + nginx -t + reload) 6. DNS + HTTP + Certbot + HTTPS (igual que en Flask)