Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.git
.github
.vscode
.idea
.claude
*.md
LICENSE
*.db
*.db-journal
*.db-wal
*.db-shm
backend
hosted-backend
.env
.env.local
48 changes: 43 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,47 @@
# Copy to .env for local dev. obby-api is built locally; obbyircd
# and the obby web client are pulled from Dockerhub. In Coolify,
# set these as application environment variables instead.

# --- DNS / Traefik routing (Coolify auto-issues LE) ---
WEB_FQDN=chat.example.com
API_FQDN=api.example.com
IRC_FQDN=irc.example.com

# --- backend tuning ---
PORT=8080
DELETE_TIMEOUT_MINUTES=2
MAX_UPLOAD_SIZE_MB=32
JWT_SECRET=testsecret
IRC_SERVER_KEY=testkey
PASSWORD_SALT_SIZE=32
UNREALIRCD_WS_URL=wss://127.0.0.1:8600/
UNREALIRCD_API_USERNAME=adminpanel
UNREALIRCD_API_PASSWORD=password

# --- secrets (set per environment) ---
JWT_SECRET=testsecret # (secret in prod) openssl rand -hex 32
IRC_SERVER_KEY=testkey # (secret in prod) shared with obbyircd
VOICE_TURN_SECRET= # (secret) openssl rand -hex 32
RPC_PASSWORD=password # (secret in prod) obbyircd JSON-RPC password

# --- IRCd reachability ---
UNREALIRCD_WS_URL=ws://obbyircd:8080
UNREALIRCD_API_USERNAME=admin
UNREALIRCD_API_PASSWORD=password # mirror of RPC_PASSWORD; unify in prod

# --- voice / TURN ---
VOICE_PUBLIC_IP= # public IP advertised in ICE candidates
TURN_PORT=3478 # UDP, open in host firewall
VOICE_TURN_REALM=obby
VOICE_MAX_ROOM=25
Comment on lines +27 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Document the new voice env vars consumed by voice.go.

VOICE_TURN_DISABLE_EMBEDDED, VOICE_WEBRTC_PORT_MIN, and VOICE_WEBRTC_PORT_MAX are read by runtime code but not surfaced in .env.example, which makes this feature hard to configure correctly in deployments.

Suggested additions
 # --- voice / TURN ---
 VOICE_PUBLIC_IP=                     # public IP advertised in ICE candidates
 TURN_PORT=3478                       # UDP, open in host firewall
 VOICE_TURN_REALM=obby
 VOICE_MAX_ROOM=25
+VOICE_TURN_DISABLE_EMBEDDED=false    # true when using external TURN infra
+VOICE_WEBRTC_PORT_MIN=40000          # UDP ICE host candidate range (server-side)
+VOICE_WEBRTC_PORT_MAX=40100
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# --- voice / TURN ---
VOICE_PUBLIC_IP= # public IP advertised in ICE candidates
TURN_PORT=3478 # UDP, open in host firewall
VOICE_TURN_REALM=obby
VOICE_MAX_ROOM=25
# --- voice / TURN ---
VOICE_PUBLIC_IP= # public IP advertised in ICE candidates
TURN_PORT=3478 # UDP, open in host firewall
VOICE_TURN_REALM=obby
VOICE_MAX_ROOM=25
VOICE_TURN_DISABLE_EMBEDDED=false # true when using external TURN infra
VOICE_WEBRTC_PORT_MIN=40000 # UDP ICE host candidate range (server-side)
VOICE_WEBRTC_PORT_MAX=40100
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 28-28: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 28-28: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 29-29: [UnorderedKey] The TURN_PORT key should go before the VOICE_PUBLIC_IP key

(UnorderedKey)


