Para portainer
This commit is contained in:
+17
-2
@@ -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/
|
||||||
|
|||||||
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -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 "$@"
|
||||||
+58
-5
@@ -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 () => {
|
||||||
console.log(`∞ Infinity server corriendo en http://localhost:${PORT}`);
|
await initDataFiles();
|
||||||
scheduleSessionCleanup();
|
app.listen(PORT, () => {
|
||||||
});
|
console.log(`∞ Infinity server corriendo en http://localhost:${PORT}`);
|
||||||
|
scheduleSessionCleanup();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|||||||
+14
-1
@@ -1 +1,14 @@
|
|||||||
[]
|
[
|
||||||
|
{
|
||||||
|
"token": "a1883d5c1fb699692056fe9d50045970fcba1070dd38fb2447658070824a3d68",
|
||||||
|
"usuario": "admin",
|
||||||
|
"rol": "admin",
|
||||||
|
"createdAt": 1782235533863
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"token": "98d1ef14b931cda699cd80b9b19bf2f1b67c6d66a15501a35a7dfde7d67cc27e",
|
||||||
|
"usuario": "admin",
|
||||||
|
"rol": "admin",
|
||||||
|
"createdAt": 1782235723745
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
+9
-2
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user