Your music, beautifully tracked. All yours. (coming soon) teal.fm
teal-fm atproto

Merge pull request #62 from fatfingers23/docker

Feature: Dev docker compose

authored by mmatt.net and committed by GitHub 5773273d c3b3be71

+18
.env.template
··· 1 + #Aqua setup 2 + NODE_ENV=development 3 + PORT=3000 4 + HOST=0.0.0.0 5 + PUBLIC_URL=A publicly accessible url for aqua 6 + DB_USER=postgres 7 + DB_PASSWORD=supersecurepassword123987 8 + DB_NAME=teal 9 + DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@localhost:5432/${DB_NAME}" 10 + DOCKER_DB_URL="postgresql://${DB_USER}:${DB_PASSWORD}@host.docker.internal:5432/${DB_NAME}" 11 + #This is not currently being used fully so can just use this default pubkey for now 12 + DID_WEB_PUBKEY=zQ3sheEnMKhEK87PSu4P2mjAevViqHcjKmgxBWsDQPjLRM9wP 13 + CLIENT_ADDRESS=A publicly accessible host for amethyst like amethyst.teal.fm 14 + PUBLIC_DID_WEB=did:web:{aqua's PUBLIC_URL goes here after did:web:} 15 + 16 + #amethyst 17 + EXPO_PUBLIC_DID_WEB=same as PUBLIC_DID_WEB 18 + EXPO_PUBLIC_BASE_URL=same as CLIENT_ADDRESS but with http scheme like https://amethyst.teal.fm
+2
.gitignore
··· 34 34 .env.development.local 35 35 .env.test.local 36 36 .env.production.local 37 + .env.air 37 38 38 39 # vercel 39 40 .vercel ··· 46 47 **/*.log 47 48 package-lock.json 48 49 **/*.bun 50 + .idea
+28
README.md
··· 23 23 ``` 24 24 25 25 Open http://localhost:3000/ with your browser to see the home page. You will need to login with Bluesky to test the posting functionality of the app. Note: if the redirect back to the app after you login isn't working correctly, you may need to replace the `127.0.0.1` with `localhost`. 26 + 27 + 28 + ### Running the full stack in docker for development 29 + _Still a work in progress and may have some hiccups_ 30 + 31 + #### 1. Set up publicly accessible endpoints. 32 + It is recommended if you are running aqua to make a publicly accessible endpoint for the app to post to. You can do that a couple of ways: 33 + 34 + * Set up the traditional port forward on your router 35 + * Use a tool like [ngrok](https://ngrok.com/) with the command `ngrok http 8080` or [Cloudflare tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-remote-tunnel/) (follow the 2a. portion of the guide when you get to that point). 36 + 37 + If you do the cloudflare tunnels for amethyst as well, 38 + you will also need 39 + to follow [this](https://caddy.community/t/caddy-with-cloudflare-tunnel/18569) for routing to work properly. 40 + Although you can just run it locally and it will work. 41 + 42 + #### 2. Set up .envs 43 + 1. copy [.env.template](.env.template) and name it [.env](.env). The docker build will pull everything from this `.env`. There are notes in the [.env.template](.env.template) on what some of the values should be. 44 + 2. Follow the instructions in [piper](https://github.com/teal-fm/piper) to set up environment variables for the music scraper. But name it `.env.air` 45 + 46 + #### 3. Run docker 47 + 1. Make sure docker and docker compose is installed 48 + 2. It is recommended to run 'docker compose -f compose.dev.yml pull' to pull the latest remote images before running the docker compose command. 49 + 3. Run `docker compose -f compose.dev.yml up -d` 50 + 51 + 52 + And that's it! You should have the full teal.fm stack running locally. Now if you are working on aqua you can do `docker container stop aqua-app` and run that locally during development while everything else is running in docker. 53 +
+1
apps/amethyst/.env.template
··· 1 + EXPO_PUBLIC_DID_WEB=did:web:your.publicly-accessible-aqua-domain.com
+5
apps/amethyst/Caddyfile
··· 1 + {env.CLIENT_ADDRESS} { 2 + try_files {path} /index.html 3 + file_server 4 + } 5 +
+53
apps/amethyst/Dockerfile
··· 1 + FROM node:22-slim AS builder 2 + ARG CLIENT_ADDRESS 3 + 4 + ENV PNPM_HOME="/pnpm" 5 + ENV PATH="$PNPM_HOME:$PATH" 6 + RUN corepack enable 7 + # Set working directory 8 + WORKDIR /app 9 + 10 + # Copy root workspace files 11 + COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ 12 + 13 + # Copy turbo.json 14 + COPY turbo.json ./ 15 + 16 + # Copy workspace packages 17 + COPY packages/db/ ./packages/db/ 18 + COPY packages/lexicons/ ./packages/lexicons/ 19 + COPY packages/tsconfig/ ./packages/tsconfig/ 20 + 21 + # Copy the aqua app 22 + COPY apps/amethyst/ ./apps/amethyst/ 23 + 24 + # Copy .env 25 + COPY ../../.env ./apps/amethyst/.env 26 + 27 + # Build the aqua app 28 + WORKDIR /app/apps/amethyst 29 + RUN pnpm install 30 + RUN pnpm run build:web 31 + 32 + #create the client-json 33 + RUN echo '{ \ 34 + "redirect_uris": ["https://'"${CLIENT_ADDRESS}"'/auth/callback"], \ 35 + "response_types": ["code"], \ 36 + "grant_types": ["authorization_code", "refresh_token"], \ 37 + "scope": "atproto transition:generic", \ 38 + "token_endpoint_auth_method": "none", \ 39 + "application_type": "web", \ 40 + "client_id": "https://'"${CLIENT_ADDRESS}"'/client-metadata.json", \ 41 + "client_name": "teal", \ 42 + "client_uri": "https://'"${CLIENT_ADDRESS}"'", \ 43 + "dpop_bound_access_tokens": true \ 44 + }' > /app/client-metadata.json \ 45 + 46 + 47 + FROM caddy:2.1.0-alpine AS caddy 48 + EXPOSE 80 49 + EXPOSE 443 50 + EXPOSE 443/udp 51 + COPY /apps/amethyst/Caddyfile /etc/caddy/Caddyfile 52 + COPY --from=builder /app/apps/amethyst/build /srv 53 + COPY --from=builder /app/client-metadata.json /srv/client-metadata.json
+1
apps/amethyst/package.json
··· 9 9 "ios": "expo run:ios", 10 10 "web": "expo start --web", 11 11 "build": "expo export --output-dir ./build --platform all", 12 + "build:web": "expo export --output-dir ./build --platform web --clear", 12 13 "test": "jest --watchAll", 13 14 "lexgen": "lex gen-server ./lexicons/generated/server/ ./lexicons/src/" 14 15 },
+6
apps/amethyst/readme.md
··· 10 10 11 11 ## Development 12 12 13 + Copy the [.env.template](.env.template) and name it `.env` 14 + 15 + set `EXPO_PUBLIC_DID_WEB` to the domain aqua is running on. 16 + For example if it's running at `aqua.teal.fm` it would be 17 + `EXPO_PUBLIC_DID_WEB=did:web:aqua.teal.fm`. This is how amethyst sends requests to the appview. 18 + 13 19 TODO
-1
apps/amethyst/stores/authenticationSlice.tsx
··· 44 44 ) => { 45 45 // check if we have CF_PAGES_URL set. if not, use localhost 46 46 const baseUrl = process.env.EXPO_PUBLIC_BASE_URL || 'http://localhost:8081'; 47 - console.log('Using base URL:', baseUrl); 48 47 const initialAuth = createOAuthClient(baseUrl, 'bsky.social'); 49 48 50 49 console.log('Auth client created!');
+1 -1
apps/amethyst/stores/preferenceSlice.tsx
··· 11 11 return { 12 12 colorTheme: 'system', 13 13 setColorTheme: (theme) => set({ colorTheme: theme }), 14 - tealDid: 'did:web:rina.z.teal.fm', 14 + tealDid: process.env.EXPO_PUBLIC_DID_WEB ?? 'did:web:rina.z.teal.fm', 15 15 setTealDid: (url) => set({ tealDid: url }), 16 16 }; 17 17 };
+11 -13
apps/aqua/Dockerfile
··· 1 - # Use the official Bun image as base 2 - FROM oven/bun:1.0 AS builder 3 - 1 + FROM node:22-slim AS builder 2 + ENV PNPM_HOME="/pnpm" 3 + ENV PATH="$PNPM_HOME:$PATH" 4 + RUN corepack enable 4 5 # Set working directory 5 6 WORKDIR /app 6 7 7 8 # Copy root workspace files 8 - COPY package.json bun.lockb ./ 9 + COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ 9 10 10 11 # Copy turbo.json 11 12 COPY turbo.json ./ ··· 18 19 # Copy the aqua app 19 20 COPY apps/aqua/ ./apps/aqua/ 20 21 21 - # Install dependencies 22 - RUN bun install 23 - 24 - # Build workspace packages (if needed) 25 - RUN bun run build --filter=@teal/db 26 - RUN bun run build --filter=@teal/lexicons 27 - 28 22 # Build the aqua app 29 23 WORKDIR /app/apps/aqua 30 - RUN bun run build 24 + RUN pnpm install 25 + RUN pnpm run build 31 26 32 27 # Start production image (node lts ideally) 33 28 FROM node:bookworm-slim ··· 41 36 COPY --from=builder /app/apps/aqua/node_modules ./node_modules 42 37 COPY --from=builder /app/apps/aqua/package.json ./ 43 38 39 + # install pino-pretty for logs 40 + RUN npm install -g pino-pretty@13.0.0 41 + 44 42 # move public to cwd 45 43 RUN mv ./dist/public ./public 46 44 ··· 51 49 EXPOSE 3000 52 50 53 51 # Start the application 54 - CMD ["npm", "run", "start"] 52 + CMD ["node", "dist/index.cjs", "|", "pino-pretty"]
+6 -5
apps/aqua/package.json
··· 20 20 "@atproto/syntax": "^0.3.0", 21 21 "@atproto/xrpc-server": "^0.7.4", 22 22 "@braintree/sanitize-url": "^7.1.0", 23 - "drizzle-orm": "^0.38.3", 24 23 "@hono/node-server": "^1.13.7", 25 24 "@libsql/client": "^0.14.0", 25 + "@teal/db": "workspace:*", 26 + "@teal/lexicons": "workspace:*", 26 27 "dotenv": "^16.4.5", 28 + "dotenv-expand": "^12.0.2", 29 + "drizzle-orm": "^0.38.3", 27 30 "envalid": "^8.0.0", 28 31 "hono": "^4.6.9", 29 32 "jose": "^5.9.6", 30 33 "pino": "^9.5.0", 31 34 "turbo": "^2.2.3", 32 - "uhtml": "^4.5.11", 33 - "@teal/db": "workspace:*", 34 - "@teal/lexicons": "workspace:*" 35 + "uhtml": "^4.5.11" 35 36 }, 36 37 "devDependencies": { 37 - "@teal/tsconfig": "workspace:*", 38 38 "@atproto/lex-cli": "^0.5.4", 39 + "@teal/tsconfig": "workspace:*", 39 40 "@types/node": "^20.17.6", 40 41 "drizzle-kit": "^0.30.1", 41 42 "pino-pretty": "^13.0.0",
+1 -1
apps/aqua/src/index.ts
··· 84 84 logger.info( 85 85 `Listening on ${ 86 86 info.address == '::1' 87 - ? 'http://0.0.0.0' 87 + ? `http://${env.HOST}` 88 88 : // TODO: below should probably be https:// 89 89 // but i just want to ctrl click in the terminal 90 90 'http://' + info.address
+3 -2
apps/aqua/src/lib/env.ts
··· 1 1 import dotenv from 'dotenv'; 2 + import dotenvExpand from "dotenv-expand"; 2 3 import { cleanEnv, host, port, str, testOnly } from 'envalid'; 3 4 import process from 'node:process'; 4 5 5 - dotenv.config(); 6 + dotenvExpand.expand(dotenv.config()); 6 7 // in case our .env file is in the root folder 7 - dotenv.config({ path: './../../.env' }); 8 + dotenvExpand.expand(dotenv.config({ path: './../../.env' })); 8 9 9 10 export const env = cleanEnv(process.env, { 10 11 NODE_ENV: str({
+76 -46
compose.dev.yml
··· 6 6 container_name: aqua-app 7 7 ports: 8 8 - "3000:3000" 9 + extra_hosts: 10 + - "host.docker.internal:host-gateway" 9 11 networks: 10 12 - app_network 11 13 depends_on: 12 14 - postgres 13 - - redis 14 15 env_file: 15 16 - .env 16 - # traefik: 17 - # image: traefik:v2.10 18 - # container_name: traefik 19 - # command: 20 - # - "--api.insecure=true" 21 - # - "--providers.file.directory=/etc/traefik/dynamic" 22 - # - "--providers.file.watch=true" 23 - # - "--entrypoints.web.address=:80" 24 - # ports: 25 - # - "80:80" # HTTP 26 - # - "8080:8080" # Dashboard 27 - # volumes: 28 - # - ./traefik/dynamic:/etc/traefik/dynamic:ro 29 - # networks: 30 - # - app_network 31 - # extra_hosts: 32 - # - "host.docker.internal:host-gateway" # This allows reaching host machine 33 - 34 - # postgres: 35 - # image: postgres:latest 36 - # container_name: postgres_db 37 - # environment: 38 - # POSTGRES_USER: postgres 39 - # POSTGRES_PASSWORD: yourpassword 40 - # POSTGRES_DB: yourdatabase 41 - # ports: 42 - # - "5432:5432" 43 - # volumes: 44 - # - postgres_data:/var/lib/postgresql/data 45 - # networks: 46 - # - app_network 47 - 48 - # redis: 49 - # image: redis:latest 50 - # container_name: redis_cache 51 - # ports: 52 - # - "6379:6379" 53 - # volumes: 54 - # - redis_data:/data 55 - # command: redis-server --appendonly yes 56 - # networks: 57 - # - app_network 58 - 17 + environment: 18 + DATABASE_URL: ${DOCKER_DB_URL} 19 + amethyst: 20 + build: 21 + context: . 22 + dockerfile: apps/amethyst/Dockerfile 23 + args: 24 + - CLIENT_ADDRESS=${CLIENT_ADDRESS} 25 + ports: 26 + - "80:80" 27 + - "443:443" 28 + - "443:443/udp" 29 + volumes: 30 + - caddy_data:/data 31 + - caddy_config:/config 32 + networks: 33 + - app_network 34 + environment: 35 + CLIENT_ADDRESS: ${CLIENT_ADDRESS} 36 + postgres: 37 + image: postgres:latest 38 + container_name: postgres_db 39 + environment: 40 + POSTGRES_USER: ${DB_USER} 41 + POSTGRES_PASSWORD: ${DB_PASSWORD} 42 + POSTGRES_DB: ${DB_NAME} 43 + ports: 44 + - "5432:5432" 45 + volumes: 46 + - postgres_data:/var/lib/postgresql/data 47 + extra_hosts: 48 + - "host.docker.internal:host-gateway" 49 + networks: 50 + - app_network 51 + cadet: 52 + image: ghcr.io/espeon/cadet 53 + ports: 54 + - "9000:9000" 55 + env_file: 56 + - .env 57 + environment: 58 + DATABASE_URL: ${DOCKER_DB_URL} 59 + extra_hosts: 60 + - "host.docker.internal:host-gateway" 61 + networks: 62 + - app_network 63 + depends_on: 64 + - postgres 65 + satellite: 66 + image: ghcr.io/espeon/satellite 67 + ports: 68 + - "3132:3000" 69 + env_file: 70 + - .env 71 + environment: 72 + DATABASE_URL: ${DOCKER_DB_URL} 73 + extra_hosts: 74 + - "host.docker.internal:host-gateway" 75 + networks: 76 + - app_network 77 + depends_on: 78 + - postgres 79 + piper: 80 + image: ghcr.io/teal-fm/piper:main 81 + # Depends on your .env.air 82 + ports: 83 + - "8080:8080" 84 + env_file: 85 + - .env.air 86 + volumes: 87 + - piper_data:/db 59 88 networks: 60 89 app_network: 61 90 driver: bridge 62 - 63 91 volumes: 64 92 postgres_data: 65 - redis_data: 93 + caddy_data: 94 + caddy_config: 95 + piper_data:
+2 -1
package.json
··· 9 9 "typecheck": "pnpm -r exec tsc --noEmit", 10 10 "fix": "biome lint --apply . && biome format --write . && biome check . --apply", 11 11 "nuke": "rimraf node_modules */*/node_modules", 12 - "lex:gen-server": "turbo lex:gen-server" 12 + "lex:gen-server": "turbo lex:gen-server", 13 + "db:migrate": "cd ./packages/db && drizzle-kit migrate" 13 14 }, 14 15 "dependencies": { 15 16 "@atproto/oauth-client": "^0.3.8"
+9
packages/db/drizzle.config.ts
··· 1 1 import { defineConfig } from "drizzle-kit"; 2 + import dotenv from "dotenv"; 3 + import dotenvExpand from "dotenv-expand"; 4 + 5 + //Loads from root .env 6 + dotenvExpand.expand(dotenv.config({ path: '../../.env' })); 7 + //Or can be overridden by .env in the current folder 8 + dotenvExpand.expand(dotenv.config()); 9 + 10 + 2 11 3 12 export default defineConfig({ 4 13 dialect: "postgresql",
+2 -1
packages/db/package.json
··· 18 18 "postgres": "^3.4.5" 19 19 }, 20 20 "devDependencies": { 21 - "@types/node": "^20.17.6" 21 + "@types/node": "^20.17.6", 22 + "dotenv-expand": "^12.0.2" 22 23 } 23 24 }
+14
pnpm-lock.yaml
··· 265 265 dotenv: 266 266 specifier: ^16.4.5 267 267 version: 16.4.7 268 + dotenv-expand: 269 + specifier: ^12.0.2 270 + version: 12.0.2 268 271 drizzle-orm: 269 272 specifier: ^0.38.3 270 273 version: 0.38.4(@libsql/client@0.14.0)(@types/react@19.0.14)(expo-sqlite@15.2.9(expo@53.0.9(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(postgres@3.4.5)(react@19.0.0) ··· 336 339 '@types/node': 337 340 specifier: ^20.17.6 338 341 version: 20.17.14 342 + dotenv-expand: 343 + specifier: ^12.0.2 344 + version: 12.0.2 339 345 340 346 packages/lexicons: 341 347 dependencies: ··· 3686 3692 3687 3693 dotenv-expand@11.0.7: 3688 3694 resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} 3695 + engines: {node: '>=12'} 3696 + 3697 + dotenv-expand@12.0.2: 3698 + resolution: {integrity: sha512-lXpXz2ZE1cea1gL4sz2Ipj8y4PiVjytYr3Ij0SWoms1PGxIv7m2CRKuRuCRtHdVuvM/hNJPMxt5PbhboNC4dPQ==} 3689 3699 engines: {node: '>=12'} 3690 3700 3691 3701 dotenv@16.4.7: ··· 11013 11023 domhandler: 5.0.3 11014 11024 11015 11025 dotenv-expand@11.0.7: 11026 + dependencies: 11027 + dotenv: 16.4.7 11028 + 11029 + dotenv-expand@12.0.2: 11016 11030 dependencies: 11017 11031 dotenv: 16.4.7 11018 11032