- Jinja 89.8%
- Python 10.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The data-freshness sweep runs after the progress sync and the pending purge (a failure there must never block them): one tiny /v2/build probe per 5-min sweep, mapping re-fetch + import only on a new build. TimeoutStartSec lifted — a patch-day import pass can outlast systemd's default 90 s, and killing a healthy mid-import sweep would churn the retry loop (the transaction survives, the next sweep retries). |
||
| ansible | ||
| scripts | ||
| .gitignore | ||
| AGENTS.md | ||
| ansible.cfg | ||
| opencode.json | ||
| pyproject.toml | ||
| README.md | ||
| seed_users.yml.sample | ||
| uv.lock | ||
GW2Suggest Deployment Documentation
This repository contains Ansible playbooks to deploy the GW2Suggest platform (API and Frontend) to a Debian 13+ host.
Prerequisites
- Ansible:
ansible-coreis the base installation on your control node. The required community collections are installed explicitly from this repository's collection requirements file. - Target Host: A Debian 13+ server with SSH access.
- Git Access: The control node must have access to the application repositories (defined in your host variables).
- Build Tools: The control node must have
npminstalled to build the frontend.
Install the Ansible collections before running a playbook:
ansible-galaxy collection install -r ansible/collections/requirements.yml
Ansible dependency policy
The project uses ansible-core as its base rather than the full ansible
community package. ansible.posix and community.general are explicitly
installed because this deployment uses ansible.posix.synchronize,
community.general.ufw, and community.postgresql. These collections are
maintained in the Ansible
community distribution and are preferred here over arbitrary third-party
collections. Keeping them in a repository-managed requirements file makes the
additional controller dependencies visible and reproducible.
Configuration
1. Inventory
Copy the example inventory and update it with your server details:
cp ansible/inventory/production.ini.example ansible/inventory/production.ini
# Edit ansible/inventory/production.ini with your server's IP and user
2. Custom Variables
We recommend using host variables to customize your deployment without modifying the versioned defaults in the roles.
Copy the example host variables file and rename it to match your host as defined in your inventory:
# Replace 'your_server_ip' with the actual IP or hostname from production.ini
cp ansible/inventory/host_vars/myserver.yml.example ansible/inventory/host_vars/your_server_ip.yml
Edit the new file to set your Git repository URLs, public URL, branch/tag, and database passwords.
Set public_url to the externally reachable site URL. It is deliberately
separate from ansible_host, which is only used for SSH and may be an alias or
an SSH ProxyJump target. The public API endpoint is {{ public_url }}/api.
The frontend build receives public_url as VITE_API_BASE_URL because the
frontend appends /api to its API requests.
app_version is the shared branch or tag used by both applications by default.
For reproducible deployments pinned to commits, set api_git_version and
frontend_git_version independently instead.
trusted_proxies (default 127.0.0.1) lists the IPs/CIDRs of proxies that
append to X-Forwarded-For, so the API can resolve the real client IP —
required for correct per-IP login rate limiting. With an external proxy in
front of nginx (e.g. Cloudflare), add its published IP ranges. Never use *:
nginx appends to client-supplied headers, so trusting all proxies lets
clients spoof their IP.
Mail (SMTP-or-Mailpit)
Outgoing mail is optional (D3) and works out of the box: the mail role
(runs during initial setup) installs Mailpit on the host unless an
external relay is configured. Mailpit binds to loopback only — SMTP on
127.0.0.1:1025 for the API, the web UI on 127.0.0.1:8025 reachable
exclusively via SSH tunnel (nginx never proxies to it):
ssh -L 8025:127.0.0.1:8025 <host> # then open http://localhost:8025
To switch to a real transactional relay, set in host vars:
mail_mode: "external"
smtp_host: "relay.provider.com"
smtp_port: 587
smtp_starttls: true
smtp_user: "no-reply@example.com"
smtp_password: "{{ vault_smtp_password }}" # keep this in Ansible Vault
mail_from: "no-reply@example.com"
mail_enabled: false disables outgoing mail entirely — the API then skips
every send with an INFO log line (mail_unconfigured) instead of failing.
Mail flows degrade gracefully either way; nothing else in the app depends
on SMTP being present. Relay credentials are provider-issued —
generate_secrets.py intentionally does not invent them; put the value in
Ansible Vault (vault_smtp_password).
Note that switching mail modes needs both plays: setup.yml provisions (or
retires) the local Mailpit, and deploy_api.yml re-renders the API's
.env — running only one of them leaves the API pointing at the wrong
relay.
Sentry error tracking (optional)
Sentry is off by default. Backend and frontend gate separately:
sentry_enabled: true (plus a required sentry_dsn_api) controls the API —
enabling installs it with the [sentry] extra and writes SENTRY_* into the
API .env, picked up by both the web app and the Celery worker. The frontend
reports as soon as sentry_dsn_frontend is non-empty — set it to "" to
keep the frontend unmonitored. The two DSNs must be separate Sentry
projects so backend and frontend events never share a stream. The frontend
value is inlined into the bundle at build time, so after changing it, redeploy
the frontend. sentry_send_pii stays false unless you deliberately accept
PII in Sentry; sentry_traces_sample_rate (0.0–1.0, shared role) bounds
tracing volume in both apps — the API refuses out-of-range values at
startup, and the frontend samples page-load/navigation spans at the same
rate (leave it unset in the build to keep the frontend error-only).
Sentry releases
Every deploy stamps a release id — gw2suggest-api@2026.9.26+a1b2c3d and
gw2suggest-web@… — composed from the deploy date and the exact commit
short SHA, and creates the release up front (API via the Sentry HTTP API,
frontend via the build's vite plugin), so release ordering follows deploys
rather than whatever happens to report first. Enabling therefore also needs
sentry_org, sentry_project_api, and sentry_auth_token (vault) on the
API side; a failed release creation is announced but does not abort the
deploy. This is what makes release health, "resolved in next release", and
per-release issue views work.
Sentry source maps
When the frontend reports (sentry_dsn_frontend set), its build also
requires sentry_auth_token + sentry_org + sentry_project_frontend
(asserted at deploy time) and uploads the Vite source maps paired with the
release id — stack traces resolve to real Component.vue:line frames
instead of minified positions. The build generates maps only for that
upload and deletes them from dist/ afterwards, so source maps never ship
to the web server. A failed upload logs loudly but does not abort the
deploy — check the release in Sentry after enabling.
Registration email mode
The register form's email requirement (D12) is controlled by two settings
that must move together: the API's REGISTRATION_EMAIL_MODE (default
optional) and the frontend build flag
frontend_registration_email_required (default empty = optional; set
"true" to match a required backend). Unlike the SMTP settings, the
frontend value is baked into the bundle at build time — switching the
mode therefore means deploy_api.yml and deploy_frontend.yml, never
just an env edit on the host.
3. Secrets & Ansible Vault
Generate secure secrets for your deployment:
python3 scripts/generate_secrets.py
It is highly recommended to use Ansible Vault to store your sensitive data. You can put your secrets directly in your host variable file and encrypt the whole file, or use a separate encrypted vault file.
To use a separate vault file:
- Create the directory:
mkdir -p ansible/group_vars/all - Create the vault:
ansible-vault create ansible/group_vars/all/vault.yml - Add your secrets:
vault_db_password: "your_generated_password" vault_secret_key: "your_generated_secret_key" vault_smtp_password: "your_smtp_relay_password" # only with mail_mode: external vault_sentry_dsn_api: "your_api_project_dsn" # only with sentry_enabled: true vault_sentry_dsn_frontend: "your_frontend_project_dsn" vault_sentry_auth_token: "your_sentry_auth_token" # release creation + source-map upload - Reference them in your host-specific file (e.g.,
ansible/inventory/host_vars/your_server_ip.yml):db_password: "{{ vault_db_password }}" secret_key: "{{ vault_secret_key }}"
4. User & API Key Seeding (Optional)
You can automatically provision initial users (including superusers) and their Guild Wars 2 API keys at deployment time.
- Copy the sample seed file:
cp ansible/inventory/seed_users.yml.sample ansible/inventory/seed_users.yml - Edit
ansible/inventory/seed_users.ymlwith your users, password hashes (or passwords), and GW2 API keys. - Reference the file in your host variables (
ansible/inventory/host_vars/your_server_ip.yml):
Or defineapi_seed_file: "ansible/inventory/seed_users.yml"api_seed_usersdirectly as a list in your vault/host variable file.
Demo account (optional)
Anonymous visitors can try the suggest flow against a public demo account.
Two halves must agree: a seed entry whose username matches
frontend_demo_account (default Demo.1234) with visibility: public and a
real GW2 API key from a dedicated demo account, and the frontend build
variable itself (set it to "" to disable the affordance). Like the
registration-email flag, the value is baked into the bundle at build time —
redeploy the frontend after changing it.
Deployment
Initial Setup
Run the setup playbook to install system dependencies (PostgreSQL, Redis, Nginx, Python 3.13, uv):
ansible-playbook -i ansible/inventory/production.ini ansible/playbooks/setup.yml --ask-vault-pass
Setup tasks are intentionally separate from application roles. Application deployments load shared role defaults but do not repeat host provisioning. Re-run the setup playbook when system packages, firewall rules, or other host infrastructure changes.
API deployments also download missing GW2 metadata with download-data and
import it into PostgreSQL with import-data. The raw metadata is retained in
the persistent API shared directory so subsequent deployments only download
missing files.
Deploy Applications
To deploy both the API and the Frontend in one go:
ansible-playbook -i ansible/inventory/production.ini ansible/playbooks/site.yml --ask-vault-pass
Alternatively, you can deploy them individually:
# Deploy API only
ansible-playbook -i ansible/inventory/production.ini ansible/playbooks/deploy_api.yml --ask-vault-pass
# Deploy Frontend only
ansible-playbook -i ansible/inventory/production.ini ansible/playbooks/deploy_frontend.yml --ask-vault-pass
Backup & Maintenance
- Backups: Automated daily database and metadata backups are stored in
/var/backups/gw2suggest. A pre-deployment backup is also triggered automatically. Metadata backups contain the API's downloaded GW2 data directory. - Logs: Application logs are located in
/var/www/gw2suggest/api/shared/logs. - Achievement syncs: the
gw2suggest-sync.timerruns the staleness-aware scheduler every 5 minutes (sync_schedule, overridable in host vars). Each run is one SELECT over the API-keys table; it enqueues Celery sync tasks only for keys older thanACHIEVEMENTS_SYNC_INTERVAL_HOURS(API default: 24 h) — so syncs happen within minutes of falling due and spread across the day instead of batching, while fresh keys cost nothing. Manually triggering a sync from the API-keys page still works independently. The same oneshot unit also runsgw2suggest purge-expired-pendings, which drops unconfirmed pending email addresses pastPENDING_EMAIL_LIFETIME_DAYS(D16). - Services: Managed via systemd (
gw2-api.service,gw2-celery.service,gw2suggest-sync.timer+gw2suggest-sync.service,mailpit.servicein the default mail mode,nginx,postgresql,redis-server).