The repository ships with a docker-compose.yml that starts the bot, dashboard, PostgreSQL, Redis, and the docs site together.
Prerequisites
- Docker with the Compose plugin (Docker Desktop includes this)
- A filled-in
.env file in the repository root
Quick start
Copy and fill the environment file
Set at minimum:DISCORD_TOKEN=your_discord_bot_token
DISCORD_CLIENT_ID=your_discord_client_id
DISCORD_CLIENT_SECRET=your_discord_client_secret
ANTHROPIC_API_KEY=your_anthropic_api_key
SESSION_SECRET=your_session_secret
NEXTAUTH_SECRET=your_nextauth_secret
BOT_API_SECRET=your_bot_api_secret
NEXT_PUBLIC_DISCORD_CLIENT_ID=your_discord_client_id
DATABASE_URL and REDIS_URL are automatically overridden by the Compose file to point at the internal db and redis service hostnames. You do not need to set them for a Docker deployment.
Build and start all services
Docker builds the bot and dashboard images, then starts all services in dependency order: db and redis first, then bot and web once their health checks pass. View logs
Stream logs from all services:Or tail a specific service:docker compose logs -f bot
docker compose logs -f web
Service ports
| Service | Port | Description |
|---|
web | 6969 | Web dashboard |
bot | 6968 | Bot REST API |
db | 5432 | PostgreSQL |
redis | 6379 | Redis |
docs | 3100 | Documentation site |
docker-compose.yml
services:
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
POSTGRES_DB: volvoxbot
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d volvoxbot"]
interval: 5s
timeout: 5s
retries: 5
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
bot:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
init: true
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "6968:6968"
volumes:
- botdata:/app/data
env_file:
- .env
environment:
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/volvoxbot
DATABASE_SSL: "false"
REDIS_URL: redis://redis:6379
PORT: "6968"
BOT_API_PORT: "6968"
DASHBOARD_URL: http://localhost:6969
DISABLE_PROMPT_CACHING: "1"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
web:
build:
context: ./web
dockerfile: Dockerfile
args:
NEXT_PUBLIC_DISCORD_CLIENT_ID: ${NEXT_PUBLIC_DISCORD_CLIENT_ID}
restart: unless-stopped
init: true
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "6969:6969"
env_file:
- .env
environment:
PORT: "6969"
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/volvoxbot
DATABASE_SSL: "false"
REDIS_URL: redis://redis:6379
NEXTAUTH_URL: http://localhost:6969
BOT_API_URL: http://bot:6968
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
docs:
build:
context: ./docs
dockerfile: Dockerfile
restart: unless-stopped
ports:
- "3100:3100"
environment:
PORT: "3100"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
pgdata:
redisdata:
botdata:
Dockerfiles
The bot Dockerfile uses a multi-stage build. The deps stage installs only production dependencies, and the runner stage copies them into a minimal Alpine image running as a non-root user.# syntax=docker/dockerfile:1
# --- Dependencies ---
FROM node:22-alpine AS deps
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm config set store-dir /tmp/pnpm-store && \\
pnpm install --frozen-lockfile --prod --ignore-scripts && \\
rm -rf /tmp/pnpm-store
# --- Production ---
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 botgroup && \\
adduser --system --uid 1001 botuser
# Copy production dependencies
COPY --from=deps --chown=botuser:botgroup /app/node_modules ./node_modules
# Copy application source, config, and migrations
COPY --chown=botuser:botgroup package.json ./
COPY --chown=botuser:botgroup config.json ./
COPY --chown=botuser:botgroup src/ ./src/
COPY --chown=botuser:botgroup migrations/ ./migrations/
# Create data directory for state persistence
RUN mkdir -p data && chown botuser:botgroup data
USER botuser
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \\
CMD wget --spider --quiet "http://localhost:${PORT:-3001}/api/v1/health" || exit 1
CMD ["node", "src/index.js"]
The dashboard Dockerfile uses three stages: deps installs all dependencies, builder compiles the Next.js app, and runner serves the standalone output as a non-root user.# syntax=docker/dockerfile:1
# --- Dependencies ---
FROM node:22-alpine AS deps
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# --- Builder ---
FROM node:22-alpine AS builder
RUN corepack enable
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG NEXT_PUBLIC_DISCORD_CLIENT_ID
ENV NEXT_PUBLIC_DISCORD_CLIENT_ID=$NEXT_PUBLIC_DISCORD_CLIENT_ID
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN pnpm build
# --- Runner ---
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs && \\
adduser --system --uid 1001 nextjs
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/public ./public
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \\
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
Volume mounts and data persistence
Three named volumes preserve data across container restarts:
| Volume | Mount | Contents |
|---|
pgdata | /var/lib/postgresql/data | PostgreSQL data directory |
redisdata | /data | Redis RDB snapshots |
botdata | /app/data | Bot state persistence (local data directory) |
Removing volumes with docker compose down -v permanently deletes your database and Redis data. Omit the -v flag to keep volumes when stopping services.
Common commands
# Start all services in the background
docker compose up -d
# Stop all services (keeps volumes)
docker compose down
# Rebuild images after code changes
docker compose up -d --build
# Stream logs from all services
docker compose logs -f
# Restart a single service
docker compose restart bot
# Open a shell in the bot container
docker compose exec bot sh
# Run a database migration manually
docker compose exec bot pnpm migrate
Railway deployment
Volvox.Bot includes a railway.toml for one-click deployment to Railway.
[build]
builder = "DOCKERFILE"
dockerfilePath = "Dockerfile"
dockerLayerCaching = true
[deploy]
healthcheckPath = "/api/v1/health"
healthcheckTimeout = 10
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
numReplicas = 1
To deploy on Railway:
- Install the Railway CLI and log in.
- Create a new Railway project and link it to your repository.
- Add a PostgreSQL and Redis service from the Railway plugin library.
- Set your environment variables in the Railway dashboard under Variables.
- Deploy with:
Railway automatically detects railway.toml, builds from the Dockerfile, and health-checks the bot at /api/v1/health before marking the deployment as live.
Railway internal networking uses .railway.internal hostnames. The bot’s SSL configuration automatically disables TLS for internal Railway connections, so you do not need to set DATABASE_SSL=false manually.