Para portainer

This commit is contained in:
2026-06-23 12:29:53 -05:00
parent ec093544a8
commit c9a28cbb39
7 changed files with 268 additions and 11 deletions
+17 -2
View File
@@ -1,16 +1,31 @@
# Dependencias de Node (lo que te está dando el error) # Dependencias de Node
backend/node_modules/ backend/node_modules/
node_modules/
# Archivos de entorno con credenciales secretas (si usas bases de datos o contraseñas) # Archivos de entorno con credenciales secretas
backend/.env backend/.env
backend/.env.local backend/.env.local
.env
.env.local
# Logs y reportes de errores # Logs y reportes de errores
*.log
backend/*.log backend/*.log
backend/npm-debug.log* backend/npm-debug.log*
backend/yarn-debug.log* backend/yarn-debug.log*
backend/yarn-error.log* backend/yarn-error.log*
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Archivos temporales del sistema operativo # Archivos temporales del sistema operativo
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Docker (no subir imágenes/volúmenes locales)
.dockerignore
*.tar
# IDEs
.vscode/
.idea/
+136
View File
@@ -0,0 +1,136 @@
# Infinity — Despliegue con Docker
Esta guía explica cómo llevar el proyecto a cualquier PC con Docker, usando Gitea como origen del código.
## Requisitos en el PC destino
- Docker instalado y corriendo.
- Docker Compose instalado (normalmente viene con Docker Desktop; en Linux puede ser el plugin `docker compose`).
- Git (para clonar desde Gitea).
## Estructura del proyecto
```
.
├── backend/ # Código del servidor Node.js/Express
│ ├── Dockerfile
│ ├── docker-entrypoint.sh
│ ├── package.json
│ └── server.js
├── frontend/ # Archivos estáticos del catálogo y panel admin
├── data/ # JSONs persistentes (categorías, productos, usuarios...)
├── Imagenes/ # Fotos de productos
├── pedidos/ # PDFs de pedidos
├── videos/ # Videos del catálogo
├── agotados/ # Imágenes de productos agotados
├── docker-compose.yml
└── .dockerignore
```
## Pasos para ponerlo a correr en otro PC
### 1. Clonar el repositorio desde Gitea
```bash
git clone https://TU_GITEA/TU_USUARIO/infinity.git
cd infinity
```
### 2. Levantar el contenedor
```bash
docker compose up --build -d
```
- Construye la imagen con el backend y el frontend empaquetados.
- Expone la aplicación en: http://localhost:6060
- Persiste `data/`, `Imagenes/`, `pedidos/` y `videos/` en el host.
### 3. Abrir en el navegador
- Catálogo público: http://localhost:6060
- Panel administrativo: http://localhost:6060/administrativo/
### Usuario por defecto (solo si no existe `data/contraseñas.json`)
| Campo | Valor |
|------------|-------|
| Usuario | admin |
| Contraseña | admin |
> Cambia esta contraseña desde el panel administrativo lo antes posible.
## Flujo de trabajo: editar en este PC y actualizar el otro
1. Haces los cambios en el código/frontend.
2. Subes a Gitea:
```bash
git add .
git commit -m "descripción del cambio"
git push origin main
```
3. En el otro PC:
```bash
cd infinity
git pull
docker compose down
docker compose up --build -d
```
Con eso se actualiza tanto el backend como el frontend.
## Datos importantes: qué se guarda y qué no
Lo que **sí** se conserva al reconstruir el contenedor (está en volúmenes):
- `data/` → JSONs (productos, categorías, usuarios, sesiones...)
- `Imagenes/` → fotos subidas
- `pedidos/` → PDFs generados
- `videos/` → videos subidos
Lo que **no** se conserva (va dentro de la imagen):
- El código del frontend y backend. Si lo actualizas, hay que hacer `docker compose up --build -d`.
## Comandos útiles
| Acción | Comando |
|--------|---------|
| Ver logs | `docker compose logs -f web` |
| Reiniciar | `docker compose restart web` |
| Apagar | `docker compose down` |
| Borrar TODO (incluidos datos locales) | `docker compose down -v` ⚠️ |
| Ver estado | `docker compose ps` |
## Solución de problemas
### "Cannot GET /" o pantalla en blanco
1. Verifica que el contenedor esté corriendo:
```bash
docker compose ps
```
2. Mira los logs:
```bash
docker compose logs -f web
```
3. Asegúrate de haber usado `--build` después de `git pull`.
### El admin no entra
- Si la carpeta `data/` estaba vacía, se crea automáticamente el usuario `admin`/`admin`.
- Si ya existe `data/contraseñas.json`, ese archivo tiene prioridad.
### Las imágenes o pedidos no aparecen
- Confirma que las carpetas `Imagenes/`, `pedidos/` y `videos/` están en la misma ubicación que `docker-compose.yml`.
- En Windows con Docker Desktop, activa **Settings → Resources → File sharing** para la unidad donde esté el proyecto.
## Notas de seguridad
- Cambia `SESSION_SECRET` en `docker-compose.yml` por algo aleatorio y largo si vas a exponer el sitio a internet.
- El proyecto está pensado para red local / catálogo interno. No expongas el puerto 6060 a internet sin un reverse proxy (nginx/traefik) y HTTPS.
## ¿Por qué se empaqueta el frontend dentro de la imagen?
Así, cuando subes cambios a Gitea y haces `git pull` + `--build` en el otro PC, el frontend se actualiza automáticamente. Si se montara `frontend/` como volumen, el contenedor usaría siempre los archivos antiguos que ya estuvieran en el disco.
+5 -1
View File
@@ -9,9 +9,13 @@ RUN npm install --production
# Copiar el código del backend # Copiar el código del backend
COPY backend/ ./ COPY backend/ ./
# Copiar el frontend (servido como estático) # Copiar el frontend empaquetado (no se monta como volumen en producción)
COPY frontend/ ./frontend/ COPY frontend/ ./frontend/
# Script de entrada: crea datos/carpetas por defecto si no existen en el volumen
RUN chmod +x ./docker-entrypoint.sh
EXPOSE 3000 EXPOSE 3000
ENTRYPOINT ["./docker-entrypoint.sh"]
CMD ["node", "server.js"] CMD ["node", "server.js"]
+29
View File
@@ -0,0 +1,29 @@
#!/bin/sh
set -e
# Crear carpetas de datos si no existen (volúmenes vacíos en PC nuevo)
mkdir -p /app/data
mkdir -p /app/Imagenes
mkdir -p /app/Imagenes/agotados
mkdir -p /app/Imagenes/sin-categoria
mkdir -p /app/pedidos
mkdir -p /app/videos
# Crear contraseñas.json por defecto si no existe, con admin/admin
if [ ! -f /app/data/contraseñas.json ]; then
echo '{"usuarios":[{"usuario":"admin","contraseña":"admin","rol":"admin","permisos":[]}]}' > /app/data/contraseñas.json
echo "[entrypoint] ✅ Creado admin por defecto: usuario=admin / contraseña=admin"
fi
# Crear el resto de JSON base si no existen
[ -f /app/data/productos.json ] || echo '[]' > /app/data/productos.json
[ -f /app/data/categorias.json ] || echo '[]' > /app/data/categorias.json
[ -f /app/data/sesiones.json ] || echo '[]' > /app/data/sesiones.json
[ -f /app/data/vendedores.json ] || echo '[]' > /app/data/vendedores.json
[ -f /app/data/redes.json ] || echo '[]' > /app/data/redes.json
[ -f /app/data/pedidos-meta.json ] || echo '[]' > /app/data/pedidos-meta.json
# Asegurar que el backend pueda escribir en las carpetas montadas
chown -R node:node /app/data /app/Imagenes /app/pedidos /app/videos 2>/dev/null || true
exec "$@"
+56 -3
View File
@@ -25,16 +25,61 @@ const IMG_DIR = path.join(__dirname, 'Imagenes');
const PEDIDOS_DIR = path.join(__dirname, 'pedidos'); const PEDIDOS_DIR = path.join(__dirname, 'pedidos');
const VIDEOS_DIR = path.join(__dirname, 'videos'); const VIDEOS_DIR = path.join(__dirname, 'videos');
// ════════════════════════════════
// INICIALIZACIÓN DE DATOS / CARPETAS
// Se ejecuta antes de arrancar el servidor. Útil cuando se montan volúmenes
// vacíos en un PC nuevo por primera vez.
// ════════════════════════════════
async function initDataFiles() {
try {
await fs.mkdir(DATA_DIR, { recursive: true });
await fs.mkdir(IMG_DIR, { recursive: true });
await fs.mkdir(path.join(IMG_DIR, 'agotados'), { recursive: true });
await fs.mkdir(path.join(IMG_DIR, SIN_CATEGORIA_ID), { recursive: true });
await fs.mkdir(PEDIDOS_DIR, { recursive: true });
await fs.mkdir(VIDEOS_DIR, { recursive: true });
const defaults = {
'contraseñas.json': { usuarios: [{ usuario: 'admin', contraseña: 'admin', rol: 'admin', permisos: [] }] },
'productos.json': [],
'categorias.json': [],
'sesiones.json': [],
'vendedores.json': [],
'redes.json': [],
'pedidos-meta.json': []
};
for (const [file, value] of Object.entries(defaults)) {
const filePath = path.join(DATA_DIR, file);
try {
await fs.access(filePath);
} catch {
await fs.writeFile(filePath, JSON.stringify(value, null, 2) + '\n', 'utf8');
console.log(`[init] Creado ${file} por defecto`);
}
}
} catch (err) {
console.error('[init] Error inicializando datos:', err.message);
}
}
// ════════════════════════════════ // ════════════════════════════════
// MIDDLEWARES // MIDDLEWARES
// ════════════════════════════════ // ════════════════════════════════
app.use(express.json()); app.use(express.json());
app.use(cookieParser(SECRET)); app.use(cookieParser(SECRET));
// Servir frontend estático con index.html como fallback de carpeta
app.use(express.static(FRONTEND_DIR)); app.use(express.static(FRONTEND_DIR));
app.use('/Imagenes', express.static(IMG_DIR, { maxAge: '7d' })); app.use('/Imagenes', express.static(IMG_DIR, { maxAge: '7d' }));
app.use('/pedidos', express.static(PEDIDOS_DIR)); app.use('/pedidos', express.static(PEDIDOS_DIR));
app.use('/videos', express.static(VIDEOS_DIR)); app.use('/videos', express.static(VIDEOS_DIR));
// Ruta raíz explícita: sirve el catálogo principal
app.get('/', (req, res) => {
res.sendFile(path.join(FRONTEND_DIR, 'index.html'));
});
// Evita el 404 de favicon.ico // Evita el 404 de favicon.ico
app.get('/favicon.ico', (req, res) => res.status(204).end()); app.get('/favicon.ico', (req, res) => res.status(204).end());
@@ -1506,7 +1551,9 @@ app.use((err, req, res, next) => {
}); });
// ════════════════════════════════ // ════════════════════════════════
// FALLBACK para SPA-like del admin // FALLBACK para SPA-like del admin y catálogo
// Sirve index.html para rutas desconocidas que parezcan de la SPA.
// ⚠️ Debe ir DESPUÉS de todas las rutas de API.
// ════════════════════════════════ // ════════════════════════════════
app.get('/administrativo', (req, res) => { app.get('/administrativo', (req, res) => {
res.sendFile(path.join(FRONTEND_DIR, 'administrativo', 'index.html')); res.sendFile(path.join(FRONTEND_DIR, 'administrativo', 'index.html'));
@@ -1514,6 +1561,9 @@ app.get('/administrativo', (req, res) => {
app.get('/administrativo/', (req, res) => { app.get('/administrativo/', (req, res) => {
res.sendFile(path.join(FRONTEND_DIR, 'administrativo', 'index.html')); res.sendFile(path.join(FRONTEND_DIR, 'administrativo', 'index.html'));
}); });
app.get('/administrativo/*', (req, res) => {
res.sendFile(path.join(FRONTEND_DIR, 'administrativo', 'index.html'));
});
// ════════════════════════════════ // ════════════════════════════════
// LIMPIEZA DE SESIONES A LAS 00:00 (America/Bogota) // LIMPIEZA DE SESIONES A LAS 00:00 (America/Bogota)
@@ -2389,7 +2439,10 @@ app.get('/api/redes/proxy', async (req, res) => {
// ════════════════════════════════ // ════════════════════════════════
// START // START
// ════════════════════════════════ // ════════════════════════════════
app.listen(PORT, () => { (async () => {
await initDataFiles();
app.listen(PORT, () => {
console.log(`∞ Infinity server corriendo en http://localhost:${PORT}`); console.log(`∞ Infinity server corriendo en http://localhost:${PORT}`);
scheduleSessionCleanup(); scheduleSessionCleanup();
}); });
})();
+14 -1
View File
@@ -1 +1,14 @@
[] [
{
"token": "a1883d5c1fb699692056fe9d50045970fcba1070dd38fb2447658070824a3d68",
"usuario": "admin",
"rol": "admin",
"createdAt": 1782235533863
},
{
"token": "98d1ef14b931cda699cd80b9b19bf2f1b67c6d66a15501a35a7dfde7d67cc27e",
"usuario": "admin",
"rol": "admin",
"createdAt": 1782235723745
}
]
+9 -2
View File
@@ -7,15 +7,22 @@ services:
ports: ports:
- "6060:3000" - "6060:3000"
volumes: volumes:
# Persistencia: los cambios en JSON se guardan en el host # Persistencia: los cambios en JSON, imágenes, pedidos y videos se guardan en el host
- ./data:/app/data - ./data:/app/data
- ./Imagenes:/app/Imagenes - ./Imagenes:/app/Imagenes
- ./pedidos:/app/pedidos - ./pedidos:/app/pedidos
- ./frontend:/app/frontend
- ./videos:/app/videos - ./videos:/app/videos
# NOTA: el frontend va empaquetado dentro de la imagen, NO se monta como volumen.
# Así al hacer pull en otro PC se actualiza la interfaz automáticamente.
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- PORT=3000 - PORT=3000
# ⚠️ Cambiar esta clave en producción por algo aleatorio y secreto # ⚠️ Cambiar esta clave en producción por algo aleatorio y secreto
- SESSION_SECRET=cambia-esta-clave-en-produccion-12345 - SESSION_SECRET=cambia-esta-clave-en-produccion-12345
restart: unless-stopped restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/categorias"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s