Consolidate readme.md and README.md into single comprehensive README
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 7s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 7s
Merges both files and adds full endpoint reference, .env variable table, setup guides (Spotify, Certbot, Obsidian/Quartz, Icecast), deprecated endpoint notes, and updated architecture with all services. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
328
README.md
328
README.md
@@ -1,54 +1,336 @@
|
||||
# web_server
|
||||
# My Web
|
||||
|
||||
Dockerized multi-service personal website.
|
||||

|
||||
|
||||
## Untracked Files Requiring Manual Setup
|
||||
Welcome to the source code for my website! Please contact me if you would like to collaborate and thank you for visiting.
|
||||
|
||||
These files are git-ignored and must be created or obtained manually before running the stack.
|
||||
This website is self-hosted on my Raspberry Pi. Any interference and the killswitch will activate and stop the UK national grid power system so please don't tamper with my domain :).
|
||||
|
||||
## Architecture
|
||||
|
||||
All services run in Docker containers orchestrated by Docker Compose behind Nginx as a reverse proxy on a single bridge network.
|
||||
|
||||
```
|
||||
vue ── Frontend build (outputs dist to shared volume)
|
||||
nginx (80, 443) ── Frontend SPA + Reverse Proxy
|
||||
backend (8080) ── Go API (GraphQL + REST)
|
||||
db (5432) ── PostgreSQL 16
|
||||
icecast2 (8000) ── Audio Streaming (Icecast2 + Liquidsoap)
|
||||
gitea (3000) ── Self-Hosted Git
|
||||
quartz (8080) ── Obsidian Notes Publisher (Quartz v4.4.0)
|
||||
searxng (8080) ── Meta Search Engine
|
||||
hasura (8080) ── Hasura GraphQL Engine
|
||||
certbot ── SSL Certificate Management (disabled in dev)
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
**Frontend** - Vue 3, Vite, Tailwind CSS v4, Pinia, Vue Router, markdown-it (wikilinks + KaTeX), Rust/WASM
|
||||
|
||||
**Backend** - Go (Gin), gqlgen (GraphQL), GORM, PostgreSQL, JWT auth, WebSockets
|
||||
|
||||
**Integrations** - Spotify API, Steam API, Anthropic Claude API, Icecast2
|
||||
|
||||
**Infrastructure** - Docker Compose, Nginx, Let's Encrypt (Certbot), Gitea + Act Runner
|
||||
|
||||
## Features
|
||||
|
||||
- Spotify integration (currently playing, recently played)
|
||||
- Steam integration (online status, recent games)
|
||||
- Obsidian note viewer via Quartz
|
||||
- Live radio streaming via Icecast2 + Liquidsoap
|
||||
- Real-time chat over WebSockets with image/video uploads
|
||||
- Blog with admin panel (CRUD)
|
||||
- Activity and rowing session tracking
|
||||
- Fan shrines (GTO, Evangelion, Demoman, Skip Skip Benben)
|
||||
- Self-hosted Git (Gitea) with CI/CD and commit feed on homepage
|
||||
- Claude AI integration
|
||||
- Printable CV with role-specific sections
|
||||
- SearXNG meta search engine
|
||||
- Hasura GraphQL console
|
||||
- Landing page with animated stamps section
|
||||
- Route transitions (slide/fade) and performance optimizations (gzip, WOFF2 fonts, lazy loading)
|
||||
|
||||
## Pages
|
||||
|
||||
| Route | Description |
|
||||
| -------------- | ------------------------------------- |
|
||||
| `/` | Landing page |
|
||||
| `/stp` | Home dashboard with grid layout |
|
||||
| `/admin` | Admin panel (authenticated) |
|
||||
| `/cv` | Curriculum Vitae (printable) |
|
||||
| `/bookmarks` | Bookmarks |
|
||||
| `/notes/:path` | Obsidian note viewer (via Quartz) |
|
||||
| `/shrines` | Fan shrine index + individual shrines |
|
||||
|
||||
## API
|
||||
|
||||
### GraphQL
|
||||
|
||||
**Endpoint:** `POST /api/graphql`
|
||||
**Playground:** `GET /api/graphql` (when `GQL_PLAYGROUND=true`)
|
||||
|
||||
| Operation | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `users` | Query | Get all users |
|
||||
| `user(id)` | Query | Get user by ID |
|
||||
| `me` | Query | Get authenticated user |
|
||||
| `posts` | Query | Get all posts |
|
||||
| `post(id)` | Query | Get post by ID |
|
||||
| `activities` | Query | Get all activities |
|
||||
| `favorites` | Query | Get all favorites |
|
||||
| `rowingSessions` | Query | Get all rowing sessions |
|
||||
| `messages` | Query | Get all messages |
|
||||
| `spotifyListening` | Query | Currently playing track |
|
||||
| `spotifyRecent` | Query | Recently played tracks |
|
||||
| `giteaFeed` | Query | Latest Gitea activity |
|
||||
| `steamStatus` | Query | Steam online status |
|
||||
| `login` | Mutation | Authenticate user |
|
||||
| `logout` | Mutation | Logout |
|
||||
| `refreshToken` | Mutation | Refresh auth token |
|
||||
| `createPost` / `updatePost` / `deletePost` | Mutation | Post CRUD (admin) |
|
||||
| `createUser` / `deleteUser` | Mutation | User management (admin) |
|
||||
| `setUserAdmin` | Mutation | Toggle admin status |
|
||||
| `createFavorite` | Mutation | Add favorite (admin) |
|
||||
| `createActivity` | Mutation | Add activity (admin) |
|
||||
|
||||
### REST Endpoints
|
||||
|
||||
**Auth**
|
||||
| Method | Path | Description |
|
||||
| --- | --- | --- |
|
||||
| POST | `/api/auth/login` | Login (rate limited: 5/min) |
|
||||
| POST | `/api/auth/refresh` | Refresh token |
|
||||
| GET | `/api/auth/check` | Check token validity |
|
||||
| POST | `/api/auth/logout` | Logout |
|
||||
|
||||
**Spotify**
|
||||
| Method | Path | Description |
|
||||
| --- | --- | --- |
|
||||
| GET | `/api/spotify/callback` | OAuth callback |
|
||||
| GET | `/api/spotify/listening` | Currently playing |
|
||||
| GET | `/api/spotify/recent` | Recently played |
|
||||
|
||||
**Public**
|
||||
| Method | Path | Description |
|
||||
| --- | --- | --- |
|
||||
| GET | `/api/favorites` | Get all favorites |
|
||||
| GET | `/api/rowing` | Get all rowing sessions |
|
||||
| GET | `/api/activity` | Get all activities |
|
||||
| GET | `/api/posts` | Get all posts |
|
||||
| GET | `/api/posts/:id` | Get post by ID |
|
||||
| GET | `/api/user` | Get all users |
|
||||
| GET | `/api/user/:id` | Get user by ID |
|
||||
|
||||
**Protected (auth required)**
|
||||
| Method | Path | Description |
|
||||
| --- | --- | --- |
|
||||
| POST | `/api/messages/upload` | Upload message file (rate limited: 5/min) |
|
||||
|
||||
**Admin (auth + admin required)**
|
||||
| Method | Path | Description |
|
||||
| --- | --- | --- |
|
||||
| POST | `/api/favorites` | Create favorite |
|
||||
| POST | `/api/rowing` | Create rowing session |
|
||||
| POST | `/api/activity` | Create activity |
|
||||
| POST | `/api/posts` | Create post |
|
||||
| PUT | `/api/posts/:id` | Update post |
|
||||
| DELETE | `/api/posts/:id` | Delete post |
|
||||
| POST | `/api/user` | Create user |
|
||||
| PUT | `/api/user/:id` | Update user |
|
||||
| DELETE | `/api/user/:id` | Delete user |
|
||||
| PATCH | `/api/user/:id/admin` | Set/unset admin |
|
||||
| POST | `/api/radio/upload` | Upload radio song |
|
||||
| GET | `/api/radio/songs` | List radio songs |
|
||||
| DELETE | `/api/radio/songs/:filename` | Delete radio song |
|
||||
| PATCH | `/api/radio/songs/:filename/disable` | Disable radio song |
|
||||
| PATCH | `/api/radio/songs/:filename/enable` | Enable radio song |
|
||||
|
||||
**WebSocket**
|
||||
| Method | Path | Description |
|
||||
| --- | --- | --- |
|
||||
| GET | `/api/ws` | WebSocket chat (10s ping keepalive) |
|
||||
|
||||
### Nginx Proxy Routes
|
||||
|
||||
| Route | Target | Notes |
|
||||
| --- | --- | --- |
|
||||
| `/api` | backend:8080 | API (rate limited: 30r/s) |
|
||||
| `/radio` | icecast:8000 | Audio streaming |
|
||||
| `/gitea` | gitea:3000 | Git service |
|
||||
| `/hasura` | hasura:8080 | GraphQL console + WebSocket |
|
||||
| `/notes` | quartz:8080 | Obsidian notes |
|
||||
| `/searxng` | searxng:8080 | Search engine |
|
||||
| `/uploads` | local alias | User-uploaded files |
|
||||
|
||||
### Deprecated Endpoints
|
||||
|
||||
**Backend note API** (`GET /api/notes/*path`) - The backend has a REST endpoint that serves note files directly from the mounted Obsidian vault. This is superseded by the Quartz service which now handles note rendering at `/notes/`. The backend endpoint still exists in code but is no longer the primary note serving path.
|
||||
|
||||
## Setup
|
||||
|
||||
### `.env`
|
||||
|
||||
Environment variables used by all services. No example file is provided — see `docker-compose.yml` for the full list of referenced variables (database credentials, hostnames, secrets, Spotify OAuth, Gitea tokens, etc.).
|
||||
Create a `.env` file in the project root. All services read from this file.
|
||||
|
||||
### `gitea/config/app.ini`
|
||||
| Variable | Description |
|
||||
| --- | --- |
|
||||
| `POSTGRES_USER` | PostgreSQL username |
|
||||
| `POSTGRES_PASSWORD` | PostgreSQL password |
|
||||
| `POSTGRES_DB` | Main app database name |
|
||||
| `POSTGRES_PORT` | PostgreSQL port (typically `5432`) |
|
||||
| `POSTGRES_HOST` | PostgreSQL hostname (use `db` for Docker) |
|
||||
| `GITEA_HOST` | Gitea hostname (use `gitea` for Docker) |
|
||||
| `GITEA_PORT` | Gitea HTTP port (typically `3000`) |
|
||||
| `GITEA_INTERNAL_TOKEN` | Gitea internal API token (generate with `gitea generate secret INTERNAL_TOKEN`) |
|
||||
| `GITEA_LFS_JWT_SECRET` | Gitea LFS JWT secret (generate with `gitea generate secret LFS_JWT_SECRET`) |
|
||||
| `GITEA_OAUTH2_JWT_SECRET` | Gitea OAuth2 JWT secret (generate with `gitea generate secret JWT_SECRET`) |
|
||||
| `POSTGRES_GITEA_DB` | Gitea database name |
|
||||
| `UPTIMEKUMA_HOST` | Uptime Kuma hostname (planned) |
|
||||
| `UPTIMEKUMA_PORT` | Uptime Kuma port (planned) |
|
||||
| `SEARXNG_HOST` | SearXNG hostname (use `searxng` for Docker) |
|
||||
| `SEARXNG_PORT` | SearXNG port (typically `8080`) |
|
||||
| `SEARXNG_SECRET_KEY` | SearXNG secret key (random hex string) |
|
||||
| `WALLABAG_HOST` | Wallabag hostname (planned) |
|
||||
| `WALLABAG_PORT` | Wallabag port (planned) |
|
||||
| `QUARTZ_HOST` | Quartz hostname (use `quartz` for Docker) |
|
||||
| `QUARTZ_PORT` | Quartz port (typically `8080`) |
|
||||
| `GITEA_RUNNER_HOST` | Gitea runner hostname |
|
||||
| `GITEA_RUNNER_NAME` | Gitea runner display name |
|
||||
| `GITEA_RUNNER_REGISTRATION_TOKEN` | Token to register Gitea Actions runner |
|
||||
| `BACKEND_PORT` | Backend port (typically `8080`) |
|
||||
| `BACKEND_HOST` | Backend hostname (use `backend` for Docker) |
|
||||
| `BACKEND_SECRET` | JWT signing secret |
|
||||
| `BACKEND_ENDPOINT` | API path prefix (typically `/api`) |
|
||||
| `OBSIDIAN_DIR` | Absolute path to Obsidian vault on host machine |
|
||||
| `SPOTIFY_CLIENT_ID` | Spotify app client ID |
|
||||
| `SPOTIFY_CLIENT_SECRET` | Spotify app client secret |
|
||||
| `SPOTIFY_REDIRECT_URI` | Spotify OAuth redirect (e.g. `https://www.<DOMAIN>/api/spotify/callback`) |
|
||||
| `SPOTIFY_AUTH_STATE` | Arbitrary state string for Spotify OAuth |
|
||||
| `ICECAST_SOURCE_PASSWORD` | Icecast source connection password |
|
||||
| `ICECAST_RELAY_PASSWORD` | Icecast relay password |
|
||||
| `ICECAST_ADMIN_USER` | Icecast admin username |
|
||||
| `ICECAST_ADMIN_PASSWORD` | Icecast admin password |
|
||||
| `ICECAST_HOST` | Icecast hostname (use `icecast` for Docker) |
|
||||
| `ICECAST_PORT` | Icecast port (typically `8000`) |
|
||||
| `ICECAST_MOUNT` | Icecast mount point (e.g. `/stream`) |
|
||||
| `LIQUIDSOAP_HARBOR_MOUNT` | Liquidsoap live input mount (e.g. `/live`) |
|
||||
| `LIQUIDSOAP_HARBOR_PORT` | Liquidsoap harbor port (e.g. `8005`) |
|
||||
| `DOMAIN` | Production domain name |
|
||||
| `EMAIL` | Email for Let's Encrypt registration |
|
||||
| `CLAUDE_API_KEY` | Anthropic Claude API key |
|
||||
| `STEAM_API_KEY` | Steam Web API key |
|
||||
| `STEAM_ID` | Steam user ID |
|
||||
| `HASURA_GRAPHQL_ADMIN_SECRET` | Hasura admin secret |
|
||||
| `HASURA_HOST` | Hasura hostname (use `hasura` for Docker) |
|
||||
| `HASURA_PORT` | Hasura port (typically `8080`) |
|
||||
| `SEED_DB` | Set to `true` to seed test data on startup |
|
||||
|
||||
Gitea application config. Copy from the template and fill in secrets:
|
||||
### Gitea Config
|
||||
|
||||
Copy from the template and fill in secrets:
|
||||
|
||||
```sh
|
||||
cp gitea/config/app.ini.template gitea/config/app.ini
|
||||
```
|
||||
|
||||
Populate `LFS_JWT_SECRET`, `SECRET_KEY`, `INTERNAL_TOKEN`, `JWT_SECRET`, and the database `PASSWD`.
|
||||
Populate `LFS_JWT_SECRET`, `SECRET_KEY`, `INTERNAL_TOKEN`, `JWT_SECRET`, and the database `PASSWD`. Alternatively, the Gitea entrypoint generates `app.ini` from the template using environment variables.
|
||||
|
||||
### `searxng/settings.yml`
|
||||
### SearXNG Config
|
||||
|
||||
SearXNG settings. Copy from the template:
|
||||
Copy from the template:
|
||||
|
||||
```sh
|
||||
cp searxng/settings.yml.template searxng/settings.yml
|
||||
```
|
||||
|
||||
The template uses environment variable substitution (`${BASE_URL}`, `${SEARXNG_SECRET_KEY}`) at container build time, so this file is generated by the Dockerfile's `entrypoint.sh`. If running outside Docker, fill in values manually.
|
||||
The Docker entrypoint handles environment variable substitution (`${BASE_URL}`, `${SEARXNG_SECRET_KEY}`) automatically, so manual setup is only needed when running outside Docker.
|
||||
|
||||
### `certbot/conf/` and `certbot/www/`
|
||||
### Spotify Token Setup
|
||||
|
||||
Let's Encrypt certificate storage. In production, certbot populates these automatically on first run. For local/dev use, either:
|
||||
1. Create a Spotify app at [developer.spotify.com](https://developer.spotify.com/dashboard) and set the redirect URI to `https://www.<DOMAIN>/api/spotify/callback`
|
||||
2. Set `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI`, and `SPOTIFY_AUTH_STATE` in `.env`
|
||||
3. Start the stack, then visit the Spotify auth URL logged by the backend on startup to authorize the app
|
||||
4. After authorization, Spotify redirects to the callback endpoint which stores tokens at `/backend/token/spotify_token.json`
|
||||
5. Tokens are refreshed automatically; the file persists across container restarts via volume mount
|
||||
|
||||
- Use dev mode (`docker-compose.dev.yml`) which skips SSL, or
|
||||
- Place self-signed certs in `certbot/conf/live/localhost/` (`fullchain.pem`, `privkey.pem`).
|
||||
### Obsidian Notes Setup
|
||||
|
||||
### `backend/token/`
|
||||
1. Set `OBSIDIAN_DIR` in `.env` to the absolute path of your Obsidian vault on the host machine
|
||||
2. The vault is mounted read-only into the Quartz container at `/quartz/content`
|
||||
3. Quartz builds a static site from the vault on startup and serves it at `/notes/`
|
||||
4. The backend also mounts the vault at `/backend/notes` (legacy, see deprecated endpoints above)
|
||||
|
||||
Directory where the backend persists Spotify OAuth tokens (`spotify_token.json`). Created automatically at runtime — no manual setup needed, but the directory is git-ignored so it won't exist on a fresh clone. Docker mounts `./backend/token/:/backend/token` so the directory is created by Docker.
|
||||
### SSL Certificates (Certbot)
|
||||
|
||||
### `icecast2/fallback_music/`
|
||||
**Initial setup (production):**
|
||||
|
||||
MP3 files used as fallback music for the Icecast2/Liquidsoap radio stream. Place at least one `.mp3` file here. A `.gitkeep` is tracked to preserve the directory.
|
||||
1. Set `DOMAIN` and `EMAIL` in `.env`
|
||||
2. On first run, Nginx starts with `nginx_setup.conf.template` which only serves the ACME challenge route at `/.well-known/acme-challenge/`
|
||||
3. Certbot requests a certificate via `certbot certonly --webroot`
|
||||
4. Once the certificate is issued to `certbot/conf/live/<DOMAIN>/`, restart Nginx — it will detect the certs and switch to the full `nginx.conf.template`
|
||||
5. Certbot checks for renewal every 12 hours automatically
|
||||
|
||||
### `gitea-runner/act_runner`
|
||||
**Dev mode:** Nginx generates a self-signed certificate for localhost automatically.
|
||||
|
||||
The Gitea Actions runner binary. Download from [Gitea's releases](https://gitea.com/gitea/act_runner/releases) for your platform and place in `gitea-runner/`.
|
||||
### Icecast Radio
|
||||
|
||||
### `gitea-runner/.runner`
|
||||
Place at least one `.mp3` file in `icecast2/fallback_music/`. Liquidsoap plays these as fallback when no live source is connected. Connect a live source to port 8001 on the `LIQUIDSOAP_HARBOR_MOUNT`.
|
||||
|
||||
Runner registration state file. Generated automatically when `gitea-runner/run.sh` runs for the first time (requires `GITEA_RUNNER_REGISTRATION_TOKEN` in `.env`).
|
||||
### Gitea Runner
|
||||
|
||||
1. Download the `act_runner` binary from [Gitea releases](https://gitea.com/gitea/act_runner/releases) and place in `gitea-runner/`
|
||||
2. Set `GITEA_RUNNER_REGISTRATION_TOKEN` in `.env`
|
||||
3. The runner registers automatically on first startup
|
||||
|
||||
## Dev Mode
|
||||
|
||||
Run the full stack over plain HTTP without SSL certificates:
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
|
||||
```
|
||||
|
||||
This:
|
||||
- Uses an HTTP-only Nginx config with all routing (SPA, backend proxy, radio, gitea, etc.)
|
||||
- Generates a self-signed certificate for localhost
|
||||
- Disables certbot
|
||||
- Seeds the database with test data (`SEED_DB=true`)
|
||||
- Enables GraphQL playground and introspection
|
||||
- Enables Hasura console and dev mode
|
||||
|
||||
Visit `http://localhost` to test.
|
||||
|
||||
### Frontend only (hot reload)
|
||||
|
||||
```sh
|
||||
cd vue && npm run dev
|
||||
```
|
||||
|
||||
Vite dev server proxies `/api` to `localhost:8080`, `/gitea` to `localhost:3000`, `/radio` to `localhost:8000`.
|
||||
|
||||
## Untracked Files
|
||||
|
||||
These files are git-ignored and must be created manually:
|
||||
|
||||
| File | Notes |
|
||||
| --- | --- |
|
||||
| `.env` | See setup section above |
|
||||
| `gitea/config/app.ini` | Copy from `app.ini.template` or let entrypoint generate it |
|
||||
| `searxng/settings.yml` | Copy from `settings.yml.template` or let entrypoint generate it |
|
||||
| `certbot/conf/`, `certbot/www/` | Created automatically by certbot; use dev mode to skip |
|
||||
| `backend/token/` | Created automatically by Docker volume mount |
|
||||
| `icecast2/fallback_music/*.mp3` | Place at least one MP3 file |
|
||||
| `gitea-runner/act_runner` | Download from Gitea releases |
|
||||
| `gitea-runner/.runner` | Generated on first runner startup |
|
||||
|
||||
## Future Ideas
|
||||
|
||||
- More Rust to WASM
|
||||
- ML for chatboards
|
||||
- Cache requests
|
||||
- Design more webpages
|
||||
- Calendar to show radio times
|
||||
- Nice smooth function background and transitions
|
||||
- Design shrines
|
||||
- Redis (not really but practical experience)
|
||||
|
||||
Reference in New Issue
Block a user