[warning] 29-29: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 31-31: [UnorderedKey] The VOICE_MAX_ROOM key should go before the VOICE_PUBLIC_IP key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example around lines 27 - 31, Add documentation lines to .env.example
for the three env vars referenced by voice.go: VOICE_TURN_DISABLE_EMBEDDED
(boolean flag to disable the embedded TURN server), VOICE_WEBRTC_PORT_MIN
(lowest UDP port in the ephemeral WebRTC port range) and VOICE_WEBRTC_PORT_MAX
(highest UDP port in the WebRTC port range). For each entry include the variable
name, a short description, expected value type (e.g., true/false or numeric
port), and the default/behavior used by voice.go when unset so operators can
configure TURN and port range correctly.


# --- filehost ---
FILEHOST_PUBLIC_URL= # https://files.example.com

# --- image overrides (optional) ---
# OBBY_API_IMAGE=obby-api:local
# OBBYIRCD_IMAGE=obbyworld/obbyircd:latest
# OBBY_WEB_IMAGE=obbyworld/obby:latest

# --- frontend profile (opt-in) ---
# COMPOSE_PROFILES=frontend

# --- bind-mount overrides (optional) ---
# OBBY_API_DATA_BIND=/srv/obby/obby-api-data
# OBBY_API_IMAGES_BIND=/srv/obby/obby-api-images
# VOICE_BRIDGE_BIND=/srv/obby/voice-bridge
105 changes: 105 additions & 0 deletions .github/workflows/docker-build-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: Reusable Docker Build and Push

on:
workflow_call:
inputs:
image_name:
required: true
type: string
context:
required: true
type: string
dockerfile:
required: true
type: string
secrets:
DOCKERHUB_USERNAME:
required: true
DOCKERHUB_TOKEN:
required: true

jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
if: ${{ github.event_name != 'pull_request' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- id: meta
run: echo "short_sha=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT"

- uses: docker/build-push-action@v6
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}
platforms: linux/${{ matrix.arch }}
push: ${{ github.event_name != 'pull_request' }}
provenance: false
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ inputs.image_name }}:sha-${{ steps.meta.outputs.short_sha }}-${{ matrix.arch }}
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/${{ inputs.image_name }}:cache-${{ matrix.arch }}
cache-to: ${{ github.event_name != 'pull_request' && format('type=registry,ref={0}/{1}:cache-{2},mode=max,image-manifest=true,oci-mediatypes=true', secrets.DOCKERHUB_USERNAME, inputs.image_name, matrix.arch) || '' }}

manifest:
needs: build
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ inputs.image_name }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,prefix=sha-,format=short
type=raw,value=latest,enable={{is_default_branch}}

- id: short
run: echo "sha=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT"

- name: Assemble multi-arch manifest
shell: bash
env:
IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/${{ inputs.image_name }}
SHORT_SHA: ${{ steps.short.outputs.sha }}
TAGS: ${{ steps.meta.outputs.tags }}
run: |
src_amd64="${IMAGE}:sha-${SHORT_SHA}-amd64"
src_arm64="${IMAGE}:sha-${SHORT_SHA}-arm64"
tag_args=()
while IFS= read -r t; do
[ -n "$t" ] && tag_args+=( --tag "$t" )
done <<< "$TAGS"
docker buildx imagetools create "${tag_args[@]}" "$src_amd64" "$src_arm64"
docker buildx imagetools inspect "${IMAGE}:sha-${SHORT_SHA}"
30 changes: 30 additions & 0 deletions .github/workflows/docker-obby-api.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Build and Push obby-api Docker Image

on:
push:
branches:
- main
tags:
- 'v*'
paths-ignore:
- '**.md'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix markdown ignore glob to match nested files.

**.md is too loose/ambiguous; use **/*.md so docs-only changes reliably skip the workflow.

Suggested patch
-      - '**.md'
+      - '**/*.md'
@@
-      - '**.md'
+      - '**/*.md'

