Aletheia Server Setup¶
Complete guide for provisioning a new server from scratch.
Prerequisites¶
- Fresh Debian 12+ server with root/sudo access
- DNS A records pointing to the server for:
aletheia.groupe-suffren.com(production)aletheia-staging.groupe-suffren.com(staging)aletheia-dev.groupe-suffren.com(dev)
Step 1: Initial Access & SSH Key¶
Connect to the server and install your SSH public key:
ssh root@<server-ip>
adduser debian
usermod -aG sudo debian
# Install SSH key
mkdir -p /home/debian/.ssh
echo "your-public-key" >> /home/debian/.ssh/authorized_keys
chmod 700 /home/debian/.ssh
chmod 600 /home/debian/.ssh/authorized_keys
chown -R debian:debian /home/debian/.ssh
Verify key-based login works from your machine before proceeding:
Warning: The setup script will disable password authentication. If you skip the SSH key step, you will be locked out.
Step 2: Clone the Repository¶
sudo mkdir -p /opt/docker/aletheia/repo
sudo chown debian:debian /opt/docker/aletheia/repo
cd /opt/docker/aletheia/repo
git clone git@github.com:baudry-suffren/aletheia_v2.git .
Step 3: Run the Setup Script¶
The script automates:
- Docker CE installation
- Utilities: git, apache2-utils, cron, fail2ban, iptables-persistent, unattended-upgrades
- Swap: creates 5GB swap file, sets
vm.swappiness=10 - Unattended upgrades: enables automatic Debian security patches
- SSH hardening: port 57361, key-only auth (checks for keys first)
- Firewall service: installs systemd service that applies iptables rules after Docker starts
- fail2ban: sshd jail on port 57361
- Directory structure:
/opt/docker/{nginx,shared,aletheia,backups} - Config files: nginx, postgres, redis configs copied into place
- Password generation: random passwords for all databases and Django SECRET_KEYs
- Basic auth: interactive prompt for staging/dev htpasswd credentials
- Docker network: creates the
backendbridge network - Shared services: starts PostgreSQL + Redis, waits for readiness
- Static volumes: creates Docker volumes for collectstatic
- Nginx: starts the reverse proxy (in maintenance mode)
- Firewall rules: applies Docker-compatible iptables rules (waits for Docker daemon)
- Backup cron: daily at 2 AM, 7-day retention + weekly copies
Interactive prompts during setup: - Basic auth username/password for staging/dev - SSH key confirmation (if no key detected)
Step 4: Post-Setup Checklist¶
After the script completes:
- [ ] Reconnect via new SSH port:
ssh -p 57361 debian@<server-ip> - [ ] Edit env files (email settings, Sentry DSN):
- [ ] Obtain SSL certificates (requires DNS to be pointing to the server already):
Certbot auto-renewal runs every 12h inside the certbot container (configured in
# Nginx must be running first (started by setup.sh in maintenance mode) docker exec certbot certbot certonly --webroot -w /var/www/certbot \ -d aletheia.groupe-suffren.com \ -d aletheia-staging.groupe-suffren.com \ -d aletheia-dev.groupe-suffren.cominfra/nginx/docker-compose.yml). Verify renewal works: - [ ] Check IPv6 firewall: The setup script only configures IPv4 iptables. IPv6 is not managed. If the server has an IPv6 address, consider blocking all IPv6 traffic or applying matching rules:
- [ ] Deploy the application:
- [ ] Create superuser:
- [ ] Verify all environments are accessible in a browser
- [ ] Test backup cron (run manually once):
Day-to-Day Operations¶
All operations go through the Makefile:
# Deploying
make deploy ENV=staging
make deploy ENV=prod REF=v1.2
# Database
make db-backup ENV=prod
make db-sync FROM=staging ENV=dev
# Logs & shell
make logs ENV=staging
make shell ENV=staging
# Infrastructure config management
make infra-diff # Compare git vs server configs
make infra-pull # Server → git (after editing on server)
make infra-deploy # Git → server (after editing in repo)
Typical workflow after editing an nginx config on the server:
1. make infra-diff — see what changed
2. make infra-pull — copy server state into git
3. git diff infra/ — review changes
4. git add infra/ && git commit — commit and push
Security Overview¶
| Layer | Configuration |
|---|---|
| SSH | Port 57361, key-only, no password auth, no root login |
| Firewall | iptables default DROP, only ports 57361/80/443 open |
| fail2ban | sshd jail: 5 retries, 10min ban |
| Rate limiting | SSH: max 4 connections/60s. HTTP: 10 req/s (nginx) |
| Basic auth | Staging and dev environments (htpasswd) |
| Docker isolation | Containers only accessible via nginx proxy |
| HTTPS | Let's Encrypt certificates via certbot |
Config files in infra/security/:
- sshd-00-hardening.conf — SSH drop-in config (/etc/ssh/sshd_config.d/00-hardening.conf)
- iptables-setup-docker.sh — Firewall rules script (Docker-compatible, flushes INPUT only)
- iptables-firewall.service — systemd service that runs the script after Docker starts
- fail2ban-aletheia.conf — fail2ban jail config (/etc/fail2ban/jail.d/aletheia.conf)
Note on cloud-init: Some cloud providers (OVH, AWS, etc.) install
/etc/ssh/sshd_config.d/50-cloud-init.confwhich may setPasswordAuthentication yes. Since sshd uses first-match-wins, our config is numbered00-to take priority. After deployment, check if50-cloud-init.confexists and consider disabling it if it conflicts with security hardening.
Secret Management (SOPS + age)¶
All secrets are encrypted with SOPS + age and committed to the repo as .enc files. Only the age private key needs to be stored externally (password manager).
Encrypted files in repo:
- infra/shared/.env.enc — Postgres admin credentials
- infra/envs/.env.{dev,staging,prod}.enc — Django secrets per environment
- infra/monitoring/.env.enc — Grafana, SMTP, Teams webhook
- infra/nginx/.htpasswd.enc — Basic auth credentials
- infra/shared/init-scripts/01-create-databases.sql.enc — DB init script
Key location on server: /opt/docker/.age-key.txt (readable by root and debian)
Makefile targets:
make infra-encrypt # Server secrets → encrypted files in repo
make infra-decrypt # Encrypted repo files → server locations
Rotating secrets:
1. Edit the plaintext file on the server
2. Run make infra-encrypt to re-encrypt
3. Commit and push the updated .enc files
New server / lost key:
- The age private key must be retrieved from the password manager
- Place it at /opt/docker/.age-key.txt with permissions 640 root:debian
- Run make infra-decrypt to restore all secrets
Disaster Recovery¶
To rebuild the server from scratch with full data restore:
Prerequisites¶
- Fresh Debian 12+ server with SSH access
- Age private key from password manager
- Latest database backup (from off-server backup or previous
/opt/docker/backups/daily/) - Latest media backup (staging + prod tar.gz)
Procedure¶
# 1. Initial access & SSH key (see Step 1 above)
ssh root@<server-ip>
# 2. Clone repo
sudo mkdir -p /opt/docker/aletheia/repo
sudo chown debian:debian /opt/docker/aletheia/repo
cd /opt/docker/aletheia/repo
git clone git@github.com:baudry-suffren/aletheia_v2.git .
# 3. Run setup script — choose option 2 (Server migration / recovery)
# This handles: Docker install, security hardening, directory setup,
# secret decryption (from age key), network creation, and starts
# shared services (postgres/redis), nginx, and monitoring stack.
sudo ./infra/setup.sh
# 4. Restore databases (from backup files)
docker cp backup_aletheia_prod_YYYYMMDD.dump shared_postgres:/tmp/
docker exec shared_postgres pg_restore -U aletheia_prod -d aletheia_prod -Fc --no-owner /tmp/backup_aletheia_prod_YYYYMMDD.dump
docker cp backup_aletheia_staging_YYYYMMDD.dump shared_postgres:/tmp/
docker exec shared_postgres pg_restore -U aletheia_staging -d aletheia_staging -Fc --no-owner /tmp/backup_aletheia_staging_YYYYMMDD.dump
# 5. Restore media files
tar xzf media_aletheia_prod_YYYYMMDD.tar.gz -C /opt/docker/aletheia/media/prod/
tar xzf media_aletheia_staging_YYYYMMDD.tar.gz -C /opt/docker/aletheia/media/staging/
# 6. Follow the post-setup instructions printed by setup.sh:
# - Obtain SSL certificates (certbot)
# - Activate HTTPS nginx configs (.conf.full)
# - Deploy application (make deploy)
# 7. Verify
curl -k https://aletheia.groupe-suffren.com/health/
curl -k https://aletheia-staging.groupe-suffren.com/health/
make infra-diff # Should show no drift
Directory Layout¶
/opt/docker/
├── nginx/ Reverse proxy + SSL
│ ├── docker-compose.yml
│ ├── nginx.conf
│ ├── .htpasswd Basic auth credentials
│ └── conf.d/
│ ├── aletheia-*.conf Active vhost configs
│ ├── aletheia-*.conf.full Full configs (restored after deploy)
│ └── aletheia-*.conf.temp Maintenance page configs
├── shared/ PostgreSQL + Redis
│ ├── docker-compose.yml
│ ├── .env Postgres admin credentials
│ └── init-scripts/ DB initialization SQL
├── aletheia/
│ ├── repo/ Git repository (this repo)
│ ├── envs/ Environment files (.env.dev/staging/prod)
│ └── media/ User uploads (dev/staging/prod)
├── monitoring/ Prometheus + Grafana + Loki + exporters
│ ├── docker-compose.yml
│ ├── .env Grafana admin, SMTP, Teams webhook
│ ├── prometheus/ Scrape configs + alert rules
│ ├── grafana/provisioning/ Dashboards, alerting, datasources
│ ├── loki/ Log storage config
│ ├── alloy/ Log collection config
│ └── blackbox/ HTTPS probe config
└── backups/
├── scripts/backup.sh Cron backup script
├── daily/ 7-day retention
└── weekly/ 28-day retention