Loading s7nsa_10_application...
networks: demo-network:
volumes: demo-storage: driver_opts: type: none o: bind device: /tmp/podman_${USER}/STORAGE
services: demo-db: image: postgres:alpine networks: - demo-network volumes: - demo-storage:/var/lib/postgresql environment: - POSTGRES_DB=demo_db - POSTGRES_USER=demo_user - POSTGRES_PASSWORD=demo_secret
demo-app: image: sh_db_demo networks: - demo-network ports: - 9988:9988 environment: - DB_HOST=demo-db - DB_PORT=5432 - DB_NAME=demo_db - DB_USER=demo_user - DB_PASS=demo_secret depends_on: - demo-db
APP_DB_NAME=demo_db APP_DB_USER=demo_user APP_DB_PASS=demo_secret
... - POSTGRES_DB=${APP_DB_NAME} - POSTGRES_USER=${APP_DB_USER} - POSTGRES_PASSWORD=${APP_DB_PASS} ... - DB_NAME=${APP_DB_NAME} - DB_USER=${APP_DB_USER} - DB_PASS=${APP_DB_PASS} ...
networks: internal:
services: backend: build: ./py_backend networks: - internal
app_demo
├── compose.yaml
└── py_backend
├── Dockerfile
├── py_backend.py
└── requirements.txt
import os import contextlib import fastapi import psycopg_pool import pydantic
@contextlib.asynccontextmanager async def app_lifespan(app: fastapi.FastAPI): print('demo starting up...', flush=True) # ... print('demo startup done', flush=True) yield print('demo shutting down...', flush=True) # ... print('demo shutdown done', flush=True)
app=fastapi.FastAPI(lifespan=app_lifespan)
@app.get('/api/health') def health(): return {'status': 'ok'}
uvicorn[standard] fastapi psycopg[binary] psycopg-pool pydantic
FROM python:alpine
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1
WORKDIR /demo COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY py_backend.py .
CMD ["uvicorn", "py_backend:app", "--host", "0.0.0.0", "--port", "9988"]
app_demo
├── compose.yaml
├── nginx.conf
├── py_backend
│ ├── Dockerfile
│ ├── py_backend.py
│ └── requirements.txt
└── site
├── app_demo.html
└── favicon.ico
access_log /dev/stdout; error_log /dev/stderr; server { listen ${NGINX_PORT}; server_name _; location /api/ { proxy_pass http://${API_HOST}:${API_PORT}; # no final / ~~> keep /api/ 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; } location / { root /srv/http; autoindex on; try_files $uri $uri/ =404; } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="app_demo"> <title>App Demo</title> </head> <body> <h3>App Demo</h3> <p><a href="/api/health"><tt>/api/health</tt></a></p> <p><a href="/api/what"><tt>/api/what</tt></a></p> <p><a href="/inexistent"><tt>/</tt></a></p> <p><a href="/"><tt>/</tt></a></p> </body> </html>
frontend: image: nginx:alpine networks: - internal ports: - 8080:8080 volumes: - ./nginx.conf:/etc/nginx/templates/default.conf.template:ro - ./site:/srv/http/:ro environment: - NGINX_PORT=8080 - API_HOST=backend - API_PORT=9988 depends_on: - backend
app_demo
├── .env
├── compose.yaml
├── init.sql
├── nginx.conf
├── py_backend
│ ├── Dockerfile
│ ├── py_backend.py
│ └── requirements.txt
└── site
├── app_demo.html
└── favicon.ico
APP_DB_NAME=demo_db APP_DB_USER=demo_user APP_DB_PASS=demo_secret
DO $$ BEGIN RAISE NOTICE 'creating tables...'; END $$; \c demo_db CREATE TABLE reminder ( id SERIAL PRIMARY KEY, item TEXT );
... volumes: storage: driver_opts: type: none o: bind device: /tmp/podman_${USER}/STORAGE
services: db: image: postgres:alpine networks: - internal volumes: - storage:/var/lib/postgresql - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro environment: - POSTGRES_DB=${APP_DB_NAME} - POSTGRES_USER=${APP_DB_USER} - POSTGRES_PASSWORD=${APP_DB_PASS}
backend: build: ./py_backend networks: - internal environment: - DB_HOST=db - DB_PORT=5432 - DB_NAME=${APP_DB_NAME} - DB_USER=${APP_DB_USER} - DB_PASS=${APP_DB_PASS} depends_on: - db ...
import os import contextlib import fastapi import psycopg_pool import pydantic
@contextlib.asynccontextmanager async def app_lifespan(app: fastapi.FastAPI): print('demo starting up...', flush=True) info='host={} port={} dbname={} user={} password={}'.format( os.getenv('DB_HOST'), os.getenv('DB_PORT'), os.getenv('DB_NAME'), os.getenv('DB_USER'), os.getenv('DB_PASS')) app.state.db_pool=psycopg_pool.ConnectionPool( conninfo=info, min_size=1, max_size=10) app.state.db_pool.wait() print('demo startup done', flush=True) yield print('demo shutting down...', flush=True) if not app.state.db_pool.closed: app.state.db_pool.close() print('demo shutdown done', flush=True)
app=fastapi.FastAPI(lifespan=app_lifespan)
@app.get('/api/health') def health(): with app.state.db_pool.connection() as conn: with conn.cursor() as cur: # cur.execute('SELECT 1 FROM unknown_table LIMIT 1') # intentional error cur.execute('SELECT 1 FROM reminder LIMIT 1') return {'status': 'ok'}
@app.get('/api/reminders') def list_reminders(): with app.state.db_pool.connection() as conn: with conn.cursor() as cur: cur.execute('SELECT id, item FROM reminder') rows=cur.fetchall() return [{'id': r[0], 'item': r[1]} for r in rows]
class AddReminderArg(pydantic.BaseModel): item: str
@app.post('/api/reminders') def add_reminder(arg: AddReminderArg): with app.state.db_pool.connection() as conn: with conn.cursor() as cur: cur.execute( 'INSERT INTO reminder (item) VALUES (%s) RETURNING id', (arg.item,)) item_id=cur.fetchone()[0] return {'id': item_id, 'item': arg.item}
@app.delete('/api/reminders') def delete_reminders(): with app.state.db_pool.connection() as conn: with conn.cursor() as cur: cur.execute('DELETE FROM reminder') return {'cleared': cur.rowcount}
@app.delete("/api/reminders/{reminder_id}") def delete_reminder(reminder_id: int): with app.state.db_pool.connection() as conn: with conn.cursor() as cur: cur.execute('DELETE FROM reminder WHERE id=%s', (reminder_id,)) return {'deleted': reminder_id}
curl http://localhost:8080/api/health
curl http://localhost:8080/api/reminders
curl -X POST http://localhost:8080/api/reminders \ -H 'Content-Type: application/json' \ -d '{"item": "Something new"}'
curl -X DELETE http://localhost:8080/api/reminders
curl -X DELETE http://localhost:8080/api/reminders/1
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="reminders"> <title>Reminders</title> <script> 'use strict'; window.addEventListener('load', () => {
//~~~~ variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const API = '/api/reminders'; const list = document.getElementById('list'); const status = document.getElementById('status'); const input = document.getElementById('inp_item');
//~~~~ functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
function set_status(msg) { status.textContent = msg; }
function escape_html(s) { return s.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
async function load_reminders() { set_status('loading...'); const res = await fetch(API); if (!res.ok) throw new Error('GET failed'); const data = await res.json(); list.innerHTML = ''; data.forEach(({ id, item }) => { const li = document.createElement('div'); li.innerHTML = ` <button data-id="${id}">✖</button> <span><strong>#${id}</strong> ${escape_html(item)}</span> `; list.appendChild(li); }); set_status(`${data.length} item(s)`); }
async function add_reminder(item) { const res = await fetch(API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item }) }); if (!res.ok) throw new Error('POST failed'); }
async function delete_reminder(id) { const res = await fetch(`${API}/${id}`, { method: 'DELETE' }); if (!res.ok) throw new Error('DELETE failed'); }
async function clear_reminders() { const res = await fetch(API, { method: 'DELETE' }); if (!res.ok) throw new Error('DELETE all failed'); }
//~~~~ initialisation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
document.getElementById('form_add').addEventListener('submit', async (e) => { e.preventDefault(); const value = input.value.trim(); if (!value) return; try { await add_reminder(value); input.value = ''; await load_reminders(); } catch (err) { set_status('Error while adding item'); console.error(err); } });
list.addEventListener('click', async (e) => { const btn = e.target.closest('button[data-id]'); if (!btn) return; const id = btn.dataset.id; try { await delete_reminder(id); await load_reminders(); } catch (err) { set_status('Error while deleting item'); console.error(err); } });
document.getElementById('btn_reload').addEventListener('click', () => load_reminders().catch(err => { set_status('Error while loading items'); console.error(err); }));
document.getElementById('btn_clear').addEventListener('click', async () => { try { await clear_reminders(); await load_reminders(); } catch (err) { set_status('Error while clearing items'); console.error(err); } });
load_reminders().catch(err => { set_status('Error while initial loading'); console.error(err); });
}); // window load event-listener </script> </head> <body> <h3>Reminders</h3> <form id="form_add"> <input id="inp_item" type="text" placeholder="New item" autocomplete="off" required> <button type="submit">Add</button> </form> <div> <button id="btn_reload" type="button">Reload</button> <button id="btn_clear" type="button">Clear</button> </div> <div id="status"></div> <hr> <div id="list"></div> <hr> </body> </html>
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \ -keyout https_server.key -out https_server.crt \ -subj '/CN=localhost'
access_log /dev/stdout; error_log /dev/stderr; server { # redirect HTTP to HTTPS listen ${NGINX_PORT}; server_name _; return 301 https://$host:${NGINX_HTTPS_PORT}$request_uri; } server { # HTTPS listen ${NGINX_HTTPS_PORT} ssl; server_name _; ssl_certificate /etc/nginx/certs/server.crt; ssl_certificate_key /etc/nginx/certs/server.key; location /api/ { proxy_pass http://${API_HOST}:${API_PORT}; # no final / ~~> keep /api/ 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; } location / { root /srv/http; autoindex on; try_files $uri $uri/ =404; } }
frontend: image: nginx:alpine networks: - internal ports: - 8080:8080 - 8443:8443 volumes: - ./nginx.conf:/etc/nginx/templates/default.conf.template:ro - ./site:/srv/http/:ro - ./https_server.crt:/etc/nginx/certs/server.crt:ro - ./https_server.key:/etc/nginx/certs/server.key:ro environment: - NGINX_PORT=8080 - NGINX_HTTPS_PORT=8443 - API_HOST=backend - API_PORT=9988 depends_on: - backend