Initial Discourse deployment config for cadCAD community forum
Sets up official Discourse Docker behind Traefik reverse proxy on Netcup RS 8000. Temp domain: cadcad-forum.jeffemmett.com (switching to community.cadcad.org later). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
efac8782ac
|
|
@ -0,0 +1,17 @@
|
|||
# Discourse Environment Variables
|
||||
# Copy to .env and fill in values
|
||||
|
||||
# Admin
|
||||
DISCOURSE_DEVELOPER_EMAILS=jeff@jeffemmett.com
|
||||
|
||||
# Domain (temporary - switch to community.cadcad.org later)
|
||||
DISCOURSE_HOSTNAME=cadcad-forum.jeffemmett.com
|
||||
|
||||
# SMTP (configure when email provider is chosen)
|
||||
# DISCOURSE_SMTP_ADDRESS=smtp.example.com
|
||||
# DISCOURSE_SMTP_PORT=587
|
||||
# DISCOURSE_SMTP_USER_NAME=
|
||||
# DISCOURSE_SMTP_PASSWORD=
|
||||
# DISCOURSE_SMTP_ENABLE_START_TLS=true
|
||||
# DISCOURSE_SMTP_DOMAIN=cadcad.org
|
||||
# DISCOURSE_NOTIFICATION_EMAIL=noreply@cadcad.org
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.env
|
||||
*.backup
|
||||
*.tar.gz
|
||||
shared/
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# cadCAD Community Discourse Forum
|
||||
|
||||
Self-hosted Discourse forum for the cadCAD community.
|
||||
|
||||
- **Temp URL**: https://cadcad-forum.jeffemmett.com
|
||||
- **Final URL**: https://community.cadcad.org (pending DNS coordination)
|
||||
- **Server**: Netcup RS 8000 at `/opt/discourse/`
|
||||
- **Memory**: 2GB container limit + 2GB swap
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
ssh netcup
|
||||
cd /opt/discourse
|
||||
|
||||
./launcher logs app # View logs
|
||||
./launcher restart app # Restart
|
||||
./launcher rebuild app # Rebuild (after config changes)
|
||||
./launcher enter app # Shell into container
|
||||
./launcher stop app # Stop
|
||||
```
|
||||
|
||||
After any rebuild, reconnect to Traefik:
|
||||
```bash
|
||||
bash scripts/post-rebuild.sh
|
||||
```
|
||||
|
||||
## Initial Setup
|
||||
|
||||
1. Run `scripts/install.sh` on Netcup (or see SPEC.md for manual steps)
|
||||
2. Add `cadcad-forum.jeffemmett.com` to Cloudflare tunnel
|
||||
3. Create admin account: `./launcher enter app` → `rake admin:create`
|
||||
|
||||
## Import Existing Forum Data
|
||||
|
||||
1. Get `.tar.gz` backup from existing community.cadcad.org admin panel (`/admin/backups`)
|
||||
2. Upload to Netcup: `scp backup.tar.gz netcup:/tmp/`
|
||||
3. Run: `bash scripts/restore-backup.sh /tmp/backup.tar.gz`
|
||||
|
||||
## Switch to community.cadcad.org
|
||||
|
||||
1. Edit `containers/app.yml`: update `DISCOURSE_HOSTNAME` and Traefik `Host()` rule
|
||||
2. `./launcher rebuild app && bash scripts/post-rebuild.sh`
|
||||
3. Add `community.cadcad.org` to Cloudflare tunnel
|
||||
4. Coordinate DNS: CNAME `community.cadcad.org` → tunnel UUID `.cfargotunnel.com`
|
||||
|
||||
## Configure Email (SMTP)
|
||||
|
||||
Uncomment and fill in the SMTP section in `containers/app.yml`, then rebuild.
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
# cadCAD Discourse Forum - Deployment Spec
|
||||
|
||||
## Summary
|
||||
|
||||
Deploy a self-hosted Discourse forum on Netcup RS 8000, initially at `cadcad-forum.jeffemmett.com` (temporary domain), to be switched to `community.cadcad.org` once DNS is coordinated. An existing Discourse backup will be imported later.
|
||||
|
||||
## Architecture Decision: Official Discourse Docker
|
||||
|
||||
**Chosen approach: Official Discourse Docker** (`discourse/discourse_docker`)
|
||||
|
||||
### Why official over alternatives?
|
||||
|
||||
| Criteria | Official | nfrastack (alternative) |
|
||||
|----------|----------|------------------------|
|
||||
| Backup import | Best (native) | Untested |
|
||||
| Long-term support | Discourse team | Community |
|
||||
| Plugin management | Easy (app.yml) | Env vars |
|
||||
| Traefik integration | Requires config tweaks | Native |
|
||||
| Docker-compose native | No (custom launcher) | Yes |
|
||||
|
||||
**Backup compatibility is the deciding factor** since we need to import an existing community.cadcad.org backup later. The official approach is the only one guaranteed to handle this reliably.
|
||||
|
||||
### Trade-off accepted
|
||||
|
||||
The official Discourse Docker uses a custom `./launcher` script instead of standard `docker-compose`. This breaks the pattern used by other services on the stack, but is necessary for reliable backup import/restore.
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Infrastructure
|
||||
- **Server**: Netcup RS 8000 (64GB RAM, 20 cores)
|
||||
- **Container type**: Monolithic (PostgreSQL + Redis + Discourse in one container)
|
||||
- **Memory limit**: 2GB container + 2GB swap (sufficient for <1k users in steady state)
|
||||
- **Temp domain**: `cadcad-forum.jeffemmett.com`
|
||||
- **Final domain**: `community.cadcad.org` (DNS controlled by someone else)
|
||||
|
||||
### Routing
|
||||
```
|
||||
Internet → Cloudflare Tunnel → Traefik (:80) → Discourse container (:80 internal)
|
||||
```
|
||||
|
||||
Traefik integration via Docker labels in `app.yml`:
|
||||
- Disable SSL templates (Cloudflare handles TLS)
|
||||
- Don't expose ports directly (Traefik routes traffic)
|
||||
- Connect to `traefik-public` network via `docker_args`
|
||||
|
||||
### Email
|
||||
- Deferred for initial setup
|
||||
- Will configure SMTP later (Resend or another provider)
|
||||
- Discourse will warn about missing email but will function for admin access
|
||||
|
||||
### Storage
|
||||
- Data stored in `/opt/discourse/shared/standalone/` on Netcup
|
||||
- PostgreSQL data, Redis data, uploads, backups all within the container's shared directory
|
||||
- Repo at `/opt/discourse/` contains config only (not data)
|
||||
|
||||
## Deployment Steps
|
||||
|
||||
1. **Create repo** with config files locally at `/home/jeffe/Github/cadcad-discourse-forum`
|
||||
2. **Clone to Netcup** at `/opt/discourse/`
|
||||
3. **Install official Discourse Docker** (`discourse_docker` launcher)
|
||||
4. **Configure `app.yml`** with Traefik labels, no SSL, 2GB memory limit
|
||||
5. **Add Cloudflare tunnel hostname** for `cadcad-forum.jeffemmett.com`
|
||||
6. **Bootstrap and start** Discourse
|
||||
7. **Verify** forum is accessible at `cadcad-forum.jeffemmett.com`
|
||||
|
||||
## Files in This Repo
|
||||
|
||||
```
|
||||
cadcad-discourse-forum/
|
||||
├── SPEC.md # This file
|
||||
├── README.md # Deployment instructions
|
||||
├── app.yml # Discourse container config (copied to /opt/discourse/containers/)
|
||||
├── .env.example # Environment variable template
|
||||
└── scripts/
|
||||
├── install.sh # Initial setup script (run on Netcup)
|
||||
└── restore-backup.sh # Backup import script (for later)
|
||||
```
|
||||
|
||||
## Migration Plan (Later)
|
||||
|
||||
1. Obtain `.tar.gz` backup from existing community.cadcad.org admin panel
|
||||
2. Upload to `/opt/discourse/shared/standalone/backups/default/`
|
||||
3. Run restore: `./launcher enter app` → `discourse restore <filename>`
|
||||
4. Coordinate DNS change: `community.cadcad.org` CNAME → tunnel
|
||||
5. Update `app.yml` hostname and rebuild
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Email configuration (deferred)
|
||||
- SSO/OAuth integration
|
||||
- Custom plugins (can be added later via `app.yml`)
|
||||
- CDN configuration
|
||||
- Automated backups (Discourse has built-in scheduled backups)
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Which email provider to use when ready (Resend, Mailgun, etc.)
|
||||
- Who to coordinate with for cadcad.org DNS
|
||||
- Whether any specific Discourse plugins are needed from the existing instance
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
## Discourse container config for cadCAD community forum
|
||||
## Deployed behind Traefik reverse proxy on Netcup RS 8000
|
||||
##
|
||||
## This file gets copied to /opt/discourse/containers/app.yml on the server.
|
||||
## After editing, rebuild with: cd /opt/discourse && ./launcher rebuild app
|
||||
|
||||
templates:
|
||||
- "templates/postgres.template.yml"
|
||||
- "templates/redis.template.yml"
|
||||
- "templates/web.template.yml"
|
||||
## SSL handled by Cloudflare/Traefik - do NOT enable these:
|
||||
# - "templates/web.ssl.template.yml"
|
||||
# - "templates/web.letsencrypt.ssl.template.yml"
|
||||
- "templates/web.ratelimited.template.yml"
|
||||
|
||||
## No direct port exposure - Traefik routes traffic via Docker network
|
||||
## expose:
|
||||
## - "80:80"
|
||||
|
||||
params:
|
||||
db_default_text_search_config: "pg_catalog.english"
|
||||
## 2GB total container limit, tune DB accordingly
|
||||
db_shared_buffers: "128MB"
|
||||
db_work_mem: "10MB"
|
||||
|
||||
env:
|
||||
LC_ALL: en_US.UTF-8
|
||||
LANG: en_US.UTF-8
|
||||
LANGUAGE: en_US.UTF-8
|
||||
|
||||
DISCOURSE_DEFAULT_LOCALE: en
|
||||
|
||||
## Temporary domain - will switch to community.cadcad.org later
|
||||
DISCOURSE_HOSTNAME: 'cadcad-forum.jeffemmett.com'
|
||||
|
||||
## Admin email for initial setup
|
||||
DISCOURSE_DEVELOPER_EMAILS: 'jeff@jeffemmett.com'
|
||||
|
||||
## SMTP - configure when ready (forum works without it but can't send emails)
|
||||
## Uncomment and fill in when email provider is chosen:
|
||||
# DISCOURSE_SMTP_ADDRESS: smtp.example.com
|
||||
# DISCOURSE_SMTP_PORT: 587
|
||||
# DISCOURSE_SMTP_USER_NAME: user@example.com
|
||||
# DISCOURSE_SMTP_PASSWORD: "password"
|
||||
# DISCOURSE_SMTP_ENABLE_START_TLS: true
|
||||
# DISCOURSE_SMTP_DOMAIN: cadcad.org
|
||||
# DISCOURSE_NOTIFICATION_EMAIL: noreply@cadcad.org
|
||||
|
||||
## Serve behind reverse proxy
|
||||
DISCOURSE_FORCE_HTTPS: true
|
||||
|
||||
volumes:
|
||||
- volume:
|
||||
host: /opt/discourse/shared/standalone
|
||||
guest: /shared
|
||||
- volume:
|
||||
host: /opt/discourse/shared/standalone/log/var-log
|
||||
guest: /var/log
|
||||
|
||||
hooks:
|
||||
after_code:
|
||||
- exec:
|
||||
cd: $home/plugins
|
||||
cmd:
|
||||
## Add plugins here (uncomment as needed):
|
||||
# - git clone https://github.com/discourse/docker_manager.git
|
||||
# - git clone https://github.com/discourse/discourse-solved.git
|
||||
# - git clone https://github.com/discourse/discourse-voting.git
|
||||
- echo "Plugin installation complete"
|
||||
|
||||
## Memory limit: 2GB container + 2GB swap
|
||||
run:
|
||||
- exec: echo "Beginning of custom commands"
|
||||
|
||||
## Traefik integration labels
|
||||
labels:
|
||||
app_name: discourse
|
||||
traefik.enable: "true"
|
||||
traefik.docker.network: traefik-public
|
||||
traefik.http.routers.discourse.rule: "Host(`cadcad-forum.jeffemmett.com`)"
|
||||
traefik.http.services.discourse.loadbalancer.server.port: "80"
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
## Discourse Installation Script for Netcup RS 8000
|
||||
## Run this ON the Netcup server: ssh netcup "bash -s" < scripts/install.sh
|
||||
## Or: ssh netcup, then cd /opt/discourse && bash scripts/install.sh
|
||||
|
||||
DISCOURSE_DIR="/opt/discourse"
|
||||
REPO_URL="https://github.com/discourse/discourse_docker.git"
|
||||
|
||||
echo "=== Discourse Installation for cadCAD Community Forum ==="
|
||||
echo ""
|
||||
|
||||
# 1. Check for swap (Discourse needs it for 2GB RAM limit)
|
||||
echo "[1/6] Checking swap space..."
|
||||
SWAP_TOTAL=$(free -m | awk '/^Swap:/ {print $2}')
|
||||
if [ "$SWAP_TOTAL" -lt 2000 ]; then
|
||||
echo " WARNING: Less than 2GB swap detected (${SWAP_TOTAL}MB)."
|
||||
echo " Creating 2GB swap file..."
|
||||
if [ ! -f /swapfile ]; then
|
||||
fallocate -l 2G /swapfile
|
||||
chmod 600 /swapfile
|
||||
mkswap /swapfile
|
||||
swapon /swapfile
|
||||
echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
||||
echo " Swap file created and enabled."
|
||||
else
|
||||
echo " /swapfile already exists. Enabling if not active..."
|
||||
swapon /swapfile 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
echo " Swap OK: ${SWAP_TOTAL}MB"
|
||||
fi
|
||||
|
||||
# 2. Clone discourse_docker
|
||||
echo ""
|
||||
echo "[2/6] Setting up Discourse Docker..."
|
||||
if [ -d "$DISCOURSE_DIR/.git" ]; then
|
||||
echo " discourse_docker already cloned at $DISCOURSE_DIR"
|
||||
cd "$DISCOURSE_DIR"
|
||||
git pull
|
||||
else
|
||||
echo " Cloning discourse_docker..."
|
||||
git clone "$REPO_URL" "$DISCOURSE_DIR"
|
||||
cd "$DISCOURSE_DIR"
|
||||
fi
|
||||
|
||||
# 3. Copy app.yml config
|
||||
echo ""
|
||||
echo "[3/6] Installing app.yml configuration..."
|
||||
if [ -f "containers/app.yml" ]; then
|
||||
echo " Backing up existing app.yml to containers/app.yml.backup"
|
||||
cp containers/app.yml "containers/app.yml.backup.$(date +%Y%m%d%H%M%S)"
|
||||
fi
|
||||
|
||||
# The app.yml should already be in the repo root from our config
|
||||
if [ -f "app.yml" ]; then
|
||||
cp app.yml containers/app.yml
|
||||
echo " app.yml copied to containers/"
|
||||
else
|
||||
echo " ERROR: app.yml not found in repo root."
|
||||
echo " Please copy it manually: cp /path/to/app.yml containers/app.yml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. Ensure traefik-public network exists
|
||||
echo ""
|
||||
echo "[4/6] Checking Docker networks..."
|
||||
if docker network inspect traefik-public >/dev/null 2>&1; then
|
||||
echo " traefik-public network exists."
|
||||
else
|
||||
echo " Creating traefik-public network..."
|
||||
docker network create traefik-public
|
||||
fi
|
||||
|
||||
# 5. Bootstrap Discourse (this takes 5-10 minutes)
|
||||
echo ""
|
||||
echo "[5/6] Bootstrapping Discourse container..."
|
||||
echo " This will take 5-10 minutes. Building image and precompiling assets..."
|
||||
echo ""
|
||||
./launcher bootstrap app
|
||||
|
||||
# 6. Start and connect to Traefik network
|
||||
echo ""
|
||||
echo "[6/6] Starting Discourse and connecting to Traefik..."
|
||||
./launcher start app
|
||||
|
||||
# Connect to traefik-public network
|
||||
CONTAINER_ID=$(docker ps -q -f name=app)
|
||||
if [ -n "$CONTAINER_ID" ]; then
|
||||
docker network connect traefik-public "$CONTAINER_ID" 2>/dev/null || echo " Already connected to traefik-public"
|
||||
echo " Container connected to traefik-public network."
|
||||
else
|
||||
echo " WARNING: Could not find running discourse container."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Discourse Installation Complete ==="
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Add cadcad-forum.jeffemmett.com to Cloudflare tunnel"
|
||||
echo " 2. Create admin account:"
|
||||
echo " cd /opt/discourse && ./launcher enter app"
|
||||
echo " rake admin:create"
|
||||
echo " 3. Configure SMTP in containers/app.yml when ready"
|
||||
echo " 4. Visit https://cadcad-forum.jeffemmett.com"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " ./launcher logs app # View logs"
|
||||
echo " ./launcher restart app # Restart"
|
||||
echo " ./launcher rebuild app # Rebuild after config changes"
|
||||
echo " ./launcher enter app # Shell into container"
|
||||
echo " ./launcher stop app # Stop"
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#!/bin/bash
|
||||
## Reconnect Discourse container to traefik-public network after rebuild
|
||||
## Run after every: ./launcher rebuild app
|
||||
##
|
||||
## The official launcher creates a NEW container on rebuild, which loses
|
||||
## the external network connection. This script reconnects it.
|
||||
|
||||
CONTAINER_ID=$(docker ps -q -f name=app)
|
||||
|
||||
if [ -z "$CONTAINER_ID" ]; then
|
||||
echo "ERROR: Discourse container not running. Start it first: ./launcher start app"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Connecting Discourse container to traefik-public network..."
|
||||
docker network connect traefik-public "$CONTAINER_ID" 2>/dev/null && \
|
||||
echo "Connected successfully." || \
|
||||
echo "Already connected."
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
## Restore a Discourse backup from an existing instance
|
||||
## Usage: bash scripts/restore-backup.sh <path-to-backup.tar.gz>
|
||||
|
||||
DISCOURSE_DIR="/opt/discourse"
|
||||
BACKUP_DIR="/opt/discourse/shared/standalone/backups/default"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 <path-to-backup.tar.gz>"
|
||||
echo ""
|
||||
echo "Steps to get a backup from the existing community.cadcad.org:"
|
||||
echo " 1. Log into the existing Discourse as admin"
|
||||
echo " 2. Go to /admin/backups"
|
||||
echo " 3. Click 'Backup' to create a new backup"
|
||||
echo " 4. Download the .tar.gz file"
|
||||
echo " 5. Upload to Netcup: scp backup.tar.gz netcup:/tmp/"
|
||||
echo " 6. Run: bash $0 /tmp/backup.tar.gz"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BACKUP_FILE="$1"
|
||||
|
||||
if [ ! -f "$BACKUP_FILE" ]; then
|
||||
echo "ERROR: Backup file not found: $BACKUP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Discourse Backup Restore ==="
|
||||
echo "Backup file: $BACKUP_FILE"
|
||||
echo ""
|
||||
|
||||
# Copy backup to Discourse backup directory
|
||||
echo "[1/4] Copying backup to Discourse backup directory..."
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
cp "$BACKUP_FILE" "$BACKUP_DIR/"
|
||||
BACKUP_FILENAME=$(basename "$BACKUP_FILE")
|
||||
echo " Copied to $BACKUP_DIR/$BACKUP_FILENAME"
|
||||
|
||||
# Enable restore mode
|
||||
echo ""
|
||||
echo "[2/4] Enabling restore mode..."
|
||||
cd "$DISCOURSE_DIR"
|
||||
./launcher enter app <<EOF
|
||||
discourse enable_restore
|
||||
EOF
|
||||
|
||||
# Restore the backup
|
||||
echo ""
|
||||
echo "[3/4] Restoring backup (this may take several minutes)..."
|
||||
./launcher enter app <<EOF
|
||||
discourse restore $BACKUP_FILENAME
|
||||
EOF
|
||||
|
||||
# Rebuild to ensure everything is clean
|
||||
echo ""
|
||||
echo "[4/4] Rebuilding container to finalize restore..."
|
||||
./launcher rebuild app
|
||||
|
||||
# Reconnect to traefik network after rebuild
|
||||
CONTAINER_ID=$(docker ps -q -f name=app)
|
||||
if [ -n "$CONTAINER_ID" ]; then
|
||||
docker network connect traefik-public "$CONTAINER_ID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Restore Complete ==="
|
||||
echo ""
|
||||
echo "The forum should now contain all data from the backup."
|
||||
echo "Visit https://cadcad-forum.jeffemmett.com to verify."
|
||||
echo ""
|
||||
echo "If switching to community.cadcad.org:"
|
||||
echo " 1. Update DISCOURSE_HOSTNAME in containers/app.yml"
|
||||
echo " 2. Update Traefik router rule in containers/app.yml"
|
||||
echo " 3. ./launcher rebuild app"
|
||||
echo " 4. Update Cloudflare tunnel + DNS"
|
||||
Loading…
Reference in New Issue