GLP Cerca (5): operación y producto — despliegue, store y honestidad
Publicado el 05/07/2026
El código que funciona en tu máquina no es un producto. Un producto es algo que otra gente usa sin que tú estés delante, que se actualiza sin romperse, y que es honesto sobre lo que es. Este último artículo va de todo eso: lo que rodea al backend del artículo 3 y a la app del artículo 4 para convertirlos en algo publicable.
Despliegue: un solo binario, una sola variable
La filosofía de despliegue es la misma que la del resto del proyecto: la cosa más pequeña que funcione.
El backend se empaqueta en un Docker multi-etapa y se despliega en un VPS con
Dokploy. La base de datos es externa (PostgreSQL gestionado), y el servicio se
configura con una única variable de base de datos: DATABASE_URL.
environment:
HOST: 0.0.0.0
PORT: ${PORT:-3000}
DATABASE_URL: ${DATABASE_URL:?define DATABASE_URL apuntando a tu PostgreSQL}
INGESTA_INTERVALO_HORAS: ${INGESTA_INTERVALO_HORAS:-6}
INGESTA_AL_ARRANCAR: ${INGESTA_AL_ARRANCAR:-true}
INGESTA_PRODUCTO_ID: ${INGESTA_PRODUCTO_ID:-17}
Decisión: el binario lee solo
DATABASE_URL, nuncaPOSTGRES_*. En producción defines esa URL y ya. En desarrollo, undocker-compose.dev.ymlaparte levanta un PostgreSQL de conveniencia, y losPOSTGRES_*viven solo ahí, como parámetros de ese contenedor. Separar "cómo me conecto a la BD" (una URL) de "cómo levanto una BD de juguete" (varios parámetros) evita que la configuración de producción herede ruido de desarrollo.
Detalles de operación que merecen mención:
- Migraciones al arrancar. La app aplica sus migraciones sola al iniciar. No hay un paso manual que se pueda olvidar.
- Ingesta al arranque (
INGESTA_AL_ARRANCAR=true): a los pocos segundos de levantar, ya hay datos frescos. Y luego cadaINGESTA_INTERVALO_HORAS(0 desactiva la tarea, útil si quieres ingerir desde fuera concargo run -- ingestar). - Healthcheck de verdad. El
docker composetiene un healthcheck que pega a/health. Y/healthno miente: consulta la bitácora de ingestas y marcastalesi llevamos mucho sin refrescar. Un contenedor "arriba" sirviendo datos rancios es un fallo, y quiero que se vea como tal.
Observabilidad: que el sistema cuente qué le pasa
Ya lo toqué en el artículo 2, pero es un principio transversal: todo proceso desatendido tiene que dejar rastro.
tracingestructurado en todo el backend (conRUST_LOGconfigurable).ingesta_log: cada ejecución, con su resultado, su error si lo hubo y sus contadores (nuevas, actualizadas, precios cambiados, descartadas).- Un backoffice mínimo (
/admin) protegido con sesión por cookie y Argon2, que muestra salud, última ingesta y un botón de forzar ingesta. Es de lectura + operación: no edita datos de estaciones, porque esos los sobrescribe la ingesta y cualquier edición manual sería una mentira efímera.
Opinión: el admin no deja editar precios a propósito. La tentación de "lo corrijo a mano" es una trampa: el dato es de MINETUR, la siguiente ingesta lo pisaría, y mientras tanto estarías mostrando un número inventado. Mejor que el sistema no pueda hacer eso.
Versionado de la app: pensado para Google Play
Google Play tiene una regla inflexible: cada subida necesita un versionCode
entero estrictamente creciente. Olvidarte de subirlo, o subirlo mal, es un
rechazo seguro. Así que lo automaticé con un script (bump-version.sh) que deriva
el versionCode del propio semver:
code = major*1_000_000 + minor*1_000 + patch
# 0.1.0 -> 1000 ; 1.2.3 -> 1002003
./bump-version.sh # patch: 0.1.0 -> 0.1.1
./bump-version.sh minor # 0.1.3 -> 0.2.0
./bump-version.sh 1.2.3 # versión exacta
Por qué derivarlo en vez de llevar un contador aparte: dos números que tienes que mantener sincronizados a mano acaban desincronizándose. Si el
versionCodees una función pura del semver, es imposible que se contradigan. Subes la versión y el build number se actualiza solo, siempre coherente, siempre creciente.
El script toca tauri.conf.json y el tauri.properties de Android a la vez, e
incluso contempla la diferencia entre el sed de macOS (BSD) y el de Linux,
porque desarrollo en Mac pero el CI podría no ser Mac. Pequeños detalles que
evitan el "en mi máquina funcionaba".
Firma y release: la llave que no se pierde
cargo tauri android build genera el APK y el AAB de producción (apuntando ya a
https://glp.rexvalentia.es) y los firma con un keystore.
Aviso a mi yo futuro, escrito en el README: el keystore y su
keystore.propertiesno se versionan y hay que guardar una copia segura. Son la llave permanente para actualizar la app en Play. Si la pierdes, no puedes volver a publicar actualizaciones de esa app, nunca. Esto no es una recomendación de estilo; es un punto de no retorno.
La honestidad como decisión de producto
Y llego a lo que, para mí, distingue este proyecto de un simple ejercicio técnico. Cómo te presentas ante el usuario es parte del producto.
GLP Cerca consume datos públicos de MINETUR. Eso me obliga a ser escrupuloso, y decidí serlo de forma agresiva, no en la letra pequeña:
- No oficial, bien grande. La ficha de Play y la propia app dejan claro, desde la primera línea, que es un proyecto privado e independiente, no asociado al Gobierno ni al Ministerio ni a ninguna estación o marca. Usar datos públicos no te hace oficial, y fingirlo sería engañar.
- Crédito a la fuente. Enlazo el catálogo de datos abiertos de MINETUR y el Geoportal de Gasolineras. El mérito del dato es de quien lo publica.
- "Verifica en el surtidor". Los precios se actualizan periódicamente y puede haber desfases con la realidad del momento. Lo digo claramente: el precio de la app es una guía, el de la pistola es el que pagas.
- Privacidad real, no declarada. Esto no es solo una frase en la política: es una propiedad de la arquitectura. Como conté en el artículo 4, la ubicación se usa en el dispositivo para pedir estaciones cercanas y nunca se almacena ni se asocia a un usuario en el servidor. La privacidad que prometo es la que el código hace cierta.
Opinión, y cierre de la serie: es tentador maquillar el origen de los datos, esconder el "no oficial" en un tooltip, o prometer una privacidad que tu arquitectura no respalda. No lo hice, y creo que es lo que diferencia una herramienta en la que confías de una que solo usas hasta que aparece otra. La confianza es la única funcionalidad que no puedes añadir en la versión 2.0.
Recapitulando la serie
- 1 · Motivación: el mercado del GLP es opaco donde duele; el dato existe pero falta acceso en el momento de decidir. De ahí: backend que carga el peso + cliente tonto y rápido.
- 2 · Datos e ingesta: normalizar en la frontera el feed de MINETUR (comas, TLS, duplicados) y el "histórico barato" que guarda solo los cambios de precio.
- 3 · Backend en Rust: Axum + SeaORM, una capa de consulta compartida, y cercanía con bounding-box + haversine en vez de PostGIS.
- 4 · La app: Tauri 2 + vanilla, color relativo al radio, una sola pulsación hasta la ruta y UX de conducción.
- 5 · Operación y producto: despliegue de una sola variable, observabilidad honesta, versionado derivado y la honestidad como feature.
El hilo conductor de todas las decisiones fue el mismo: elegir la cosa más pequeña que resuelva el problema real, y ser honesto sobre lo que es. Tanto con el usuario como con el yo del futuro que tendrá que mantener esto.