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
# start from an existing image FROM python:alpine
# set environment variables (used by python) ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1
# create a working directory and execute every remaining command from it WORKDIR /py_backend
# copy the specified file(s) from the host to the working directory COPY requirements.txt .
# run this command in the guest system (package installation) RUN pip install --no-cache-dir -r requirements.txt
# copy the specified file(s) from the host to the working directory COPY py_backend.py .
# run this command by default (from the working directory) 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="/unknown"><tt>/unknown</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
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