Production-Ready · Python 3.10

Flask SOAP Boilerplate
that ships fast

Kyryl Pavlov Built by Kyryl Pavlov · Full-Stack Engineer

Production-ready Python 3.10 / Flask boilerplate for scalable SOAP APIs (Spyne, SOAP 1.1). Ships with JWT auth, PostgreSQL + SQLAlchemy, AWS S3 uploads, async SQS + Lambda event processing, Redis caching, Nginx reverse proxy with DDoS protection, Prometheus metrics, Grafana dashboards, Loki log aggregation, Sentry error tracking, Docker Compose infrastructure, and a full pytest test suite — all wired from day one.

Up in three commands

# 1. Clone and enter the project
git clone https://github.com/Kyryl-Pavlov/python-flask-soap-boilerplate
cd python-flask-soap-boilerplate

# 2. Create .env.local and set your environment variables
#    See README → Environment Setup for the full variable list
touch .env.local

# 3. Start the full stack
docker compose up --build

Service communication map

Hover a node to highlight its connections and see how data flows through the stack.

Hover a node to see its connections.

Everything you need

Skip the boilerplate setup and start building features on day one.

📡

SOAP 1.1 API

Full SOAP service built with Spyne at /soap. WSDL always available at /soap?wsdl — import into SoapUI, Postman, or any SOAP client for schema-validated access.

🔐

JWT authentication

Access tokens (15 min) and refresh tokens (30 days) out of the box. Auth token passed via SOAP <AuthHeader> element for protected operations.

🗄️

Postgres + migrations

SQLAlchemy models, Flask-Migrate (Alembic) for schema changes, and an interactive migrate.sh helper. Migrations run automatically on container startup.

☁️

S3 media uploads

Upload files to S3 (real AWS in production, LocalStack locally). Presigned URLs generated on demand — S3 keys stored in the DB, not URLs.

📋

Structured logging

Fanout logger dispatches to Console, Sentry, CloudWatch, and Loki simultaneously. Sensitive data (passwords, tokens) masked automatically before any backend sees it.

📈

Prometheus metrics

Per-endpoint request rate, error rate, and latency histograms at /metrics. Scraped every 15 s — query raw at :9090 or via pre-built Grafana dashboards.

📊

Grafana dashboards

Flask App and Host Metrics dashboards auto-provisioned on startup. No manual setup — open localhost:3000 and they're already there.

🖥️

Host metrics

Node Exporter exposes CPU, memory, disk I/O, network, and load average from the host OS. Works on Docker Desktop for Windows via WSL2.

🐛

VSCode debugger

Two pre-configured launch profiles: attach to the running Docker container, or run Flask directly on the host with infrastructure auto-started.

Pre-commit hooks

Ruff format + lint on every commit. Autofixable violations are fixed and staged automatically — the commit only aborts for issues that need manual attention.

🛡️

Nginx reverse proxy

Single entry point on port 80 with built-in DDoS protection: per-IP rate limiting (strict on auth endpoints), connection capping, Slowloris mitigation, and buffer limits. Add new microservices with two config blocks.

Async event processing

Flask publishes events to SQS. A Lambda function (locally: a worker container) consumes them and writes to Postgres. Idempotent via ON CONFLICT DO NOTHING — safe for at-least-once delivery.

🗂️

Redis caching

Opt-in per endpoint via CacheService. The app degrades gracefully when Redis is unavailable — current_app.cache is None and cache calls are skipped without errors.

🚀

GitHub Actions CI/CD

Three pre-built workflows: ci.yml runs lint + unit + e2e on every push; deploy-dev.yml and deploy-prod.yml each build images, migrate the database, and roll out to ECS and Lambda. Production deploys require manual approval.

🏗️

Terraform infrastructure

Complete AWS infrastructure as code — VPC, ECS Fargate, RDS, ElastiCache, S3, SQS, Lambda, ALB, WAF, and IAM — split into 11 reusable modules with separate dev and prod configurations.

