Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae1b508726 | |||
| 26c05912fc |
@@ -0,0 +1,84 @@
|
|||||||
|
# Production Deployment Fixes
|
||||||
|
|
||||||
|
This document describes the fixes applied to resolve production deployment issues in Docker.
|
||||||
|
|
||||||
|
## Issues Fixed
|
||||||
|
|
||||||
|
### 1. Database Connection Error: "getaddrinfo ENOTFOUND postgres"
|
||||||
|
**Problem**: The backend was trying to connect to hostname "postgres" but the database service is named "db" in docker-compose.
|
||||||
|
**Solution**:
|
||||||
|
- Updated `knexfile.js` to use correct default host "db" instead of "postgres"
|
||||||
|
- Added `depends_on: db` to backend service in docker-compose.prod.yml
|
||||||
|
|
||||||
|
### 2. Backend Starting Before Database Ready
|
||||||
|
**Problem**: Backend service started before PostgreSQL was ready, causing connection failures.
|
||||||
|
**Solution**:
|
||||||
|
- Created `wait-for-db.sh` script that waits for PostgreSQL to be ready
|
||||||
|
- Updated Dockerfile to install postgresql-client and use the wait script
|
||||||
|
- Script also runs migrations automatically on startup
|
||||||
|
|
||||||
|
### 3. Email Processor Initialization Failure
|
||||||
|
**Problem**: Email processor tried to initialize on module load before database was available.
|
||||||
|
**Solution**:
|
||||||
|
- Modified `emailProcessor.js` to export initialization functions
|
||||||
|
- Updated `server.js` to call initialization after database is ready
|
||||||
|
- Added proper error handling for email service initialization
|
||||||
|
|
||||||
|
### 4. Missing Environment Variables
|
||||||
|
**Problem**: Critical storage path environment variables were missing.
|
||||||
|
**Solution**:
|
||||||
|
- Added STORAGE_PATH, EVENTS_PATH, and ARCHIVE_PATH to docker-compose.prod.yml
|
||||||
|
- Created `.env.example` documenting all required environment variables
|
||||||
|
|
||||||
|
### 5. Enhanced Health Check
|
||||||
|
**Problem**: Basic health check didn't verify database connectivity.
|
||||||
|
**Solution**:
|
||||||
|
- Updated `/api/health` endpoint to check database connection
|
||||||
|
- Returns proper HTTP 503 status when unhealthy
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. **backend/knexfile.js** - Fixed production database defaults
|
||||||
|
2. **backend/wait-for-db.sh** - Created database wait script
|
||||||
|
3. **backend/Dockerfile** - Added postgresql-client and wait script
|
||||||
|
4. **docker-compose.prod.yml** - Added dependencies and environment variables
|
||||||
|
5. **backend/src/services/emailProcessor.js** - Disabled auto-initialization
|
||||||
|
6. **backend/server.js** - Added email initialization and improved health check
|
||||||
|
7. **backend/.env.example** - Created environment variable documentation
|
||||||
|
|
||||||
|
## Deployment Steps
|
||||||
|
|
||||||
|
1. Ensure all environment variables are set according to `.env.example`
|
||||||
|
2. Build and deploy with docker-compose:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yml build
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
3. The backend will now:
|
||||||
|
- Wait for PostgreSQL to be ready
|
||||||
|
- Run migrations automatically
|
||||||
|
- Initialize all services in proper order
|
||||||
|
- Provide health status at `/api/health`
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Check deployment health:
|
||||||
|
```bash
|
||||||
|
curl http://localhost/api/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"database": "connected",
|
||||||
|
"timestamp": "2025-07-13T20:30:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Email Configuration
|
||||||
|
|
||||||
|
Email service requires configuration in the database. If email is not configured:
|
||||||
|
- The service will log a warning but continue running
|
||||||
|
- Emails will be queued but not sent
|
||||||
|
- Configure email settings in the admin panel after deployment
|
||||||
+33
-34
@@ -1,42 +1,41 @@
|
|||||||
NODE_ENV=development
|
# Backend Environment Variables Example
|
||||||
|
# Copy this file to .env and update with your values
|
||||||
|
|
||||||
|
# Application
|
||||||
|
NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|
||||||
# URLs
|
|
||||||
ADMIN_URL=http://localhost:3000
|
|
||||||
FRONTEND_URL=http://localhost:3001
|
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
JWT_SECRET=dev-secret-key
|
JWT_SECRET=your-very-secure-jwt-secret-at-least-32-characters-long
|
||||||
|
|
||||||
# Email Configuration
|
# URLs
|
||||||
SMTP_HOST=mailhog
|
ADMIN_URL=https://yourdomain.com
|
||||||
SMTP_PORT=1025
|
FRONTEND_URL=https://yourdomain.com
|
||||||
SMTP_SECURE=false
|
|
||||||
SMTP_USER=
|
|
||||||
SMTP_PASS=
|
|
||||||
EMAIL_FROM=noreply@localhost
|
|
||||||
|
|
||||||
# Storage Paths (relative to project root)
|
|
||||||
STORAGE_PATH=./storage
|
|
||||||
EVENTS_PATH=./storage/events
|
|
||||||
ARCHIVE_PATH=./storage/events/archived
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_LEVEL=info
|
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
# Development: Use SQLite
|
DATABASE_CLIENT=pg
|
||||||
DATABASE_CLIENT=sqlite3
|
DB_HOST=db
|
||||||
DATABASE_PATH=./data/photo_sharing.db
|
DB_PORT=5432
|
||||||
|
DB_USER=picpeak
|
||||||
|
DB_PASSWORD=your-secure-database-password
|
||||||
|
DB_NAME=picpeak
|
||||||
|
|
||||||
# Production: Use PostgreSQL
|
# Email Configuration
|
||||||
# DATABASE_CLIENT=pg
|
SMTP_HOST=smtp.example.com
|
||||||
# DB_HOST=localhost
|
SMTP_PORT=587
|
||||||
# DB_PORT=5432
|
SMTP_SECURE=false
|
||||||
# DB_USER=picpeak
|
SMTP_USER=your-smtp-username
|
||||||
# DB_PASSWORD=your-secure-password
|
SMTP_PASS=your-smtp-password
|
||||||
# DB_NAME=picpeak
|
EMAIL_FROM=noreply@yourdomain.com
|
||||||
|
|
||||||
# Umami Analytics (optional)
|
# Storage Paths (Docker)
|
||||||
UMAMI_URL=
|
STORAGE_PATH=/app/storage
|
||||||
UMAMI_WEBSITE_ID=
|
EVENTS_PATH=/app/storage/events
|
||||||
|
ARCHIVE_PATH=/app/storage/events/archived
|
||||||
|
|
||||||
|
# Analytics (Optional)
|
||||||
|
UMAMI_URL=https://analytics.yourdomain.com
|
||||||
|
UMAMI_WEBSITE_ID=your-website-id
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=info
|
||||||
+6
-3
@@ -16,8 +16,8 @@ FROM node:18-alpine
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install dumb-init for proper signal handling
|
# Install dumb-init for proper signal handling and postgresql-client for database checks
|
||||||
RUN apk add --no-cache dumb-init
|
RUN apk add --no-cache dumb-init postgresql-client
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
|
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
|
||||||
@@ -26,6 +26,9 @@ RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
|
|||||||
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
|
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
|
||||||
COPY --chown=nodejs:nodejs . .
|
COPY --chown=nodejs:nodejs . .
|
||||||
|
|
||||||
|
# Make wait script executable
|
||||||
|
RUN chmod +x wait-for-db.sh
|
||||||
|
|
||||||
# Create necessary directories
|
# Create necessary directories
|
||||||
RUN mkdir -p storage/events/active storage/events/archived storage/thumbnails data logs && \
|
RUN mkdir -p storage/events/active storage/events/archived storage/thumbnails data logs && \
|
||||||
chown -R nodejs:nodejs storage data logs
|
chown -R nodejs:nodejs storage data logs
|
||||||
@@ -35,4 +38,4 @@ USER nodejs
|
|||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
ENTRYPOINT ["dumb-init", "--"]
|
ENTRYPOINT ["dumb-init", "--"]
|
||||||
CMD ["node", "server.js"]
|
CMD ["./wait-for-db.sh", "node", "server.js"]
|
||||||
|
|||||||
+4
-4
@@ -27,11 +27,11 @@ const config = {
|
|||||||
production: {
|
production: {
|
||||||
client: process.env.DATABASE_CLIENT || 'pg',
|
client: process.env.DATABASE_CLIENT || 'pg',
|
||||||
connection: {
|
connection: {
|
||||||
host: process.env.DB_HOST || 'postgres',
|
host: process.env.DB_HOST || 'db',
|
||||||
port: process.env.DB_PORT || 5432,
|
port: process.env.DB_PORT || 5432,
|
||||||
user: process.env.DB_USER || 'postgres',
|
user: process.env.DB_USER || 'picpeak',
|
||||||
password: process.env.DB_PASSWORD || 'postgres',
|
password: process.env.DB_PASSWORD,
|
||||||
database: process.env.DB_NAME || 'photo_sharing'
|
database: process.env.DB_NAME || 'picpeak'
|
||||||
},
|
},
|
||||||
pool: {
|
pool: {
|
||||||
min: 2,
|
min: 2,
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+22
-4
@@ -13,7 +13,7 @@ const path = require('path');
|
|||||||
const { initializeDatabase } = require('./src/database/db');
|
const { initializeDatabase } = require('./src/database/db');
|
||||||
const { startFileWatcher } = require('./src/services/fileWatcher');
|
const { startFileWatcher } = require('./src/services/fileWatcher');
|
||||||
const { startExpirationChecker } = require('./src/services/expirationChecker');
|
const { startExpirationChecker } = require('./src/services/expirationChecker');
|
||||||
const { startEmailQueueProcessor } = require('./src/services/emailProcessor');
|
const { initializeTransporter, startEmailQueueProcessor } = require('./src/services/emailProcessor');
|
||||||
const { maintenanceMiddleware } = require('./src/middleware/maintenance');
|
const { maintenanceMiddleware } = require('./src/middleware/maintenance');
|
||||||
const { sessionTimeoutMiddleware } = require('./src/middleware/sessionTimeout');
|
const { sessionTimeoutMiddleware } = require('./src/middleware/sessionTimeout');
|
||||||
const logger = require('./src/utils/logger');
|
const logger = require('./src/utils/logger');
|
||||||
@@ -152,8 +152,25 @@ app.use('/thumbnails', require('./src/middleware/photoAuth'), setCorsHeaders, se
|
|||||||
app.use('/uploads', setCorsHeaders, secureStatic(path.join(__dirname, 'storage/uploads')));
|
app.use('/uploads', setCorsHeaders, secureStatic(path.join(__dirname, 'storage/uploads')));
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
app.get('/api/health', (req, res) => {
|
app.get('/api/health', async (req, res) => {
|
||||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
try {
|
||||||
|
// Check database connectivity
|
||||||
|
await db.raw('SELECT 1');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
database: 'connected',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Health check failed:', error);
|
||||||
|
res.status(503).json({
|
||||||
|
status: 'error',
|
||||||
|
database: 'disconnected',
|
||||||
|
error: error.message,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
@@ -189,7 +206,8 @@ async function startServer() {
|
|||||||
// Start expiration checker
|
// Start expiration checker
|
||||||
startExpirationChecker();
|
startExpirationChecker();
|
||||||
|
|
||||||
// Start email queue processor
|
// Initialize email transporter and start queue processor
|
||||||
|
await initializeTransporter();
|
||||||
startEmailQueueProcessor();
|
startEmailQueueProcessor();
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
|
|||||||
@@ -395,12 +395,15 @@ function stopEmailQueueProcessor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on module load
|
// Initialize on module load - DISABLED for production startup
|
||||||
initializeTransporter().then(() => {
|
// This will be called from server.js after database is ready
|
||||||
startEmailQueueProcessor();
|
// initializeTransporter().then(() => {
|
||||||
});
|
// startEmailQueueProcessor();
|
||||||
|
// });
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
initializeTransporter,
|
||||||
|
startEmailQueueProcessor,
|
||||||
sendTemplateEmail,
|
sendTemplateEmail,
|
||||||
processEmailQueue,
|
processEmailQueue,
|
||||||
queueEmail,
|
queueEmail,
|
||||||
|
|||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# wait-for-db.sh - Wait for PostgreSQL to be ready before starting the application
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
host="$DB_HOST"
|
||||||
|
port="${DB_PORT:-5432}"
|
||||||
|
user="${DB_USER:-picpeak}"
|
||||||
|
|
||||||
|
echo "Waiting for PostgreSQL at $host:$port..."
|
||||||
|
|
||||||
|
# Wait for PostgreSQL to be ready
|
||||||
|
until PGPASSWORD=$DB_PASSWORD psql -h "$host" -p "$port" -U "$user" -d "${DB_NAME:-picpeak}" -c '\q' 2>/dev/null; do
|
||||||
|
>&2 echo "PostgreSQL is unavailable - sleeping"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
>&2 echo "PostgreSQL is up - executing command"
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
echo "Running database migrations..."
|
||||||
|
npm run migrate
|
||||||
|
|
||||||
|
# Execute the main command
|
||||||
|
exec "$@"
|
||||||
@@ -8,6 +8,8 @@ services:
|
|||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
@@ -31,6 +33,10 @@ services:
|
|||||||
# Analytics
|
# Analytics
|
||||||
- UMAMI_URL=${UMAMI_URL}
|
- UMAMI_URL=${UMAMI_URL}
|
||||||
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
|
- UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
|
||||||
|
# Storage paths
|
||||||
|
- STORAGE_PATH=/app/storage
|
||||||
|
- EVENTS_PATH=/app/storage/events
|
||||||
|
- ARCHIVE_PATH=/app/storage/events/archived
|
||||||
volumes:
|
volumes:
|
||||||
- ./storage:/app/storage
|
- ./storage:/app/storage
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@tiptap/extension-link": "^2.25.0",
|
"@tiptap/extension-link": "^2.25.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Reference in New Issue
Block a user