Multi-Site Decap CMS OAuth Provider
Initial Setup
This section converts your existing single-site OAuth provider to support multiple sites.
Step 1: Backup Current Configuration
Before making changes, backup your working setup:
cd /opt/docker/decap-oauth
cp server.js server.js.backup
cp docker-compose.yml docker-compose.yml.backup
If something goes wrong, you can restore:
cp server.js.backup server.js
cp docker-compose.yml.backup docker-compose.yml
Step 2: Update server.js for Multi-Site Support
Replace the contents of server.js:
nano server.js
New multi-site server.js:
const express = require('express');
const cors = require('cors');
const { AuthorizationCode } = require('simple-oauth2');
const app = express();
const port = process.env.PORT || 3000;
// CORS configuration - allow all origins
app.use(cors({
origin: true,
credentials: true
}));
// Parse query parameters
app.use(express.urlencoded({ extended: true }));
// ============================================
// CONFIGURATION: Add your sites here
// ============================================
const clients = {
// Site 1: Boeygembos (example)
boeygembos: {
client: new AuthorizationCode({
client: {
id: process.env.BOEYGEMBOS_CLIENT_ID,
secret: process.env.BOEYGEMBOS_CLIENT_SECRET,
},
auth: {
tokenHost: 'https://github.com',
tokenPath: '/login/oauth/access_token',
authorizePath: '/login/oauth/authorize',
},
}),
allowedOrigins: ['https://www.boeygembos.be'],
description: 'Boeygembos Forest Information Site'
},
// Site 2: Add your next site here (template)
// site2: {
// client: new AuthorizationCode({
// client: {
// id: process.env.SITE2_CLIENT_ID,
// secret: process.env.SITE2_CLIENT_SECRET,
// },
// auth: {
// tokenHost: 'https://github.com',
// tokenPath: '/login/oauth/access_token',
// authorizePath: '/login/oauth/authorize',
// },
// }),
// allowedOrigins: ['https://www.site2.com'],
// description: 'Site 2 Description'
// },
};
// ============================================
// Helper Functions
// ============================================
// Get client configuration by site name
function getClientBySite(siteName) {
return clients[siteName];
}
// Validate origin against allowed origins
function isOriginAllowed(siteName, origin) {
const clientConfig = getClientBySite(siteName);
if (!clientConfig) return false;
return clientConfig.allowedOrigins.includes(origin);
}
// ============================================
// Routes
// ============================================
// Root endpoint - shows configuration
app.get('/', (req, res) => {
const sites = Object.keys(clients).map(siteName => ({
name: siteName,
description: clients[siteName].description,
allowedOrigins: clients[siteName].allowedOrigins,
endpoints: {
auth: `/auth/${siteName}`,
callback: `/callback/${siteName}`
}
}));
res.json({
message: 'Multi-Site OAuth Provider for Decap CMS',
version: '2.0',
configured_sites: sites.length,
sites: sites,
health_check: '/health'
});
});
// Health check endpoint
app.get('/health', (req, res) => {
const siteCount = Object.keys(clients).length;
res.json({
status: 'healthy',
message: 'OAuth provider is running',
configured_sites: siteCount,
uptime: process.uptime()
});
});
// Auth endpoint - redirects to GitHub OAuth
app.get('/auth/:site', (req, res) => {
const siteName = req.params.site;
const clientConfig = getClientBySite(siteName);
if (!clientConfig) {
console.error(`Unknown site requested: ${siteName}`);
return res.status(400).json({
error: 'Unknown site',
message: `Site '${siteName}' is not configured`,
configured_sites: Object.keys(clients)
});
}
const authorizationUri = clientConfig.client.authorizeURL({
redirect_uri: `${process.env.BASE_URL}/callback/${siteName}`,
scope: 'repo,user',
state: req.query.state || '',
});
console.log(`Auth request for site: ${siteName}`);
res.redirect(authorizationUri);
});
// Callback endpoint - handles GitHub OAuth callback
app.get('/callback/:site', async (req, res) => {
const { code, state } = req.query;
const siteName = req.params.site;
const clientConfig = getClientBySite(siteName);
if (!clientConfig) {
console.error(`Unknown site in callback: ${siteName}`);
return res.status(400).json({
error: 'Unknown site',
message: `Site '${siteName}' is not configured`
});
}
try {
const tokenParams = {
code,
redirect_uri: `${process.env.BASE_URL}/callback/${siteName}`,
};
const accessToken = await clientConfig.client.getToken(tokenParams);
const token = accessToken.token.access_token;
// Get the origin from allowed origins
const redirectOrigin = clientConfig.allowedOrigins[0];
console.log(`Successfully authenticated for site: ${siteName}`);
// Send token back to CMS via postMessage
const script = `
<!DOCTYPE html>
<html>
<head>
<title>Authentication Successful</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.message {
text-align: center;
padding: 2rem;
background: rgba(255,255,255,0.1);
border-radius: 10px;
backdrop-filter: blur(10px);
}
</style>
</head>
<body>
<div class="message">
<h1>✓ Authentication Successful</h1>
<p>You can close this window.</p>
</div>
<script>
(function() {
function receiveMessage(e) {
console.log("receiveMessage %o", e);
// Send success message to opener
window.opener.postMessage(
'authorization:github:success:${JSON.stringify({ token, provider: 'github' })}',
e.origin
);
}
window.addEventListener("message", receiveMessage, false);
// Notify opener that we're authorizing
window.opener.postMessage("authorizing:github", "${redirectOrigin}");
// Auto-close after 2 seconds
setTimeout(function() {
window.close();
}, 2000);
})();
</script>
</body>
</html>
`;
res.send(script);
} catch (error) {
console.error(`Authentication failed for site ${siteName}:`, error.message);
res.status(500).send(`
<!DOCTYPE html>
<html>
<head>
<title>Authentication Failed</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.message {
text-align: center;
padding: 2rem;
background: rgba(255,255,255,0.1);
border-radius: 10px;
backdrop-filter: blur(10px);
}
.error {
background: rgba(0,0,0,0.2);
padding: 1rem;
border-radius: 5px;
margin-top: 1rem;
font-family: monospace;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="message">
<h1>✗ Authentication Failed</h1>
<p>There was an error during authentication.</p>
<div class="error">${error.message}</div>
<p style="margin-top: 1rem; font-size: 0.9em;">You can close this window and try again.</p>
</div>
</body>
</html>
`);
}
});
// ============================================
// Start Server
// ============================================
app.listen(port, () => {
const siteNames = Object.keys(clients);
console.log('='.repeat(50));
console.log('Multi-Site OAuth Provider for Decap CMS');
console.log('='.repeat(50));
console.log(`Server listening on port ${port}`);
console.log(`Base URL: ${process.env.BASE_URL}`);
console.log(`Configured sites: ${siteNames.length}`);
console.log('');
console.log('Sites:');
siteNames.forEach(siteName => {
const config = clients[siteName];
console.log(` - ${siteName}`);
console.log(` Description: ${config.description}`);
console.log(` Allowed origins: ${config.allowedOrigins.join(', ')}`);
console.log(` Auth endpoint: ${process.env.BASE_URL}/auth/${siteName}`);
});
console.log('='.repeat(50));
});
Save with Ctrl+X, Y, Enter.
Key changes from single-site version:
clientsobject holds multiple site configurations- Routes include
:siteparameter (e.g.,/auth/boeygembos) - Each site has its own credentials and allowed origins
- Root endpoint (
/) shows all configured sites - Better error handling and logging
Step 3: Update docker-compose.yml
nano docker-compose.yml
New multi-site docker-compose.yml:
version: '3.8'
services:
decap-oauth:
build: .
container_name: decap-oauth
restart: unless-stopped
networks:
- web
environment:
# Server configuration
- NODE_ENV=production
- PORT=3000
- BASE_URL=https://oauth.boeygembos.be
# Site 1: Boeygembos
- BOEYGEMBOS_CLIENT_ID=your_boeygembos_client_id_here
- BOEYGEMBOS_CLIENT_SECRET=your_boeygembos_client_secret_here
# Site 2: Add credentials for next site
# - SITE2_CLIENT_ID=xxx
# - SITE2_CLIENT_SECRET=xxx
# Site 3: Add credentials for another site
# - SITE3_CLIENT_ID=xxx
# - SITE3_CLIENT_SECRET=xxx
labels:
- "traefik.enable=true"
- "traefik.http.routers.decap-oauth.rule=Host(`oauth.boeygembos.be`)"
- "traefik.http.routers.decap-oauth.entrypoints=websecure"
- "traefik.http.routers.decap-oauth.tls.certresolver=letsencrypt"
- "traefik.http.services.decap-oauth.loadbalancer.server.port=3000"
networks:
web:
external: true
IMPORTANT: Replace your_boeygembos_client_id_here and your_boeygembos_client_secret_here with your actual GitHub OAuth App credentials.
Save with Ctrl+X, Y, Enter.
Step 4: Update Existing Site’s Decap CMS Config
Your existing site (Boeygembos) needs a small config update:
File: boeygembos-app/public/admin/config.yml
Change:
backend:
name: github
repo: boeygembos/boeygembos.github.io
branch: release
base_url: https://oauth.boeygembos.be
auth_endpoint: auth # Old way
To:
backend:
name: github
repo: boeygembos/boeygembos.github.io
branch: release
base_url: https://oauth.boeygembos.be
auth_endpoint: auth/boeygembos # New way - includes site name
Note: The only change is adding /boeygembos to the auth_endpoint.
Commit and push:
git add public/admin/config.yml
git commit -m "Update OAuth endpoint for multi-site support"
git push origin release
Step 5: Rebuild and Deploy
cd /opt/docker/decap-oauth
# Rebuild container with new code
docker compose down
docker compose build --no-cache
docker compose up -d
# Watch logs
docker compose logs -f
You should see:
==================================================
Multi-Site OAuth Provider for Decap CMS
==================================================
Server listening on port 3000
Base URL: https://oauth.boeygembos.be
Configured sites: 1
Sites:
- boeygembos
Description: Boeygembos Forest Information Site
Allowed origins: https://www.boeygembos.be
Auth endpoint: https://oauth.boeygembos.be/auth/boeygembos
==================================================
Press Ctrl+C to exit logs.
Step 6: Test Existing Site
- Wait ~2 minutes for GitHub Actions to deploy the config change
- Go to: https://www.boeygembos.be/admin/
- Click “Login with GitHub”
- Should work exactly as before ✅
If it doesn’t work, check Troubleshooting section.
Step 7: Verify Multi-Site Setup
Check the provider info endpoint:
curl https://oauth.boeygembos.be/ | jq
Should return:
{
"message": "Multi-Site OAuth Provider for Decap CMS",
"version": "2.0",
"configured_sites": 1,
"sites": [
{
"name": "boeygembos",
"description": "Boeygembos Forest Information Site",
"allowedOrigins": ["https://www.boeygembos.be"],
"endpoints": {
"auth": "/auth/boeygembos",
"callback": "/callback/boeygembos"
}
}
],
"health_check": "/health"
}
Adding Additional Sites
Now that you have multi-site support, adding new sites is straightforward.
Overview: 5-Step Process
For each new site:
- Create GitHub OAuth App
- Add site to
server.js - Add credentials to
docker-compose.yml - Restart container
- Configure site’s Decap CMS
Time per site: ~15 minutes
Example: Adding “Client Portfolio” Site
Let’s walk through adding a second site: www.clientportfolio.com
Step 1: Create GitHub OAuth App
- Go to: https://github.com/settings/developers
- Click New OAuth App
- Fill in:
- Application name:
Client Portfolio CMS - Homepage URL:
https://www.clientportfolio.com - Authorization callback URL:
https://oauth.boeygembos.be/callback/clientportfolio- ⚠️ Important: Use site’s short name in path (no spaces, lowercase)
- Application name:
- Click Register application
- Copy Client ID
- Click Generate a new client secret
- Copy Client Secret
Save these somewhere secure!
Step 2: Add Site to server.js
SSH to VPS and edit server.js:
cd /opt/docker/decap-oauth
nano server.js
Find the clients object and add your new site:
const clients = {
// Existing site
boeygembos: {
client: new AuthorizationCode({
client: {
id: process.env.BOEYGEMBOS_CLIENT_ID,
secret: process.env.BOEYGEMBOS_CLIENT_SECRET,
},
auth: {
tokenHost: 'https://github.com',
tokenPath: '/login/oauth/access_token',
authorizePath: '/login/oauth/authorize',
},
}),
allowedOrigins: ['https://www.boeygembos.be'],
description: 'Boeygembos Forest Information Site'
},
// NEW SITE - Add this
clientportfolio: {
client: new AuthorizationCode({
client: {
id: process.env.CLIENTPORTFOLIO_CLIENT_ID,
secret: process.env.CLIENTPORTFOLIO_CLIENT_SECRET,
},
auth: {
tokenHost: 'https://github.com',
tokenPath: '/login/oauth/access_token',
authorizePath: '/login/oauth/authorize',
},
}),
allowedOrigins: ['https://www.clientportfolio.com'],
description: 'Client Portfolio Website'
},
};
Naming conventions:
- Object key:
clientportfolio(lowercase, no spaces - used in URL paths) - Environment variables:
CLIENTPORTFOLIO_CLIENT_ID(uppercase with underscores) - Description: Human-readable name
Save with Ctrl+X, Y, Enter.
Step 3: Add Credentials to docker-compose.yml
nano docker-compose.yml
Add environment variables:
environment:
# Server configuration
- NODE_ENV=production
- PORT=3000
- BASE_URL=https://oauth.boeygembos.be
# Site 1: Boeygembos
- BOEYGEMBOS_CLIENT_ID=xxx
- BOEYGEMBOS_CLIENT_SECRET=xxx
# Site 2: Client Portfolio - ADD THESE
- CLIENTPORTFOLIO_CLIENT_ID=paste_client_id_here
- CLIENTPORTFOLIO_CLIENT_SECRET=paste_client_secret_here
Replace paste_client_id_here and paste_client_secret_here with actual values from Step 1.
Save with Ctrl+X, Y, Enter.
Step 4: Rebuild and Restart Container
docker compose down
docker compose build --no-cache
docker compose up -d
# Check logs - should show 2 configured sites
docker compose logs --tail=30
Verify it’s configured:
curl https://oauth.boeygembos.be/ | jq
Should show 2 sites now.
Step 5: Configure New Site’s Decap CMS
In your Client Portfolio repository, edit the Decap CMS config:
File: public/admin/config.yml (or wherever your config is)
backend:
name: github
repo: username/clientportfolio-repo # Your actual repo
branch: main # Your branch
base_url: https://oauth.boeygembos.be
auth_endpoint: auth/clientportfolio # Match the key from server.js
media_folder: "public/images"
public_folder: "/images"
collections:
# Your collections here
Key points:
base_urlpoints to the shared OAuth providerauth_endpointincludes the site name:auth/clientportfolio- Site name must match the key in
server.jsclients object
Commit, push, and deploy the site.
Step 6: Test New Site
- Go to:
https://www.clientportfolio.com/admin/ - Click “Login with GitHub”
- Should redirect to GitHub
- Authorize
- Should be logged in ✅
Quick Add Template
For future sites, use this template:
1. GitHub OAuth App:
- Homepage:
https://www.YOURSITE.com - Callback:
https://oauth.boeygembos.be/callback/SITENAME
2. server.js addition:
SITENAME: {
client: new AuthorizationCode({
client: {
id: process.env.SITENAME_CLIENT_ID,
secret: process.env.SITENAME_CLIENT_SECRET,
},
auth: {
tokenHost: 'https://github.com',
tokenPath: '/login/oauth/access_token',
authorizePath: '/login/oauth/authorize',
},
}),
allowedOrigins: ['https://www.YOURSITE.com'],
description: 'SITE DESCRIPTION'
},
3. docker-compose.yml addition:
- SITENAME_CLIENT_ID=xxx
- SITENAME_CLIENT_SECRET=xxx
4. Site’s config.yml:
backend:
base_url: https://oauth.boeygembos.be
auth_endpoint: auth/SITENAME
5. Restart:
docker compose down && docker compose build --no-cache && docker compose up -d
Configuration Reference
Environment Variable Naming
Pattern: SITENAME_CREDENTIAL_TYPE
Where:
SITENAME= Uppercase version of site keyCREDENTIAL_TYPE=CLIENT_IDorCLIENT_SECRET
Examples:
BOEYGEMBOS_CLIENT_ID
BOEYGEMBOS_CLIENT_SECRET
CLIENTSITE_CLIENT_ID
CLIENTSITE_CLIENT_SECRET
MYPORTFOLIO_CLIENT_ID
MYPORTFOLIO_CLIENT_SECRET
Site Key Naming Conventions
Good site keys:
boeygembos✅clientsite✅portfolio2024✅myblog✅
Bad site keys:
Client Site❌ (spaces)my-blog❌ (hyphens cause issues)Site#2❌ (special characters)2ndsite❌ (starts with number)
Rules:
- Lowercase only
- No spaces
- No special characters
- Use letters and numbers only
- Can’t start with a number
Callback URL Format
Pattern: https://oauth.boeygembos.be/callback/SITENAME
Where SITENAME matches the key in server.js.
Examples:
https://oauth.boeygembos.be/callback/boeygembos
https://oauth.boeygembos.be/callback/clientsite
https://oauth.boeygembos.be/callback/portfolio2024
Decap CMS Config Format
Minimal required config:
backend:
name: github
repo: username/repository
branch: main
base_url: https://oauth.boeygembos.be
auth_endpoint: auth/SITENAME
With additional options:
backend:
name: github
repo: username/repository
branch: main
base_url: https://oauth.boeygembos.be
auth_endpoint: auth/SITENAME
open_authoring: true # Optional: enable open authoring
squash_merges: true # Optional: squash commits
Troubleshooting
Issue 1: “Unknown site” Error
Symptoms:
- Clicking “Login with GitHub” shows error page
- Error message: “Site ‘xyz’ is not configured”
Causes:
- Site key mismatch between
server.jsandauth_endpoint - Typo in site name
- Forgot to restart container after adding site
Solutions:
- Check site key matches:
// In server.js
const clients = {
mysite: { ... } // Key is 'mysite'
};
# In site's config.yml
auth_endpoint: auth/mysite # Must match exactly
- Verify container has latest code:
cd /opt/docker/decap-oauth
docker compose down
docker compose build --no-cache
docker compose up -d
docker compose logs | grep "Configured sites"
- Check configured sites:
curl https://oauth.boeygembos.be/ | jq '.sites[].name'
Issue 2: Authentication Fails with 401
Symptoms:
- Redirects to GitHub successfully
- Authorization fails with error
- Console shows 401 or “Bad credentials”
Causes:
- Wrong Client ID or Secret in
docker-compose.yml - Client Secret was regenerated on GitHub
- Environment variables not loaded
Solutions:
- Verify credentials in GitHub:
- Go to: https://github.com/settings/developers
- Find your OAuth App
- Check Client ID matches
- If needed, regenerate Client Secret
- Update docker-compose.yml:
nano docker-compose.yml
# Update the credentials
docker compose down
docker compose up -d
- Check environment variables loaded:
docker exec decap-oauth env | grep CLIENT
Should show all your CLIENT_ID and CLIENT_SECRET variables.
Issue 3: CORS Error in Browser Console
Symptoms:
- Browser console shows CORS error
- “Access-Control-Allow-Origin” error
- Authentication popup closes immediately
Causes:
- Site origin not in
allowedOriginsarray - HTTP vs HTTPS mismatch
- Wrong domain in allowedOrigins
Solutions:
- Check allowedOrigins in server.js:
allowedOrigins: ['https://www.yoursite.com'] // Must match exactly
Must match:
- Protocol (http vs https)
- Subdomain (www vs non-www)
- Domain spelling
- For development (testing locally):
Add localhost to allowedOrigins temporarily:
allowedOrigins: [
'https://www.yoursite.com',
'http://localhost:3000' // For local testing
]
Remember to remove localhost in production!
Issue 4: Container Won’t Start
Symptoms:
docker compose up -dfails- Container keeps restarting
- Can’t access OAuth provider
Causes:
- Syntax error in server.js
- Missing dependencies
- Port conflict
Solutions:
- Check logs for errors:
docker compose logs
- Common errors:
Syntax Error:
SyntaxError: Unexpected token
→ Check server.js for missing commas, brackets, etc.
Module not found:
Error: Cannot find module 'simple-oauth2'
→ Rebuild container:
docker compose build --no-cache
Port in use:
Error starting userland proxy: listen tcp 0.0.0.0:3000: bind: address already in use
→ Another container using port 3000. Check with:
docker ps | grep 3000
- Test server.js syntax locally:
node -c server.js
Should return nothing if syntax is valid.
Issue 5: GitHub OAuth App Restrictions
Symptoms:
- Error: “organization has enabled OAuth App access restrictions”
- Authentication fails after authorization
Cause:
- Organization has strict OAuth policies
Solution:
- Go to: https://github.com/organizations/ORG_NAME/settings/oauth_application_policy
- Find your OAuth App in the list
- Click Grant access or Request access
- If you’re org owner, access grants immediately
- If not, org admin must approve
Issue 6: Sites Work Individually but Not Together
Symptoms:
- Site 1 works fine
- Adding Site 2 breaks Site 1
- Authentication fails for both
Causes:
- Duplicate site keys
- Environment variable conflicts
- Syntax error breaking all sites
Solutions:
- Check for duplicate keys:
cd /opt/docker/decap-oauth
grep -n "^ [a-z]*:" server.js
Each site should appear only once.
- Test configuration endpoint:
curl https://oauth.boeygembos.be/ | jq
Should list all sites. If it errors, there’s a syntax problem.
- Roll back to working version:
cp server.js.backup server.js
docker compose restart
Then add sites one at a time, testing after each.
Maintenance
Monitoring
Check Provider Status
# Health check
curl https://oauth.boeygembos.be/health
# List configured sites
curl https://oauth.boeygembos.be/ | jq '.sites'
# View container logs
cd /opt/docker/decap-oauth
docker compose logs --tail=50
Watch Live Traffic
docker compose logs -f
You’ll see:
- Auth requests:
Auth request for site: sitename - Successful auth:
Successfully authenticated for site: sitename - Errors: Any authentication failures
Check Container Health
docker ps | grep decap-oauth
# Should show:
# - STATUS: Up X minutes/hours
# - PORTS: Not externally exposed (Traefik handles routing)
Updating Sites
Modify Existing Site
- Update server.js (e.g., change allowedOrigins)
nano /opt/docker/decap-oauth/server.js
# Make changes
- Restart container:
docker compose restart
No rebuild needed for config changes.
Remove a Site
- Delete from server.js:
// Remove the entire site object
- Remove from docker-compose.yml:
# Remove SITENAME_CLIENT_ID and SITENAME_CLIENT_SECRET
- Restart:
docker compose down
docker compose build --no-cache
docker compose up -d
- Revoke GitHub OAuth App (optional but recommended):
- Go to: https://github.com/settings/developers
- Find the OAuth App
- Click Delete
Rotating Secrets
If a Client Secret is compromised:
- Generate new secret on GitHub:
- Settings → Developer settings → OAuth Apps
- Find the app
- Click Revoke all user tokens (optional but recommended)
- Click Generate a new client secret
- Copy new secret
- Update docker-compose.yml:
nano docker-compose.yml
# Update SITENAME_CLIENT_SECRET
- Restart container:
docker compose restart
- Test authentication:
- Users will need to re-authorize
- Existing sessions will be invalidated
Backing Up Configuration
Backup regularly:
cd /opt/docker/decap-oauth
# Create backup directory
mkdir -p ../backups/decap-oauth
# Backup with timestamp
DATE=$(date +%Y%m%d-%H%M%S)
tar -czf ../backups/decap-oauth/backup-$DATE.tar.gz .
# Keep last 10 backups
cd ../backups/decap-oauth
ls -t | tail -n +11 | xargs rm -f
Restore from backup:
cd /opt/docker/decap-oauth
docker compose down
# Extract backup (replace DATE with actual backup)
tar -xzf ../backups/decap-oauth/backup-DATE.tar.gz
docker compose up -d
Updating Dependencies
Check for updates:
cd /opt/docker/decap-oauth
# Check current versions
docker exec decap-oauth npm list --depth=0
# Update package.json with latest versions
nano package.json
Update and rebuild:
# Update package.json
nano package.json
# Rebuild
docker compose down
docker compose build --no-cache
docker compose up -d
# Test all sites
Test thoroughly after dependency updates!
Logs Management
View logs:
docker compose logs --tail=100
docker compose logs -f # Follow mode
docker compose logs --since=1h # Last hour
Log rotation (handled by Docker automatically):
- Default: Logs rotate at 10MB
- Keeps last 3 files
- No action needed
Manual cleanup (if needed):
docker compose down
docker system prune -a # Remove all unused data
docker compose up -d
Security Best Practices
Secrets Management
✅ DO:
- Store Client Secrets in docker-compose.yml only
- Set file permissions:
chmod 600 docker-compose.yml - Keep backups secure and encrypted
- Rotate secrets periodically (every 6-12 months)
- Use different credentials for each site
❌ DON’T:
- Commit docker-compose.yml to git with secrets
- Share Client Secrets via insecure channels
- Reuse same credentials across sites
- Store secrets in plain text files
- Log secrets to console
Access Control
GitHub Repository Access:
- Only add collaborators who need CMS access
- Review access regularly
- Remove access for former team members
- Use teams instead of individual access when possible
OAuth App Access:
- Don’t approve unknown OAuth apps
- Review authorized apps: https://github.com/settings/applications
- Revoke unused apps
- Monitor organization OAuth policies
Network Security
Container Isolation:
- OAuth provider only on
webnetwork - No direct port exposure (3000 is internal only)
- Traffic always through Traefik (HTTPS)
Firewall:
- Only ports 80, 443 open on VPS
- SSH restricted to key-only auth
- fail2ban monitoring enabled
Monitoring
Watch for:
- Unusual authentication patterns
- Failed authentication attempts
- Unknown sites in logs
- Unexpected traffic spikes
Set up alerts (optional but recommended):
- UptimeRobot for provider availability
- GitHub notifications for OAuth app changes
- Log monitoring for errors
Performance Optimization
Current Performance
Typical metrics:
- Authentication time: 1-2 seconds
- Memory usage: ~50-100 MB
- CPU usage: Minimal (< 1%)
- Can handle 50+ sites easily
Scaling Considerations
When to optimize:
- 50+ configured sites
- High traffic (1000+ auth/day)
- Slow authentication times
- Memory issues
Optimization strategies:
- Enable caching (for very high traffic):
// Add to server.js
const NodeCache = require('node-cache');
const tokenCache = new NodeCache({ stdTTL: 600 }); // 10 min cache
- Rate limiting (prevent abuse):
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/auth/', limiter);
- Horizontal scaling (load balancing):
- Deploy multiple OAuth provider instances
- Use Traefik load balancing
- Share state via Redis
For most use cases, no optimization needed!
Advanced Configurations
Multiple Domains for Same Site
If a site is accessible via multiple domains:
allowedOrigins: [
'https://www.site.com',
'https://site.com',
'https://staging.site.com'
]
Different Git Providers
GitLab example:
clientsite: {
client: new AuthorizationCode({
client: {
id: process.env.CLIENTSITE_CLIENT_ID,
secret: process.env.CLIENTSITE_CLIENT_SECRET,
},
auth: {
tokenHost: 'https://gitlab.com', // Changed
tokenPath: '/oauth/token', // Changed
authorizePath: '/oauth/authorize', // Changed
},
}),
allowedOrigins: ['https://www.clientsite.com'],
description: 'Client Site (GitLab)'
}
Bitbucket similar to GitLab.
Custom Authentication Messages
Customize the callback HTML in server.js:
// In /callback/:site route
const script = `
<!DOCTYPE html>
<html>
<head>
<title>Welcome to ${siteName}!</title>
<style>
/* Your custom styles */
</style>
</head>
<body>
<div class="message">
<h1>✓ Welcome!</h1>
<p>Successfully logged into ${clientConfig.description}</p>
</div>
<script>
// postMessage code here
</script>
</body>
</html>
`;
Logging to External Service
Add structured logging:
// At top of server.js
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Use in routes
logger.info('Auth request', { site: siteName, timestamp: new Date() });
Migration Guide
From Single-Site to Multi-Site
If you have existing single-site OAuth providers and want to consolidate:
- Inventory existing providers:
# List all OAuth containers
docker ps -a | grep oauth
- Document each site’s credentials:
- GitHub OAuth App Client ID
- GitHub OAuth App Client Secret
- Site domain
- Repository
- Set up multi-site provider (follow Initial Setup in this guide)
- Add all sites to multi-site config
- Update each site’s Decap CMS config:
base_url: https://oauth.boeygembos.be
auth_endpoint: auth/SITENAME
- Test each site individually
- Remove old single-site containers:
cd /opt/docker/old-oauth-provider
docker compose down
- Update DNS if needed (if using site-specific OAuth domains)
From Netlify/External OAuth
If migrating from Netlify or another OAuth service:
- Create GitHub OAuth Apps for each site
- Deploy multi-site provider on VPS
- Update Decap CMS configs to point to your provider
- Test thoroughly before removing old OAuth
- Update in batches to minimize risk
Cost Analysis
Resource Usage
Single OAuth provider serving 10 sites:
- Memory: ~100 MB
- CPU: < 0.5%
- Disk: ~50 MB
- Network: Minimal
Vs. 10 separate OAuth containers:
- Memory: ~1 GB (10x)
- CPU: ~5% (10x)
- Disk: ~500 MB (10x)
- Management overhead: 10x
Savings
VPS resources saved:
- Can host 50+ sites vs 10 with separate containers
- Lower memory pressure
- Faster deployments
- Simpler architecture
Time saved:
- Add new site: 15 min vs 45 min
- Updates: One change vs updating each container
- Monitoring: One service vs many
Troubleshooting Decision Tree
Authentication not working?
├─ Error: "Unknown site"
│ ├─ Check site key matches in server.js and config.yml
│ └─ Verify container restarted after adding site
│
├─ Error: 401 Unauthorized
│ ├─ Verify Client ID/Secret in docker-compose.yml
│ └─ Check if secret was regenerated on GitHub
│
├─ CORS Error in browser
│ ├─ Check allowedOrigins includes correct domain
│ └─ Verify protocol (http vs https) matches
│
├─ Popup closes immediately
│ └─ Check browser console for errors
│
└─ OAuth provider not accessible
├─ Check DNS: dig oauth.boeygembos.be
├─ Check container: docker ps | grep decap-oauth
└─ Check logs: docker compose logs
FAQ
Q: How many sites can one provider handle?
A: Theoretically unlimited. Practically tested with 50+ sites without issues. Limited only by VPS resources.
Q: Can I use different OAuth providers (GitHub, GitLab) together?
A: Yes! Each site in the clients object can configure different OAuth providers.
Q: What if one site’s OAuth App is compromised?
A: Only that site is affected. Regenerate its Client Secret and update docker-compose.yml. Other sites unaffected.
Q: Can I use custom domains for the OAuth provider?
A: Yes, but not recommended. Using oauth.boeygembos.be for all sites simplifies management. If needed, configure multiple Traefik routes.
Q: Do users need to authorize each time?
A: No. GitHub stores authorization. Users only authorize once per OAuth App (unless token expires or is revoked).
Q: Can I restrict which GitHub users can access each site?
A: Yes, via GitHub repository permissions. Only users with write access to the repo can save changes via CMS.
Q: What happens if the OAuth provider is down?
A: Users can’t log into CMS, but websites remain accessible. OAuth only needed for CMS authentication, not site access.
Q: Can I test locally before deploying?
A: Yes. Run the OAuth provider locally with ngrok or similar tunnel, then update base_url in test site’s config.yml to point to your local instance.