- gRPC service with MusicBrainz provider - PostgreSQL schema with migrations - Service layer with database-first caching - Repository pattern for data access - YAML configuration support - Research documentation for 17 music metadata projects
23 KiB
Bedrock-API Deployment
Containerization
Dockerfile
File: Dockerfile
Strategy: Multi-stage build (builder + runtime)
# Builder stage
FROM golang:1.23-alpine AS builder
WORKDIR /app
# Install git (required for submodules)
RUN apk add --no-cache git
# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Initialize submodules
RUN git submodule update --init --recursive
# Build binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o bedrock-server ./bedrock_server
# Runtime stage
FROM alpine:latest
# Install ca-certificates (required for HTTPS requests to provider APIs)
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Copy binary from builder
COPY --from=builder /app/bedrock-server .
# Copy migrations (if needed)
COPY --from=builder /app/db/migrations ./db/migrations
# Expose ports
EXPOSE 50052 8080
# Run server
CMD ["./bedrock-server"]
Build Stages:
-
Builder (
golang:1.23-alpine):- Installs git for submodule support
- Downloads Go dependencies
- Initializes spotapi-go submodule
- Compiles binary with optimizations (
-ldflags="-w -s") - CGO disabled for static binary
-
Runtime (
alpine:latest):- Minimal image (~5 MB base)
- Installs ca-certificates for HTTPS
- Copies binary from builder
- Exposes gRPC (50052) and HTTP (8080) ports
Image Size: ~20 MB (builder stage discarded)
Version Mismatch: Dockerfile uses Go 1.23, but go.mod specifies 1.25
Fix:
FROM golang:1.25-alpine AS builder
Docker Build
Build Command:
docker build -t bedrock-api:latest .
Build Arguments (not implemented):
ARG GO_VERSION=1.25
FROM golang:${GO_VERSION}-alpine AS builder
Build Time: ~2-3 minutes (first build), ~30 seconds (cached)
Docker Run
Run Command:
docker run -d \
--name bedrock-api \
-p 50052:50052 \
-p 8080:8080 \
-e DATABASE_URL=postgresql://user:pass@host:5432/bedrock \
-e JWT_SECRET=your-secret \
-e SPOTIFY_CLIENT_ID=your-id \
-e SPOTIFY_CLIENT_SECRET=your-secret \
-e SOUNDCLOUD_CLIENT_IDS=id1,id2,id3 \
-e GENIUS_ACCESS_TOKEN=your-token \
bedrock-api:latest
Environment Variables: Passed via -e flags (no .env file in container)
Port Mapping:
50052:50052- gRPC server8080:8080- HTTP proxy
No Volume Mounts: Binary is stateless (no local file storage)
Docker Compose
Compose File
File: docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: bedrock-postgres
environment:
POSTGRES_USER: bedrock
POSTGRES_PASSWORD: bedrock
POSTGRES_DB: bedrock
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U bedrock"]
interval: 10s
timeout: 5s
retries: 5
networks:
- bedrock-network
volumes:
postgres_data:
driver: local
networks:
bedrock-network:
driver: bridge
Services: PostgreSQL only (application not included)
Missing Services:
- No application service (must be added or run separately)
- No Redis (planned for caching)
- No reverse proxy (nginx, Caddy)
- No monitoring (Prometheus, Grafana)
Complete Compose File (Recommended)
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: bedrock-postgres
environment:
POSTGRES_USER: bedrock
POSTGRES_PASSWORD: bedrock
POSTGRES_DB: bedrock
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/migrations:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U bedrock"]
interval: 10s
timeout: 5s
retries: 5
networks:
- bedrock-network
bedrock-api:
build: .
container_name: bedrock-api
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql://bedrock:bedrock@postgres:5432/bedrock?sslmode=disable
JWT_SECRET: ${JWT_SECRET}
SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID}
SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET}
SOUNDCLOUD_CLIENT_IDS: ${SOUNDCLOUD_CLIENT_IDS}
GENIUS_ACCESS_TOKEN: ${GENIUS_ACCESS_TOKEN}
YOUTUBE_COOKIES: ${YOUTUBE_COOKIES}
ports:
- "50052:50052"
- "8080:8080"
networks:
- bedrock-network
restart: unless-stopped
volumes:
postgres_data:
networks:
bedrock-network:
Improvements:
- Application service added
- Health check dependency (waits for PostgreSQL)
- Environment variables from
.envfile - Automatic restart policy
- Migration initialization via volume mount
Compose Commands
Start Services:
docker-compose up -d
View Logs:
docker-compose logs -f bedrock-api
Stop Services:
docker-compose down
Rebuild:
docker-compose up -d --build
Clean Volumes:
docker-compose down -v
Local Development
Prerequisites
- Go 1.25+
- PostgreSQL 15+
- Git (for submodules)
Setup Steps
1. Clone Repository:
git clone https://github.com/feralbureau/bedrock-api
cd bedrock-api
2. Initialize Submodules:
git submodule update --init --recursive
3. Install Dependencies:
go mod download
4. Setup Database:
# Start PostgreSQL (Docker)
docker run -d \
--name bedrock-postgres \
-e POSTGRES_USER=bedrock \
-e POSTGRES_PASSWORD=bedrock \
-e POSTGRES_DB=bedrock \
-p 5432:5432 \
postgres:15-alpine
# Run migrations
psql postgresql://bedrock:bedrock@localhost:5432/bedrock -f db/migrations/001_create_users_table.up.sql
5. Configure Environment:
cp .env.example .env
# Edit .env with your credentials
Example .env:
DATABASE_URL=postgresql://bedrock:bedrock@localhost:5432/bedrock?sslmode=disable
JWT_SECRET=your-secret-key-change-this-in-production
SPOTIFY_CLIENT_ID=your_spotify_client_id
SPOTIFY_CLIENT_SECRET=your_spotify_client_secret
SOUNDCLOUD_CLIENT_IDS=client_id_1,client_id_2,client_id_3
DEEZER_APP_ID=your_deezer_app_id
YOUTUBE_COOKIES=your_youtube_cookies
GENIUS_ACCESS_TOKEN=your_genius_access_token
6. Run Server:
go run ./bedrock_server
7. Verify:
# gRPC health check (requires grpcurl)
grpcurl -plaintext localhost:50052 bedrock.BedrockService/GetServiceStatus
# HTTP proxy check
curl http://localhost:8080/stream/soundcloud/1234567890
Development Workflow
Hot Reload (not configured):
# Install air
go install github.com/cosmtrek/air@latest
# Run with hot reload
air
Example .air.toml:
root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -o ./tmp/main ./bedrock_server"
bin = "tmp/main"
include_ext = ["go", "proto"]
exclude_dir = ["tmp", "vendor"]
delay = 1000
Testing
Run Tests:
go test ./...
Integration Tests (requires provider credentials):
export SPOTIFY_CLIENT_ID=your_id
export SPOTIFY_CLIENT_SECRET=your_secret
export SOUNDCLOUD_CLIENT_IDS=your_ids
export GENIUS_ACCESS_TOKEN=your_token
export BEDROCK_TEST_ADDR=localhost:50052
go test -v ./tests/
Test Coverage:
go test -cover ./...
CI/CD Pipeline
GitHub Actions
Workflows:
test.yml- Integration testslint.yml- Code linting
Test Workflow
File: .github/workflows/test.yml
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_USER: bedrock
POSTGRES_PASSWORD: bedrock
POSTGRES_DB: bedrock
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.24'
- name: Download dependencies
run: go mod download
- name: Run migrations
run: |
psql postgresql://bedrock:bedrock@localhost:5432/bedrock -f db/migrations/001_create_users_table.up.sql
- name: Run tests
env:
DATABASE_URL: postgresql://bedrock:bedrock@localhost:5432/bedrock?sslmode=disable
JWT_SECRET: test-secret
SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }}
SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }}
SOUNDCLOUD_CLIENT_IDS: ${{ secrets.SOUNDCLOUD_CLIENT_IDS }}
GENIUS_ACCESS_TOKEN: ${{ secrets.GENIUS_ACCESS_TOKEN }}
YOUTUBE_COOKIES: ${{ secrets.YOUTUBE_COOKIES }}
run: go test -v -timeout 120s ./tests/
Features:
- PostgreSQL service container
- Submodule initialization
- Go 1.24 (should be 1.25 to match go.mod)
- Migration execution
- Integration tests with provider secrets
- 120 second timeout
Required Secrets:
SPOTIFY_CLIENT_IDSPOTIFY_CLIENT_SECRETSOUNDCLOUD_CLIENT_IDSGENIUS_ACCESS_TOKENYOUTUBE_COOKIES
Lint Workflow
File: .github/workflows/lint.yml
name: Lint
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
golangci-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.24'
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
comment-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Check for decorative comments
run: |
# Fail if decorative comments found (e.g., // ========)
if grep -r "^[[:space:]]*//[[:space:]]*[=\-*#]\{3,\}" --include="*.go" .; then
echo "Decorative comments found"
exit 1
fi
- name: Check for uppercase-leading comments
run: |
# Fail if comments start with uppercase (except TODO, FIXME, NOTE)
if grep -r "^[[:space:]]*//[[:space:]]*[A-Z]" --include="*.go" . | grep -v "TODO\|FIXME\|NOTE"; then
echo "Uppercase-leading comments found"
exit 1
fi
Linters:
golangci-lint- Standard Go linting (gofmt, govet, staticcheck, etc.)- Custom comment linter - Enforces comment style (no decorative comments, no uppercase-leading)
Comment Rules:
- No decorative comments (
// ========,// --------, etc.) - No uppercase-leading comments (except
TODO,FIXME,NOTE)
Production Deployment
Reverse Proxy (TLS Termination)
No Built-in TLS: Application must be deployed behind reverse proxy
Nginx Example:
upstream bedrock_grpc {
server localhost:50052;
}
upstream bedrock_http {
server localhost:8080;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
# gRPC endpoint
location /bedrock.BedrockService/ {
grpc_pass grpc://bedrock_grpc;
grpc_set_header X-Real-IP $remote_addr;
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# HTTP proxy endpoints
location /stream/ {
proxy_pass http://bedrock_http;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off;
}
location /cover/ {
proxy_pass http://bedrock_http;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Caddy Example (simpler):
api.example.com {
reverse_proxy /bedrock.BedrockService/* h2c://localhost:50052
reverse_proxy /stream/* localhost:8080
reverse_proxy /cover/* localhost:8080
}
Systemd Service
File: /etc/systemd/system/bedrock-api.service
[Unit]
Description=Bedrock API Server
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=bedrock
Group=bedrock
WorkingDirectory=/opt/bedrock-api
EnvironmentFile=/opt/bedrock-api/.env
ExecStart=/opt/bedrock-api/bedrock-server
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=bedrock-api
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/bedrock-api/logs
[Install]
WantedBy=multi-user.target
Commands:
# Enable service
sudo systemctl enable bedrock-api
# Start service
sudo systemctl start bedrock-api
# Check status
sudo systemctl status bedrock-api
# View logs
sudo journalctl -u bedrock-api -f
Environment Variables (Production)
Secure Storage: Use secrets management (not .env file)
AWS Secrets Manager:
aws secretsmanager get-secret-value --secret-id bedrock-api/production --query SecretString --output text > /tmp/secrets.env
source /tmp/secrets.env
rm /tmp/secrets.env
HashiCorp Vault:
vault kv get -format=json secret/bedrock-api/production | jq -r '.data.data | to_entries[] | "\(.key)=\(.value)"' > /tmp/secrets.env
source /tmp/secrets.env
rm /tmp/secrets.env
Kubernetes Secrets:
apiVersion: v1
kind: Secret
metadata:
name: bedrock-api-secrets
type: Opaque
stringData:
DATABASE_URL: postgresql://user:pass@postgres:5432/bedrock
JWT_SECRET: your-secret
SPOTIFY_CLIENT_ID: your-id
SPOTIFY_CLIENT_SECRET: your-secret
SOUNDCLOUD_CLIENT_IDS: id1,id2,id3
GENIUS_ACCESS_TOKEN: your-token
Database Migrations (Production)
Manual Execution (current):
psql $DATABASE_URL -f db/migrations/001_create_users_table.up.sql
Automated with golang-migrate (recommended):
# Install migrate
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.16.2/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/local/bin/
# Run migrations
migrate -path db/migrations -database $DATABASE_URL up
# Rollback
migrate -path db/migrations -database $DATABASE_URL down 1
Migration Tracking:
-- golang-migrate creates this table automatically
SELECT * FROM schema_migrations;
Monitoring (Not Implemented)
Recommended Stack:
- Prometheus (metrics collection)
- Grafana (visualization)
- Loki (log aggregation)
- Jaeger (distributed tracing)
Prometheus Metrics (to implement):
import "github.com/prometheus/client_golang/prometheus"
var (
requestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "bedrock_requests_total",
Help: "Total number of requests",
},
[]string{"method", "status"},
)
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "bedrock_request_duration_seconds",
Help: "Request duration in seconds",
},
[]string{"method"},
)
providerErrors = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "bedrock_provider_errors_total",
Help: "Total provider errors",
},
[]string{"provider"},
)
)
Grafana Dashboard (example queries):
# Request rate
rate(bedrock_requests_total[5m])
# Error rate
rate(bedrock_requests_total{status="error"}[5m]) / rate(bedrock_requests_total[5m])
# P95 latency
histogram_quantile(0.95, rate(bedrock_request_duration_seconds_bucket[5m]))
# Provider error rate
rate(bedrock_provider_errors_total[5m])
Logging (Production)
Structured Logging (to implement):
import "go.uber.org/zap"
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("search request",
zap.String("query", query),
zap.Int32("limit", limit),
zap.String("user_id", userID),
)
logger.Error("provider failed",
zap.String("provider", "spotify"),
zap.Error(err),
)
Log Aggregation (Loki):
# promtail config
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: bedrock-api
static_configs:
- targets:
- localhost
labels:
job: bedrock-api
__path__: /var/log/bedrock-api/*.log
Backup Strategy
PostgreSQL Backups:
# Daily backup script
#!/bin/bash
BACKUP_DIR=/backups/bedrock-api
DATE=$(date +%Y%m%d_%H%M%S)
pg_dump $DATABASE_URL | gzip > $BACKUP_DIR/bedrock_$DATE.sql.gz
# Keep last 30 days
find $BACKUP_DIR -name "bedrock_*.sql.gz" -mtime +30 -delete
# Upload to S3
aws s3 cp $BACKUP_DIR/bedrock_$DATE.sql.gz s3://backups/bedrock-api/
Cron Schedule:
0 2 * * * /opt/bedrock-api/scripts/backup.sh
Point-in-Time Recovery (WAL archiving):
-- Enable WAL archiving in postgresql.conf
wal_level = replica
archive_mode = on
archive_command = 'aws s3 cp %p s3://backups/bedrock-api/wal/%f'
Scaling Strategies
Vertical Scaling:
- Increase CPU/RAM for single instance
- Increase PostgreSQL resources
- Increase connection pool size
Horizontal Scaling:
- Run multiple application instances behind load balancer
- Use read replicas for PostgreSQL (if read-heavy)
- Add Redis for caching (reduce provider API calls)
Load Balancer (nginx):
upstream bedrock_grpc {
server bedrock-api-1:50052;
server bedrock-api-2:50052;
server bedrock-api-3:50052;
}
server {
listen 443 ssl http2;
location /bedrock.BedrockService/ {
grpc_pass grpc://bedrock_grpc;
}
}
Kubernetes Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: bedrock-api
spec:
replicas: 3
selector:
matchLabels:
app: bedrock-api
template:
metadata:
labels:
app: bedrock-api
spec:
containers:
- name: bedrock-api
image: bedrock-api:latest
ports:
- containerPort: 50052
name: grpc
- containerPort: 8080
name: http
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: bedrock-api-secrets
key: DATABASE_URL
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
exec:
command:
- grpc_health_probe
- -addr=:50052
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
exec:
command:
- grpc_health_probe
- -addr=:50052
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: bedrock-api
spec:
selector:
app: bedrock-api
ports:
- name: grpc
port: 50052
targetPort: 50052
- name: http
port: 8080
targetPort: 8080
type: LoadBalancer
Deployment Checklist
Pre-Deployment
- Update Go version in Dockerfile to match go.mod (1.25)
- Configure environment variables (secrets management)
- Run database migrations
- Test provider credentials
- Configure reverse proxy (TLS)
- Set up monitoring (Prometheus, Grafana)
- Set up logging (structured logs, aggregation)
- Configure backups (PostgreSQL, WAL archiving)
- Load test (ensure performance under load)
- Security audit (JWT secret, database credentials, etc.)
Post-Deployment
- Verify gRPC endpoint (grpcurl)
- Verify HTTP proxy endpoints (curl)
- Check logs for errors
- Monitor metrics (request rate, error rate, latency)
- Test authentication (register, login, refresh)
- Test search (all providers)
- Test streaming (SoundCloud, YouTube Music)
- Test lyrics (LrcLib, Genius)
- Verify database connection
- Test backup restoration
Ongoing Maintenance
- Monitor provider API changes
- Rotate JWT secret periodically
- Update dependencies (go mod tidy)
- Review logs for errors
- Monitor disk usage (PostgreSQL, logs)
- Test backup restoration monthly
- Update TLS certificates (Let's Encrypt auto-renewal)
- Review security advisories (Go, dependencies)
Deployment Environments
Development
Infrastructure: Local machine or Docker Compose
Database: PostgreSQL in Docker
Secrets: .env file
TLS: No (HTTP only)
Monitoring: No
Backups: No
Staging
Infrastructure: Single VM or Kubernetes cluster
Database: Managed PostgreSQL (AWS RDS, Google Cloud SQL)
Secrets: Secrets manager (AWS Secrets Manager, Vault)
TLS: Yes (Let's Encrypt)
Monitoring: Prometheus + Grafana
Backups: Daily automated backups
Production
Infrastructure: Kubernetes cluster (multi-region)
Database: Managed PostgreSQL with read replicas
Secrets: Secrets manager with rotation
TLS: Yes (Let's Encrypt or commercial cert)
Monitoring: Full observability stack (Prometheus, Grafana, Loki, Jaeger)
Backups: Hourly backups + WAL archiving + point-in-time recovery
Scaling: Horizontal pod autoscaling (HPA)
High Availability: Multi-zone deployment, load balancing
Cost Estimation (AWS)
Small Deployment (1000 requests/day)
| Resource | Specification | Monthly Cost |
|---|---|---|
| EC2 Instance | t3.small (2 vCPU, 2 GB RAM) | $15 |
| RDS PostgreSQL | db.t3.micro (1 vCPU, 1 GB RAM) | $15 |
| Load Balancer | Application Load Balancer | $20 |
| Data Transfer | 100 GB/month | $9 |
| Total | $59/month |
Medium Deployment (100k requests/day)
| Resource | Specification | Monthly Cost |
|---|---|---|
| EC2 Instances | 3x t3.medium (2 vCPU, 4 GB RAM) | $90 |
| RDS PostgreSQL | db.t3.small (2 vCPU, 2 GB RAM) | $30 |
| ElastiCache Redis | cache.t3.micro (1 vCPU, 0.5 GB RAM) | $12 |
| Load Balancer | Application Load Balancer | $20 |
| Data Transfer | 1 TB/month | $90 |
| Total | $242/month |
Large Deployment (1M requests/day)
| Resource | Specification | Monthly Cost |
|---|---|---|
| EKS Cluster | Control plane | $73 |
| EC2 Instances | 10x t3.large (2 vCPU, 8 GB RAM) | $600 |
| RDS PostgreSQL | db.r5.large (2 vCPU, 16 GB RAM) + read replica | $300 |
| ElastiCache Redis | cache.r5.large (2 vCPU, 13 GB RAM) | $150 |
| Load Balancer | Application Load Balancer | $20 |
| Data Transfer | 10 TB/month | $900 |
| Total | $2,043/month |
Note: Costs exclude provider API fees (Spotify, Genius, etc.)
Deployment Recommendations for Metadata Aggregator
Adopt
- Multi-stage Docker build (minimal runtime image)
- Docker Compose for local development
- GitHub Actions for CI/CD
- Reverse proxy for TLS termination
- Systemd service for production
Avoid
- Manual migrations (use golang-migrate)
- No monitoring (implement Prometheus)
- No structured logging (use zap or zerolog)
- Go version mismatch (keep Dockerfile and go.mod in sync)
Enhance
- Add health check endpoint (implement GetServiceStatus properly)
- Add graceful shutdown (handle SIGTERM)
- Add readiness probe (check database connection)
- Add metrics endpoint (/metrics for Prometheus)
- Add Redis for caching
- Add backup automation
- Add deployment documentation