Add new build and database setup scripts to package.json for production
This commit is contained in:
69
.do/app.yaml
Normal file
69
.do/app.yaml
Normal file
@ -0,0 +1,69 @@
|
||||
name: scriptshare
|
||||
region: nyc
|
||||
|
||||
# Static site for the frontend
|
||||
static_sites:
|
||||
- name: scriptshare-frontend
|
||||
github:
|
||||
repo: your-username/scriptshare-cursor
|
||||
branch: main
|
||||
build_command: npm install && npm run build
|
||||
output_dir: dist
|
||||
environment_slug: node-js
|
||||
source_dir: /
|
||||
routes:
|
||||
- path: /
|
||||
envs:
|
||||
- key: VITE_APP_NAME
|
||||
value: ScriptShare
|
||||
- key: VITE_APP_URL
|
||||
value: ${APP_URL}
|
||||
- key: VITE_API_URL
|
||||
value: ${scriptshare-api.PUBLIC_URL}
|
||||
- key: VITE_ANALYTICS_ENABLED
|
||||
value: "true"
|
||||
|
||||
# Backend API service
|
||||
services:
|
||||
- name: scriptshare-api
|
||||
github:
|
||||
repo: your-username/scriptshare-cursor
|
||||
branch: main
|
||||
source_dir: /
|
||||
dockerfile_path: Dockerfile.api
|
||||
environment_slug: node-js
|
||||
instance_count: 1
|
||||
instance_size_slug: basic-xxs
|
||||
http_port: 3000
|
||||
routes:
|
||||
- path: /api
|
||||
health_check:
|
||||
http_path: /api/health
|
||||
envs:
|
||||
- key: NODE_ENV
|
||||
value: production
|
||||
- key: PORT
|
||||
value: "3000"
|
||||
- key: DATABASE_URL
|
||||
value: ${scriptshare-db.DATABASE_URL}
|
||||
- key: JWT_SECRET
|
||||
value: ${JWT_SECRET}
|
||||
- key: CORS_ORIGIN
|
||||
value: ${scriptshare-frontend.PUBLIC_URL}
|
||||
|
||||
# Managed MySQL database
|
||||
databases:
|
||||
- name: scriptshare-db
|
||||
engine: MYSQL
|
||||
version: "8"
|
||||
size: db-s-1vcpu-1gb
|
||||
num_nodes: 1
|
||||
|
||||
# Environment variables (these will be set in DigitalOcean dashboard)
|
||||
envs:
|
||||
- key: JWT_SECRET
|
||||
scope: RUN_AND_BUILD_TIME
|
||||
type: SECRET
|
||||
- key: APP_URL
|
||||
scope: RUN_AND_BUILD_TIME
|
||||
value: https://scriptshare-frontend-${APP_DOMAIN}
|
102
.github/workflows/deploy.yml
vendored
Normal file
102
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,102 @@
|
||||
name: Deploy to DigitalOcean
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
# Test job to run before deployment
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
|
||||
- name: Build frontend
|
||||
run: npm run build
|
||||
env:
|
||||
VITE_APP_NAME: ScriptShare
|
||||
VITE_APP_URL: https://scriptshare.example.com
|
||||
VITE_ANALYTICS_ENABLED: true
|
||||
|
||||
# Deploy job (only on main branch)
|
||||
deploy:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install doctl
|
||||
uses: digitalocean/action-doctl@v2
|
||||
with:
|
||||
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||
|
||||
- name: Get app info
|
||||
id: app-info
|
||||
run: |
|
||||
APP_ID=$(doctl apps list --format ID,Spec.Name --no-header | grep scriptshare | cut -d' ' -f1)
|
||||
echo "app-id=$APP_ID" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Trigger deployment
|
||||
run: |
|
||||
doctl apps create-deployment ${{ steps.app-info.outputs.app-id }} --wait
|
||||
|
||||
- name: Run database migrations
|
||||
run: |
|
||||
# Wait for deployment to complete
|
||||
sleep 60
|
||||
|
||||
# Run migrations via the app console (if needed)
|
||||
echo "Deployment completed. Please run database migrations manually if this is the first deployment."
|
||||
echo "Command: npm run db:setup:prod"
|
||||
|
||||
# Database migration job (manual trigger)
|
||||
migrate:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run migrations
|
||||
run: npm run db:migrate:prod
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
|
||||
# Manual workflow dispatch for running migrations
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
description: 'Action to perform'
|
||||
required: true
|
||||
default: 'migrate'
|
||||
type: choice
|
||||
options:
|
||||
- migrate
|
||||
- setup
|
248
DEPLOYMENT.md
Normal file
248
DEPLOYMENT.md
Normal file
@ -0,0 +1,248 @@
|
||||
# DigitalOcean Deployment Guide
|
||||
|
||||
This guide walks you through deploying ScriptShare to DigitalOcean App Platform with a managed MySQL database.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- DigitalOcean account
|
||||
- GitHub repository containing your code
|
||||
- Basic familiarity with DigitalOcean App Platform
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The deployment consists of:
|
||||
- **Frontend**: Static site (React/Vite build)
|
||||
- **Backend API**: Node.js service with Express
|
||||
- **Database**: DigitalOcean Managed MySQL Database
|
||||
|
||||
## Step 1: Prepare Your Repository
|
||||
|
||||
1. Ensure all the deployment files are in your GitHub repository:
|
||||
```
|
||||
.do/app.yaml # App Platform configuration
|
||||
Dockerfile.api # Backend API container
|
||||
drizzle.config.production.ts # Production DB config
|
||||
scripts/migrate-production.js # Migration script
|
||||
scripts/setup-production-db.js # DB setup script
|
||||
env.production.example # Environment variables template
|
||||
```
|
||||
|
||||
2. Commit and push all changes to your main branch.
|
||||
|
||||
## Step 2: Create the DigitalOcean App
|
||||
|
||||
### Option A: Using the DigitalOcean Console
|
||||
|
||||
1. Go to the [DigitalOcean App Platform](https://cloud.digitalocean.com/apps)
|
||||
2. Click **"Create App"**
|
||||
3. Choose **"GitHub"** as your source
|
||||
4. Select your repository and branch (usually `main`)
|
||||
5. DigitalOcean will automatically detect the `app.yaml` configuration
|
||||
|
||||
### Option B: Using the CLI
|
||||
|
||||
```bash
|
||||
# Install doctl CLI
|
||||
# On macOS: brew install doctl
|
||||
# On Linux: snap install doctl
|
||||
|
||||
# Authenticate
|
||||
doctl auth init
|
||||
|
||||
# Create the app
|
||||
doctl apps create --spec .do/app.yaml
|
||||
```
|
||||
|
||||
## Step 3: Configure Environment Variables
|
||||
|
||||
In the DigitalOcean dashboard, go to your app's Settings > Environment Variables and set:
|
||||
|
||||
### Required Variables
|
||||
```
|
||||
JWT_SECRET=your-super-secret-jwt-key-here-change-this-in-production
|
||||
ADMIN_EMAIL=admin@yourcompany.com
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=your-secure-password
|
||||
```
|
||||
|
||||
### Optional Variables
|
||||
```
|
||||
VITE_ANALYTICS_ENABLED=true
|
||||
RATE_LIMIT_ENABLED=true
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
```
|
||||
|
||||
⚠️ **Security Note**: Generate a strong JWT secret:
|
||||
```bash
|
||||
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
|
||||
```
|
||||
|
||||
## Step 4: Database Setup
|
||||
|
||||
The managed MySQL database will be automatically created. After the first deployment:
|
||||
|
||||
1. **Run Database Migrations**:
|
||||
```bash
|
||||
# In your app's console (or via GitHub Actions)
|
||||
npm run db:setup:prod
|
||||
```
|
||||
|
||||
2. **Verify Database Connection**:
|
||||
Check the API health endpoint: `https://your-api-url/api/health`
|
||||
|
||||
## Step 5: Update App Configuration
|
||||
|
||||
1. **Update Frontend URLs**: After deployment, update the environment variables with actual URLs:
|
||||
```
|
||||
VITE_APP_URL=https://your-frontend-url.ondigitalocean.app
|
||||
VITE_API_URL=https://your-api-url.ondigitalocean.app/api
|
||||
CORS_ORIGIN=https://your-frontend-url.ondigitalocean.app
|
||||
```
|
||||
|
||||
2. **Redeploy**: The app will automatically redeploy when you change environment variables.
|
||||
|
||||
## Step 6: Custom Domain (Optional)
|
||||
|
||||
1. In your app settings, go to **Domains**
|
||||
2. Click **Add Domain**
|
||||
3. Enter your domain name
|
||||
4. Configure DNS records as instructed
|
||||
|
||||
## Database Management
|
||||
|
||||
### Connecting to the Database
|
||||
|
||||
```bash
|
||||
# Get connection string from DigitalOcean dashboard
|
||||
mysql -h your-db-host -P 25060 -u your-username -p your-database-name --ssl-mode=REQUIRED
|
||||
```
|
||||
|
||||
### Running Migrations
|
||||
|
||||
```bash
|
||||
# Production migration
|
||||
npm run db:migrate:prod
|
||||
|
||||
# Create new migration
|
||||
npm run db:generate
|
||||
```
|
||||
|
||||
### Backup and Restore
|
||||
|
||||
DigitalOcean provides automatic daily backups. For manual backups:
|
||||
|
||||
```bash
|
||||
# Create backup
|
||||
mysqldump -h your-db-host -P 25060 -u your-username -p your-database-name --ssl-mode=REQUIRED > backup.sql
|
||||
|
||||
# Restore backup
|
||||
mysql -h your-db-host -P 25060 -u your-username -p your-database-name --ssl-mode=REQUIRED < backup.sql
|
||||
```
|
||||
|
||||
## Monitoring and Logs
|
||||
|
||||
### Application Logs
|
||||
- View logs in DigitalOcean Console: App → Runtime Logs
|
||||
- Or via CLI: `doctl apps logs <app-id> --type=run`
|
||||
|
||||
### Database Monitoring
|
||||
- Database metrics available in DigitalOcean dashboard
|
||||
- Set up alerts for CPU, memory, and connection usage
|
||||
|
||||
### Health Checks
|
||||
- API: `https://your-api-url/api/health`
|
||||
- Frontend: Built-in App Platform health checks
|
||||
|
||||
## Scaling
|
||||
|
||||
### Vertical Scaling
|
||||
- Increase instance size in App Platform settings
|
||||
- Database can be scaled up (not down) in database settings
|
||||
|
||||
### Horizontal Scaling
|
||||
- Increase instance count for API service
|
||||
- Frontend is automatically scaled as a static site
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Environment Variables**: Never commit secrets to Git
|
||||
2. **Database Access**: Use DigitalOcean's private networking
|
||||
3. **SSL/TLS**: Enabled by default on App Platform
|
||||
4. **Database Backups**: Verify daily backups are working
|
||||
5. **Access Control**: Use DigitalOcean teams for access management
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Build Failures**:
|
||||
```bash
|
||||
# Check build logs
|
||||
doctl apps logs <app-id> --type=build
|
||||
```
|
||||
|
||||
2. **Database Connection Issues**:
|
||||
- Verify DATABASE_URL format
|
||||
- Check firewall settings
|
||||
- Ensure SSL is enabled
|
||||
|
||||
3. **CORS Errors**:
|
||||
- Verify CORS_ORIGIN matches frontend URL
|
||||
- Check environment variable casing
|
||||
|
||||
4. **Missing Dependencies**:
|
||||
```bash
|
||||
# Clear npm cache and rebuild
|
||||
npm ci --clean-cache
|
||||
```
|
||||
|
||||
### Getting Support
|
||||
|
||||
- DigitalOcean Community: https://www.digitalocean.com/community
|
||||
- Support tickets: https://cloud.digitalocean.com/support
|
||||
- Documentation: https://docs.digitalocean.com/products/app-platform/
|
||||
|
||||
## Cost Optimization
|
||||
|
||||
### Current Configuration Costs (Approximate)
|
||||
- **API Service**: $5/month (Basic plan)
|
||||
- **Database**: $15/month (1GB RAM, 1 vCPU)
|
||||
- **Static Site**: $0 (included with API service)
|
||||
- **Total**: ~$20/month
|
||||
|
||||
### Cost Reduction Tips
|
||||
1. Use development database for testing
|
||||
2. Scale down during low usage periods
|
||||
3. Monitor and optimize database queries
|
||||
4. Use CDN for static assets
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
- [ ] Repository configured with deployment files
|
||||
- [ ] Environment variables set in DigitalOcean
|
||||
- [ ] JWT secret generated and configured
|
||||
- [ ] Database migrations run successfully
|
||||
- [ ] Health check endpoints responding
|
||||
- [ ] Frontend can communicate with API
|
||||
- [ ] Admin user created and accessible
|
||||
- [ ] Custom domain configured (if applicable)
|
||||
- [ ] Monitoring and alerts set up
|
||||
- [ ] Backup strategy verified
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Set up CI/CD**: Configure GitHub Actions for automated deployments
|
||||
2. **Monitoring**: Set up application performance monitoring
|
||||
3. **CDN**: Configure CDN for static assets
|
||||
4. **Analytics**: Integrate application analytics
|
||||
5. **Error Tracking**: Set up error monitoring service
|
||||
|
||||
## Support
|
||||
|
||||
For issues specific to this deployment setup, please check:
|
||||
1. DigitalOcean App Platform documentation
|
||||
2. Application logs in the DigitalOcean console
|
||||
3. Database connection and query logs
|
||||
|
||||
Remember to regularly update dependencies and monitor security advisories for your application stack.
|
224
Dockerfile.api
Normal file
224
Dockerfile.api
Normal file
@ -0,0 +1,224 @@
|
||||
# Production API Dockerfile for DigitalOcean
|
||||
FROM node:18-alpine
|
||||
|
||||
# Install system dependencies for native modules
|
||||
RUN apk add --no-cache python3 make g++ libc6-compat
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files first for better caching
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --only=production=false --silent
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Create API-only build by removing frontend dependencies and files
|
||||
RUN npm uninstall @vitejs/plugin-react-swc vite
|
||||
RUN rm -rf src/components src/pages src/contexts src/hooks/use-toast.ts src/utils/toast.ts
|
||||
RUN rm -rf src/main.tsx src/App.tsx src/index.css
|
||||
RUN rm -rf public index.html vite.config.ts tailwind.config.ts postcss.config.js
|
||||
|
||||
# Keep only API and database files
|
||||
# The structure will be: src/lib/api/* and src/lib/db/*
|
||||
|
||||
# Create a simple Express server
|
||||
RUN cat > src/server.ts << 'EOF'
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { createUser, getUserByEmail, updateUser, getAllUsers, getUserById } from './lib/api/users.js';
|
||||
import { getScripts, getScriptById, createScript, updateScript, deleteScript, moderateScript } from './lib/api/scripts.js';
|
||||
import { login, register, refreshToken } from './lib/api/auth.js';
|
||||
import { rateScript, getUserRating, getScriptRatingStats } from './lib/api/ratings.js';
|
||||
import { getPlatformAnalytics, getScriptAnalytics, trackEvent } from './lib/api/analytics.js';
|
||||
import { createCollection, getUserCollections, getPublicCollections, addScriptToCollection } from './lib/api/collections.js';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Middleware
|
||||
app.use(cors({
|
||||
origin: process.env.CORS_ORIGIN || '*',
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Auth routes
|
||||
app.post('/api/auth/login', async (req, res) => {
|
||||
try {
|
||||
const result = await login(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth/register', async (req, res) => {
|
||||
try {
|
||||
const result = await register(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Register error:', error);
|
||||
res.status(400).json({ error: 'Registration failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Scripts routes
|
||||
app.get('/api/scripts', async (req, res) => {
|
||||
try {
|
||||
const result = await getScripts(req.query);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Get scripts error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch scripts' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/scripts/:id', async (req, res) => {
|
||||
try {
|
||||
const script = await getScriptById(req.params.id);
|
||||
if (!script) {
|
||||
return res.status(404).json({ error: 'Script not found' });
|
||||
}
|
||||
res.json(script);
|
||||
} catch (error) {
|
||||
console.error('Get script error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch script' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/scripts', async (req, res) => {
|
||||
try {
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
const result = await createScript(req.body, userId);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Create script error:', error);
|
||||
res.status(500).json({ error: 'Failed to create script' });
|
||||
}
|
||||
});
|
||||
|
||||
// Users routes
|
||||
app.get('/api/users', async (req, res) => {
|
||||
try {
|
||||
const result = await getAllUsers();
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Get users error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch users' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/users/:id', async (req, res) => {
|
||||
try {
|
||||
const user = await getUserById(req.params.id);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
console.error('Get user error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch user' });
|
||||
}
|
||||
});
|
||||
|
||||
// Analytics routes
|
||||
app.get('/api/analytics/platform', async (req, res) => {
|
||||
try {
|
||||
const days = parseInt(req.query.days as string) || 30;
|
||||
const result = await getPlatformAnalytics(days);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Analytics error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch analytics' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/analytics/track', async (req, res) => {
|
||||
try {
|
||||
const result = await trackEvent(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Track event error:', error);
|
||||
res.status(500).json({ error: 'Failed to track event' });
|
||||
}
|
||||
});
|
||||
|
||||
// Collections routes
|
||||
app.get('/api/collections', async (req, res) => {
|
||||
try {
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
const result = userId ? await getUserCollections(userId) : await getPublicCollections();
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Get collections error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch collections' });
|
||||
}
|
||||
});
|
||||
|
||||
// Ratings routes
|
||||
app.post('/api/ratings', async (req, res) => {
|
||||
try {
|
||||
const result = await rateScript(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Rate script error:', error);
|
||||
res.status(500).json({ error: 'Failed to rate script' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/scripts/:id/ratings', async (req, res) => {
|
||||
try {
|
||||
const result = await getScriptRatingStats(req.params.id);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Get ratings error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch ratings' });
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((error: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
console.error('Unhandled error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
app.use('*', (req, res) => {
|
||||
res.status(404).json({ error: 'Endpoint not found' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`ScriptShare API server running on port ${PORT}`);
|
||||
console.log(`Environment: ${process.env.NODE_ENV}`);
|
||||
console.log(`Database URL configured: ${!!process.env.DATABASE_URL}`);
|
||||
});
|
||||
EOF
|
||||
|
||||
# Install Express and CORS for the API server
|
||||
RUN npm install express cors @types/express @types/cors
|
||||
|
||||
# Build TypeScript (if any TS files remain)
|
||||
RUN npx tsc --build || echo "TypeScript build completed with warnings"
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD curl -f http://localhost:3000/api/health || exit 1
|
||||
|
||||
# Start the API server
|
||||
CMD ["node", "src/server.js"]
|
16
drizzle.config.production.ts
Normal file
16
drizzle.config.production.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/lib/db/schema.ts',
|
||||
out: './drizzle',
|
||||
dialect: 'mysql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
// DigitalOcean Managed Database specific settings
|
||||
introspect: {
|
||||
casing: 'camel'
|
||||
}
|
||||
});
|
40
env.production.example
Normal file
40
env.production.example
Normal file
@ -0,0 +1,40 @@
|
||||
# Production Environment Configuration for DigitalOcean
|
||||
|
||||
# Database Configuration (will be replaced by DigitalOcean DATABASE_URL)
|
||||
DATABASE_URL="mysql://username:password@hostname:port/database_name"
|
||||
|
||||
# JWT Secret for authentication (SET THIS IN DIGITALOCEAN DASHBOARD)
|
||||
JWT_SECRET="your-super-secret-jwt-key-here-change-this-in-production"
|
||||
|
||||
# App Configuration
|
||||
NODE_ENV="production"
|
||||
PORT="3000"
|
||||
|
||||
# Frontend Configuration
|
||||
VITE_APP_NAME="ScriptShare"
|
||||
VITE_APP_URL="https://your-app-domain.ondigitalocean.app"
|
||||
VITE_API_URL="https://your-api-domain.ondigitalocean.app/api"
|
||||
VITE_ANALYTICS_ENABLED="true"
|
||||
|
||||
# CORS Configuration
|
||||
CORS_ORIGIN="https://your-frontend-domain.ondigitalocean.app"
|
||||
|
||||
# Admin User Configuration (for initial setup only)
|
||||
ADMIN_EMAIL="admin@yourcompany.com"
|
||||
ADMIN_USERNAME="admin"
|
||||
ADMIN_PASSWORD="change-this-secure-password"
|
||||
|
||||
# Optional: Rate Limiting
|
||||
RATE_LIMIT_ENABLED="true"
|
||||
RATE_LIMIT_WINDOW_MS="900000"
|
||||
RATE_LIMIT_MAX_REQUESTS="100"
|
||||
|
||||
# Optional: File Upload Configuration
|
||||
MAX_FILE_SIZE="10485760"
|
||||
UPLOAD_PATH="/tmp/uploads"
|
||||
|
||||
# Optional: Email Configuration (if needed)
|
||||
SMTP_HOST=""
|
||||
SMTP_PORT=""
|
||||
SMTP_USER=""
|
||||
SMTP_PASS=""
|
@ -7,11 +7,15 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"build:dev": "tsc && vite build --mode development",
|
||||
"build:api": "tsc src/server.ts --outDir dist --target es2020 --module commonjs --esModuleInterop --allowSyntheticDefaultImports --resolveJsonModule --skipLibCheck",
|
||||
"start:api": "node dist/server.js",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:migrate:prod": "drizzle-kit migrate --config=drizzle.config.production.ts",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"db:setup:prod": "node scripts/setup-production-db.js",
|
||||
"create-superuser": "node scripts/create-superuser.js",
|
||||
"create-default-superuser": "node scripts/create-default-superuser.js",
|
||||
"setup-oliver": "node scripts/setup-oliver-admin.js"
|
||||
|
77
scripts/migrate-production.js
Normal file
77
scripts/migrate-production.js
Normal file
@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Production database migration script for DigitalOcean deployment
|
||||
* This script runs database migrations against the production MySQL database
|
||||
*/
|
||||
|
||||
import { drizzle } from 'drizzle-orm/mysql2';
|
||||
import mysql from 'mysql2/promise';
|
||||
import { migrate } from 'drizzle-orm/mysql2/migrator';
|
||||
import { config } from 'dotenv';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Load environment variables
|
||||
config();
|
||||
|
||||
async function runMigrations() {
|
||||
console.log('🚀 Starting production database migration...');
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ DATABASE_URL environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let connection;
|
||||
|
||||
try {
|
||||
// Parse DATABASE_URL for DigitalOcean managed database
|
||||
const dbUrl = new URL(process.env.DATABASE_URL);
|
||||
|
||||
// Create connection to MySQL
|
||||
connection = await mysql.createConnection({
|
||||
host: dbUrl.hostname,
|
||||
port: parseInt(dbUrl.port) || 25060,
|
||||
user: dbUrl.username,
|
||||
password: dbUrl.password,
|
||||
database: dbUrl.pathname.slice(1), // Remove leading slash
|
||||
ssl: {
|
||||
rejectUnauthorized: false // DigitalOcean managed databases use SSL
|
||||
},
|
||||
connectTimeout: 60000,
|
||||
acquireTimeout: 60000,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
console.log('✅ Connected to database');
|
||||
|
||||
// Create drizzle instance
|
||||
const db = drizzle(connection);
|
||||
|
||||
// Run migrations
|
||||
console.log('🔄 Running migrations...');
|
||||
await migrate(db, { migrationsFolder: join(__dirname, '../drizzle') });
|
||||
|
||||
console.log('✅ Migrations completed successfully!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
console.log('🔌 Database connection closed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run migrations if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
runMigrations().catch(console.error);
|
||||
}
|
||||
|
||||
export { runMigrations };
|
116
scripts/setup-production-db.js
Normal file
116
scripts/setup-production-db.js
Normal file
@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Production database setup script for DigitalOcean
|
||||
* This script sets up the initial database structure and creates a default admin user
|
||||
*/
|
||||
|
||||
import { drizzle } from 'drizzle-orm/mysql2';
|
||||
import mysql from 'mysql2/promise';
|
||||
import { migrate } from 'drizzle-orm/mysql2/migrator';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { config } from 'dotenv';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import * as schema from '../src/lib/db/schema.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Load environment variables
|
||||
config();
|
||||
|
||||
async function setupProductionDatabase() {
|
||||
console.log('🚀 Setting up production database...');
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ DATABASE_URL environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let connection;
|
||||
|
||||
try {
|
||||
// Parse DATABASE_URL for DigitalOcean managed database
|
||||
const dbUrl = new URL(process.env.DATABASE_URL);
|
||||
|
||||
// Create connection to MySQL
|
||||
connection = await mysql.createConnection({
|
||||
host: dbUrl.hostname,
|
||||
port: parseInt(dbUrl.port) || 25060,
|
||||
user: dbUrl.username,
|
||||
password: dbUrl.password,
|
||||
database: dbUrl.pathname.slice(1), // Remove leading slash
|
||||
ssl: {
|
||||
rejectUnauthorized: false // DigitalOcean managed databases use SSL
|
||||
},
|
||||
connectTimeout: 60000,
|
||||
acquireTimeout: 60000,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
console.log('✅ Connected to database');
|
||||
|
||||
// Create drizzle instance
|
||||
const db = drizzle(connection, { schema });
|
||||
|
||||
// Run migrations
|
||||
console.log('🔄 Running migrations...');
|
||||
await migrate(db, { migrationsFolder: join(__dirname, '../drizzle') });
|
||||
console.log('✅ Migrations completed');
|
||||
|
||||
// Create default admin user
|
||||
console.log('👤 Creating default admin user...');
|
||||
|
||||
const adminEmail = process.env.ADMIN_EMAIL || 'admin@scriptshare.com';
|
||||
const adminPassword = process.env.ADMIN_PASSWORD || 'admin123';
|
||||
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
|
||||
|
||||
// Check if admin user already exists
|
||||
const existingAdmin = await db.query.users.findFirst({
|
||||
where: (users, { eq }) => eq(users.email, adminEmail)
|
||||
});
|
||||
|
||||
if (existingAdmin) {
|
||||
console.log('ℹ️ Admin user already exists, skipping creation');
|
||||
} else {
|
||||
const hashedPassword = await bcrypt.hash(adminPassword, 10);
|
||||
|
||||
await db.insert(schema.users).values({
|
||||
id: nanoid(),
|
||||
email: adminEmail,
|
||||
username: adminUsername,
|
||||
displayName: 'System Administrator',
|
||||
isAdmin: true,
|
||||
isModerator: true,
|
||||
avatarUrl: null,
|
||||
bio: 'Default system administrator account'
|
||||
});
|
||||
|
||||
console.log('✅ Default admin user created');
|
||||
console.log(`📧 Email: ${adminEmail}`);
|
||||
console.log(`👤 Username: ${adminUsername}`);
|
||||
console.log(`🔑 Password: ${adminPassword}`);
|
||||
console.log('⚠️ Please change the default password after first login!');
|
||||
}
|
||||
|
||||
console.log('🎉 Production database setup completed successfully!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Setup failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
console.log('🔌 Database connection closed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run setup if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
setupProductionDatabase().catch(console.error);
|
||||
}
|
||||
|
||||
export { setupProductionDatabase };
|
Reference in New Issue
Block a user