Initial Docmost Cloudron package

This commit is contained in:
Andreas Dueren
2025-07-14 21:01:40 -06:00
commit 816b0b4115
7 changed files with 408 additions and 0 deletions

36
CloudronManifest.json Normal file
View File

@@ -0,0 +1,36 @@
{
"id": "com.docmost.app",
"title": "Docmost",
"author": "Docmost Team",
"description": "Open-source collaborative wiki and documentation platform. A modern alternative to Confluence and Notion with real-time collaboration, diagram support, and multilingual capabilities.",
"tagline": "Collaborative wiki and documentation platform",
"version": "1.0.0",
"healthCheckPath": "/api/health",
"httpPort": 3000,
"addons": {
"postgresql": {},
"localstorage": {},
"sendmail": {},
"redis": {}
},
"manifestVersion": 2,
"website": "https://docmost.com",
"contactEmail": "support@docmost.com",
"icon": "logo.png",
"tags": [
"productivity",
"collaboration",
"documentation",
"wiki",
"knowledge-base"
],
"postInstallMessage": "Docmost has been installed successfully!\n\nTo get started:\n1. Access your Docmost instance at https://%s\n2. Create your first workspace and admin account\n3. Start collaborating on documentation\n\nDefault features include:\n- Real-time collaborative editing\n- Diagram support (Draw.io, Excalidraw, Mermaid)\n- Page history and permissions\n- Multilingual support\n\nFor more information, visit: https://docmost.com/docs",
"minBoxVersion": "7.0.0",
"maxBoxVersion": "8.0.0",
"memoryLimit": 512000000,
"optionalSso": true,
"mediaLinks": [
"https://docmost.com",
"https://github.com/docmost/docmost"
]
}