🛡️

WAF + private network

AWS WAF in front of the ALB with OWASP Top 10, SQLi, bad-input rules, and per-IP rate limiting. App runs in private subnets — never directly reachable from the internet.

Three-tier test pyramid

Unit and integration tests need no Docker and run in seconds. E2E tests run in CI/CD against the real stack.

Unit tests

Pure functions only — zero external dependencies. Covers sensitive-data masking, AppLogger fanout and level routing, CloudWatch JSON serialization, and CacheService JSON wrap / TTL / ping.

🔌

Integration tests

Flask test client with SQLite in-memory database — no Docker required. Tests every SOAP operation (auth, media, events, cache, health) with AWS services mocked at the function boundary.

🌐

E2E tests

Real HTTP through Nginx to a fully running stack — Postgres, LocalStack S3/SQS, Redis. Happy paths only, runs in CI/CD via docker-compose.ci.yml. Target URL overridable via E2E_BASE_URL.

Unit & integration — no Docker
pip install -r requirements-test.txt
pytest tests/app/unit tests/app/integration
# with coverage report
pytest tests/app/unit tests/app/integration \
    --cov=app --cov-report=term-missing
E2E — CI/CD stack
docker compose -f docker-compose.ci.yml up -d --wait
pytest tests/app/e2e/
docker compose -f docker-compose.ci.yml down

Two workflows, zero manual steps

GitOps model: merging to develop deploys to dev, merging to main deploys to production (with a manual approval gate).

ci.yml
Triggers on every push and pull request
🔍
Lint
ruff format --check + ruff check — no Docker needed
Unit & Integration Tests
pytest with SQLite in-memory — runs in ~2 s, no Docker needed
🌐
E2E Tests
Spins up docker-compose.ci.yml, runs full HTTP suite through Nginx, tears down
deploy-dev.yml
Targets dev environment — merge to develop
📦
Build
All service images pushed to ECR tagged :{git-sha} and :develop
🗄️
Migrate
All service migrations run as one-off ECS tasks before any service is touched
🚀
Deploy + Deploy Workers
Services (tier order) and Lambda workers deploy in parallel — both need migrate to complete first
deploy-prod.yml
Targets production environment — merge to main
📦
Build
Same as dev — images tagged :{git-sha} and :main
🔒
Migrate (approval gate)
Pauses for a required reviewer. Approving this job unlocks the entire prod pipeline for this run
🚀
Deploy + Deploy Workers
Identical to dev — services then workers, both in parallel after migrate
Job 1
Build
All service images built and pushed to ECR in parallel. If any build fails, nothing deploys.
Job 2
Migrate
ALL service migrations run as one-off ECS tasks before any service is touched. Schema must be backward-compatible — old and new code run together during rolling update.
Jobs 3a + 3b (parallel)
Services + Workers
Services deploy in tier order (dependencies first). Lambda workers deploy in parallel — they consume queues, not serve requests, so they're independent of service tiers.
⚠️
Backward-compatible migrations are required

During a rolling ECS update, old and new task instances run simultaneously against the same database. Never drop a column the deployed code still reads. Use a two-phase approach: add the new column first, remove the old one in a later deploy.

🔑
No long-lived AWS credentials in GitHub

The deploy workflow authenticates via OIDC — GitHub exchanges a short-lived token for an IAM role scoped to your repo and branch. Only one GitHub secret is needed: AWS_ROLE_ARN. All other config (cluster name, service name, Lambda function) lives in GitHub environment variables, populated from terraform output.

Hardened baseline out of the box

Every layer secured by default — application code, network traffic, and cloud infrastructure. Nothing to enable; everything is on from day one.

⚙️ Application
SECRET_KEY fail-fast
App refuses to start if SECRET_KEY is missing — no silent fallback to None that would break session signing
File upload allowlist + size limit
Only jpg, png, gif, webp, pdf, mp4, mov accepted (415 otherwise). 50 MB hard cap enforced before any handler runs
SQL stripped from logs
SQLAlchemy query text and bound parameters redacted from every traceback before reaching Console, Sentry, CloudWatch, or Loki
JWT algorithm pinned
HS256 explicit in config — immune to library-default changes and alg: none bypass attempts
Non-root containers
Flask app runs as app system user; Lambda image uses nobody — no root access if a container is compromised
🌐 Network (Nginx)
Security response headers
X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy — applied to every response including errors
Rate limiting
300 req/min general; 20 req/min on auth endpoints to slow brute-force. Per-IP connection cap of 20 concurrent
Slowloris mitigation
Client body/header/send timeouts set to 10 s. Keeps slow-connection attacks from holding workers indefinitely
Method allowlist
Only GET, POST, PUT, PATCH, DELETE, OPTIONS pass through — all other HTTP methods return 405
/metrics locked to internal network
Prometheus can scrape it; browsers on the public internet cannot reach it
Version hidden
server_tokens off — Nginx version not disclosed in response headers or error pages
☁️ Cloud (AWS / Terraform)
WAF in front of ALB
OWASP Top 10, SQLi, known bad inputs, per-IP rate limit. All blocked requests logged to CloudWatch (90-day retention)
Private subnets
ECS tasks have no public IP — only the ALB is internet-facing. RDS, Redis, and Lambda are unreachable from outside the VPC
Redis encrypted in transit
transit_encryption_enabled = true on ElastiCache. Client connects via rediss:// (TLS)
Secrets Manager — no env file secrets
DB password, JWT key, Flask secret generated by Terraform and injected at container startup. Never stored in task definitions or committed files
Least-privilege IAM
Flask app: SQS send-only. Lambda: SQS receive/delete only. GitHub Actions OIDC: scoped to specific repo and branch
VPC flow logs
All network traffic logged to CloudWatch — full audit trail including rejected connection attempts

Production AWS infrastructure as code

One terraform apply provisions everything. Secrets are generated and stored in Secrets Manager — never in env files.

  GitHub Actions
       │ push images (git SHA tag)
       ▼
  ECR Repositories ─────────────────────────────────┐
       │ pull on ECS startup              pull on invocation │
       ▼                                                     ▼
Internet ──► WAF ──► ALB (public subnets)      Lambda Worker
  (OWASP, SQLi,          │                             │
   rate limit)           │ forward                    consumes
                         ▼                             │
                  ECS Fargate (private subnets)    SQS Queue ◄── Flask publishes
                  Flask app on port 5000          (+ DLQ after 3 failures)
                  /         |          \
                 ▼          ▼          ▼
              RDS       Redis      S3
           PostgreSQL   ElastiCache  Media bucket
              │         (cache)     (presigned URLs)
              ▲
              └────── Lambda also writes events here

  Secrets Manager: DATABASE_URL · JWT_SECRET_KEY · SECRET_KEY
  (injected into ECS containers at startup — never in env files)
networking
VPC, public/private subnets, NAT gateway, 5 security groups, VPC flow logs
ecr
Two container registries (app + worker), scan on push, keep last 10 images
iam
ECS roles, Lambda role, GitHub OIDC deploy role — all least-privilege
rds
PostgreSQL 16, encrypted, DATABASE_URL stored in Secrets Manager
elasticache
Redis 7 replication group, encrypted at rest
s3
Media bucket, public access blocked, HTTPS-only bucket policy, CORS
sqs
Events queue + dead-letter queue, SSE, redrive after 3 failures
alb
Application Load Balancer, HTTP→HTTPS redirect, TLS 1.3 policy
waf
OWASP Top 10, SQLi, bad inputs, per-IP rate limit, CloudWatch metrics
ecs
Fargate cluster, task definition (secrets from Secrets Manager), service
lambda
Container image function in VPC, SQS event source mapping
bootstrap
One-time: S3 state bucket + DynamoDB lock table with local state
Dev environment
~$83 / mo
NAT Gateway~$32
ECS Fargate (1 task)~$18
RDS db.t3.micro~$13
ElastiCache t3.micro~$13
ALB~$7
HTTP only (no ACM cert). NAT Gateway is the biggest fixed cost.
Production environment
~$134 / mo
ECS Fargate (2 tasks)~$36
NAT Gateway~$32
RDS db.t3.small Multi-AZ~$26
WAF managed rules~$20
ElastiCache t3.micro~$13
ALB + HTTPS~$7
Multi-AZ RDS + deletion protection. Lambda and SQS are pay-per-use (effectively free at low volume).
CI/CD Cost

What a build costs on GitHub Actions

Linux runners at $0.008 / min. LocalStack and the production Docker image are the two main cost drivers.

Cost per pipeline
⚡ Unit Tests
Every push & PR
Runner setup~30s
pip install (cached)~10s
pytest tests/app/unit~2s
Total~1 min
Cost / build~$0.008
🔌 Integration Tests
Every push & PR
Runner setup~30s
pip install (cached)~10s
pytest tests/app/integration~5s
Total~1 min
Cost / build~$0.008
🌐 E2E Tests
Merge to main only
Runner setup~30s
pip install (cached)~10s
Pull images (cached)~15s
Build app image (layer cache)~30s
Postgres + Redis startup~30s
LocalStack S3 + SQS init~75s ⚠ fixed
DB migrate + app health check~35s
pytest tests/app/e2e + teardown~35s
Total~4–5 min
Cost / build~$0.03
LocalStack is the only non-cacheable cost
⚠️
E2E floor: ~75 s per build no matter what

LocalStack must start S3 and SQS, pass its health check, and run the bucket init script on every run. Everything else in the E2E pipeline can be cached — this phase cannot.

Technology

Built on proven tools

Carefully chosen defaults — swap any layer as your project grows.

RuntimePython 3.10
FrameworkFlask 3.x
SOAPSpyne
DatabasePostgreSQL
ORMSQLAlchemy 2
MigrationsAlembic / Flask-Migrate
AuthFlask-JWT-Extended
StorageAWS S3 / LocalStack
QueueAWS SQS / LocalStack
ServerlessAWS Lambda (worker)
CacheRedis
Proxy / LBNginx
MetricsPrometheus + Grafana
LogsLoki + Grafana
Error trackingSentry
Production serverGunicorn
Linting / FormatRuff
Testingpytest · pytest-cov
Local infraDocker Compose
Cloud infraTerraform ≥ 1.6
CI/CDGitHub Actions
Container registryAWS ECR
Load balancerAWS ALB
DDoS / WAFAWS WAF v2
SecretsAWS Secrets Manager
Compute (cloud)AWS ECS Fargate

Full dev infrastructure in one command

docker compose up --build starts everything below — development environment only. No manual service installs required.

ServicePortPurpose
nginx80Reverse proxy + load balancer — single entry point with DDoS protection
app5000 · 5678Flask API + debugpy (direct access for local dev)
workerLambda handler in polling mode — consumes SQS, writes events to Postgres
postgres5432Primary database
redis6379Cache layer
localstack4566AWS S3 + SQS
pgadmin5050Postgres GUI
s3-console8080S3 bucket browser
loki3100Log aggregation
prometheus9090Metrics database + query UI
grafana3000Dashboards — metrics & logs
node-exporter9100Host OS metrics
About the author
Kyryl Pavlov
Kyryl Pavlov
Full-Stack Web Developer · Valencia, Spain

Software Engineer with 5+ years of commercial experience building e-commerce projects. Strong background in server-side technologies (Node.js, TypeScript, Python) and cloud infrastructure (AWS), combined with modern front-end tools like React and Next.js. Proven ability to design scalable APIs, integrate security protocols, and collaborate with cross-functional teams. This boilerplate distils production patterns from real projects into a clean, ready-to-use foundation.