diff --git a/deploy/stc/.gitattributes b/deploy/stc/.gitattributes new file mode 100644 index 00000000000..8da64cd3424 --- /dev/null +++ b/deploy/stc/.gitattributes @@ -0,0 +1,2 @@ +# Deploy files run on Linux; a stray CR breaks env parsing and Caddyfile directives +deploy/stc/** text eol=lf diff --git a/deploy/stc/Caddyfile b/deploy/stc/Caddyfile new file mode 100644 index 00000000000..8cda89d8c57 --- /dev/null +++ b/deploy/stc/Caddyfile @@ -0,0 +1,4 @@ +{$CHAT_DOMAIN} { + encode zstd gzip + reverse_proxy traefik:80 +} diff --git a/deploy/stc/README-STC.md b/deploy/stc/README-STC.md new file mode 100644 index 00000000000..5b3c154e0db --- /dev/null +++ b/deploy/stc/README-STC.md @@ -0,0 +1,60 @@ +# STC Tailchat deployment — netcup-vps-01 + +Production chat for STC Worldwide, served publicly at `https://$CHAT_DOMAIN`. + +- **Host:** netcup-vps-01 (152.53.82.15 public / 100.66.129.67 tailnet), Debian 13, root over key-only SSH +- **Location on host:** `/opt/tailchat/` +- **Image:** `moonrailgun/tailchat:1.11.12` (pinned; official upstream image — the STC fork is source custody, not the build source) +- **Topology:** Caddy (public 80/443, Let's Encrypt) → traefik (internal path router) → tailchat services. MongoDB/Redis/MinIO are internal-only. + +## Deploy / redeploy + +```sh +cd /opt/tailchat +docker compose pull +docker compose up -d +``` + +First-time setup: copy `docker-compose.env.example` → `docker-compose.env`, fill every +blank value, `chmod 600 docker-compose.env`, `ln -s docker-compose.env .env`. +DNS A record for `$CHAT_DOMAIN` → 152.53.82.15 must exist before first start or +Let's Encrypt issuance fails (Caddy retries automatically once DNS resolves). + +## Verify what is INSTALLED, not what is checked out + +The compose file in the git fork is not what runs — `/opt/tailchat/docker-compose.yml` +on the VPS is. After changing the fork, copy the files over and `docker compose up -d`, +then check `docker compose ps` and `docker inspect --format '{{.Config.Image}}' `. + +## Update to a newer Tailchat + +1. Check upstream releases / Docker Hub tags (`moonrailgun/tailchat`). +2. Edit the pinned tag in `deploy/stc/docker-compose.yml` (all four app services), PR it. +3. Copy to VPS, `docker compose pull && docker compose up -d`. +4. Smoke-test: login, send message, upload image, `/admin/`. + +## Registration control + +`EMAIL_VERIFY=true` gates signup on a verified email. To freeze signups entirely once +the org is onboarded, add `DISABLE_USER_REGISTER=true` to `docker-compose.env` and +`docker compose up -d` (recreates app containers). + +## Backups + +Nightly cron on the VPS (`/etc/cron.d/tailchat-backup`) dumps MongoDB and tars the +MinIO volume into `/root/backups/tailchat/`, keeping 14 days. + +Restore: stop the stack, `mongorestore` the dump into the `mongo` container, untar the +MinIO volume back into `tailchat-server_storage`, start the stack. + +## Ports exposed publicly + +Only Caddy's 80/443 (+443/udp for HTTP/3). Docker-published ports bypass the host's +default-deny nftables — never add a `ports:` mapping to any other service unless it is +bound to `127.0.0.1` or the tailnet IP. + +## Admin panel + +`https://$CHAT_DOMAIN/admin/` (trailing slash required). Credentials: `ADMIN_USER` / +`ADMIN_PASS` from `docker-compose.env`. The newer `admin-next` app is NOT deployed — +it postdates the 1.11.12 image. diff --git a/deploy/stc/docker-compose.env.example b/deploy/stc/docker-compose.env.example new file mode 100644 index 00000000000..1d94ea452e4 --- /dev/null +++ b/deploy/stc/docker-compose.env.example @@ -0,0 +1,41 @@ +# STC Tailchat environment — copy to docker-compose.env on the VPS, chmod 600. +# Then: ln -s docker-compose.env .env +# (compose reads .env for ${...} interpolation; containers read docker-compose.env) +# NEVER commit the real file. + +LOGGER=true +LOGLEVEL=info +SERVICEDIR=services + +TRANSPORTER=redis://redis:6379 +REDIS_URL=redis://redis:6379 +MONGO_URL=mongodb://mongo/tailchat + +# Public hostname. CHAT_DOMAIN drives Caddy TLS; API_URL must be https://. +CHAT_DOMAIN=chat.example.com +API_URL=https://chat.example.com + +# JWT signing key — generate: openssl rand -hex 24. Never leave default. +SECRET= + +# Registration requires a verified email address. +EMAIL_VERIFY=true +# Example: SMTP_SENDER="STC Chat" chat@stcbas.com +SMTP_SENDER= +# Example: SMTP_URI=smtp://user:password@smtp.example.com/?pool=true +SMTP_URI= + +# MinIO file storage (internal). MINIO_USER/PASS are also injected as the +# MinIO root credentials via compose interpolation — one source of truth. +MINIO_URL=minio:9000 +MINIO_USER=tailchat +MINIO_PASS= + +# 25 MB upload limit (default is 1 MB) +FILE_LIMIT=26214400 + +PROMETHEUS=1 + +# Admin panel at /admin/ +ADMIN_USER=stcadmin +ADMIN_PASS= diff --git a/deploy/stc/docker-compose.yml b/deploy/stc/docker-compose.yml new file mode 100644 index 00000000000..fe9c7bc5289 --- /dev/null +++ b/deploy/stc/docker-compose.yml @@ -0,0 +1,155 @@ +# STC Worldwide production deployment — chat. on netcup-vps-01 +# Adapted from /docker-compose.yml + /docker/admin.yml upstream. +# Differences from upstream: +# - pinned published image instead of local build +# - Caddy terminates public TLS (80/443); traefik stays internal (loopback debug bind only) +# - MinIO root credentials come from .env (symlink to docker-compose.env), not hard-coded +# - legacy admin only; admin-next postdates the 1.11.12 image and is not in it +# +# The ONLY publicly exposed ports are Caddy's 80/443. Docker published ports +# bypass the host's default-deny nftables, so every other service must stay +# unpublished (loopback binds are fine). + +services: + service-core: + image: moonrailgun/tailchat:1.11.12 + restart: unless-stopped + env_file: docker-compose.env + environment: + SERVICES: core/gateway,core/user/*.service.js,core/group/*.service.js,core/chat/*.service.js,core/file,core/plugin/registry,core/config + PORT: 3000 + depends_on: + - mongo + - redis + - minio + healthcheck: + test: ["CMD", "wget", "localhost:3000/health", "-q", "-O", "-"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 15s + labels: + - "traefik.enable=true" + - "traefik.http.routers.api-gw.rule=PathPrefix(`/`)" + - "traefik.http.services.api-gw.loadbalancer.server.port=3000" + networks: + - internal + + service-openapi: + image: moonrailgun/tailchat:1.11.12 + restart: unless-stopped + env_file: docker-compose.env + environment: + SERVICES: openapi/app,openapi/bot,openapi/integration,openapi/oidc/oidc + OPENAPI_PORT: 3003 + OPENAPI_UNDER_PROXY: "true" + depends_on: + - mongo + - redis + - minio + labels: + - "traefik.enable=true" + - "traefik.http.routers.openapi-oidc.rule=PathPrefix(`/open`)" + - "traefik.http.services.openapi-oidc.loadbalancer.server.port=3003" + networks: + - internal + + service-all-plugins: + image: moonrailgun/tailchat:1.11.12 + restart: unless-stopped + env_file: docker-compose.env + environment: + SERVICEDIR: plugins + depends_on: + - mongo + - redis + - minio + networks: + - internal + + tailchat-admin: + image: moonrailgun/tailchat:1.11.12 + restart: unless-stopped + env_file: docker-compose.env + depends_on: + - mongo + - redis + labels: + - "traefik.enable=true" + - "traefik.http.routers.admin.rule=PathPrefix(`/admin`)" + - "traefik.http.routers.admin.priority=50" + - "traefik.http.services.admin.loadbalancer.server.port=3000" + networks: + - internal + command: pnpm start:admin + + mongo: + image: mongo:4 + restart: on-failure + volumes: + - data:/data/db + networks: + - internal + + redis: + image: redis:alpine + restart: on-failure + networks: + - internal + + minio: + image: minio/minio + restart: on-failure + networks: + - internal + environment: + MINIO_ROOT_USER: ${MINIO_USER:?set MINIO_USER in .env (symlink docker-compose.env)} + MINIO_ROOT_PASSWORD: ${MINIO_PASS:?set MINIO_PASS in .env (symlink docker-compose.env)} + volumes: + - storage:/data + command: minio server /data --console-address ":9001" + + traefik: + image: traefik:v3.7.8 + restart: unless-stopped + command: + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entryPoints.web.address=:80" + # Only Caddy can reach this entrypoint (internal network); headers it + # forwards are trustworthy in this topology. + - "--entryPoints.web.forwardedHeaders.insecure" + ports: + - 127.0.0.1:11000:80 # host-local debug access only + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - internal + + caddy: + image: caddy:2-alpine + restart: unless-stopped + depends_on: + - traefik + ports: + - 80:80 + - 443:443 + - 443:443/udp + environment: + CHAT_DOMAIN: ${CHAT_DOMAIN:?set CHAT_DOMAIN in .env (symlink docker-compose.env)} + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + networks: + - internal + +networks: + internal: + name: tailchat-internal + +volumes: + data: + storage: + caddy_data: + caddy_config: