feat(docker): add Docker deployment configuration

Add production-ready Docker setup with multi-stage Dockerfile, docker-compose
orchestration for API, PostgreSQL, Redis, and Nginx services. Includes
health checks, non-root user execution, and proper networking.

- Add multi-stage Dockerfile with gunicorn/gevent workers
- Add docker-compose.yml with api, db, redis, nginx services
- Add nginx reverse proxy configuration with security headers
- Update .env.example with Docker and production variables
- Add email provider configuration (Mailgun, SendGrid)
- Add requests dependency for HTTP client support
- Update documentation with Docker deployment guide
- Rebrand project name from Gatehouse to Secuird
This commit is contained in:
2026-04-04 16:51:19 +10:30
parent 2f2a20adfb
commit d90a06437e
10 changed files with 414 additions and 23 deletions
+33 -4
View File
@@ -2,8 +2,24 @@ FLASK_APP=manage.py
FLASK_ENV=development FLASK_ENV=development
FLASK_DEBUG=1 FLASK_DEBUG=1
# Database # ═════════════════════════════════════════════════════════════════════════════
DATABASE_URL=postgresql://user:password@localhost:5432/gatehouse_dev # Docker / Production
# ═════════════════════════════════════════════════════════════════════════════
COMPOSE_PROJECT_NAME=authy2
FLASK_ENV=production
POSTGRES_USER=authy2
POSTGRES_PASSWORD=changeme-in-production
POSTGRES_DB=authy2
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
SQLALCHEMY_DATABASE_URI=${DATABASE_URL}
REDIS_URL=redis://redis:6379/0
SESSION_REDIS_URL=redis://redis:6379/0
RATELIMIT_STORAGE_URL=redis://redis:6379/1
HTTP_PORT=80
HTTPS_PORT=443
API_PORT=5000
# Database (overridden by Docker values above)
SQLALCHEMY_ECHO=False SQLALCHEMY_ECHO=False
SQLALCHEMY_LOG_LEVEL=WARNING SQLALCHEMY_LOG_LEVEL=WARNING
@@ -15,7 +31,7 @@ CA_ENCRYPTION_KEY=change-me-in-production
BCRYPT_LOG_ROUNDS=12 BCRYPT_LOG_ROUNDS=12
# Session cookies # Session cookies
SESSION_COOKIE_SECURE=False SESSION_COOKIE_SECURE=True
SESSION_COOKIE_SAMESITE=Lax SESSION_COOKIE_SAMESITE=Lax
# Only needed when sharing cookies across subdomains (e.g. api.example.com + ui.example.com) # Only needed when sharing cookies across subdomains (e.g. api.example.com + ui.example.com)
# SESSION_COOKIE_DOMAIN=example.com # SESSION_COOKIE_DOMAIN=example.com
@@ -61,7 +77,7 @@ OIDC_BASE_URL=http://localhost:5000
# WebAuthn # WebAuthn
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
WEBAUTHN_RP_ID=localhost WEBAUTHN_RP_ID=localhost
WEBAUTHN_RP_NAME=Gatehouse WEBAUTHN_RP_NAME=Secuird
WEBAUTHN_ORIGIN=http://localhost:8080 WEBAUTHN_ORIGIN=http://localhost:8080
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@@ -81,6 +97,19 @@ SMTP_USERNAME=
SMTP_PASSWORD= SMTP_PASSWORD=
FROM_ADDRESS=noreply@gatehouse.local FROM_ADDRESS=noreply@gatehouse.local
# Email Provider (smtp, mailgun, sendgrid)
# Note: SMTP is the default. Set to "mailgun" or "sendgrid" to use those providers
EMAIL_PROVIDER=smtp
# Mailgun Configuration (used when EMAIL_PROVIDER=mailgun)
# MAILGUN_API_KEY=your-mailgun-api-key
# MAILGUN_DOMAIN=mg.yourdomain.com
# MAILGUN_API_URL=https://api.mailgun.net/v3
# SendGrid Configuration (used when EMAIL_PROVIDER=sendgrid)
# SENDGRID_API_KEY=SG.your-sendgrid-api-key
# SENDGRID_FROM_EMAIL=noreply@yourdomain.com
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Logging # Logging
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
+1
View File
@@ -136,6 +136,7 @@ Thumbs.db
# Project specific # Project specific
*.db *.db
flask_session/
# Opencode files and folders # Opencode files and folders
.opencode/ .opencode/
+68
View File
@@ -0,0 +1,68 @@
# Multi-stage build for Gatehouse Auth API
# Build stage
FROM python:3.11-slim as builder
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Copy requirements files
WORKDIR /app
COPY requirements/base.txt requirements/base.txt
COPY requirements/production.txt requirements/production.txt
# Install dependencies
RUN pip install --no-cache-dir --upgrade pip wheel && \
pip install --no-cache-dir -r requirements/production.txt
# Production stage
FROM python:3.11-slim
# Install runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd --gid 1000 appgroup && \
useradd --uid 1000 --gid appgroup --shell /bin/bash --create-home appuser
# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Copy application code
WORKDIR /app
COPY --chown=appuser:appgroup . .
# Create log and session directories
RUN mkdir -p /app/logs /app/flask_session && chown -R appuser:appgroup /app/logs /app/flask_session
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 5000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:5000/api/health || exit 1
# Run gunicorn with gevent workers
CMD ["gunicorn", "--bind", "0.0.0.0:5000", \
"--workers", "4", \
"--worker-class", "gevent", \
"--worker-connections", "1000", \
"--timeout", "120", \
"--access-logfile", "-", \
"--error-logfile", "-", \
"--log-level", "info", \
"wsgi:application"]
+102 -14
View File
@@ -64,7 +64,7 @@ python scripts/init_db.py
6. **Seed sample data** (optional): 6. **Seed sample data** (optional):
```bash ```bash
python scripts/seed_data.py python -m scripts.seed_data
``` ```
7. **Run the application**: 7. **Run the application**:
@@ -77,6 +77,71 @@ python wsgi.py
The API will be available at `http://localhost:5000` The API will be available at `http://localhost:5000`
## Docker Deployment
### Prerequisites
- Docker 20.10+
- Docker Compose 2.0+
### Quick Start
1. **Start all services**:
```bash
docker-compose up -d
```
2. **Initialize the database** (run migrations):
```bash
docker-compose exec api python manage.py db upgrade
```
3. **Seed sample data** (optional):
```bash
docker-compose exec api python scripts/seed_data.py
```
4. **Verify health**:
```bash
curl http://localhost:5000/api/health
```
### Useful Commands
```bash
# View logs
docker-compose logs -f api
# Run migrations
docker-compose exec api python manage.py db upgrade
# Open shell in container
docker-compose exec api /bin/bash
# Rebuild after changes
docker-compose up -d --build
# Stop all services
docker-compose down
```
### Environment Variables
Copy `.env.example` to `.env` and configure:
- `POSTGRES_USER` / `POSTGRES_PASSWORD` - Database credentials
- `SECRET_KEY` - Flask secret key (required in production)
- `ENCRYPTION_KEY` - Data encryption key
- `CA_ENCRYPTION_KEY` - CA private key encryption
- `CORS_ORIGINS` - Allowed CORS origins (comma-separated)
### Production Considerations
- Use a strong `SECRET_KEY` (256-bit random)
- Enable HTTPS via nginx (configure SSL certificates)
- Set `BCRYPT_LOG_ROUNDS=13` for stronger password hashing
- Use Redis persistence (`--appendonly yes`)
- Configure log aggregation as needed
## API Endpoints ## API Endpoints
### Authentication ### Authentication
@@ -197,22 +262,45 @@ python manage.py db upgrade
## running seed ## Development Commands
python -m scripts.seed_data
## Running flask in dev ### Run Flask in Development
```bash
FLASK_ENV=development flask run --debug --port 8888 FLASK_ENV=development flask run --debug --port 8888
```
### Seed Sample Data
```bash
python -m scripts.seed_data
# Or with Docker:
docker-compose exec api python scripts/seed_data.py
```
### Database Migration
```bash
# Apply migrations
flask db upgrade
# With Docker:
docker-compose exec api python manage.py db upgrade
```
### SQLite Browser (Development)
```bash
sqlite_web instance/db_file.db --port 9999 --host 0.0.0.0
```
# Test creds ## Test Credentials
## OIDC Client
client_id: acme-portal-001
client_secret: acme_secret_portal_2024
## User ### OIDC Client
email: bob@acme-corp.com | Field | Value |
password: UserPass123! |-------|-------|
| client_id | `acme-portal-001` |
| client_secret | `acme_secret_portal_2024` |
### Test User
## Sqlite editor | Field | Value |
sqlite_web instance/db_file.db --port 9999 --host 0.0.0.0 |-------|-------|
| email | `bob@acme-corp.com` |
| password | `UserPass123!` |
+13 -1
View File
@@ -123,7 +123,7 @@ class BaseConfig:
# WebAuthn Configuration # WebAuthn Configuration
WEBAUTHN_RP_ID = os.getenv("WEBAUTHN_RP_ID", "localhost") WEBAUTHN_RP_ID = os.getenv("WEBAUTHN_RP_ID", "localhost")
WEBAUTHN_RP_NAME = os.getenv("WEBAUTHN_RP_NAME", "Gatehouse") WEBAUTHN_RP_NAME = os.getenv("WEBAUTHN_RP_NAME", "Secuird")
WEBAUTHN_ORIGIN = os.getenv("WEBAUTHN_ORIGIN", "https://ui.webauthn.local") WEBAUTHN_ORIGIN = os.getenv("WEBAUTHN_ORIGIN", "https://ui.webauthn.local")
# Frontend URL (for OAuth callback redirects) # Frontend URL (for OAuth callback redirects)
@@ -140,3 +140,15 @@ class BaseConfig:
SMTP_USERNAME = os.getenv("SMTP_USERNAME", "") SMTP_USERNAME = os.getenv("SMTP_USERNAME", "")
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "") SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "")
FROM_ADDRESS = os.getenv("FROM_ADDRESS", "noreply@gatehouse.local") FROM_ADDRESS = os.getenv("FROM_ADDRESS", "noreply@gatehouse.local")
# Email Provider Configuration
EMAIL_PROVIDER = os.getenv("EMAIL_PROVIDER", "smtp").lower() # smtp, mailgun, sendgrid
# Mailgun Configuration (used when EMAIL_PROVIDER=mailgun)
MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY", "")
MAILGUN_DOMAIN = os.getenv("MAILGUN_DOMAIN", "")
MAILGUN_API_URL = os.getenv("MAILGUN_API_URL", "https://api.mailgun.net/v3")
# SendGrid Configuration (used when EMAIL_PROVIDER=sendgrid)
SENDGRID_API_KEY = os.getenv("SENDGRID_API_KEY", "")
SENDGRID_FROM_EMAIL = os.getenv("SENDGRID_FROM_EMAIL", "")
+93
View File
@@ -0,0 +1,93 @@
version: '3.8'
services:
api:
build:
context: .
dockerfile: Dockerfile
env_file:
- .env
environment:
- FLASK_ENV=production
- CORS_ORIGINS=http://192.168.50.124:8080,http://localhost:8080,http://localhost:5173
- DATABASE_URL=postgresql://${POSTGRES_USER:-gatehouse}:${POSTGRES_PASSWORD:-gatehouse}@db:5432/${POSTGRES_DB:-gatehouse}
- SQLALCHEMY_DATABASE_URI=postgresql://${POSTGRES_USER:-gatehouse}:${POSTGRES_PASSWORD:-gatehouse}@db:5432/${POSTGRES_DB:-gatehouse}
- REDIS_URL=redis://redis:6379/0
- SESSION_REDIS_URL=redis://redis:6379/0
- RATELIMIT_STORAGE_URL=redis://redis:6379/1
ports:
- "${API_PORT:-5000}:5000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- authy2-network
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=${POSTGRES_USER:-gatehouse}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-gatehouse}
- POSTGRES_DB=${POSTGRES_DB:-gatehouse}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- authy2-network
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-gatehouse} -d ${POSTGRES_DB:-gatehouse}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
ports:
- "5432:5432"
redis:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
- authy2-network
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
nginx:
image: nginx:1.27-alpine
volumes:
- ./docker/nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
depends_on:
- api
networks:
- authy2-network
restart: unless-stopped
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 30s
timeout: 10s
retries: 3
networks:
authy2-network:
driver: bridge
volumes:
postgres_data:
redis_data:
+97
View File
@@ -0,0 +1,97 @@
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types text/plain text/css text/xml application/json application/javascript
application/xml application/xml+rss text/javascript application/x-javascript;
upstream api {
server api:5000;
}
server {
listen 80;
server_name localhost;
# Health check endpoint
location /health {
proxy_pass http://api;
proxy_http_version 1.1;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# API routes
location /api/ {
proxy_pass http://api;
proxy_http_version 1.1;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
# Increase buffer for larger responses
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 24k;
}
# Catch-all proxy (for any other routes)
location / {
proxy_pass http://api;
proxy_http_version 1.1;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
}
+3
View File
@@ -49,5 +49,8 @@ Flask-Limiter==3.5.0
python-json-logger==2.0.7 python-json-logger==2.0.7
qrcode[pil] qrcode[pil]
# HTTP requests
requests>=2.31.0
# SSH CA Certificate signing # SSH CA Certificate signing
sshkey-tools==0.11.3 sshkey-tools==0.11.3
+2 -2
View File
@@ -1,6 +1,6 @@
# Gatehouse Scripts # Secuird Scripts
This directory contains utility scripts for managing and configuring Gatehouse. This directory contains utility scripts for managing and configuring Secuird.
## OAuth Provider Configuration Script ## OAuth Provider Configuration Script
+2 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
OAuth Provider Configuration Script for Gatehouse OAuth Provider Configuration Script for Secuird
This script allows administrators to configure OAuth providers at the application level This script allows administrators to configure OAuth providers at the application level
using the new ApplicationProviderConfig architecture. using the new ApplicationProviderConfig architecture.
@@ -457,7 +457,7 @@ def delete_provider(args):
def main(): def main():
"""Main entry point for the script.""" """Main entry point for the script."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Configure OAuth providers for Gatehouse authentication", description="Configure OAuth providers for Secuird authentication",
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=""" epilog="""
Examples: Examples: