Deployment Guide — Nginx + AWS EC2 + PostgreSQL
Target: Ubuntu 22.04 LTS on AWS EC2. App served by Gunicorn behind Nginx (TLS termination).
1. System packages
sudo apt update
sudo apt install -y python3.13 python3.13-venv python3-pip nginx postgresql
2. PostgreSQL
sudo -u postgres psql <<'SQL'
CREATE USER iam_user WITH PASSWORD 'CHANGE_ME_STRONG';
CREATE DATABASE iam OWNER iam_user;
SQL
DATABASE_URL=postgresql+psycopg2://iam_user:CHANGE_ME_STRONG@localhost:5432/iam
3. Application
sudo mkdir -p /opt/sims && sudo chown $USER /opt/sims
git clone <repo> /opt/sims/app && cd /opt/sims/app
python3.13 -m venv /opt/sims/venv
/opt/sims/venv/bin/pip install -r requirements.txt gunicorn
Create /opt/sims/app/.env (never commit it):
APP_ENV=production
SECRET_KEY=$(python -c "import secrets;print(secrets.token_hex(32))")
JWT_SECRET_KEY=$(python -c "import secrets;print(secrets.token_hex(32))")
DATABASE_URL=postgresql+psycopg2://iam_user:CHANGE_ME_STRONG@localhost:5432/iam
In production the app refuses to boot unless SECRET_KEY, JWT_SECRET_KEY and
DATABASE_URL are all set, and the two keys differ.
4. Migrate & seed
cd /opt/sims/app
/opt/sims/venv/bin/python -m alembic upgrade head
/opt/sims/venv/bin/flask --app src.app seed # standard roles + permissions
Create the first super-admin via flask shell (assign the super_admin role in an org).
5. Gunicorn systemd unit — /etc/systemd/system/sims.service
[Unit]
Description=SIMS IAM API
After=network.target postgresql.service
[Service]
User=www-data
WorkingDirectory=/opt/sims/app
EnvironmentFile=/opt/sims/app/.env
ExecStart=/opt/sims/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 "src.app:create_app()"
Restart=always
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now sims # start
sudo systemctl restart sims # restart after deploy
sudo systemctl stop sims # stop
Multi-worker security state (SIMS-189). The auth rate limiter (
src/middlewares/security.py) and the pending-token JTI replay guard (src/utils/jwt_utils.py) are backed by the database — therate_limit_bucketsandused_pending_jtistables — whenever the app runs production-like (DEBUGandTESTINGboth off; seesrc.utils.shared_state.use_shared_store), which is always true for this systemd unit. That makes both correct across the 3 Gunicorn workers above: each worker used to keep its own in-memory copy, so a worker that hadn't seen a given IP or a given pending token yet would silently let a rate-limited request or a replayed token through. Local dev/test keep the original in-process dict/deque instead (no extra setup needed to run the test suite). No separate service (e.g. Redis) is required — this reusesDATABASE_URL, already configured above. Verify locally withpython3 scripts/repro_multiworker_state.py, which boots the app under 2 real Gunicorn workers and confirms both the rate limit and the replay guard are enforced across them. Expired rows in both tables are purged by the existingflask --app src.app cleanup-tokenscron job (§ install output, "Optional — token cleanup cron").
6. Nginx — /etc/nginx/sites-available/sims
server {
listen 80;
server_name iam.example.com;
return 301 https://$host$request_uri; # HTTPS only
}
server {
listen 443 ssl;
server_name iam.example.com;
ssl_certificate /etc/letsencrypt/live/iam.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/iam.example.com/privkey.pem;
location /static/ {
alias /opt/sims/app/src/static/;
}
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}
sudo ln -s /etc/nginx/sites-available/sims /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Use Certbot (sudo certbot --nginx) for the TLS certificate.
7. Verifying the deployment
After the service and proxy are up, the site serves three public entry points:
| URL | Purpose |
|---|---|
/ |
Public landing page (no auth) |
/docs |
Rendered documentation site (API ref, deploy guide, ER diagram, dev guide, spec) |
/admin/login |
Admin portal login |
/api/health |
Liveness + database connectivity check |
curl -sI http://127.0.0.1:8000/ | head -1 # expect 200
curl -s http://127.0.0.1:8000/api/health # expect success envelope
8. Deploy updates
cd /opt/sims/app && git pull
/opt/sims/venv/bin/pip install -r requirements.txt
/opt/sims/venv/bin/python -m alembic upgrade head
sudo systemctl restart sims
9. Secret-key rotation (no downtime)
JWT_SECRET_KEY rotation invalidates outstanding access tokens (max 15 min
lifetime), so refresh tokens let clients recover automatically. To rotate
gracefully: deploy the new key, then over the next 15 minutes clients refresh
and obtain tokens signed with the new key. For zero failed requests, support a
short overlap by validating against both old and new keys during the window
(extend decode_access_token to try a list of keys).
10. Production security checklist
- [ ]
APP_ENV=production, Flask debug disabled - [ ] HTTPS enforced (HTTP → 301); HSTS header emitted by the app
- [ ]
SECRET_KEY≠JWT_SECRET_KEY, both ≥32 bytes - [ ] PostgreSQL not exposed publicly; strong DB password
- [ ] Regular
alembic upgrade headon deploy - [ ] Audit logs retained and backed up
- [ ] Rate limiter / JTI replay guard verified across workers (SIMS-189):
python3 scripts/repro_multiworker_state.py