Also applies to: 16-16

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-obby-api.yml at line 10, Replace the incorrect glob
'**.md' with the correct '**/*.md' in the workflow so markdown changes in nested
directories are matched; update every occurrence (e.g., the entries currently at
the positions shown around the top of the file) to '**/*.md' inside the
workflow's paths/ignore list (look for the literal '**.md' string and change it
to '**/*.md').

- 'LICENSE'
pull_request:
branches:
- main
paths-ignore:
- '**.md'
- 'LICENSE'
workflow_dispatch:

jobs:
build:
name: Build obby-api Image
uses: ./.github/workflows/docker-build-push.yml
with:
image_name: obby-api
context: .
dockerfile: ./Dockerfile
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
28 changes: 28 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# syntax=docker/dockerfile:1.6
#
# obby-api / hosted-backend image -- REST + JWT + WebRTC SFU + TURN.
# CGO_ENABLED=1 is required because mattn/go-sqlite3 ships as cgo.
#
# Build: docker build -t obbyworld/obby-api .
# Compose: docker compose up -d

FROM golang:1.25-alpine AS builder
RUN apk add --no-cache build-base sqlite-dev opus-dev opusfile-dev pkgconfig
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o /out/backend .

FROM alpine:3.20
RUN apk add --no-cache ca-certificates sqlite-libs tzdata netcat-openbsd opus opusfile \
&& adduser -D -u 1000 backend \
&& mkdir -p /app/data /app/images /run/obbyirc \
&& chown -R backend:backend /app /run/obbyirc
WORKDIR /app
COPY --from=builder /out/backend /usr/local/bin/backend
USER backend
EXPOSE 8080 3478/udp
HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=3 \
CMD nc -z 127.0.0.1 8080 || exit 1
CMD ["backend"]
111 changes: 111 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Local dev stack for obby-api.
#
# This service is built from the local checkout; obbyircd and the
# web client are pulled from Dockerhub. For full-stack development
# in one of the other repos, swap which service has `build:` and
# which has only `image:`.
#
# docker compose up -d # api + obbyircd
# docker compose --profile frontend up -d # + obby (web)

services:
obby-api:
build:
context: .
dockerfile: ./Dockerfile
image: ${OBBY_API_IMAGE:-obby-api:local}
container_name: obby-api
restart: unless-stopped
depends_on:
obbyircd:
condition: service_healthy
expose:
- "8080"
ports:
- "${TURN_PORT:-3478}:3478/udp"
Comment on lines +24 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Publish the server WebRTC UDP range when using NAT1:1 host candidates.

Only 3478/udp is exposed now. If VOICE_WEBRTC_PORT_MIN/MAX is configured, those UDP ports must also be published or external ICE will fail to connect to SFU candidates.

Suggested patch
     ports:
       - "${TURN_PORT:-3478}:3478/udp"
