progressively working deploy scenario

This commit is contained in:
2026-06-19 19:17:22 +00:00
parent 2d2b2d040b
commit c4cc268ea8
14 changed files with 125 additions and 72 deletions
+11
View File
@@ -0,0 +1,11 @@
node_modules
dist
.git
.devcontainer
.vscode
.claude
backend
docker-compose*.yml
.env*
*.md
.DS_Store
+21 -6
View File
@@ -1,9 +1,24 @@
# KMountain Flower Radio Station — Environment Variables # KMountain Flower Radio Station — Environment Variables
# Copy this file to .env and fill in the values. Do NOT commit .env. # Copy this file to .env and adjust values for your environment. Do NOT commit .env.
# PostgreSQL connection string (backend) # ── Database (db service) ─────────────────────────────────────────────
# Format: postgresql+asyncpg://user:password@host:port/database POSTGRES_DB=kmountain
KMTN_DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
# CORS allowlist for the backend (JSON array of allowed origins) # ── Backend API (api service) ─────────────────────────────────────────
KMTN_CORS_ORIGINS=["http://localhost:4200"] # Database connection string. Use postgresql+asyncpg for PostgreSQL, sqlite+aiosqlite for local dev without a DB.
KMTN_DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/kmountain
# JSON array of allowed CORS origins.
# Dev: include localhost variants. Prod: your actual domain(s).
KMTN_CORS_ORIGINS='["http://localhost:4200", "http://localhost"]'
# Environment name. Set to "production" to disable the admin router.
# KMTN_ENV=development
# ── Frontend (frontend service) ───────────────────────────────────────
# URL of the API backend that nginx should proxy /api/ requests to.
# Dev (Docker Compose): http://api:8000 (DNS resolves to the api container)
# Prod: https://api.yourdomain.com (or whatever your API endpoint is)
API_UPSTREAM=http://api:8000
+7 -4
View File
@@ -1,5 +1,5 @@
# ── Build stage ─────────────────────────────────────────────────────── # ── Build stage ───────────────────────────────────────────────────────
FROM node:20-alpine AS build FROM docker.io/library/node:22-alpine AS build
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm ci RUN npm ci
@@ -7,8 +7,11 @@ COPY . .
RUN npx ng build --configuration production RUN npx ng build --configuration production
# ── Serve stage ──────────────────────────────────────────────────────── # ── Serve stage ────────────────────────────────────────────────────────
FROM docker.io/library/nginx:1.30-perl FROM docker.io/library/nginx:alpine
COPY --from=build /app/dist/radio-station/browser /usr/share/nginx/html COPY --from=build /app/dist/radio-station/browser /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf.template /etc/nginx/conf.d/nginx.conf.template
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENV API_UPSTREAM=http://api:8000
EXPOSE 80 EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] ENTRYPOINT ["/docker-entrypoint.sh"]
+13
View File
@@ -0,0 +1,13 @@
__pycache__
*.pyc
*.pyo
.git
.vscode
.claude
*.egg-info
.pytest_cache
.mypy_cache
.env*
docker-compose*.yml
*.md
.DS_Store
+3 -2
View File
@@ -2,11 +2,12 @@ FROM docker.io/library/python:3.12-slim AS base
WORKDIR /app WORKDIR /app
RUN pip install --no-cache-dir gunicorn uvicorn psycopg2-binary ENV PYTHONUNBUFFERED=1
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY . . COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"] EXPOSE 8000
CMD ["gunicorn", "app.main:app", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000", "--workers", "2", "--timeout", "120"]
-33
View File
@@ -1,33 +0,0 @@
# Dev-sidecar compose: app container only (no database service).
# Used by .devcontainer/devcontainer.json via dockerComposeFile.
# Development uses SQLite — no Docker-in-Docker database needed.
services:
app:
build:
context: .
dockerfile: .devcontainer/Dockerfile
restart: "no"
ports:
- "8000:8000" # FastAPI
- "4200:4200" # Angular dev server
environment:
KMTN_DATABASE_URL: sqlite+aiosqlite:///./backend/kmountain.db
KMTN_CORS_ORIGINS: '["http://localhost:4200"]'
volumes:
- ..:/workspaces/web_app:cached
- node_modules:/workspaces/web_app/node_modules
- backend_venv:/app/.venv
command: >
sh -c "
pip install -r backend/requirements.txt &&
cd backend &&
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload &
cd /workspaces/web_app &&
npm install &&
npx ng serve --host 0.0.0.0 --port 4200
"
volumes:
node_modules:
backend_venv:
+19
View File
@@ -0,0 +1,19 @@
# Production override — use with: docker compose -f docker-compose.yml -f docker-compose.prod.yml up
# Assumes: managed PostgreSQL, external reverse proxy / load balancer handling TLS and port 80/443.
services:
# No db service — managed PostgreSQL is assumed.
# Set KMTN_DATABASE_URL in your environment or a .env file.
api:
environment:
KMTN_ENV: production
# Override with your actual production domain(s)
KMTN_CORS_ORIGINS: '["https://yourdomain.com"]'
# No port mapping — traffic arrives via reverse proxy / load balancer
frontend:
environment:
# Point to your API's production URL
API_UPSTREAM: https://api.yourdomain.com
# No port mapping — traffic arrives via reverse proxy / load balancer
+3 -1
View File
@@ -25,7 +25,7 @@ services:
restart: unless-stopped restart: unless-stopped
environment: environment:
KMTN_DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/kmountain KMTN_DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/kmountain
KMTN_CORS_ORIGINS: '["http://localhost:4200"]' KMTN_CORS_ORIGINS: '["http://localhost:4200", "http://localhost"]'
ports: ports:
- "8000:8000" - "8000:8000"
depends_on: depends_on:
@@ -37,6 +37,8 @@ services:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
restart: unless-stopped restart: unless-stopped
environment:
API_UPSTREAM: http://api:8000
ports: ports:
- "4200:80" - "4200:80"
depends_on: depends_on:
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
# Render the nginx config template with environment variables, then start nginx.
set -e
envsubst '${API_UPSTREAM}' < /etc/nginx/conf.d/nginx.conf.template > /etc/nginx/conf.d/default.conf
exec nginx -g 'daemon off;'
-26
View File
@@ -1,26 +0,0 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# SPA fallback — Angular router handles its own paths
location / {
try_files $uri $uri/ /index.html;
}
# Proxy API calls to the backend service (only when running inside Docker)
location /api/ {
# When the env var is set, nginx resolves "api" to the backend container.
# In dev (HOST_MODE=dev) we proxy to localhost:8000 instead.
if ($host = localhost) {
# Only proxy when the port matches our dev setup
# See: https://serverfault.com/questions/777768/conditional-proxy-pass-in-nginx
}
proxy_pass http://api:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
+20
View File
@@ -0,0 +1,20 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# SPA fallback — Angular router handles its own paths
location / {
try_files $uri $uri/ /index.html;
}
# Proxy API calls to the backend service
location /api/ {
proxy_pass ${API_UPSTREAM};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
@@ -50,6 +50,13 @@ export class AdminEventFormComponent implements OnChanges {
this.active = event.active; this.active = event.active;
} }
formatError(err: any): string {
if (err?.error?.detail) return err.error.detail;
if (err?.error?.message) return err.error.message;
if (err?.message) return err.message;
return 'Failed to save. Please try again.';
}
reset(): void { reset(): void {
this.id = null; this.id = null;
this.date = ''; this.date = '';
@@ -60,6 +60,13 @@ export class AdminProgramFormComponent implements OnChanges {
} }
/** Reset the form for a new entry. */ /** Reset the form for a new entry. */
formatError(err: any): string {
if (err?.error?.detail) return err.error.detail;
if (err?.error?.message) return err.error.message;
if (err?.message) return err.message;
return 'Failed to save. Please try again.';
}
reset(): void { reset(): void {
this.id = null; this.id = null;
this.day_of_week = 1; this.day_of_week = 1;
@@ -46,6 +46,13 @@ export class AdminTierFormComponent implements OnChanges {
this.display_order = tier.display_order; this.display_order = tier.display_order;
} }
formatError(err: any): string {
if (err?.error?.detail) return err.error.detail;
if (err?.error?.message) return err.error.message;
if (err?.message) return err.message;
return 'Failed to save. Please try again.';
}
reset(): void { reset(): void {
this.id = null; this.id = null;
this.name = ''; this.name = '';