Skip to main content

Self-hosting

Everything you need to run guacamoleninja on your own infrastructure — locally for development or in production with Docker.


Architecture​

guacamoleninja is composed of two independent components:

  • guacamoleninja-web — the dashboard and OAuth flow
  • guacamoleninja-bot — the Discord bot and its HTTP API

The components communicate over a secured HTTP API using a shared bearer token. Each has its own PostgreSQL database — they share no data directly.


Local development​

Prerequisites​

See Requirements for versions.

1. Clone both repos​

git clone https://github.com/kreativermario/guacamoleninja-web.git
git clone https://github.com/kreativermario/guacamoleninja-bot.git

2. Configure environment — web​

cd guacamoleninja-web
cp .env.local.example .env.local # create if it doesn't exist

.env.local for the web app:

# Discord OAuth — from https://discord.com/developers/applications
DISCORD_CLIENT_ID=your_client_id
DISCORD_CLIENT_SECRET=your_client_secret

# NextAuth
AUTH_SECRET=any_long_random_string_32_chars_min
AUTH_URL=http://localhost:3000

# PostgreSQL (web auth DB — started by Docker Compose)
POSTGRES_USER=guacweb
POSTGRES_PASSWORD=guacweb
POSTGRES_DB=guacweb
DATABASE_URL=postgresql://guacweb:guacweb@localhost:5432/guacweb

# Bot API — must match BOT_API_SECRET in the bot .env
BOT_API_URL=http://localhost:3002
BOT_API_SECRET=any_long_random_string

3. Configure environment — bot​

cd guacamoleninja-bot
cp .env.example .env

.env for the bot:

# Discord bot — from https://discord.com/developers/applications
BOT_TOKEN=your_bot_token
CLIENT_ID=your_client_id

# Slash command registration target (dev only — set to your test server ID)
GUILD_ID=your_guild_id

# PostgreSQL (bot DB — started by Docker Compose)
POSTGRES_USER=guacbot
POSTGRES_PASSWORD=guacbot
POSTGRES_DB=guacbot
DATABASE_URL=postgresql://guacbot:guacbot@localhost:5432/guacbot

# HTTP API
BOT_API_SECRET=any_long_random_string # must match web BOT_API_SECRET
BOT_API_PORT=3002

TIMEZONE=Europe/Lisbon

4. Start the web stack​

cd guacamoleninja-web
docker compose -f docker/docker-compose.local.yml up

This starts:

ServiceImagePortDescription
postgrespostgres:17-alpine5432Web auth database
migratebuilt from repo—Runs prisma migrate deploy, exits when done
webbuilt from repo3000Next.js app

The web service waits for migrate to complete successfully before starting.

5. Start the bot stack​

In a second terminal:

cd guacamoleninja-bot
docker compose -f docker/docker-compose.local.yml up

This starts:

ServiceImagePortDescription
postgrespostgres:17-alpine5432Bot database
bot-migratebuilt from repo—Runs prisma db push, exits when done
botbuilt from repo—Discord bot process (watch mode)
apibuilt from repo3002HTTP API consumed by the web app

Both bot and api wait for bot-migrate to complete before starting.

6. Verify​

# Bot API health
curl http://localhost:3002/health

# Expected
{"status":"ok","db":"ok","uptime":12.34}

Open http://localhost:3000 to see the web app.


Docker Compose — annotated​

Web (docker/docker-compose.local.yml)​

services:
postgres:
image: postgres:17-alpine
env_file: ../.env.local # reads POSTGRES_USER/PASSWORD/DB
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $$POSTGRES_DB -U $$POSTGRES_USER"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s

migrate:
build:
context: .. # build context is repo root
target: migrator # uses the migrator stage from docker/Dockerfile
env_file: ../.env.local
depends_on:
postgres:
condition: service_healthy # waits for postgres to be ready

web:
build:
context: .. # full Next.js build
env_file: ../.env.local
ports:
- "3000:3000"
environment:
BOT_API_URL: ${BOT_API_URL:-http://localhost:3002}
BOT_API_SECRET: ${BOT_API_SECRET:-}
depends_on:
migrate:
condition: service_completed_successfully
postgres:
condition: service_healthy

volumes:
postgres_data:

Bot (docker/docker-compose.local.yml)​

services:
bot-migrate:
build:
context: ..
target: api # uses the api stage — runs db-migrate.js via CMD override
command: ["dist/db-migrate.js"]
env_file: ../.env
depends_on:
postgres:
condition: service_healthy
restart: "no" # exits after migration completes

bot:
build:
context: ..
target: dev # development stage — tsx watch mode
env_file: ../.env
command: node_modules/.bin/tsx src/start.ts
depends_on:
bot-migrate:
condition: service_completed_successfully
postgres:
condition: service_healthy

api:
build:
context: ..
target: api
env_file: ../.env
ports:
- "${BOT_API_PORT:-3002}:3002"
depends_on:
bot-migrate:
condition: service_completed_successfully
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "node", "-e",
"fetch('http://localhost:3002/health',{signal:AbortSignal.timeout(3000)}).then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
restart: unless-stopped

postgres:
image: postgres:17-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER:-guacamoleninja}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-guacamoleninja}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $POSTGRES_DB -U $POSTGRES_USER"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s

volumes:
postgres_data:

Dockerfile stages​

Both repos use multi-stage builds. Understanding the stages helps when debugging or extending:

Web (docker/Dockerfile)​

StageBasePurpose
basenode:24Enables corepack (pnpm)
depsbaseInstalls all dependencies
builderbaseGenerates Prisma client, runs pnpm build
migratorbaseRuns prisma migrate deploy at container start
runnerdistroless/nodejs24-debian13:nonrootProduction image — copies only built output

Bot (docker/Dockerfile)​

StageBasePurpose
basenode:24-alpineEnables corepack
depsbaseInstalls all dependencies
prod-depsbaseProduction-only deps + Prisma client generate
devbaseDevelopment stage with tsx watch
builderbaseCompiles TypeScript, generates Prisma client
runnerdistroless/nodejs24-debian13:nonrootBot process
apidistroless/nodejs24-debian13:nonrootHTTP API process

Both production images use distroless — no shell, no package manager, runs as uid 65532 (nonroot). This minimises attack surface and image size.


Production deployment​

Both repos include a Dockerfile and docker/docker-compose.prod.yml ready for production use. The production compose file uses image references instead of build contexts — build your images, push them to a registry of your choice, and set the WEB_IMAGE / BOT_IMAGE / BOT_API_IMAGE environment variables accordingly.

Configure your environment variables for your infrastructure and bring your own secrets management.


Discord Developer Portal setup​

  1. Go to discord.com/developers/applications and create a new application.
  2. Under Bot, copy the bot token → BOT_TOKEN.
  3. Enable Server Members Intent (required for welcome messages to fire).
  4. Under OAuth2 → General, copy Client ID → CLIENT_ID / DISCORD_CLIENT_ID.
  5. Copy Client Secret → DISCORD_CLIENT_SECRET (web only).
  6. Add redirect URI: http://localhost:3000/api/auth/callback/discord (dev) or your production URL.