+      - "${VOICE_WEBRTC_PORT_MIN:-40000}-${VOICE_WEBRTC_PORT_MAX:-40100}:${VOICE_WEBRTC_PORT_MIN:-40000}-${VOICE_WEBRTC_PORT_MAX:-40100}/udp"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ports:
- "${TURN_PORT:-3478}:3478/udp"
ports:
- "${TURN_PORT:-3478}:3478/udp"
- "${VOICE_WEBRTC_PORT_MIN:-40000}-${VOICE_WEBRTC_PORT_MAX:-40100}:${VOICE_WEBRTC_PORT_MIN:-40000}-${VOICE_WEBRTC_PORT_MAX:-40100}/udp"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@compose.yaml` around lines 24 - 25, The compose file only publishes 3478/udp;
when VOICE_WEBRTC_PORT_MIN and VOICE_WEBRTC_PORT_MAX are configured you must
also publish that UDP port range to allow external ICE to reach SFU
candidates—update the service ports stanza to add a mapping for the range using
the VOICE_WEBRTC_PORT_MIN/VOICE_WEBRTC_PORT_MAX env vars (alongside TURN_PORT),
ensuring the format publishes host-range:container-range/udp (or individual
ports if range unsupported) so the TURN/WebRTC UDP relay and SFU (references:
TURN_PORT, VOICE_WEBRTC_PORT_MIN, VOICE_WEBRTC_PORT_MAX) are reachable from
outside.

volumes:
- ${OBBY_API_DATA_BIND:-obby_api_data}:/app/data
- ${OBBY_API_IMAGES_BIND:-obby_api_images}:/app/images
- ${VOICE_BRIDGE_BIND:-obbyircd_voice_bridge}:/run/obbyirc
environment:
PORT: "8080"
VOICE_BRIDGE_SOCKET: /run/obbyirc/voice.sock
VOICE_TURN_SECRET: ${VOICE_TURN_SECRET}
VOICE_PUBLIC_IP: ${VOICE_PUBLIC_IP}
VOICE_TURN_PORT: "${TURN_PORT:-3478}"
VOICE_TURN_REALM: ${VOICE_TURN_REALM:-obby}
VOICE_MAX_ROOM: "${VOICE_MAX_ROOM:-25}"
JWT_SECRET: ${JWT_SECRET}
IRC_SERVER_KEY: ${IRC_SERVER_KEY}
UNREALIRCD_WS_URL: ${UNREALIRCD_WS_URL:-ws://obbyircd:8080}
UNREALIRCD_API_USERNAME: ${UNREALIRCD_API_USERNAME:-admin}
UNREALIRCD_API_PASSWORD: ${RPC_PASSWORD}
FILEHOST_PUBLIC_URL: ${FILEHOST_PUBLIC_URL}
env_file:
- path: .env
required: false
labels:
- traefik.enable=true
- traefik.http.routers.api.rule=Host(`${API_FQDN}`)
- traefik.http.routers.api.tls=true
- traefik.http.services.api.loadbalancer.server.port=8080

obbyircd:
image: ${OBBYIRCD_IMAGE:-obbyworld/obbyircd:latest}
container_name: obbyircd
restart: unless-stopped
expose:
- "8080"
ports:
- "${SSL_HOST_PORT:-6697}:${SSL_PORT:-6697}"
volumes:
- obbyircd_conf:/home/obbyircd/obby/conf
- obbyircd_data:/home/obbyircd/obby/data
- obbyircd_logs:/home/obbyircd/obby/logs
- obbyircd_tls:/home/obbyircd/obby/tls
- obbyircd_custom_modules:/home/obbyircd/obby/custom-modules
- ${VOICE_BRIDGE_BIND:-obbyircd_voice_bridge}:/run/obbyirc
environment:
WS_PORT: "${WS_PORT:-8080}"
SSL_PORT: "${SSL_PORT:-6697}"
VOICE_BRIDGE_SOCKET: /run/obbyirc/voice.sock
env_file:
- path: .env
required: false
healthcheck:
test: ["CMD-SHELL", "nc -z localhost ${WS_PORT:-8080}"]
interval: 5s
timeout: 5s
retries: 6
start_period: 30s
labels:
- traefik.enable=true
- traefik.http.routers.obbyircd.rule=Host(`${IRC_FQDN}`)
- traefik.http.routers.obbyircd.tls=true
- traefik.http.services.obbyircd.loadbalancer.server.port=8080

obby:
image: ${OBBY_WEB_IMAGE:-obbyworld/obby:latest}
container_name: obby-web
restart: unless-stopped
profiles: ["frontend"]
expose:
- "80"
env_file:
- path: .env
required: false
labels:
- traefik.enable=true
- traefik.http.routers.web.rule=Host(`${WEB_FQDN}`)
- traefik.http.routers.web.tls=true
- traefik.http.services.web.loadbalancer.server.port=80

volumes:
obby_api_data:
obby_api_images:
obbyircd_conf:
obbyircd_data:
obbyircd_logs:
obbyircd_tls:
obbyircd_custom_modules:
obbyircd_voice_bridge:
Loading
Loading