Docker hardening + first-run fixes

This commit is contained in:
Christomatt
2026-02-05 02:49:34 +01:00
parent 38ddbf8bc1
commit a686db38b0
19 changed files with 346 additions and 53 deletions
+9 -4
View File
@@ -30,8 +30,8 @@ WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
# Install tools needed for port detection (netcat) # Install tools needed for port detection and healthchecks
RUN apk add --no-cache netcat-openbsd RUN apk add --no-cache netcat-openbsd wget
# Create non-root user # Create non-root user
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
@@ -41,6 +41,11 @@ RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./
COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle
COPY --from=builder --chown=nextjs:nodejs /app/drizzle.config.ts ./
COPY --from=builder --chown=nextjs:nodejs /app/src/db/schema.ts ./src/db/schema.ts
# Copy and set up entrypoint script # Copy and set up entrypoint script
COPY --chown=nextjs:nodejs scripts/docker-entrypoint.sh /usr/local/bin/ COPY --chown=nextjs:nodejs scripts/docker-entrypoint.sh /usr/local/bin/
@@ -59,9 +64,9 @@ EXPOSE 3000-3020
ENV PORT=3000 ENV PORT=3000
ENV HOSTNAME="0.0.0.0" ENV HOSTNAME="0.0.0.0"
# Health check (will use the actual PORT at runtime) # Health check (reads dynamic port file when PORT=auto)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget -q --spider http://localhost:${PORT:-3000}/api/health || exit 1 CMD sh -c 'PORT_FILE=/var/run/synapsis/port; PORT=3000; if [ -f "$PORT_FILE" ]; then PORT=$(cat "$PORT_FILE"); fi; case "$PORT" in ""|*[!0-9]*) PORT=3000;; esac; wget -q --spider "http://127.0.0.1:${PORT}/api/health" || exit 1'
# Use entrypoint script for port detection # Use entrypoint script for port detection
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
+9 -6
View File
@@ -2,7 +2,7 @@
**Your node. Your network. Your identity.** **Your node. Your network. Your identity.**
Synapsis is an open-source, federated social network built for the decentralized web. Run your own node, own your identity with DIDs, and communicate across the Swarm network with end-to-end encryption. No corporations. No lock-in. Just you and the network. Synapsis is an open-source, federated social network built for the decentralized web. Run your own node, own your identity with DIDs, and communicate across the Swarm network with plain text server-stored messaging. No corporations. No lock-in. Just you and the network.
--- ---
@@ -15,19 +15,22 @@ Run your personal Synapsis node with a single command:
mkdir -p /opt/synapsis && cd /opt/synapsis mkdir -p /opt/synapsis && cd /opt/synapsis
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker-compose.yml curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/Caddyfile curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/Caddyfile
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/caddy-entrypoint.sh
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/.env.example curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/.env.example
cp .env.example .env cp .env.example .env
# 2. Edit .env with your domain and secrets (takes 2 minutes) # 2. Edit .env with your domain and secrets (takes 2 minutes)
nano .env nano .env
# Use a host-only domain (no scheme), e.g. synapsis.example.com
# 3. Start your node # 3. Start your node
docker compose up -d docker compose up -d
``` ```
Done! Your node is live at `https://your-domain.com` with automatic SSL. No build step. No dependencies. No fuss. Done! Your node is live at `https://your-domain.com` with automatic SSL. No build step. No dependencies. No fuss.
Database migrations run automatically on startup and during updates.
**Updating:** **Updating (migrations run automatically):**
```bash ```bash
docker compose pull && docker compose up -d docker compose pull && docker compose up -d
``` ```
@@ -39,7 +42,7 @@ For detailed Docker setup, see [docker/README.md](docker/README.md).
## ✨ Features ## ✨ Features
- **🌐 Swarm Network** — Native peer-to-peer network with automatic node discovery and gossip protocol - **🌐 Swarm Network** — Native peer-to-peer network with automatic node discovery and gossip protocol
- **💬 Swarm Chat** — End-to-end encrypted messaging across the entire network - **💬 Swarm Chat** — Direct messaging across the entire network
- **🔐 Decentralized Identity (DIDs)** — Cryptographic identity you truly own, portable between nodes - **🔐 Decentralized Identity (DIDs)** — Cryptographic identity you truly own, portable between nodes
- **🤖 AI Bots** — Create AI-powered bot accounts with custom personalities - **🤖 AI Bots** — Create AI-powered bot accounts with custom personalities
- **🎨 Modern UI** — Clean, responsive interface inspired by Vercel's design system - **🎨 Modern UI** — Clean, responsive interface inspired by Vercel's design system
@@ -83,7 +86,7 @@ Synapsis operates on the **Swarm** — a native peer-to-peer network designed sp
- **Gossip Protocol** — Nodes discover each other automatically and exchange information - **Gossip Protocol** — Nodes discover each other automatically and exchange information
- **Swarm Timeline** — Aggregated feed of posts from across all Synapsis nodes - **Swarm Timeline** — Aggregated feed of posts from across all Synapsis nodes
- **Swarm Chat** — End-to-end encrypted direct messaging between users on any Synapsis node - **Swarm Chat** — Direct messaging between users on any Synapsis node
- **Handle Registry** — Distributed directory of user handles across the swarm - **Handle Registry** — Distributed directory of user handles across the swarm
- **Instant Interactions** — Likes, reposts, follows, and mentions delivered in real-time - **Instant Interactions** — Likes, reposts, follows, and mentions delivered in real-time
@@ -94,7 +97,7 @@ Synapsis operates on the **Swarm** — a native peer-to-peer network designed sp
| **Identity** | Server-bound (`@user@server`) | DID-based (cryptographic, portable) | | **Identity** | Server-bound (`@user@server`) | DID-based (cryptographic, portable) |
| **Account Migration** | Limited (followers don't auto-migrate) | **Full** — DID-based migration with auto-follow | | **Account Migration** | Limited (followers don't auto-migrate) | **Full** — DID-based migration with auto-follow |
| **Cryptographic Signing** | HTTP Signatures only | Full post signing with user keys | | **Cryptographic Signing** | HTTP Signatures only | Full post signing with user keys |
| **Direct Messages** | Posts with limited visibility | True E2E encrypted chat | | **Direct Messages** | Posts with limited visibility | Direct messaging between users |
| **Network Discovery** | Manual server discovery | Automatic gossip protocol | | **Network Discovery** | Manual server discovery | Automatic gossip protocol |
| **AI Bots** | Not supported | Native bot framework with LLM integration | | **AI Bots** | Not supported | Native bot framework with LLM integration |
| **Interactions** | Queue-based, delayed | Instant delivery via Swarm | | **Interactions** | Queue-based, delayed | Instant delivery via Swarm |
@@ -108,7 +111,7 @@ Want to hack on Synapsis? Here's how to run it locally:
### Prerequisites ### Prerequisites
- Node.js 20+ - Node.js 20+
- PostgreSQL 15+ - PostgreSQL 15+
- S3-compatible storage (or MinIO for local dev) - S3-compatible storage (AWS S3, Cloudflare R2, Backblaze B2, Wasabi, or Contabo)
### Local Setup ### Local Setup
+6 -3
View File
@@ -6,7 +6,7 @@
# mkdir -p /opt/synapsis && cd /opt/synapsis # mkdir -p /opt/synapsis && cd /opt/synapsis
# curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker-compose.yml # curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker-compose.yml
# curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/Caddyfile # curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/Caddyfile
# curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/scripts/caddy-entrypoint.sh # curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/caddy-entrypoint.sh
# curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/.env.example # curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/.env.example
# cp .env.example .env # cp .env.example .env
# 2. Configure (edit with your domain and secrets): nano .env # 2. Configure (edit with your domain and secrets): nano .env
@@ -61,7 +61,10 @@ services:
AUTH_SECRET: ${AUTH_SECRET} AUTH_SECRET: ${AUTH_SECRET}
# Domain configuration # Domain configuration
NEXT_PUBLIC_NODE_DOMAIN: ${DOMAIN:-localhost} DOMAIN: ${DOMAIN:-localhost}
NEXT_PUBLIC_NODE_DOMAIN: ${NEXT_PUBLIC_NODE_DOMAIN:-}
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-}
ALLOW_LOCALHOST: ${ALLOW_LOCALHOST:-}
# Admin emails # Admin emails
ADMIN_EMAILS: ${ADMIN_EMAILS} ADMIN_EMAILS: ${ADMIN_EMAILS}
@@ -89,7 +92,7 @@ services:
condition: service_healthy condition: service_healthy
# Increased start period to allow migrations to run # Increased start period to allow migrations to run
healthcheck: healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/health"] test: ["CMD-SHELL", "PORT=$(cat /var/run/synapsis/port 2>/dev/null || echo 3000); case \"$PORT\" in ''|*[!0-9]*) PORT=3000;; esac; wget -q --spider http://127.0.0.1:${PORT}/api/health || exit 1"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
+10 -1
View File
@@ -7,9 +7,15 @@
# REQUIRED: Core Settings # REQUIRED: Core Settings
# =========================================== # ===========================================
# Your domain name (e.g., synapsis.example.com) # Your domain name (no scheme, no path). Example: synapsis.example.com
DOMAIN=your-domain.com DOMAIN=your-domain.com
# Optional: Override node domain (host only, defaults to DOMAIN)
# NEXT_PUBLIC_NODE_DOMAIN=your-domain.com
# Optional: Public app URL for background jobs (auto-derived from node domain if unset)
# NEXT_PUBLIC_APP_URL=https://your-domain.com
# Admin email addresses (comma-separated for multiple) # Admin email addresses (comma-separated for multiple)
ADMIN_EMAILS=admin@your-domain.com ADMIN_EMAILS=admin@your-domain.com
@@ -32,5 +38,8 @@ DB_NAME=synapsis
# Or set a specific port: PORT=3000 # Or set a specific port: PORT=3000
PORT=auto PORT=auto
# Allow localhost domains in production (set only for local testing)
# ALLOW_LOCALHOST=1
# Maximum AI bots per user (default: 5) # Maximum AI bots per user (default: 5)
# BOT_MAX_PER_USER=5 # BOT_MAX_PER_USER=5
+3 -4
View File
@@ -43,7 +43,7 @@ RUN npm run build
# Stage 3: Production Runner # Stage 3: Production Runner
# ============================================ # ============================================
FROM node:22-alpine AS runner FROM node:22-alpine AS runner
RUN apk add --no-cache libc6-compat openssl netcat-openbsd RUN apk add --no-cache libc6-compat openssl netcat-openbsd wget
WORKDIR /app WORKDIR /app
@@ -87,10 +87,9 @@ USER nextjs
EXPOSE 3000-3020 EXPOSE 3000-3020
# Health check - longer start period to allow migrations to run # Health check - longer start period to allow migrations to run
# Uses PORT environment variable (set by entrypoint) # Reads dynamic port file when PORT=auto
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \
CMD wget -q --spider http://127.0.0.1:${PORT:-3000}/api/health || exit 1 CMD sh -c 'PORT_FILE=/var/run/synapsis/port; PORT=3000; if [ -f "$PORT_FILE" ]; then PORT=$(cat "$PORT_FILE"); fi; case "$PORT" in ""|*[!0-9]*) PORT=3000;; esac; wget -q --spider "http://127.0.0.1:${PORT}/api/health" || exit 1'
# Set the entrypoint # Set the entrypoint
ENTRYPOINT ["./docker-entrypoint.sh"] ENTRYPOINT ["./docker-entrypoint.sh"]
+8 -2
View File
@@ -10,6 +10,7 @@ Production Docker deployment using pre-built images from GitHub Container Regist
mkdir -p /opt/synapsis && cd /opt/synapsis mkdir -p /opt/synapsis && cd /opt/synapsis
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker-compose.yml curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/Caddyfile curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/Caddyfile
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/caddy-entrypoint.sh
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/.env.example curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/.env.example
cp .env.example .env cp .env.example .env
nano .env # Add your domain and secrets nano .env # Add your domain and secrets
@@ -39,7 +40,7 @@ newgrp docker
## ⚙️ Configuration ## ⚙️ Configuration
Edit `.env` and set these required values: Edit `.env` and set these required values (domain should be host only, no scheme or path):
| Variable | What to put | | Variable | What to put |
|----------|-------------| |----------|-------------|
@@ -48,13 +49,18 @@ Edit `.env` and set these required values:
| `AUTH_SECRET` | Run: `openssl rand -hex 32` | | `AUTH_SECRET` | Run: `openssl rand -hex 32` |
| `ADMIN_EMAILS` | Your email address | | `ADMIN_EMAILS` | Your email address |
Optional (advanced):
- `NEXT_PUBLIC_NODE_DOMAIN` to override the node domain (defaults to `DOMAIN`)
- `NEXT_PUBLIC_APP_URL` to override the public app URL used by background jobs (auto-derived from the node domain)
- `ALLOW_LOCALHOST=1` to allow `localhost` in production containers for local testing
**Port Configuration:** **Port Configuration:**
- `PORT=auto` (default) — Automatically finds an available port between 3000-3020 - `PORT=auto` (default) — Automatically finds an available port between 3000-3020
- `PORT=3000` — Use a specific port instead - `PORT=3000` — Use a specific port instead
--- ---
## 🔄 Updates ## 🔄 Updates (migrations run automatically)
```bash ```bash
cd /opt/synapsis cd /opt/synapsis
+18
View File
@@ -7,6 +7,24 @@ set -e
SHARED_PORT_FILE="/var/run/synapsis/port" SHARED_PORT_FILE="/var/run/synapsis/port"
DEFAULT_PORT=3000 DEFAULT_PORT=3000
# Normalize DOMAIN (strip scheme/path) to avoid invalid Caddyfile hostnames
sanitize_domain() {
echo "$1" | sed -E 's#^[a-zA-Z]+://##; s#/.*$##'
}
if [ -n "$DOMAIN" ]; then
CLEAN_DOMAIN=$(sanitize_domain "$DOMAIN")
if [ "$CLEAN_DOMAIN" != "$DOMAIN" ]; then
export DOMAIN="$CLEAN_DOMAIN"
echo "🌐 DOMAIN normalized to $DOMAIN"
fi
fi
if [ -z "$DOMAIN" ]; then
echo "❌ DOMAIN is not set. Exiting."
exit 1
fi
# Wait for the port file to exist (with timeout) # Wait for the port file to exist (with timeout)
echo "⏳ Waiting for Synapsis app to announce its port..." echo "⏳ Waiting for Synapsis app to announce its port..."
TIMEOUT=60 TIMEOUT=60
+5 -2
View File
@@ -57,7 +57,10 @@ services:
AUTH_SECRET: ${AUTH_SECRET} AUTH_SECRET: ${AUTH_SECRET}
# Domain configuration # Domain configuration
NEXT_PUBLIC_NODE_DOMAIN: ${DOMAIN:-localhost} DOMAIN: ${DOMAIN:-localhost}
NEXT_PUBLIC_NODE_DOMAIN: ${NEXT_PUBLIC_NODE_DOMAIN:-}
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-}
ALLOW_LOCALHOST: ${ALLOW_LOCALHOST:-}
# Admin emails # Admin emails
ADMIN_EMAILS: ${ADMIN_EMAILS} ADMIN_EMAILS: ${ADMIN_EMAILS}
@@ -89,7 +92,7 @@ services:
postgres: postgres:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"] test: ["CMD-SHELL", "PORT=$(cat /var/run/synapsis/port 2>/dev/null || echo 3000); case \"$PORT\" in ''|*[!0-9]*) PORT=3000;; esac; wget -q --spider http://127.0.0.1:${PORT}/api/health || exit 1"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
+72 -6
View File
@@ -82,14 +82,79 @@ echo " Database URL: ${DATABASE_URL%%:*}://***@***"
echo " Port: $PORT" echo " Port: $PORT"
echo "========================================" echo "========================================"
# Ensure DATABASE_URL is set
if [ -z "$DATABASE_URL" ]; then
echo "❌ DATABASE_URL is not set. Exiting."
exit 1
fi
# Normalize domain inputs (strip scheme/path) to avoid localhost/protocol issues
sanitize_domain() {
echo "$1" | sed -E 's#^[a-zA-Z]+://##; s#/.*$##'
}
# Normalize DOMAIN if provided
if [ -n "$DOMAIN" ]; then
CLEAN_DOMAIN=$(sanitize_domain "$DOMAIN")
if [ "$CLEAN_DOMAIN" != "$DOMAIN" ]; then
export DOMAIN="$CLEAN_DOMAIN"
echo "🌐 DOMAIN normalized to $DOMAIN"
fi
fi
# Normalize NEXT_PUBLIC_NODE_DOMAIN if provided
if [ -n "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
CLEAN_NODE_DOMAIN=$(sanitize_domain "$NEXT_PUBLIC_NODE_DOMAIN")
if [ "$CLEAN_NODE_DOMAIN" != "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
export NEXT_PUBLIC_NODE_DOMAIN="$CLEAN_NODE_DOMAIN"
echo "🌐 NEXT_PUBLIC_NODE_DOMAIN normalized to $NEXT_PUBLIC_NODE_DOMAIN"
fi
fi
# Ensure NEXT_PUBLIC_NODE_DOMAIN is set (fallback to DOMAIN)
if [ -z "$NEXT_PUBLIC_NODE_DOMAIN" ] && [ -n "$DOMAIN" ]; then
export NEXT_PUBLIC_NODE_DOMAIN="$DOMAIN"
echo "🌐 NEXT_PUBLIC_NODE_DOMAIN set to $NEXT_PUBLIC_NODE_DOMAIN"
fi
# Fail fast if running in production with localhost domain
if [ "$NODE_ENV" = "production" ]; then
case "$NEXT_PUBLIC_NODE_DOMAIN" in
""|localhost|localhost:*|127.0.0.1|127.0.0.1:*)
if [ -z "$ALLOW_LOCALHOST" ]; then
echo "❌ NEXT_PUBLIC_NODE_DOMAIN is set to localhost in production."
echo " Set DOMAIN to your public domain in .env, or set ALLOW_LOCALHOST=1 to bypass."
exit 1
fi
;;
esac
fi
# Ensure NEXT_PUBLIC_APP_URL is set for background jobs
if [ -z "$NEXT_PUBLIC_APP_URL" ] && [ -n "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
case "$NEXT_PUBLIC_NODE_DOMAIN" in
http://*|https://*)
NEXT_PUBLIC_APP_URL="$NEXT_PUBLIC_NODE_DOMAIN"
;;
localhost*|127.0.0.1*)
NEXT_PUBLIC_APP_URL="http://$NEXT_PUBLIC_NODE_DOMAIN"
;;
*)
NEXT_PUBLIC_APP_URL="https://$NEXT_PUBLIC_NODE_DOMAIN"
;;
esac
export NEXT_PUBLIC_APP_URL
echo "🌐 NEXT_PUBLIC_APP_URL set to $NEXT_PUBLIC_APP_URL"
fi
# Function to wait for database # Function to wait for database
wait_for_db() { wait_for_db() {
echo "" echo ""
echo "⏳ Waiting for PostgreSQL..." echo "⏳ Waiting for PostgreSQL..."
# Extract host and port from DATABASE_URL # Extract host and port from DATABASE_URL
DB_HOST=$(echo "$DATABASE_URL" | sed -n 's/.*@\([^:]*\):.*/\1/p') DB_HOST=$(echo "$DATABASE_URL" | sed -n 's#.*@\([^/:]*\).*#\1#p')
DB_PORT=$(echo "$DATABASE_URL" | sed -n 's/.*:\([0-9]*\)\/.*/\1/p') DB_PORT=$(echo "$DATABASE_URL" | sed -n 's#.*:\([0-9][0-9]*\)/.*#\1#p')
DB_HOST=${DB_HOST:-postgres} DB_HOST=${DB_HOST:-postgres}
DB_PORT=${DB_PORT:-5432} DB_PORT=${DB_PORT:-5432}
@@ -119,11 +184,12 @@ run_migrations() {
# Run migrations using npm script # Run migrations using npm script
echo " Executing: npm run db:push" echo " Executing: npm run db:push"
npm run db:push 2>&1 || { if npm run db:push 2>&1; then
echo "⚠️ Migration command exited with error (may be already up to date)"
}
echo "✅ Migration step complete" echo "✅ Migration step complete"
else
echo "❌ Migration failed"
exit 1
fi
} }
# Wait for database # Wait for database
@@ -0,0 +1,2 @@
-- Migration: Add storage_public_base_url column to users table
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "storage_public_base_url" text;
+7
View File
@@ -92,6 +92,13 @@
"when": 1769586699065, "when": 1769586699065,
"tag": "0012_quick_mockingbird", "tag": "0012_quick_mockingbird",
"breakpoints": true "breakpoints": true
},
{
"idx": 13,
"version": "7",
"when": 1769638500000,
"tag": "0013_add_storage_public_base_url",
"breakpoints": true
} }
] ]
} }
+131 -1
View File
@@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
# Synapsis Docker Entrypoint Script # Synapsis Docker Entrypoint Script
# Handles automatic port detection when PORT=auto # Handles database migrations, port detection, and application startup
set -e set -e
@@ -66,6 +66,136 @@ echo "📝 Port $PORT written to $SHARED_PORT_FILE"
# Export HOSTNAME for Next.js # Export HOSTNAME for Next.js
export HOSTNAME="0.0.0.0" export HOSTNAME="0.0.0.0"
# ============================================
# Database Migrations
# ============================================
echo ""
echo "========================================"
echo " Synapsis - Starting Application"
echo "========================================"
echo " Time: $(date)"
echo " Working Dir: $(pwd)"
echo " Database URL: ${DATABASE_URL%%:*}://***@***"
echo " Port: $PORT"
echo "========================================"
# Ensure DATABASE_URL is set
if [ -z "$DATABASE_URL" ]; then
echo "❌ DATABASE_URL is not set. Exiting."
exit 1
fi
# Normalize domain inputs (strip scheme/path) to avoid localhost/protocol issues
sanitize_domain() {
echo "$1" | sed -E 's#^[a-zA-Z]+://##; s#/.*$##'
}
# Normalize DOMAIN if provided
if [ -n "$DOMAIN" ]; then
CLEAN_DOMAIN=$(sanitize_domain "$DOMAIN")
if [ "$CLEAN_DOMAIN" != "$DOMAIN" ]; then
export DOMAIN="$CLEAN_DOMAIN"
echo "🌐 DOMAIN normalized to $DOMAIN"
fi
fi
# Normalize NEXT_PUBLIC_NODE_DOMAIN if provided
if [ -n "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
CLEAN_NODE_DOMAIN=$(sanitize_domain "$NEXT_PUBLIC_NODE_DOMAIN")
if [ "$CLEAN_NODE_DOMAIN" != "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
export NEXT_PUBLIC_NODE_DOMAIN="$CLEAN_NODE_DOMAIN"
echo "🌐 NEXT_PUBLIC_NODE_DOMAIN normalized to $NEXT_PUBLIC_NODE_DOMAIN"
fi
fi
# Ensure NEXT_PUBLIC_NODE_DOMAIN is set (fallback to DOMAIN)
if [ -z "$NEXT_PUBLIC_NODE_DOMAIN" ] && [ -n "$DOMAIN" ]; then
export NEXT_PUBLIC_NODE_DOMAIN="$DOMAIN"
echo "🌐 NEXT_PUBLIC_NODE_DOMAIN set to $NEXT_PUBLIC_NODE_DOMAIN"
fi
# Fail fast if running in production with localhost domain
if [ "$NODE_ENV" = "production" ]; then
case "$NEXT_PUBLIC_NODE_DOMAIN" in
""|localhost|localhost:*|127.0.0.1|127.0.0.1:*)
if [ -z "$ALLOW_LOCALHOST" ]; then
echo "❌ NEXT_PUBLIC_NODE_DOMAIN is set to localhost in production."
echo " Set DOMAIN to your public domain in .env, or set ALLOW_LOCALHOST=1 to bypass."
exit 1
fi
;;
esac
fi
# Ensure NEXT_PUBLIC_APP_URL is set for background jobs
if [ -z "$NEXT_PUBLIC_APP_URL" ] && [ -n "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
case "$NEXT_PUBLIC_NODE_DOMAIN" in
http://*|https://*)
NEXT_PUBLIC_APP_URL="$NEXT_PUBLIC_NODE_DOMAIN"
;;
localhost*|127.0.0.1*)
NEXT_PUBLIC_APP_URL="http://$NEXT_PUBLIC_NODE_DOMAIN"
;;
*)
NEXT_PUBLIC_APP_URL="https://$NEXT_PUBLIC_NODE_DOMAIN"
;;
esac
export NEXT_PUBLIC_APP_URL
echo "🌐 NEXT_PUBLIC_APP_URL set to $NEXT_PUBLIC_APP_URL"
fi
# Function to wait for database
wait_for_db() {
echo ""
echo "⏳ Waiting for PostgreSQL..."
# Extract host and port from DATABASE_URL
DB_HOST=$(echo "$DATABASE_URL" | sed -n 's#.*@\([^/:]*\).*#\1#p')
DB_PORT=$(echo "$DATABASE_URL" | sed -n 's#.*:\([0-9][0-9]*\)/.*#\1#p')
DB_HOST=${DB_HOST:-postgres}
DB_PORT=${DB_PORT:-5432}
max_retries=30
retry_count=0
while ! nc -z "$DB_HOST" "$DB_PORT" 2>/dev/null; do
retry_count=$((retry_count + 1))
if [ $retry_count -ge $max_retries ]; then
echo "❌ Failed to connect to database after $max_retries attempts"
exit 1
fi
echo " Attempt $retry_count/$max_retries - waiting for $DB_HOST:$DB_PORT..."
sleep 2
done
echo "✅ Database is ready!"
}
# Function to run database migrations
run_migrations() {
echo ""
echo "🔄 Running database migrations..."
echo " Current directory: $(pwd)"
echo " Drizzle directory contents:"
ls -la drizzle/ 2>/dev/null || echo " (drizzle dir not found or empty)"
# Run migrations using npm script
echo " Executing: npm run db:push"
if npm run db:push 2>&1; then
echo "✅ Migration step complete"
else
echo "❌ Migration failed"
exit 1
fi
}
# Wait for database
wait_for_db
# Run migrations
run_migrations
echo "🚀 Starting Synapsis on port $PORT..." echo "🚀 Starting Synapsis on port $PORT..."
echo "" echo ""
+21 -2
View File
@@ -1,9 +1,16 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { db, users } from '@/db'; import { db, users, isDbAvailable } from '@/db';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
try { try {
if (!isDbAvailable()) {
return NextResponse.json(
{ available: false, error: 'Database not configured' },
{ status: 503 }
);
}
const { searchParams } = new URL(req.url); const { searchParams } = new URL(req.url);
const handle = searchParams.get('handle')?.toLowerCase().trim(); const handle = searchParams.get('handle')?.toLowerCase().trim();
@@ -15,9 +22,21 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ available: false, error: 'Invalid characters' }); return NextResponse.json({ available: false, error: 'Invalid characters' });
} }
const existingUser = await db.query.users.findFirst({ let existingUser = null;
try {
existingUser = await db.query.users.findFirst({
where: eq(users.handle, handle), where: eq(users.handle, handle),
}); });
} catch (err: any) {
// Handle fresh installs where the users table isn't created yet.
if (err?.code === '42P01' || /relation .*users.* does not exist/i.test(err?.message || '')) {
return NextResponse.json(
{ available: true, handle, warning: 'Database not initialized' },
{ status: 503 }
);
}
throw err;
}
return NextResponse.json({ return NextResponse.json({
available: !existingUser, available: !existingUser,
+29 -6
View File
@@ -1,12 +1,29 @@
import { NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { db, nodes } from '@/db'; import { db, nodes } from '@/db';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
export async function GET() { function getRequestBaseUrl(req: NextRequest, fallbackDomain: string): string {
const forwardedHost = req.headers.get('x-forwarded-host');
const forwardedProto = req.headers.get('x-forwarded-proto');
const host = forwardedHost?.split(',')[0]?.trim() || req.headers.get('host');
const protocol =
forwardedProto?.split(',')[0]?.trim() ||
(host && host.includes('localhost') ? 'http' : 'https');
if (host) {
return `${protocol}://${host}`;
}
return `https://${fallbackDomain}`;
}
export async function GET(req: NextRequest) {
try { try {
if (!db) { if (!db) {
// Redirect to default favicon // Redirect to default favicon
return NextResponse.redirect(new URL('/favicon.png', process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000')); const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
const baseUrl = getRequestBaseUrl(req, domain);
return NextResponse.redirect(new URL('/favicon.png', baseUrl));
} }
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000'; const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
@@ -17,14 +34,20 @@ export async function GET() {
if (node?.faviconUrl) { if (node?.faviconUrl) {
// Redirect to custom favicon // Redirect to custom favicon
return NextResponse.redirect(node.faviconUrl); const baseUrl = getRequestBaseUrl(req, domain);
const target = node.faviconUrl.startsWith('/')
? new URL(node.faviconUrl, baseUrl)
: node.faviconUrl;
return NextResponse.redirect(target);
} }
// Redirect to default favicon // Redirect to default favicon
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || `https://${domain}`; const baseUrl = getRequestBaseUrl(req, domain);
return NextResponse.redirect(new URL('/favicon.png', baseUrl)); return NextResponse.redirect(new URL('/favicon.png', baseUrl));
} catch (error) { } catch (error) {
console.error('Favicon error:', error); console.error('Favicon error:', error);
return NextResponse.redirect(new URL('/favicon.png', process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000')); const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
const baseUrl = getRequestBaseUrl(req, domain);
return NextResponse.redirect(new URL('/favicon.png', baseUrl));
} }
} }
+1 -1
View File
@@ -22,7 +22,7 @@ export async function POST(req: NextRequest) {
accessKeyId: process.env.STORAGE_ACCESS_KEY || '', accessKeyId: process.env.STORAGE_ACCESS_KEY || '',
secretAccessKey: process.env.STORAGE_SECRET_KEY || '', secretAccessKey: process.env.STORAGE_SECRET_KEY || '',
}, },
forcePathStyle: true, // Needed for many S3-compatible providers (MinIO, etc.) forcePathStyle: true, // Needed for many S3-compatible providers
}); });
const bucket = process.env.STORAGE_BUCKET || 'synapsis'; const bucket = process.env.STORAGE_BUCKET || 'synapsis';
+4 -6
View File
@@ -577,8 +577,6 @@ export default function LoginPage() {
<option value="wasabi">Wasabi</option> <option value="wasabi">Wasabi</option>
<option value="contabo">Contabo S3</option> <option value="contabo">Contabo S3</option>
<option value="s3">AWS S3</option> <option value="s3">AWS S3</option>
<option value="minio">MinIO / Self-hosted</option>
<option value="other">Other S3-compatible</option>
</select> </select>
</div> </div>
</div> </div>
@@ -614,8 +612,8 @@ export default function LoginPage() {
</div> </div>
</div> </div>
{/* Endpoint URL - only show for providers that need it (R2, B2, Contabo, MinIO, Other) */} {/* Endpoint URL - only show for providers that need it (R2, B2, Contabo) */}
{(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo' || storageProvider === 'minio' || storageProvider === 'other') && ( {(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo') && (
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}> <label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Endpoint URL Endpoint URL
@@ -635,8 +633,8 @@ export default function LoginPage() {
</div> </div>
)} )}
{/* Public Base URL - only show for providers that need it (R2, B2, Contabo, Other) */} {/* Public Base URL - only show for providers that need it (R2, B2, Contabo) */}
{(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo' || storageProvider === 'other') && ( {(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo') && (
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}> <label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Public Base URL Public Base URL
+3 -3
View File
@@ -65,7 +65,7 @@ export const users = pgTable('users', {
movedFrom: text('moved_from'), // Old actor URL if this account migrated here movedFrom: text('moved_from'), // Old actor URL if this account migrated here
migratedAt: timestamp('migrated_at'), // When the migration occurred migratedAt: timestamp('migrated_at'), // When the migration occurred
// User-owned S3-compatible storage - required for new users // User-owned S3-compatible storage - required for new users
storageProvider: text('storage_provider'), // 's3', 'r2', 'b2', 'wasabi', 'contabo', etc storageProvider: text('storage_provider'), // 's3', 'r2', 'b2', 'wasabi', 'contabo'
storageEndpoint: text('storage_endpoint'), // S3 endpoint URL (optional for AWS) storageEndpoint: text('storage_endpoint'), // S3 endpoint URL (optional for AWS)
storagePublicBaseUrl: text('storage_public_base_url'), // Public URL for viewing files (required for R2, B2, Contabo) storagePublicBaseUrl: text('storage_public_base_url'), // Public URL for viewing files (required for R2, B2, Contabo)
storageRegion: text('storage_region'), // Region (e.g., 'us-east-1') storageRegion: text('storage_region'), // Region (e.g., 'us-east-1')
@@ -919,8 +919,8 @@ export const chatConversationsRelations = relations(chatConversations, ({ one, m
/** /**
* Individual chat messages within conversations. * Individual chat messages within conversations.
* Messages are encrypted end-to-end using recipient's public key. * Messages are stored as plain text on the server.
* Sender also gets a copy encrypted with their own public key. * Both sender and recipient can view the message content.
*/ */
export const chatMessages = pgTable('chat_messages', { export const chatMessages = pgTable('chat_messages', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
+2
View File
@@ -1,3 +1,5 @@
'use client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
interface RuntimeConfig { interface RuntimeConfig {
+3 -3
View File
@@ -1,13 +1,13 @@
/** /**
* User-Owned S3-Compatible Storage Utilities * User-Owned S3-Compatible Storage Utilities
* *
* Supports AWS S3, Cloudflare R2, Backblaze B2, Wasabi, MinIO, etc. * Supports AWS S3, Cloudflare R2, Backblaze B2, Wasabi, and Contabo.
*/ */
import { S3Client, PutObjectCommand, HeadBucketCommand } from '@aws-sdk/client-s3'; import { S3Client, PutObjectCommand, HeadBucketCommand } from '@aws-sdk/client-s3';
import { decryptPrivateKey, deserializeEncryptedKey } from '@/lib/crypto/private-key'; import { decryptPrivateKey, deserializeEncryptedKey } from '@/lib/crypto/private-key';
export type StorageProvider = 's3' | 'r2' | 'b2' | 'wasabi' | 'minio' | 'other'; export type StorageProvider = 's3' | 'r2' | 'b2' | 'wasabi' | 'contabo';
interface S3Credentials { interface S3Credentials {
endpoint?: string; endpoint?: string;
@@ -105,7 +105,7 @@ export async function uploadToUserStorage(
if (publicBaseUrl) { if (publicBaseUrl) {
url = `${publicBaseUrl.replace(/\/$/, '')}/${key}`; url = `${publicBaseUrl.replace(/\/$/, '')}/${key}`;
} else if (endpoint) { } else if (endpoint) {
// Custom endpoint (MinIO, etc) // Custom endpoint (R2, B2, Contabo)
url = `${endpoint}/${bucket}/${key}`; url = `${endpoint}/${bucket}/${key}`;
} else { } else {
// AWS S3 standard URL // AWS S3 standard URL