38
Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
FROM cloudron/base:5.0.0
MAINTAINER Cloudron Support <support@cloudron.io>
# Install Node.js 20 and pnpm
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs && \
npm install -g pnpm@latest
# Set up application directory
WORKDIR /app/code
# Clone and build Docmost
RUN git clone https://github.com/docmost/docmost.git . && \
pnpm install && \
pnpm build
# Create necessary directories
RUN mkdir -p /tmp/data /app/data && \
chown -R cloudron:cloudron /app/data /tmp/data
# Copy startup scripts
COPY start.sh /app/code/
COPY nginx.conf /etc/nginx/sites-available/default
# Make scripts executable
RUN chmod +x /app/code/start.sh
# Install supervisord for process management
RUN apt-get update && \
apt-get install -y supervisor && \
rm -rf /var/lib/apt/lists/*
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
EXPOSE 3000
CMD [ "/app/code/start.sh" ]

82
nginx.conf Normal file
View File

@@ -0,0 +1,82 @@
server {
listen 3000 default_server;
listen [::]:3000 default_server;
root /app/code;
index index.html;
client_max_body_size 100m;
# Enable compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss;
# Proxy to Docmost application
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400;
}
# WebSocket support for real-time collaboration
location /socket.io/ {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
# OIDC callback endpoint
location /api/v1/session/callback {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API endpoints
location /api/ {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300;
}
# Health check endpoint
location /api/health {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
access_log off;
}
# Static files (if served by nginx)
location /uploads/ {
alias /app/data/uploads/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}

145
oidc-middleware.js Normal file
View File

@@ -0,0 +1,145 @@
const express = require('express');
const jwt = require('jsonwebtoken');
const axios = require('axios');
class CloudronOIDCMiddleware {
constructor(options = {}) {
this.clientId = process.env.CLOUDRON_OIDC_CLIENT_ID;
this.clientSecret = process.env.CLOUDRON_OIDC_CLIENT_SECRET;
this.issuer = process.env.CLOUDRON_OIDC_ISSUER;
this.redirectUri = process.env.OIDC_REDIRECT_URI;
this.appOrigin = process.env.CLOUDRON_APP_ORIGIN;
}
// Middleware to check authentication
authenticate() {
return async (req, res, next) => {
try {
// Check for existing session/token
const token = req.headers.authorization?.replace('Bearer ', '') ||
req.cookies?.authToken ||
req.session?.token;
if (token && this.verifyToken(token)) {
req.user = jwt.decode(token);
return next();
}
// If no valid token, redirect to OIDC login
if (req.path.startsWith('/api/')) {
return res.status(401).json({ error: 'Authentication required' });
}
// Redirect to OIDC authorization
const authUrl = this.buildAuthUrl();
res.redirect(authUrl);
} catch (error) {
console.error('Authentication error:', error);
res.status(500).json({ error: 'Authentication failed' });
}
};
}
// Build OIDC authorization URL
buildAuthUrl() {
const params = new URLSearchParams({
response_type: 'code',
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: 'openid profile email',
state: this.generateState()
});
return `${this.issuer}/auth?${params.toString()}`;
}
// Handle OIDC callback
async handleCallback(req, res) {
try {
const { code, state } = req.query;
if (!code) {
return res.status(400).json({ error: 'Authorization code required' });
}
// Exchange code for tokens
const tokenResponse = await this.exchangeCodeForTokens(code);
const { access_token, id_token } = tokenResponse.data;
// Verify and decode the ID token
const userInfo = jwt.decode(id_token);
// Create user session
const sessionToken = this.createSessionToken(userInfo);
// Set cookie and redirect
res.cookie('authToken', sessionToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
});
res.redirect('/');
} catch (error) {
console.error('OIDC callback error:', error);
res.status(500).json({ error: 'Authentication callback failed' });
}
}
// Exchange authorization code for tokens
async exchangeCodeForTokens(code) {
const params = new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: this.redirectUri,
client_id: this.clientId,
client_secret: this.clientSecret
});
return axios.post(`${this.issuer}/token`, params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
}
// Create session token
createSessionToken(userInfo) {
const payload = {
sub: userInfo.sub,
email: userInfo.email,
name: userInfo.name,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30 days
};
return jwt.sign(payload, process.env.APP_SECRET);
}
// Verify JWT token
verifyToken(token) {
try {
const decoded = jwt.verify(token, process.env.APP_SECRET);
return decoded.exp > Math.floor(Date.now() / 1000);
} catch (error) {
return false;
}
}
// Generate random state for CSRF protection
generateState() {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
// Logout handler
logout() {
return (req, res) => {
res.clearCookie('authToken');
res.redirect('/');
};
}
}
module.exports = CloudronOIDCMiddleware;

14
package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "docmost-cloudron",
"version": "1.0.0",
"description": "Cloudron package for Docmost",
"scripts": {
"start": "./start.sh"
},
"dependencies": {
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"axios": "^1.6.0",
"cookie-parser": "^1.4.6"
}
}

67
start.sh Normal file
View File

@@ -0,0 +1,67 @@
#!/bin/bash
set -eu
echo "=> Starting Docmost setup"
# Initialize /app/data if it's empty (first run)
if [ ! -f /app/data/.initialized ]; then
echo "=> Initializing data directory"
mkdir -p /app/data/uploads /app/data/logs
chown -R cloudron:cloudron /app/data
touch /app/data/.initialized
fi
# Generate APP_SECRET if not exists
if [ ! -f /app/data/app_secret ]; then
echo "=> Generating APP_SECRET"
openssl rand -hex 32 > /app/data/app_secret
chown cloudron:cloudron /app/data/app_secret
chmod 600 /app/data/app_secret
fi
APP_SECRET=$(cat /app/data/app_secret)
# Set up environment variables
export NODE_ENV=production
export APP_URL="${CLOUDRON_APP_ORIGIN}"
export APP_SECRET="${APP_SECRET}"
export PORT=3000
# Database configuration
export DATABASE_URL="${CLOUDRON_POSTGRESQL_URL}"
# Redis configuration
export REDIS_URL="${CLOUDRON_REDIS_URL}"
# Email configuration
export MAIL_DRIVER=smtp
export SMTP_HOST="${CLOUDRON_MAIL_SMTP_SERVER}"
export SMTP_PORT="${CLOUDRON_MAIL_SMTP_PORT}"
export SMTP_USERNAME="${CLOUDRON_MAIL_SMTP_USERNAME}"
export SMTP_PASSWORD="${CLOUDRON_MAIL_SMTP_PASSWORD}"
export MAIL_FROM_ADDRESS="${CLOUDRON_MAIL_FROM}"
# Storage configuration (using local storage)
export STORAGE_DRIVER=local
export STORAGE_LOCAL_PATH="/app/data/uploads"
# OIDC configuration for Cloudron authentication
export OIDC_CLIENT_ID="${CLOUDRON_OIDC_CLIENT_ID}"
export OIDC_CLIENT_SECRET="${CLOUDRON_OIDC_CLIENT_SECRET}"
export OIDC_ISSUER="${CLOUDRON_OIDC_ISSUER}"
export OIDC_REDIRECT_URI="${CLOUDRON_APP_ORIGIN}/api/v1/session/callback"
# File upload configuration
export FILE_UPLOAD_SIZE_LIMIT="50mb"
# JWT configuration
export JWT_TOKEN_EXPIRES_IN="30d"
echo "=> Running database migrations"
cd /app/code
chown -R cloudron:cloudron /app/data
sudo -u cloudron pnpm prisma migrate deploy || true
echo "=> Starting services with supervisor"
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf

26
supervisord.conf Normal file
View File

@@ -0,0 +1,26 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/stdout
logfile_maxbytes=0
pidfile=/run/supervisord.pid
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
priority=100
[program:docmost]
command=/usr/bin/sudo -u cloudron /usr/bin/node /app/code/apps/server/dist/main.js
directory=/app/code
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
priority=200
environment=NODE_ENV=production