33 Commits
Author SHA1 Message Date
zwitschi 47740e3f4a debug: adding debug output to workflow
CI / test (push) Successful in 1m36s
CI / docker (push) Successful in 3m4s
CI / release (push) Skipped
2026-08-04 18:04:37 +02:00
zwitschi 693b4a42f6 fix: improve CI workflows and README documentation 2026-08-04 16:58:05 +02:00
zwitschi 8f1e227d50 fix: Dockerfile parameter
CI / test (push) Successful in 1m35s
CI / docker (push) Successful in 3m12s
CI / release (push) Skipped
2026-08-04 13:44:00 +02:00
zwitschi 0891606321 fix: context
CI / test (push) Successful in 1m35s
CI / docker (push) Successful in 2m46s
CI / release (push) Skipped
2026-08-04 13:17:13 +02:00
zwitschi c6f28ff7af fix: rearrange steps again
CI / test (push) Successful in 1m35s
CI / docker (push) Failing after 30s
CI / release (push) Skipped
2026-08-04 13:00:23 +02:00
zwitschi 072a91961f fix: rearrange steps again
CI / test (push) Successful in 6m3s
CI / docker (push) Failing after 28s
CI / release (push) Skipped
2026-08-04 12:51:34 +02:00
zwitschi 7184240dd4 fix: move copy step up
CI / test (push) Successful in 1m34s
CI / docker (push) Failing after 15s
CI / release (push) Skipped
2026-08-04 12:46:47 +02:00
zwitschi 9cd0419c0c fix: only one media folder
CI / test (push) Successful in 6m5s
CI / docker (push) Failing after 1m52s
CI / release (push) Skipped
2026-08-04 12:37:11 +02:00
zwitschi d3266f8feb fix: reorder copy steps
CI / test (push) Successful in 1m36s
CI / docker (push) Failing after 29s
CI / release (push) Skipped
2026-08-04 12:33:05 +02:00
zwitschi b532507374 fix: adjust copy
CI / test (push) Successful in 1m32s
CI / docker (push) Failing after 15s
CI / release (push) Skipped
2026-08-04 12:27:44 +02:00
zwitschi 5c1a8cbe4f reorder copy steps
CI / test (push) Successful in 1m40s
CI / docker (push) Failing after 24s
CI / release (push) Skipped
2026-08-04 12:19:17 +02:00
zwitschi 502fefff33 fix: move demodata into playout build context, revert root media changes
CI / test (push) Successful in 1m30s
CI / docker (push) Failing after 30s
CI / release (push) Skipped
2026-08-04 12:10:38 +02:00
zwitschi bc3a950d99 fix: use repo root as build context for playout to include demodata
CI / test (push) Failing after 13s
CI / docker (push) Skipped
CI / release (push) Skipped
2026-08-04 12:01:03 +02:00
zwitschi 1af9e59a56 rename data directory for initial demo data
CI / test (push) Failing after 2m26s
CI / docker (push) Skipped
CI / release (push) Skipped
2026-08-04 11:54:44 +02:00
zwitschi 13d25a7ae0 remove empty line
CI / test (push) Failing after 16s
CI / docker (push) Skipped
CI / release (push) Skipped
2026-08-04 11:49:49 +02:00
zwitschi fc799da000 copy full folder instead of playlist.txt
CI / test (push) Failing after 2m25s
CI / docker (push) Skipped
CI / release (push) Skipped
2026-08-04 11:46:21 +02:00
zwitschi 2b091adce8 move demo-data to its own folder
CI / test (push) Failing after 2m24s
CI / docker (push) Skipped
CI / release (push) Skipped
2026-08-04 11:40:22 +02:00
zwitschi c54210434e feat: bundle demo media and playlist in playout image for out-of-the-box HLS
CI / test (push) Failing after 2m30s
CI / docker (push) Skipped
CI / release (push) Skipped
2026-08-04 11:34:20 +02:00
zwitschi fcb68cfd8d fix: add curl to chat runtime image for healthcheck
CI / test (push) Successful in 1m39s
CI / docker (push) Successful in 3m9s
CI / release (push) Skipped
2026-08-04 11:00:49 +02:00
zwitschi cdecdd2d4a fix: add curl to chat runtime image for healthcheck 2026-08-04 11:00:00 +02:00
zwitschi 47ffae5dea fix: make playlist volume rw so playout can create demo playlist
CD / deploy (push) Successful in 37s
CI / test (push) Successful in 6m2s
CI / docker (push) Successful in 2m54s
CI / release (push) Skipped
2026-08-04 10:30:02 +02:00
zwitschi 43c1a31660 fix: nginx build context, playout health check retry loop
CI / test (push) Successful in 1m39s
CI / docker (push) Successful in 3m4s
CI / release (push) Skipped
CD / deploy (push) Failing after 1m17s
2026-08-04 10:21:18 +02:00
zwitschi afb9a41404 fix: use docker exec for playout health check in CD workflow
CD / deploy (push) Failing after 23s
CI / test (push) Successful in 6m6s
CI / docker (push) Failing after 2m42s
CI / release (push) Skipped
2026-08-04 10:12:38 +02:00
zwitschi 1f2082e0fb fix: use named volume for prometheus config, seed in CD workflow
CD / deploy (push) Failing after 26s
CI / test (push) Successful in 6m2s
CI / docker (push) Failing after 2m41s
CI / release (push) Skipped
2026-08-04 10:05:59 +02:00
zwitschi 7ce6a24a77 reinstated indentation for env
CD / deploy (push) Failing after 25s
CI / test (push) Successful in 5m59s
CI / docker (push) Failing after 2m43s
CI / release (push) Skipped
2026-08-04 09:45:58 +02:00
zwitschi 450a93150c ci: fix indentation in .env generation step of CD workflow
CI / test (push) Successful in 6m4s
CI / docker (push) Failing after 3m6s
CI / release (push) Skipped
2026-08-04 09:08:52 +02:00
zwitschi dc53bce6c9 ci: remove GitHub Actions cache configuration from Docker build steps
CD / deploy (push) Failing after 1m8s
CI / test (push) Successful in 7m49s
CI / docker (push) Failing after 2m49s
CI / release (push) Skipped
2026-08-04 08:59:45 +02:00
zwitschi 32f2b7c7ab ci: use ubuntu-latest runner for docker and release jobs
CD / deploy (push) Failing after 1m54s
CI / test (push) Successful in 1m59s
CI / docker (push) Failing after 3m41s
CI / release (push) Skipped
2026-08-04 08:24:12 +02:00
zwitschi 72422f4336 feat: add Prometheus and Grafana configurations and document Alpine Docker setup
CD / deploy (push) Failing after 34s
CI / test (push) Successful in 1m40s
CI / docker (push) Canceled after 0s
CI / release (push) Canceled after 0s
2026-08-03 22:02:09 +02:00
zwitschi 78508ed16e ci: configure CD workflow to deploy to remote Docker host and document test environment
CI / test (push) Successful in 1m33s
CD / deploy (push) Failing after 15s
CI / docker (push) Canceled after 0s
CI / release (push) Canceled after 0s
2026-08-03 19:57:58 +02:00
zwitschi 8fd80c1a4c ci: generate production .env dynamically in CD workflow and update secrets documentation
CI / test (push) Successful in 1m59s
CD / deploy (push) Failing after 16s
CI / docker (push) Canceled after 0s
CI / release (push) Canceled after 0s
2026-08-03 19:37:35 +02:00
zwitschi 5fc9bc6191 build: integrate frontend build into Nginx Dockerfile, remove separate frontend image
CD / deploy (push) Failing after 1m38s
CI / test (push) Successful in 2m7s
CI / docker (push) Canceled after 0s
CI / release (push) Canceled after 0s
2026-08-03 19:17:53 +02:00
zwitschi 3c2b29ad26 refactor: rename Gitea environment variables to CI registry prefix
CD / deploy (push) Failing after 17s
CI / test (push) Successful in 1m36s
CI / docker (push) Canceled after 0s
CI / release (push) Canceled after 0s
update SETUP.md and .env.example
2026-08-03 19:02:30 +02:00
20 changed files with 519 additions and 150 deletions

No files matched your search

+6 -9
View File
@@ -31,10 +31,12 @@ ADMIN_SESSION_TTL_HOURS=12
# VITE_PUBLIC_HLS_URL=http://localhost/hls/playlist.m3u8 # VITE_PUBLIC_HLS_URL=http://localhost/hls/playlist.m3u8
# Gitea Container Registry (for self-hosted Docker images) # Gitea Container Registry (for self-hosted Docker images)
GITEA_REGISTRY_URL=git.allucanget.biz CI_REGISTRY_URL=git.allucanget.biz
GITEA_USERNAME=your-gitea-username CI_REGISTRY_USER=your-gitea-username
GITEA_TOKEN=your-gitea-token CI_REGISTRY_TOKEN=your-gitea-token
GITEA_REGISTRY_NAMESPACE=zwitschi/linearvoid CI_REGISTRY_NAMESPACE=zwitschi/linearvoid
# Gitea API access (optional, for CI/release automation)
CI_SERVER_URL=https://git.allucanget.biz
# Chat / Application Database (compose-safe PostgreSQL by default, SQLite optional) # Chat / Application Database (compose-safe PostgreSQL by default, SQLite optional)
DATABASE_URL=postgres://linearvoid:linearvoid@db:5432/linearvoid?sslmode=disable DATABASE_URL=postgres://linearvoid:linearvoid@db:5432/linearvoid?sslmode=disable
@@ -62,8 +64,3 @@ GRAFANA_PORT=3001
# Development flags # Development flags
NODE_ENV=development NODE_ENV=development
GO_ENV=development GO_ENV=development
# Gitea Configuration
GITEA_SERVER_URL=https://git.allucanget.biz
GITEA_TOKEN=your-gitea-token
GITEA_USERNAME=your-gitea-username
+61 -12
View File
@@ -1,13 +1,17 @@
name: CD name: CD
on: on:
push: workflow_run:
workflows: ["CI"]
types:
- completed
branches: [main] branches: [main]
workflow_dispatch: workflow_dispatch:
jobs: jobs:
deploy: deploy:
runs-on: [ubuntu-latest] runs-on: [ubuntu-latest]
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
environment: production environment: production
steps: steps:
- name: Checkout code - name: Checkout code
@@ -22,28 +26,73 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: git.allucanget.biz registry: git.allucanget.biz
username: ${{ secrets.GITEA_USERNAME }} username: ${{ secrets.CI_REGISTRY_USER }}
password: ${{ secrets.GITEA_TOKEN }} password: ${{ secrets.CI_REGISTRY_TOKEN }}
- name: Pull latest images - name: Prepare environment
run: | run: |
docker pull git.allucanget.biz/zwitschi/linearvoid/playout:latest || true cat > .env << 'EOF'
docker pull git.allucanget.biz/zwitschi/linearvoid/chat:latest || true DATABASE_URL="${{ secrets.DATABASE_URL }}"
docker pull git.allucanget.biz/zwitschi/linearvoid/frontend:latest || true REDIS_URL=redis://redis:6379/0
docker pull git.allucanget.biz/zwitschi/linearvoid/nginx:latest || true PLAYLIST_PATH=/app/playlist/playlist.txt
HLS_SEGMENT_DURATION=6
HLS_WINDOW_SIZE=24
LOG_LEVEL=info
ADMIN_USERNAME="${{ secrets.ADMIN_USERNAME }}"
ADMIN_PASSWORD="${{ secrets.ADMIN_PASSWORD }}"
ADMIN_SESSION_TTL_HOURS=12
NGINX_PORT=80
EOF
- name: Seed Prometheus config
run: |
mkdir -p monitoring/prometheus
cat > monitoring/prometheus/prometheus.yml << 'EOF'
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'linearvoid'
static_configs:
- targets: ['playout:8081', 'chat:3000', 'nginx:80']
EOF
- name: Deploy with docker-compose - name: Deploy with docker-compose
env:
DOCKER_HOST: tcp://192.168.88.40:2375
run: | run: |
docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d --remove-orphans docker compose -f docker-compose.prod.yml up -d --remove-orphans
- name: Health check - name: Health check
env:
DOCKER_HOST: tcp://192.168.88.40:2375
run: | run: |
sleep 10 # Wait for playout to stabilize (it may restart during init)
curl -f http://localhost:8080/health || exit 1 sleep 20
curl -f http://localhost:3000/health || exit 1 curl -f http://192.168.88.40:3000/health || exit 1
curl -f http://localhost:8081/health || exit 1 # Retry playout health check since it may still be initializing
for i in $(seq 1 6); do
if docker exec linearvoid-playout curl -sf http://localhost:8081/health; then
echo "Playout is healthy"
exit 0
fi
echo "Playout not ready yet (attempt $i/6), waiting 5s..."
sleep 5
done
echo "Playout failed to become healthy after 6 attempts"
docker logs linearvoid-playout --tail 50
exit 1
curl -f http://192.168.88.40:3000/health || exit 1
docker exec linearvoid-playout curl -sf http://localhost:8081/health || exit 1
- name: Cleanup old images - name: Cleanup old images
env:
DOCKER_HOST: tcp://192.168.88.40:2375
run: | run: |
docker image prune -f --filter "until=24h" docker image prune -f --filter "until=24h"
+23 -39
View File
@@ -48,7 +48,7 @@ jobs:
docker: docker:
needs: test needs: test
runs-on: [self-hosted, linux, docker] runs-on: [ubuntu-latest]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps: steps:
- name: Checkout code - name: Checkout code
@@ -63,8 +63,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: git.allucanget.biz registry: git.allucanget.biz
username: ${{ secrets.GITEA_USERNAME }} username: ${{ secrets.CI_REGISTRY_USER }}
password: ${{ secrets.GITEA_TOKEN }} password: ${{ secrets.CI_REGISTRY_TOKEN }}
- name: Extract metadata for Playout - name: Extract metadata for Playout
id: meta-playout id: meta-playout
@@ -82,11 +82,10 @@ jobs:
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: ./backend/playout context: ./backend/playout
file: ./backend/playout/Dockerfile
push: true push: true
tags: ${{ steps.meta-playout.outputs.tags }} tags: ${{ steps.meta-playout.outputs.tags }}
labels: ${{ steps.meta-playout.outputs.labels }} labels: ${{ steps.meta-playout.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Extract metadata for Chat - name: Extract metadata for Chat
id: meta-chat id: meta-chat
@@ -107,30 +106,6 @@ jobs:
push: true push: true
tags: ${{ steps.meta-chat.outputs.tags }} tags: ${{ steps.meta-chat.outputs.tags }}
labels: ${{ steps.meta-chat.outputs.labels }} labels: ${{ steps.meta-chat.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Extract metadata for Frontend
id: meta-frontend
uses: docker/metadata-action@v5
with:
images: git.allucanget.biz/zwitschi/linearvoid/frontend
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Frontend image
uses: docker/build-push-action@v5
with:
context: ./frontend
push: true
tags: ${{ steps.meta-frontend.outputs.tags }}
labels: ${{ steps.meta-frontend.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Extract metadata for Nginx - name: Extract metadata for Nginx
id: meta-nginx id: meta-nginx
@@ -145,18 +120,27 @@ jobs:
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Nginx image - name: Build and push Nginx image
uses: docker/build-push-action@v5 run: |
with: TAGS="${{ steps.meta-nginx.outputs.tags }}"
context: ./infrastructure/nginx LABELS="${{ steps.meta-nginx.outputs.labels }}"
push: true TAG_ARGS=""
tags: ${{ steps.meta-nginx.outputs.tags }} for tag in $TAGS; do
labels: ${{ steps.meta-nginx.outputs.labels }} TAG_ARGS="$TAG_ARGS --tag $tag"
cache-from: type=gha done
cache-to: type=gha,mode=max LABEL_ARGS=""
while IFS= read -r label; do
[ -n "$label" ] && LABEL_ARGS="$LABEL_ARGS --label $label"
done <<< "$LABELS"
docker buildx build \
--push \
--file infrastructure/nginx/Dockerfile \
$TAG_ARGS \
$LABEL_ARGS \
.
release: release:
needs: docker needs: docker
runs-on: [self-hosted, linux, docker] runs-on: [ubuntu-latest]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
steps: steps:
- name: Checkout code - name: Checkout code
@@ -169,4 +153,4 @@ jobs:
with: with:
generate_release_notes: true generate_release_notes: true
env: env:
GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }} GITHUB_TOKEN: ${{ secrets.CI_REGISTRY_TOKEN }}
+34 -17
View File
@@ -13,7 +13,6 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GITEA_TOKEN }}
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@@ -39,20 +38,38 @@ jobs:
cd backend/playout && go get -u ./... && go mod tidy cd backend/playout && go get -u ./... && go mod tidy
cd ../chat && go get -u ./... && go mod tidy cd ../chat && go get -u ./... && go mod tidy
- name: Create Pull Request - name: Commit and push changes
uses: peter-evans/create-pull-request@v5 run: |
with: git config user.name "linearvoid-bot"
token: ${{ secrets.GITEA_TOKEN }} git config user.email "bot@linearvoid.local"
commit-message: "chore: update dependencies" git add frontend/package*.json backend/playout/go.* backend/chat/go.*
title: "chore: weekly dependency updates" if git diff --cached --quiet; then
body: | echo "No dependency changes detected."
Automated weekly dependency updates. exit 0
fi
git commit -m "chore: update dependencies"
git push origin HEAD:dependency-updates --force
This PR updates: - name: Create Pull Request via Gitea API
- Frontend npm dependencies run: |
- Go module dependencies RESPONSE=$(curl -s -o /tmp/pr-response.json -w "%{http_code}" \
-X POST "$CI_SERVER_URL/api/v1/repos/zwitschi/linearvoid/pulls" \
Please review the changes and ensure tests pass before merging. -H "Authorization: token ${{ secrets.CI_REGISTRY_TOKEN }}" \
branch: dependency-updates -H "Content-Type: application/json" \
delete-branch: true -d '{
labels: dependencies, automated "title": "chore: weekly dependency updates",
"head": "dependency-updates",
"base": "main",
"body": "Automated weekly dependency updates.\n\nThis PR updates:\n- Frontend npm dependencies\n- Go module dependencies\n\nPlease review the changes and ensure tests pass before merging."
}')
if [ "$RESPONSE" -eq 201 ]; then
PR_URL=$(cat /tmp/pr-response.json | jq -r '.html_url')
echo "PR created: $PR_URL"
elif [ "$RESPONSE" -eq 409 ]; then
echo "PR already exists. Pushing updated commits..."
git push origin dependency-updates --force
else
echo "Error creating PR (HTTP $RESPONSE):"
cat /tmp/pr-response.json
exit 1
fi
+27 -30
View File
@@ -27,17 +27,32 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: git.allucanget.biz registry: git.allucanget.biz
username: ${{ secrets.GITEA_USERNAME }} username: ${{ secrets.CI_REGISTRY_USER }}
password: ${{ secrets.GITEA_TOKEN }} password: ${{ secrets.CI_REGISTRY_TOKEN }}
- name: Extract version - name: Extract version
id: version id: version
run: | run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT VERSION="${{ github.event.inputs.version }}"
else else
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT VERSION="${GITHUB_REF#refs/tags/}"
fi fi
if [[ -z "$VERSION" ]]; then
echo "::error::VERSION is empty. Provide a version via workflow_dispatch input or push a tag."
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Releasing version: $VERSION"
- name: Print version to gitea runner log
run: |
echo "Releasing version: ${{ steps.version.outputs.version }}"
echo "===DEBUG==="
echo "VERSION: $VERSION"
echo "GITHUB_REF: $GITHUB_REF"
echo "steps.version.outputs.version: ${{ steps.version.outputs.version }}"
echo "github.event_name: ${{ github.event_name }}"
- name: Extract metadata for Playout - name: Extract metadata for Playout
id: meta-playout id: meta-playout
@@ -75,24 +90,6 @@ jobs:
tags: ${{ steps.meta-chat.outputs.tags }} tags: ${{ steps.meta-chat.outputs.tags }}
labels: ${{ steps.meta-chat.outputs.labels }} labels: ${{ steps.meta-chat.outputs.labels }}
- name: Extract metadata for Frontend
id: meta-frontend
uses: docker/metadata-action@v5
with:
images: git.allucanget.biz/zwitschi/linearvoid/frontend
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Build and push Frontend image
uses: docker/build-push-action@v5
with:
context: ./frontend
push: true
tags: ${{ steps.meta-frontend.outputs.tags }}
labels: ${{ steps.meta-frontend.outputs.labels }}
- name: Extract metadata for Nginx - name: Extract metadata for Nginx
id: meta-nginx id: meta-nginx
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
@@ -106,15 +103,15 @@ jobs:
- name: Build and push Nginx image - name: Build and push Nginx image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: ./infrastructure/nginx context: .
file: infrastructure/nginx/Dockerfile
push: true push: true
tags: ${{ steps.meta-nginx.outputs.tags }} tags: ${{ steps.meta-nginx.outputs.tags }}
labels: ${{ steps.meta-nginx.outputs.labels }} labels: ${{ steps.meta-nginx.outputs.labels }}
- name: Create Release - name: Create Release on Gitea
uses: softprops/action-gh-release@v1 run: |
with: curl -X POST "https://git.allucanget.biz/api/v1/repos/zwitschi/linearvoid/releases" \
generate_release_notes: true -H "Authorization: token ${{ secrets.CI_REGISTRY_TOKEN }}" \
tag_name: ${{ steps.version.outputs.version }} -H "Content-Type: application/json" \
env: -d "{\"tag_name\":\"${{ steps.version.outputs.version }}\",\"name\":\"${{ steps.version.outputs.version }}\",\"body\":\"Release ${{ steps.version.outputs.version }}\n\nSee [CHANGELOG.md](https://git.allucanget.biz/zwitschi/linearvoid/src/branch/main/CHANGELOG.md) for details.\",\"draft\":false,\"prerelease\":false}"
GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }}
+10
View File
@@ -9,3 +9,13 @@ Agents instructions for the linearvoid project.
- Update documentation first before implementing changes to ensure traceability of decisions. - Update documentation first before implementing changes to ensure traceability of decisions.
- Plan any task in the `TODO.md` file with clear steps and mark them as completed when done. Move completed sections to DONE.md. Write entry in root `CHANGELOG.md` for any significant changes. Avoid using in-memory todo lists or update them frequently to sync from the `TODO.md` file. - Plan any task in the `TODO.md` file with clear steps and mark them as completed when done. Move completed sections to DONE.md. Write entry in root `CHANGELOG.md` for any significant changes. Avoid using in-memory todo lists or update them frequently to sync from the `TODO.md` file.
- Before taking any action except for planning and writing `TODO.md`, always ask user for confirmation, accept `go`, `ok`, `yes`, `next`, or `continue` to proceed. If the user says `no`, `stop`, or `cancel`, abort the action and return to planning. - Before taking any action except for planning and writing `TODO.md`, always ask user for confirmation, accept `go`, `ok`, `yes`, `next`, or `continue` to proceed. If the user says `no`, `stop`, or `cancel`, abort the action and return to planning.
## Security & Secrets Protection
- **Never commit raw secrets or credentials** to the repository.
- **Never print or log raw secrets** in terminal commands or output files.
- **Use Gitea Actions Secrets** for all production credentials and environment variables.
- **Avoid hardcoding sensitive values** in workflow files (`.yml`). Instead, reference them using `${{ secrets.SECRET_NAME }}`.
- **Do not use the `GITEA_` prefix** for custom secrets or environment variables in Gitea, as it is reserved internally and will be silently ignored or returned as empty. Use the `CI_` prefix instead (e.g., `CI_REGISTRY_USER`, `CI_REGISTRY_TOKEN`).
- **Keep `.env` gitignored** and use `.env.example` as the template for local development.
- **For CD deployments**, generate the `.env` file dynamically on the runner using a secure step that populates values from Gitea Actions Secrets.
+3 -4
View File
@@ -50,6 +50,9 @@ data/
*.sqlite *.sqlite
*.sqlite3 *.sqlite3
# Demo data (tracked in backend/playout/demodata/ for out-of-the-box playout)
!backend/playout/demodata/
# Docker # Docker
.dockerignore .dockerignore
**/.dockerignore **/.dockerignore
@@ -75,10 +78,6 @@ Thumbs.db
*.tfstate.backup *.tfstate.backup
.terraform/ .terraform/
# Monitoring & temp
prometheus/
grafana/
# Agent / Tool files # Agent / Tool files
.github/instructions/TODO.md .github/instructions/TODO.md
.github/instructions/DONE.md .github/instructions/DONE.md
+83
View File
@@ -7,6 +7,8 @@
[![Build Status](https://git.allucanget.biz/zwitschi/linearvoid/actions/workflows/ci.yml/badge.svg)](https://git.allucanget.biz/zwitschi/linearvoid/actions) [![Build Status](https://git.allucanget.biz/zwitschi/linearvoid/actions/workflows/ci.yml/badge.svg)](https://git.allucanget.biz/zwitschi/linearvoid/actions)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Gitea](https://img.shields.io/badge/Gitea-1.27.1-blue)](https://git.allucanget.biz) [![Gitea](https://img.shields.io/badge/Gitea-1.27.1-blue)](https://git.allucanget.biz)
[![Version](https://img.shields.io/badge/version-1.0.3-blue)](https://git.allucanget.biz/zwitschi/linearvoid/releases)
[![Release Notes](https://img.shields.io/badge/release-notes-green)](CHANGELOG.md)
## Architecture ## Architecture
@@ -27,6 +29,23 @@ See [`docs/architecture/adr/`](docs/architecture/adr/) for the full set of decis
- Video normalization, monitoring, fallback slate, and easy playlist management. - Video normalization, monitoring, fallback slate, and easy playlist management.
- US-centric performance (Ashburn origin + Cloudflare POPs). - US-centric performance (Ashburn origin + Cloudflare POPs).
## Architecture Diagram
```mermaid
flowchart TD
V[US Viewers] -->|HTTPS| CF[Cloudflare CDN]
CF -->|Cache HIT| R2[Cloudflare R2]
CF -->|Cache MISS| N[Nginx :80]
N -->|/ws| CH[Chat :3000]
N -->|/admin| CH
N -->|/hls| P[Playout :8081]
N -->|/*| FE[Frontend SPA]
CH --> R[(Redis)]
CH --> DB[(PostgreSQL)]
P -->|ffmpeg| HLS[/data/hls/]
P -->|sync| R2
```
## Project Structure ## Project Structure
```text ```text
@@ -97,9 +116,16 @@ This structure directly reflects the building blocks, runtime flows, deployment
git clone https://git.allucanget.biz/zwitschi/linearvoid.git git clone https://git.allucanget.biz/zwitschi/linearvoid.git
cd linearvoid cd linearvoid
cp .env.example .env cp .env.example .env
# Edit .env with your R2 credentials and domain settings
docker compose up --build docker compose up --build
``` ```
Then open:
- **Viewer**: [https://linearvoid.tv](https://linearvoid.tv) (or `http://localhost`)
- **Admin**: [https://linearvoid.tv/admin](https://linearvoid.tv/admin) (or `http://localhost/admin`)
- **Metrics**: [https://linearvoid.tv/metrics](https://linearvoid.tv/metrics)
Full setup guide: [`SETUP.md`](SETUP.md). Full setup guide: [`SETUP.md`](SETUP.md).
See the [architecture docs](docs/architecture/README.md) for detailed architecture and ADRs. See the [architecture docs](docs/architecture/README.md) for detailed architecture and ADRs.
@@ -111,6 +137,50 @@ See the [architecture docs](docs/architecture/README.md) for detailed architectu
- Normalize all source videos before adding to playlist (see ADR-001 for exact codec/fps/keyframe requirements). - Normalize all source videos before adding to playlist (see ADR-001 for exact codec/fps/keyframe requirements).
- Monitor cache hit ratio and R2 operations closely — they determine real cost. - Monitor cache hit ratio and R2 operations closely — they determine real cost.
### Environment Variables
| Variable | Default | Description |
| ---------------------- | ----------------------------------------------- | -------------------------------- |
| `R2_ACCESS_KEY_ID` | — | Cloudflare R2 access key |
| `R2_SECRET_ACCESS_KEY` | — | Cloudflare R2 secret key |
| `R2_BUCKET_NAME` | `linearvoid-media` | R2 bucket for HLS segments |
| `DATABASE_URL` | `postgres://...@db:5432/...` | Chat DB (Postgres or SQLite) |
| `REDIS_URL` | `redis://redis:6379/0` | Redis for presence/rate limiting |
| `VITE_PUBLIC_APP_URL` | `https://linearvoid.tv` | Public viewer URL |
| `VITE_PUBLIC_WS_URL` | `wss://linearvoid.tv/ws` | WebSocket endpoint |
| `VITE_PUBLIC_HLS_URL` | `https://media.linearvoid.tv/hls/playlist.m3u8` | HLS playlist URL |
| `ADMIN_USERNAME` | `admin` | Admin panel username |
| `ADMIN_PASSWORD` | — | Admin panel password |
| `HLS_SEGMENT_DURATION` | `6` | HLS segment duration in seconds |
| `HLS_WINDOW_SIZE` | `24` | HLS playlist window size |
Full reference: [`.env.example`](.env.example).
### Troubleshooting
| Problem | Check |
| ------------------------ | ------------------------------------------------------------------------------------- |
| **Playout not starting** | Verify playlist exists in `data/playlist/playlist.txt` and media files are accessible |
| **HLS not playing** | Check `docker compose logs playout` for ffmpeg errors; verify `data/hls/` permissions |
| **Chat not connecting** | Ensure Redis is running: `docker compose logs redis`; check `DATABASE_URL` |
| **Admin panel 401** | Verify `ADMIN_USERNAME`/`ADMIN_PASSWORD` in `.env` match login credentials |
| **Docker build fails** | Run `docker compose build --no-cache`; check `backend/playout/.dockerignore` |
| **R2 upload failures** | Verify `R2_ACCESS_KEY_ID` and `R2_SECRET_ACCESS_KEY`; check bucket CORS settings |
### API Endpoints
| Service | Endpoint | Description |
| ----------- | ------------------------ | -------------------------- |
| **Chat** | `GET /health` | Health check |
| **Chat** | `GET /metrics` | Prometheus metrics |
| **Chat** | `GET /ws` | WebSocket upgrade for chat |
| **Chat** | `GET /api/admin/*` | Admin API (auth required) |
| **Chat** | `POST /api/admin/login` | Admin login |
| **Playout** | `GET /health` | Health check (port 8081) |
| **Nginx** | `GET /` | Viewer SPA |
| **Nginx** | `GET /admin/` | Admin panel |
| **Nginx** | `GET /hls/playlist.m3u8` | HLS manifest |
## Architecture Documentation ## Architecture Documentation
The architecture is fully documented using the [arc42 template](docs/architecture/README.md) (with populated sections, Mermaid context and building-block diagrams, deployment view, cross-cutting concepts, quality goals, risks, and glossary). The architecture is fully documented using the [arc42 template](docs/architecture/README.md) (with populated sections, Mermaid context and building-block diagrams, deployment view, cross-cutting concepts, quality goals, risks, and glossary).
@@ -141,4 +211,17 @@ Contributions, issues, and further evolution (LL-HLS, ABR, multi-channel, E2EE)
**Container Registry**: `git.allucanget.biz/zwitschi/linearvoid/<service>:latest` **Container Registry**: `git.allucanget.biz/zwitschi/linearvoid/<service>:latest`
## Contributing
Contributions are welcome! See [`CONTRIBUTING.md`](CONTRIBUTING.md) for guidelines. Quick overview:
1. Read the [architecture docs](docs/architecture/README.md) and ADRs first.
2. Plan tasks in `.github/instructions/TODO.md`.
3. Open PRs on [Gitea](https://git.allucanget.biz/zwitschi/linearvoid).
4. CI validates builds and tests automatically.
Since this a linear project, we also rely on a linear development and release model: Only main branch is used for releases, and all features are merged into main after review and CI validation. Releases are tagged with semantic versioning (v1.0.3, v1.0.4, etc.) and automatically built/pushed to the container registry.
[MIT License](LICENSE)
The project is ready for use. See [`SETUP.md`](SETUP.md) for detailed installation instructions. The project is ready for use. See [`SETUP.md`](SETUP.md) for detailed installation instructions.
+129 -18
View File
@@ -12,6 +12,13 @@ This guide covers prerequisites, environment setup, running the application loca
- **Git**: For cloning and version control. - **Git**: For cloning and version control.
- **Cloudflare Account**: For R2 storage and CDN (free tier sufficient for most use cases). - **Cloudflare Account**: For R2 storage and CDN (free tier sufficient for most use cases).
- **Gitea Account**: Required if you want to push images to the self-hosted registry or use the repository's CI/CD flow. - **Gitea Account**: Required if you want to push images to the self-hosted registry or use the repository's CI/CD flow.
- Set up the required Actions secrets in Gitea at: [https://git.allucanget.biz/zwitschi/linearvoid/settings/actions/secrets](https://git.allucanget.biz/zwitschi/linearvoid/settings/actions/secrets)
- Add the following secrets:
- `CI_REGISTRY_USER`: Your Gitea username (e.g., `zwitschi`)
- `CI_REGISTRY_TOKEN`: A Gitea Personal Access Token with `write:packages` scope
- `ADMIN_USERNAME`: The administrator username for the production admin panel (e.g., `admin`)
- `ADMIN_PASSWORD`: A strong password for the production admin panel
- `DATABASE_URL`: The production database connection string (e.g., `postgres://linearvoid:<password>@db:5432/linearvoid?sslmode=disable`)
- **Hetzner Cloud Account** (optional for production): For the US-East VPS (Ashburn, VA recommended per ADR-001). - **Hetzner Cloud Account** (optional for production): For the US-East VPS (Ashburn, VA recommended per ADR-001).
## Development Prerequisites ## Development Prerequisites
@@ -70,10 +77,12 @@ This guide covers prerequisites, environment setup, running the application loca
# VITE_PUBLIC_HLS_URL=http://localhost/hls/playlist.m3u8 # VITE_PUBLIC_HLS_URL=http://localhost/hls/playlist.m3u8
# Gitea Container Registry (for self-hosted Docker images) # Gitea Container Registry (for self-hosted Docker images)
GITEA_REGISTRY_URL=git.allucanget.biz CI_REGISTRY_URL=git.allucanget.biz
GITEA_USERNAME=your-gitea-username CI_REGISTRY_USER=your-gitea-username
GITEA_TOKEN=your-gitea-token CI_REGISTRY_TOKEN=your-gitea-token
GITEA_REGISTRY_NAMESPACE=zwitschi/linearvoid CI_REGISTRY_NAMESPACE=zwitschi/linearvoid
# Gitea API access (optional, for CI/release automation)
CI_SERVER_URL=https://git.allucanget.biz
# Chat / Database # Chat / Database
DATABASE_URL=postgres://linearvoid:linearvoid@db:5432/linearvoid?sslmode=disable DATABASE_URL=postgres://linearvoid:linearvoid@db:5432/linearvoid?sslmode=disable
@@ -99,11 +108,6 @@ This guide covers prerequisites, environment setup, running the application loca
# Development flags # Development flags
NODE_ENV=development NODE_ENV=development
GO_ENV=development GO_ENV=development
# Gitea API access (optional, for CI/release automation)
GITEA_SERVER_URL=https://git.allucanget.biz
GITEA_TOKEN=your-gitea-token
GITEA_USERNAME=your-gitea-username
``` ```
4. **Check local value overrides for Docker Compose**: 4. **Check local value overrides for Docker Compose**:
@@ -162,24 +166,33 @@ This guide covers prerequisites, environment setup, running the application loca
2. **Create an access token**: 2. **Create an access token**:
- Generate a token with the scopes needed for package/registry access and, if applicable, repository automation. - Generate a token with the scopes needed for package/registry access and, if applicable, repository automation.
- Store it as `GITEA_TOKEN` in `.env`. - Store it as `CI_REGISTRY_TOKEN` in `.env`.
3. **Configure registry variables**: 3. **Configure Gitea Actions Secrets**:
- Set `GITEA_REGISTRY_URL` to the Gitea host. - Go to the repository settings page: [https://git.allucanget.biz/zwitschi/linearvoid/settings/actions/secrets](https://git.allucanget.biz/zwitschi/linearvoid/settings/actions/secrets)
- Set `GITEA_REGISTRY_NAMESPACE` to the image namespace, for example `zwitschi/linearvoid`. - Add the following secrets to enable automated CI/CD and production deployment:
- Set `GITEA_USERNAME` to the account that will push or pull images. - `CI_REGISTRY_USER`: Your Gitea username (e.g., `zwitschi`).
- `CI_REGISTRY_TOKEN`: Your Gitea Personal Access Token with `write:packages` scope.
- `ADMIN_USERNAME`: The administrator username for the production admin panel (e.g., `admin`).
- `ADMIN_PASSWORD`: A strong password for the production admin panel.
- `DATABASE_URL`: The production database connection string (e.g., `postgres://linearvoid:<password>@db:5432/linearvoid?sslmode=disable`).
4. **Authenticate Docker to the registry**: 4. **Configure registry variables**:
- Set `CI_REGISTRY_URL` to the Gitea host.
- Set `CI_REGISTRY_NAMESPACE` to the image namespace, for example `zwitschi/linearvoid`.
- Set `CI_REGISTRY_USER` to the account that will push or pull images.
5. **Authenticate Docker to the registry**:
```bash ```bash
docker login git.allucanget.biz docker login git.allucanget.biz
``` ```
5. **Build and publish images when needed**: 6. **Build and publish images when needed**:
- Local development uses `docker-compose.yml`, which builds from source. - Local development uses `docker-compose.yml`, which builds from source.
- Production uses `docker-compose.prod.yml`, which expects the `playout`, `chat`, and `nginx` images to exist in the configured Gitea registry namespace. - Production uses `docker-compose.prod.yml`, which expects the `playout`, `chat`, and `nginx` images to exist in the configured Gitea registry namespace.
6. **Validate image availability**: 7. **Validate image availability**:
- Before a production rollout, verify that the expected `:latest` tags can be pulled from the registry on the target server. - Before a production rollout, verify that the expected `:latest` tags can be pulled from the registry on the target server.
## Running the Application ## Running the Application
@@ -230,6 +243,104 @@ This guide covers prerequisites, environment setup, running the application loca
docker compose down docker compose down
``` ```
### Local Test Environment Deployment (Remote Docker Host)
For testing deployments on a local network server without setting up a full production VPS, you can deploy directly to a remote Docker host (e.g., a home lab server or test machine) using Docker's TCP socket.
#### Test Environment Prerequisites
- A remote server running Docker at IP `192.168.88.40` (running Alpine Linux).
- Docker TCP socket enabled and listening on port `2375` on the remote server.
##### Enabling Docker TCP Socket on Alpine Linux
To allow the Gitea Actions runner (or your local machine) to deploy to the remote Alpine host, you must configure the Docker daemon to listen on the TCP socket:
1. Edit `/etc/conf.d/docker` on the Alpine host:
```bash
vi /etc/conf.d/docker
```
2. Update the `DOCKER_OPTS` line to include both the unix socket and the TCP socket:
```bash
DOCKER_OPTS="-H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375"
```
3. Restart the Docker service:
```bash
rc-service docker restart
```
4. Verify that Docker is listening on port `2375`:
```bash
netstat -tlnp | grep 2375
```
5. Ensure any firewall (like `awall` or `iptables`) on the Alpine host allows incoming traffic on port `2375` from your local network subnet (e.g., `192.168.88.0/24`).
#### Deployment Steps
1. **Configure Docker CLI to use the Remote Host**:
You can point your local Docker CLI to the remote server by setting the `DOCKER_HOST` environment variable:
- **PowerShell (Windows)**:
```powershell
$env:DOCKER_HOST="tcp://192.168.88.40:2375"
```
- **Bash (Linux/macOS)**:
```bash
export DOCKER_HOST="tcp://192.168.88.40:2375"
```
2. **Verify Connection**:
Run `docker info` to confirm your local CLI is communicating with the remote server at `192.168.88.40`:
```bash
docker info
```
The output should show the remote server's hostname and system details.
3. **Deploy the Stack**:
With `DOCKER_HOST` set, any `docker` or `docker compose` commands you run locally will execute directly on the remote server.
- **Deploy from Registry (Production-like)**:
```bash
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d --remove-orphans
```
- **Build and Deploy from Source**:
```bash
docker compose up -d --build
```
4. **Access the Test Environment**:
- Playout Health: `http://192.168.88.40:8081/health`
- Chat Health: `http://192.168.88.40:3000/health`
- Nginx Entrypoint: `http://192.168.88.40:8080` (or the port mapped to `NGINX_PORT` in `.env`)
5. **Reset to Local Docker Host**:
Once testing is complete, clear the `DOCKER_HOST` variable to point back to your local machine:
- **PowerShell**:
```powershell
Remove-Item env:DOCKER_HOST
```
- **Bash**:
```bash
unset DOCKER_HOST
```
### Production Setup Guide ### Production Setup Guide
1. **Prepare the target host**: 1. **Prepare the target host**:
@@ -260,7 +371,7 @@ This guide covers prerequisites, environment setup, running the application loca
``` ```
5. **Publish or verify required images in Gitea**: 5. **Publish or verify required images in Gitea**:
- Ensure the `playout`, `chat`, and `nginx` images exist under `${GITEA_REGISTRY_URL}/${GITEA_REGISTRY_NAMESPACE}`. - Ensure the `playout`, `chat`, and `nginx` images exist under `${CI_REGISTRY_URL}/${CI_REGISTRY_NAMESPACE}`.
- Log in to the registry from the VPS before pulling. - Log in to the registry from the VPS before pulling.
6. **Deploy the production stack**: 6. **Deploy the production stack**:
+1 -1
View File
@@ -9,7 +9,7 @@ RUN go mod tidy
RUN CGO_ENABLED=1 GOOS=linux go build -o /chat-server ./src/main.go RUN CGO_ENABLED=1 GOOS=linux go build -o /chat-server ./src/main.go
FROM alpine:3.20 FROM alpine:3.20
RUN apk --no-cache add ca-certificates RUN apk --no-cache add ca-certificates curl curl
WORKDIR /app WORKDIR /app
COPY --from=builder /chat-server /usr/local/bin/chat-server COPY --from=builder /chat-server /usr/local/bin/chat-server
+8 -10
View File
@@ -16,20 +16,18 @@ RUN apk add --no-cache \
ffmpeg \ ffmpeg \
curl \ curl \
bash bash
# Set working directory for playout
WORKDIR /app WORKDIR /app
# Copy the playout start script and the built playlist manager binary
COPY --from=builder /playlist-manager /app/playlist-manager
COPY start-playout.sh /app/ COPY start-playout.sh /app/
COPY *.sh /app/ # Copy demo data for out-of-the-box playout
COPY demodata/ /app/
RUN chmod +x /app/*.sh /app/playlist-manager # Copy the built playlist manager binary from the builder stage
COPY --from=builder /playlist-manager /app/playlist-manager
# Create directories for playlist, media, and HLS output # Create directories for playlist, media, and HLS output
RUN mkdir -p /app/playlist /app/media /app/hls RUN mkdir -p /app/hls
RUN chmod +x /app/start-playout.sh /app/playlist-manager
# Expose HLS directory (will be served by nginx) # Expose HLS directory (will be served by nginx)
VOLUME ["/app/hls"] VOLUME ["/app/hls"]
# Default command runs the playout loop (which calls the Go validator) # Default command runs the playout loop (which calls the Go validator)
CMD ["./start-playout.sh"] CMD ["./start-playout.sh"]
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
# Linear Void Demo Playlist
file '/app/media/demo1.mp4'
file '/app/media/demo2.mp4'
+3 -2
View File
@@ -6,7 +6,7 @@ services:
env_file: env_file:
- .env - .env
volumes: volumes:
- ./data/playlist:/app/playlist:ro - ./data/playlist:/app/playlist:rw
- ./data/media:/app/media:ro - ./data/media:/app/media:ro
- ./data/hls:/app/hls:rw - ./data/hls:/app/hls:rw
networks: networks:
@@ -102,7 +102,7 @@ services:
container_name: linearvoid-prometheus container_name: linearvoid-prometheus
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-config:/etc/prometheus
- prometheus-data:/prometheus - prometheus-data:/prometheus
ports: ports:
- "9090:9090" - "9090:9090"
@@ -142,4 +142,5 @@ volumes:
redis-data: redis-data:
postgres-data: postgres-data:
prometheus-data: prometheus-data:
prometheus-config:
grafana-data: grafana-data:
+5 -5
View File
@@ -1,6 +1,6 @@
services: services:
playout: playout:
image: ${GITEA_REGISTRY_URL:-git.allucanget.biz}/${GITEA_REGISTRY_NAMESPACE:-zwitschi/linearvoid}/playout:latest image: ${CI_REGISTRY_URL:-git.allucanget.biz}/${CI_REGISTRY_NAMESPACE:-zwitschi/linearvoid}/playout:latest
build: build:
context: ./backend/playout context: ./backend/playout
dockerfile: Dockerfile dockerfile: Dockerfile
@@ -24,7 +24,7 @@ services:
start_period: 10s start_period: 10s
chat: chat:
image: ${GITEA_REGISTRY_URL:-git.allucanget.biz}/${GITEA_REGISTRY_NAMESPACE:-zwitschi/linearvoid}/chat:latest image: ${CI_REGISTRY_URL:-git.allucanget.biz}/${CI_REGISTRY_NAMESPACE:-zwitschi/linearvoid}/chat:latest
build: build:
context: ./backend/chat context: ./backend/chat
dockerfile: Dockerfile dockerfile: Dockerfile
@@ -51,10 +51,10 @@ services:
start_period: 10s start_period: 10s
nginx: nginx:
image: ${GITEA_REGISTRY_URL:-git.allucanget.biz}/${GITEA_REGISTRY_NAMESPACE:-zwitschi/linearvoid}/nginx:latest image: ${CI_REGISTRY_URL:-git.allucanget.biz}/${CI_REGISTRY_NAMESPACE:-zwitschi/linearvoid}/nginx:latest
build: build:
context: ./infrastructure/nginx context: .
dockerfile: Dockerfile dockerfile: infrastructure/nginx/Dockerfile
container_name: linearvoid-nginx container_name: linearvoid-nginx
restart: unless-stopped restart: unless-stopped
ports: ports:
+59
View File
@@ -217,6 +217,65 @@ Notable aspects: Manifest freshness ensures all viewers stay on same timeline; c
## Deployment View ## Deployment View
### CI/CD Pipeline Architecture
The deployment follows a two-stage pipeline: **CI builds and pushes images**, then **CD pulls and deploys** them to the production host.
```mermaid
flowchart LR
subgraph "Gitea Actions"
CI[CI Workflow<br/>Build + Push Images]
CD[CD Workflow<br/>Pull + Deploy]
end
subgraph "Gitea Container Registry"
REGISTRY[git.allucanget.biz<br/>playout:latest<br/>chat:latest<br/>nginx:latest]
end
subgraph "Production Host (192.168.88.40)"
DOCKER[Docker Engine<br/>via TCP :2375]
PLAYOUT[linearvoid-playout]
CHAT[linearvoid-chat]
NGINX[linearvoid-nginx]
REDIS[linearvoid-redis]
DB[linearvoid-db]
PROM[linearvoid-prometheus]
GRAF[linearvoid-grafana]
end
CI -->|push| REGISTRY
REGISTRY -->|pull| CD
CD -->|deploy via DOCKER_HOST| DOCKER
DOCKER --> PLAYOUT
DOCKER --> CHAT
DOCKER --> NGINX
DOCKER --> REDIS
DOCKER --> DB
DOCKER --> PROM
DOCKER --> GRAF
classDef ci fill:#e1f5fe,stroke:#0288d1
classDef reg fill:#fff3e0,stroke:#f57c00
classDef prod fill:#e8f5e9,stroke:#388e3c
class CI,CD ci
class REGISTRY reg
class DOCKER,PLAYOUT,CHAT,NGINX,REDIS,DB,PROM,GRAF prod
```
**Pipeline Sequence:**
1. **CI Workflow** (`.gitea/workflows/ci.yml`):
- Triggers on `push` to `main` or `pull_request`
- Runs `test` job: frontend lint/build, Go validation, `docker compose build`
- Runs `docker` job (after `test` passes, only on `main` push):
- Builds `playout`, `chat`, `nginx` images with Buildx
- Pushes to Gitea Container Registry with `main` and `latest` tags
2. **CD Workflow** (`.gitea/workflows/cd.yml`):
- Triggers on `workflow_run` when CI completes successfully, or manually via `workflow_dispatch`
- Pulls latest images from registry
- Generates `.env` from secrets
- Seeds Prometheus config into named volume
- Deploys via `docker compose -f docker-compose.prod.yml up -d` to remote host (`192.168.88.40:2375`)
- Runs health checks with retry logic for playout initialization
### Infrastructure Level 1 ### Infrastructure Level 1
**Overview Diagram**: See Mermaid in Building Block section (VPS + Cloudflare R2/Pages/CDN). **Overview Diagram**: See Mermaid in Building Block section (VPS + Cloudflare R2/Pages/CDN).
+15 -3
View File
@@ -1,9 +1,21 @@
# Stage 1: Build frontend
FROM node:20-alpine AS builder
WORKDIR /build
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build
# Stage 2: Nginx with bundled frontend
FROM nginx:alpine FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf COPY infrastructure/nginx/nginx.conf /etc/nginx/nginx.conf
# Create directories for HLS and frontend # Copy prebuilt frontend from builder stage
RUN mkdir -p /srv/frontend /srv/hls /srv/frontend/admin COPY --from=builder /build/dist/ /srv/frontend/
# Create HLS directory
RUN mkdir -p /srv/hls
EXPOSE 80 EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]
+39
View File
@@ -0,0 +1,39 @@
{
"title": "Linear Void Monitoring",
"panels": [
{
"title": "Concurrent Viewers",
"type": "timeseries",
"targets": [{ "expr": "linearvoid_chat_connected_clients" }]
},
{
"title": "Stored Messages",
"type": "stat",
"targets": [
{
"expr": "linearvoid_chat_messages_total"
}
]
},
{
"title": "Moderation Actions",
"type": "timeseries",
"targets": [{ "expr": "linearvoid_chat_moderation_actions_total" }]
},
{
"title": "Redis Reachable",
"type": "stat",
"targets": [{ "expr": "linearvoid_chat_redis_up" }]
},
{
"title": "Admin Sessions",
"type": "timeseries",
"targets": [{ "expr": "linearvoid_chat_admin_sessions_active" }]
},
{
"title": "Chat Scrape Health",
"type": "stat",
"targets": [{ "expr": "up{job=\"chat\"}" }]
}
]
}
+10
View File
@@ -0,0 +1,10 @@
global:
scrape_interval: 15s
scrape_configs:
- job_name: "chat"
static_configs:
- targets: ["chat:3000"]
- job_name: "prometheus"
static_configs:
- targets: ["prometheus:9090"]