Compare commits

...

2 Commits

Author SHA1 Message Date
Cauê Faleiros
9d348ca893 fix: support arbitrary production database passwords
All checks were successful
Build and deploy / Validate source (push) Successful in 7s
Build and deploy / Publish images and notify Portainer (push) Successful in 49s
2026-09-18 12:18:43 -03:00
Cauê Faleiros
fbb620bd85 fix: keep web services up during API rollout 2026-09-18 12:17:34 -03:00
4 changed files with 38 additions and 5 deletions

View File

@@ -3,6 +3,11 @@ server {
listen 8080; listen 8080;
server_name ${PUBLIC_HOST}; server_name ${PUBLIC_HOST};
if ($host != ${PUBLIC_HOST}) { return 400; } if ($host != ${PUBLIC_HOST}) { return 400; }
# Resolve through Docker's embedded DNS at request time. This prevents
# Nginx from exiting during a Swarm rollout when the API task is briefly
# unavailable or still creating its database schema.
resolver 127.0.0.11 ipv6=off valid=10s;
set $api_upstream api:8000;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index ${WEB_INDEX}; index ${WEB_INDEX};
@@ -17,7 +22,7 @@ server {
location /api/ { location /api/ {
limit_req zone=api_limit burst=100 nodelay; limit_req zone=api_limit burst=100 nodelay;
limit_req_status 429; limit_req_status 429;
proxy_pass http://api:8000; proxy_pass http://$api_upstream;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Host $host;

View File

@@ -2,7 +2,10 @@ version: "3.8"
x-app-environment: &app-environment x-app-environment: &app-environment
APP_ENV: production APP_ENV: production
DATABASE_URL: postgresql://dtf_app:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@db:5432/dtf DATABASE_HOST: db
DATABASE_NAME: dtf
DATABASE_USER: dtf_app
DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
S3_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT} S3_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT}
S3_PUBLIC_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT} S3_PUBLIC_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT}
S3_BUCKET: ${R2_BUCKET:?set R2_BUCKET} S3_BUCKET: ${R2_BUCKET:?set R2_BUCKET}
@@ -51,7 +54,10 @@ services:
image: ${API_IMAGE:-gitea.blyzer.com.br/blyzer/dtf-api}:${IMAGE_TAG:-latest} image: ${API_IMAGE:-gitea.blyzer.com.br/blyzer/dtf-api}:${IMAGE_TAG:-latest}
command: python -m local.bootstrap command: python -m local.bootstrap
environment: environment:
DATABASE_ADMIN_URL: postgresql://dtf_admin:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@db:5432/dtf DATABASE_ADMIN_HOST: db
DATABASE_ADMIN_NAME: dtf
DATABASE_ADMIN_USER: dtf_admin
DATABASE_ADMIN_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
APP_DB_USER: dtf_app APP_DB_USER: dtf_app
APP_DB_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} APP_DB_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
networks: [backend] networks: [backend]

View File

@@ -4,9 +4,21 @@ from pathlib import Path
import psycopg import psycopg
from psycopg import sql from psycopg import sql
def admin_connect():
if os.environ.get('DATABASE_ADMIN_HOST'):
return psycopg.connect(
host=os.environ['DATABASE_ADMIN_HOST'],
dbname=os.environ['DATABASE_ADMIN_NAME'],
user=os.environ['DATABASE_ADMIN_USER'],
password=os.environ['DATABASE_ADMIN_PASSWORD'],
)
return psycopg.connect(os.environ['DATABASE_ADMIN_URL'])
def main(): def main():
role = os.environ['APP_DB_USER'] role = os.environ['APP_DB_USER']
with psycopg.connect(os.environ['DATABASE_ADMIN_URL']) as c: with admin_connect() as c:
admin, database = c.execute('SELECT current_user,current_database()').fetchone() admin, database = c.execute('SELECT current_user,current_database()').fetchone()
if role == admin: if role == admin:
raise RuntimeError('Application and database administrator must differ') raise RuntimeError('Application and database administrator must differ')

View File

@@ -4,9 +4,19 @@ import psycopg
from psycopg.rows import dict_row from psycopg.rows import dict_row
def connect(): def connect():
# Production passes credentials as discrete libpq fields. This avoids
# treating characters in a generated password as URL syntax. Local and
# test environments retain DATABASE_URL compatibility.
if os.environ.get('DATABASE_HOST'):
return psycopg.connect(
host=os.environ['DATABASE_HOST'],
dbname=os.environ['DATABASE_NAME'],
user=os.environ['DATABASE_USER'],
password=os.environ['DATABASE_PASSWORD'],
row_factory=dict_row,
)
return psycopg.connect(os.environ['DATABASE_URL'], row_factory=dict_row) return psycopg.connect(os.environ['DATABASE_URL'], row_factory=dict_row)
def initialize(): def initialize():
with connect() as c: with connect() as c:
c.execute(Path(__file__).with_name('schema.sql').read_text()) c.execute(Path(__file__).with_name('schema.sql').read_text())