progressively working deploy scenario
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.devcontainer
|
||||||
|
.vscode
|
||||||
|
.claude
|
||||||
|
backend
|
||||||
|
docker-compose*.yml
|
||||||
|
.env*
|
||||||
|
*.md
|
||||||
|
.DS_Store
|
||||||
+21
-6
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -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:
|
|
||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user