From c481ebf9e76c8aac040d9ecd47aa1491de086ff5 Mon Sep 17 00:00:00 2001 From: Coornhert Date: Sun, 29 Mar 2026 21:24:47 +0200 Subject: [PATCH] feat: ansible deployment setup voor dt-prod-01 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Forgejo + Redis Docker stack (wetgit-forgejo role) - FastAPI + Celery systemd services (wetgit-app role) - Nginx vhosts voor git.wetgit.nl en api.wetgit.nl (wetgit-nginx role) - SSL via Let's Encrypt (certbot webroot) - Backup script (forgejo dump, geen downtime) - Codeberg mirror script - Cron jobs voor backup/mirror/log cleanup - Ansible vault voor secrets (encrypted) Geïsoleerd van dt-platform: eigen poorten, users, directories. --- ansible/ansible.cfg | 10 ++ ansible/group_vars/wetgit/main.yml | 45 ++++++ ansible/group_vars/wetgit/vault.yml | 14 ++ ansible/inventory/hosts | 2 + ansible/roles/wetgit-app/handlers/main.yml | 12 ++ ansible/roles/wetgit-app/tasks/main.yml | 79 ++++++++++ .../templates/wetgit-celery.service.j2 | 17 ++ .../roles/wetgit-app/templates/wetgit.env.j2 | 19 +++ .../wetgit-app/templates/wetgit.service.j2 | 17 ++ .../roles/wetgit-forgejo/handlers/main.yml | 5 + ansible/roles/wetgit-forgejo/tasks/main.yml | 148 ++++++++++++++++++ .../roles/wetgit-forgejo/templates/app.ini.j2 | 75 +++++++++ .../wetgit-forgejo/templates/backup.sh.j2 | 37 +++++ .../templates/docker-compose.yml.j2 | 45 ++++++ .../templates/mirror-to-codeberg.sh.j2 | 55 +++++++ ansible/roles/wetgit-nginx/handlers/main.yml | 5 + ansible/roles/wetgit-nginx/tasks/main.yml | 118 ++++++++++++++ .../wetgit-nginx/templates/wetgit-api.conf.j2 | 51 ++++++ .../wetgit-nginx/templates/wetgit-git.conf.j2 | 52 ++++++ ansible/site.yml | 27 ++++ 20 files changed, 833 insertions(+) create mode 100644 ansible/ansible.cfg create mode 100644 ansible/group_vars/wetgit/main.yml create mode 100644 ansible/group_vars/wetgit/vault.yml create mode 100644 ansible/inventory/hosts create mode 100644 ansible/roles/wetgit-app/handlers/main.yml create mode 100644 ansible/roles/wetgit-app/tasks/main.yml create mode 100644 ansible/roles/wetgit-app/templates/wetgit-celery.service.j2 create mode 100644 ansible/roles/wetgit-app/templates/wetgit.env.j2 create mode 100644 ansible/roles/wetgit-app/templates/wetgit.service.j2 create mode 100644 ansible/roles/wetgit-forgejo/handlers/main.yml create mode 100644 ansible/roles/wetgit-forgejo/tasks/main.yml create mode 100644 ansible/roles/wetgit-forgejo/templates/app.ini.j2 create mode 100644 ansible/roles/wetgit-forgejo/templates/backup.sh.j2 create mode 100644 ansible/roles/wetgit-forgejo/templates/docker-compose.yml.j2 create mode 100644 ansible/roles/wetgit-forgejo/templates/mirror-to-codeberg.sh.j2 create mode 100644 ansible/roles/wetgit-nginx/handlers/main.yml create mode 100644 ansible/roles/wetgit-nginx/tasks/main.yml create mode 100644 ansible/roles/wetgit-nginx/templates/wetgit-api.conf.j2 create mode 100644 ansible/roles/wetgit-nginx/templates/wetgit-git.conf.j2 create mode 100644 ansible/site.yml diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..3b7272b --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,10 @@ +[defaults] +inventory = inventory/hosts +remote_tmp = /tmp/.ansible/tmp +host_key_checking = True +retry_files_enabled = False +roles_path = roles +vault_password_file = .vault_pass + +[ssh_connection] +pipelining = True diff --git a/ansible/group_vars/wetgit/main.yml b/ansible/group_vars/wetgit/main.yml new file mode 100644 index 0000000..070cf2a --- /dev/null +++ b/ansible/group_vars/wetgit/main.yml @@ -0,0 +1,45 @@ +# WetGIT - Nederlandse wetgeving als code +# Deployment variables for dt-prod-01 +# +# IMPORTANT: This server is shared with dt-platform. +# Do NOT use ports 8001 (dt-chatbot), 8200 (grimoire). +# Do NOT modify /opt/dt-chatbot, /opt/dt-skills-portal, /opt/grimoire. +# Do NOT modify the global nginx.conf — only add vhost configs. + +# --- Application --- +app_name: wetgit +app_dir: /opt/wetgit +data_dir: /data/wetgit + +# FastAPI backend +backend_port: 8002 +backend_workers: 1 +backend_host: "127.0.0.1" + +# --- Domains --- +server_name: "api.wetgit.nl" +forgejo_domain: "git.wetgit.nl" + +# --- Forgejo --- +forgejo_port: 3000 +forgejo_data_dir: /opt/wetgit/data +forgejo_admin_user: coornhert +forgejo_admin_email: coornhert@wetgit.nl + +# --- Redis (Docker, shared network with Forgejo) --- +redis_port: 6379 +redis_host: "127.0.0.1" + +# --- Celery --- +celery_concurrency: 2 + +# --- Codeberg mirror --- +codeberg_api_token: "{{ vault_codeberg_api_token | default('') }}" + +# --- AgentMail --- +agentmail_api_key: "{{ vault_agentmail_api_key }}" + +# --- Secrets (from vault.yml) --- +# vault_agentmail_api_key +# vault_codeberg_api_token (add when Codeberg account is ready) +# vault_forgejo_admin_password (initial admin password) diff --git a/ansible/group_vars/wetgit/vault.yml b/ansible/group_vars/wetgit/vault.yml new file mode 100644 index 0000000..117dd74 --- /dev/null +++ b/ansible/group_vars/wetgit/vault.yml @@ -0,0 +1,14 @@ +$ANSIBLE_VAULT;1.1;AES256 +35323237613730303463313335643433616238663932643630636530356461323433666435653436 +3433343462343538333335343165353538613435613962650a656166366364393564353733343561 +66643462313261643538653839393365643634376432373665653133383464313636633762366163 +6562336332396535390a333062323534373963356439353336633964383832313431623934653739 +37646339376338623536323336353931343039323263666265363763373266343533333236346635 +37656436623764393037393138343536313666613439666535656631313031343061346130376136 +64383164643466643162393537343265313632343432336238393030306164636434356463396434 +34656334383731326131393061333138643435366534333965376666393535316334396662633561 +61386636336438383563326565336635643663313934326333323939663637653531363261613733 +38646631333739303737616630663337663265616462346637326539306338613866313762306662 +38633066323936623233336631653836656531633839643739313966623065313931356630613134 +39636539643065663963626437383637643932633164306337626330623466313737623532366631 +6435 diff --git a/ansible/inventory/hosts b/ansible/inventory/hosts new file mode 100644 index 0000000..b9ca886 --- /dev/null +++ b/ansible/inventory/hosts @@ -0,0 +1,2 @@ +[wetgit] +dt-prod-01 ansible_host=100.98.29.89 ansible_user=deploy ansible_become=yes diff --git a/ansible/roles/wetgit-app/handlers/main.yml b/ansible/roles/wetgit-app/handlers/main.yml new file mode 100644 index 0000000..0778b3b --- /dev/null +++ b/ansible/roles/wetgit-app/handlers/main.yml @@ -0,0 +1,12 @@ +--- +- name: restart wetgit + systemd: + name: wetgit + state: restarted + daemon_reload: yes + +- name: restart wetgit-celery + systemd: + name: wetgit-celery + state: restarted + daemon_reload: yes diff --git a/ansible/roles/wetgit-app/tasks/main.yml b/ansible/roles/wetgit-app/tasks/main.yml new file mode 100644 index 0000000..cfa8710 --- /dev/null +++ b/ansible/roles/wetgit-app/tasks/main.yml @@ -0,0 +1,79 @@ +--- +# WetGIT FastAPI application + Celery worker +# Deploys to /opt/wetgit/backend with own venv and systemd services +# +# Directories are created by wetgit-forgejo role (runs first). +# This role only manages the FastAPI app and Celery worker. +# +# NOTE: Services are only enabled when application code exists. +# On first deploy (no code yet), this role is effectively a no-op. + +- name: Check if application code exists + stat: + path: "{{ app_dir }}/backend/requirements.txt" + register: app_code + +- name: Create Python venv + command: python3 -m venv {{ app_dir }}/backend/venv + args: + creates: "{{ app_dir }}/backend/venv/bin/python" + when: app_code.stat.exists + +- name: Set venv ownership + file: + path: "{{ app_dir }}/backend/venv" + owner: www-data + group: www-data + recurse: yes + when: app_code.stat.exists + +- name: Install Python dependencies + pip: + requirements: "{{ app_dir }}/backend/requirements.txt" + virtualenv: "{{ app_dir }}/backend/venv" + when: app_code.stat.exists + notify: restart wetgit + +- name: Deploy environment file + template: + src: wetgit.env.j2 + dest: "{{ app_dir }}/backend/.env" + owner: www-data + group: www-data + mode: "0600" + notify: restart wetgit + +- name: Deploy WetGIT systemd service + template: + src: wetgit.service.j2 + dest: /etc/systemd/system/wetgit.service + owner: root + group: root + mode: "0644" + notify: restart wetgit + +- name: Deploy Celery worker systemd service + template: + src: wetgit-celery.service.j2 + dest: /etc/systemd/system/wetgit-celery.service + owner: root + group: root + mode: "0644" + notify: restart wetgit-celery + +# Only start services when app code is deployed +- name: Enable and start WetGIT service + systemd: + name: wetgit + enabled: yes + state: started + daemon_reload: yes + when: app_code.stat.exists + +- name: Enable and start Celery worker + systemd: + name: wetgit-celery + enabled: yes + state: started + daemon_reload: yes + when: app_code.stat.exists diff --git a/ansible/roles/wetgit-app/templates/wetgit-celery.service.j2 b/ansible/roles/wetgit-app/templates/wetgit-celery.service.j2 new file mode 100644 index 0000000..f04c628 --- /dev/null +++ b/ansible/roles/wetgit-app/templates/wetgit-celery.service.j2 @@ -0,0 +1,17 @@ +[Unit] +Description=WetGIT Celery Worker +After=network.target docker.service +Requires=docker.service + +[Service] +Type=simple +User=www-data +Group=www-data +WorkingDirectory={{ app_dir }}/backend +EnvironmentFile={{ app_dir }}/backend/.env +ExecStart={{ app_dir }}/backend/venv/bin/celery -A tasks worker --loglevel=info --concurrency={{ celery_concurrency }} +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target diff --git a/ansible/roles/wetgit-app/templates/wetgit.env.j2 b/ansible/roles/wetgit-app/templates/wetgit.env.j2 new file mode 100644 index 0000000..e59264b --- /dev/null +++ b/ansible/roles/wetgit-app/templates/wetgit.env.j2 @@ -0,0 +1,19 @@ +# WetGIT environment — managed by Ansible +# Do NOT edit manually on the server + +# FastAPI +WETGIT_HOST={{ backend_host }} +WETGIT_PORT={{ backend_port }} +WETGIT_WORKERS={{ backend_workers }} + +# Redis / Celery +REDIS_URL=redis://{{ redis_host }}:{{ redis_port }}/0 +CELERY_BROKER_URL=redis://{{ redis_host }}:{{ redis_port }}/0 +CELERY_RESULT_BACKEND=redis://{{ redis_host }}:{{ redis_port }}/1 + +# AgentMail +AGENTMAIL_API_KEY={{ agentmail_api_key }} + +# Data +WETGIT_DATA_DIR={{ data_dir }} +WETGIT_GIT_REPOS_DIR={{ data_dir }}/git-repos diff --git a/ansible/roles/wetgit-app/templates/wetgit.service.j2 b/ansible/roles/wetgit-app/templates/wetgit.service.j2 new file mode 100644 index 0000000..9e83dba --- /dev/null +++ b/ansible/roles/wetgit-app/templates/wetgit.service.j2 @@ -0,0 +1,17 @@ +[Unit] +Description=WetGIT API - Nederlandse wetgeving als code +After=network.target docker.service +Wants=wetgit-celery.service + +[Service] +Type=simple +User=www-data +Group=www-data +WorkingDirectory={{ app_dir }}/backend +EnvironmentFile={{ app_dir }}/backend/.env +ExecStart={{ app_dir }}/backend/venv/bin/uvicorn main:app --host {{ backend_host }} --port {{ backend_port }} --workers {{ backend_workers }} +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/ansible/roles/wetgit-forgejo/handlers/main.yml b/ansible/roles/wetgit-forgejo/handlers/main.yml new file mode 100644 index 0000000..90d076a --- /dev/null +++ b/ansible/roles/wetgit-forgejo/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart forgejo + community.docker.docker_compose_v2: + project_src: "{{ app_dir }}/docker" + state: restarted diff --git a/ansible/roles/wetgit-forgejo/tasks/main.yml b/ansible/roles/wetgit-forgejo/tasks/main.yml new file mode 100644 index 0000000..5350af2 --- /dev/null +++ b/ansible/roles/wetgit-forgejo/tasks/main.yml @@ -0,0 +1,148 @@ +--- +# WetGIT Forgejo (self-hosted Git) + Redis +# +# Deploys Forgejo and Redis as Docker containers. +# Forgejo serves git.wetgit.nl (HTTPS-only, no SSH — blocked by firewall). +# Redis provides Celery broker for the WetGIT pipeline. +# +# IMPORTANT: Does NOT touch dt-platform's Docker services (grimoire). +# All containers use the 'wetgit-network' Docker network. + +# --- System user --- + +- name: Create wetgit system user + user: + name: wetgit + system: yes + home: /opt/wetgit + shell: /bin/bash + create_home: no + +- name: Get wetgit user UID + command: id -u wetgit + register: wetgit_uid_result + changed_when: false + check_mode: false + +- name: Get wetgit user GID + command: id -g wetgit + register: wetgit_gid_result + changed_when: false + check_mode: false + +- name: Store wetgit UID/GID as facts + set_fact: + wetgit_uid: "{{ wetgit_uid_result.stdout }}" + wetgit_gid: "{{ wetgit_gid_result.stdout }}" + +# --- Directories --- + +- name: Create WetGIT directories + file: + path: "{{ item.path }}" + state: directory + owner: "{{ item.owner }}" + group: "{{ item.group }}" + mode: "0755" + loop: + # Forgejo directories (owned by wetgit user) + - { path: "{{ app_dir }}/docker", owner: wetgit, group: wetgit } + - { path: "{{ forgejo_data_dir }}", owner: wetgit, group: wetgit } + - { path: "{{ forgejo_data_dir }}/gitea/conf", owner: wetgit, group: wetgit } + - { path: "{{ data_dir }}/redis", owner: wetgit, group: wetgit } + - { path: "{{ app_dir }}/scripts", owner: wetgit, group: wetgit } + - { path: "{{ app_dir }}/backups", owner: wetgit, group: wetgit } + - { path: "{{ app_dir }}/logs", owner: wetgit, group: wetgit } + - { path: "{{ app_dir }}/mirrors", owner: wetgit, group: wetgit } + # Application directories (owned by www-data for FastAPI/Celery) + - { path: "{{ app_dir }}", owner: root, group: root } + - { path: "{{ app_dir }}/backend", owner: www-data, group: www-data } + - { path: "{{ data_dir }}", owner: root, group: root } + - { path: "{{ data_dir }}/git-repos", owner: www-data, group: www-data } + +# --- Forgejo config --- + +- name: Deploy Forgejo app.ini (initial seed) + template: + src: app.ini.j2 + dest: "{{ forgejo_data_dir }}/gitea/conf/app.ini" + owner: wetgit + group: wetgit + mode: "0644" + # Don't overwrite if Forgejo has already modified it + force: no + notify: restart forgejo + +# --- Docker Compose --- + +- name: Deploy Docker Compose stack + template: + src: docker-compose.yml.j2 + dest: "{{ app_dir }}/docker/docker-compose.yml" + owner: wetgit + group: wetgit + mode: "0644" + notify: restart forgejo + +- name: Start WetGIT Docker stack + community.docker.docker_compose_v2: + project_src: "{{ app_dir }}/docker" + state: present + +# --- Backup script --- + +- name: Deploy backup script + template: + src: backup.sh.j2 + dest: "{{ app_dir }}/scripts/backup.sh" + owner: wetgit + group: wetgit + mode: "0755" + +# --- Mirror script --- + +- name: Deploy Codeberg mirror script + template: + src: mirror-to-codeberg.sh.j2 + dest: "{{ app_dir }}/scripts/mirror-to-codeberg.sh" + owner: wetgit + group: wetgit + mode: "0755" + +- name: Deploy Codeberg token + copy: + content: "{{ codeberg_api_token }}" + dest: "{{ app_dir }}/.codeberg-token" + owner: wetgit + group: wetgit + mode: "0600" + when: codeberg_api_token is defined and codeberg_api_token | length > 0 + +# --- Cron jobs --- + +- name: Configure backup cron (weekly Sunday 02:00) + cron: + name: "wetgit-backup" + user: root + weekday: "0" + hour: "2" + minute: "0" + job: "{{ app_dir }}/scripts/backup.sh >> {{ app_dir }}/logs/backup.log 2>&1" + +- name: Configure Codeberg mirror cron (daily 04:00) + cron: + name: "wetgit-codeberg-mirror" + user: wetgit + hour: "4" + minute: "0" + job: "{{ app_dir }}/scripts/mirror-to-codeberg.sh >> {{ app_dir }}/logs/mirror.log 2>&1" + when: codeberg_api_token is defined and codeberg_api_token | length > 0 + +- name: Configure log cleanup cron (monthly) + cron: + name: "wetgit-log-cleanup" + user: wetgit + day: "1" + hour: "5" + minute: "0" + job: "find {{ app_dir }}/logs -name '*.log' -mtime +30 -delete" diff --git a/ansible/roles/wetgit-forgejo/templates/app.ini.j2 b/ansible/roles/wetgit-forgejo/templates/app.ini.j2 new file mode 100644 index 0000000..35cc027 --- /dev/null +++ b/ansible/roles/wetgit-forgejo/templates/app.ini.j2 @@ -0,0 +1,75 @@ +; WetGit Forgejo configuration — managed by Ansible +; This file is merged with Forgejo's defaults on first boot. +; After first boot, Forgejo writes its own app.ini in /data/gitea/conf/. +; This template is used to seed initial configuration. + +[DEFAULT] +APP_NAME = WetGit + +[server] +DOMAIN = {{ forgejo_domain }} +SSH_DOMAIN = {{ forgejo_domain }} +ROOT_URL = https://{{ forgejo_domain }}/ +HTTP_PORT = 3000 +; HTTPS-only — no SSH, firewall blocks port 2222 +DISABLE_SSH = true +LFS_START_SERVER = true +OFFLINE_MODE = false + +[database] +DB_TYPE = sqlite3 +PATH = /data/gitea/forgejo.db + +[service] +DISABLE_REGISTRATION = true +REQUIRE_SIGNIN_VIEW = false +DEFAULT_KEEP_EMAIL_PRIVATE = true + +[repository] +DEFAULT_BRANCH = main +PREFERRED_LICENSES = MIT License,CC0-1.0 +MAX_CREATION_LIMIT = -1 +ENABLE_PUSH_CREATE_USER = true +ENABLE_PUSH_CREATE_ORG = true +; 100 MB max file size for large law datasets +MAX_FILE_SIZE = 104857600 + +[git] +MAX_GIT_DIFF_LINES = 10000 +MAX_GIT_DIFF_FILES = 1000 + +[git.timeout] +DEFAULT = 600 +MIGRATE = 1200 +MIRROR = 600 +CLONE = 600 +PULL = 600 +GC = 120 + +[lfs] +PATH = /data/git/lfs + +[ui] +DEFAULT_THEME = forgejo-auto +SHOW_USER_EMAIL = false + +[actions] +ENABLED = true + +[indexer] +REPO_INDEXER_ENABLED = true +REPO_INDEXER_PATH = /data/gitea/indexers/repos.bleve +REPO_INDEXER_EXCLUDE = node_modules/** + +[markup.markdown] +ENABLED = true +FILE_EXTENSIONS = .md,.markdown + +[mailer] +ENABLED = true +PROTOCOL = smtp+starttls +SMTP_ADDR = {{ forgejo_smtp_host | default('smtp.email.undefined') }} +SMTP_PORT = {{ forgejo_smtp_port | default(587) }} +FROM = Coornhert +USER = {{ forgejo_smtp_user | default('') }} +PASSWD = {{ forgejo_smtp_password | default('') }} diff --git a/ansible/roles/wetgit-forgejo/templates/backup.sh.j2 b/ansible/roles/wetgit-forgejo/templates/backup.sh.j2 new file mode 100644 index 0000000..09f816f --- /dev/null +++ b/ansible/roles/wetgit-forgejo/templates/backup.sh.j2 @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +# WetGIT Forgejo backup — managed by Ansible +# Uses Forgejo's built-in dump command (no downtime). + +BACKUP_DIR="{{ app_dir }}/backups" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +RETENTION_DAYS=14 +LOG_PREFIX="[$(date '+%Y-%m-%d %H:%M:%S')]" + +echo "$LOG_PREFIX Starting WetGit backup..." + +# Forgejo dump (runs inside container, no service stop needed) +docker exec wetgit-forgejo forgejo dump \ + --type tar.gz \ + --file /data/backup-${TIMESTAMP}.tar.gz \ + 2>&1 || { + echo "$LOG_PREFIX ERROR: Forgejo dump failed" + exit 1 +} + +# Move dump from container volume to backup dir +mv "{{ forgejo_data_dir }}/backup-${TIMESTAMP}.tar.gz" \ + "$BACKUP_DIR/wetgit-forgejo-${TIMESTAMP}.tar.gz" + +# Also backup Redis AOF +docker exec wetgit-redis redis-cli BGSAVE 2>/dev/null || true +sleep 2 +cp "{{ data_dir }}/redis/dump.rdb" \ + "$BACKUP_DIR/wetgit-redis-${TIMESTAMP}.rdb" 2>/dev/null || true + +# Clean old backups +find "$BACKUP_DIR" -name "wetgit-forgejo-*.tar.gz" -mtime +${RETENTION_DAYS} -delete +find "$BACKUP_DIR" -name "wetgit-redis-*.rdb" -mtime +${RETENTION_DAYS} -delete + +echo "$LOG_PREFIX Backup complete: wetgit-forgejo-${TIMESTAMP}.tar.gz" diff --git a/ansible/roles/wetgit-forgejo/templates/docker-compose.yml.j2 b/ansible/roles/wetgit-forgejo/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..22917dc --- /dev/null +++ b/ansible/roles/wetgit-forgejo/templates/docker-compose.yml.j2 @@ -0,0 +1,45 @@ +services: + forgejo: + image: codeberg.org/forgejo/forgejo:10 + container_name: wetgit-forgejo + restart: unless-stopped + environment: + - USER_UID={{ wetgit_uid }} + - USER_GID={{ wetgit_gid }} + volumes: + - {{ forgejo_data_dir }}:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "{{ backend_host }}:{{ forgejo_port }}:3000" + deploy: + resources: + limits: + memory: 1G + cpus: "2.0" + reservations: + memory: 256M + cpus: "0.5" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/v1/version"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - wetgit + + redis: + image: redis:7-alpine + container_name: wetgit-redis + restart: unless-stopped + ports: + - "{{ backend_host }}:{{ redis_port }}:6379" + volumes: + - {{ data_dir }}/redis:/data + command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru + networks: + - wetgit + +networks: + wetgit: + name: wetgit-network diff --git a/ansible/roles/wetgit-forgejo/templates/mirror-to-codeberg.sh.j2 b/ansible/roles/wetgit-forgejo/templates/mirror-to-codeberg.sh.j2 new file mode 100644 index 0000000..63897b9 --- /dev/null +++ b/ansible/roles/wetgit-forgejo/templates/mirror-to-codeberg.sh.j2 @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Mirror WetGit repos from self-hosted Forgejo to Codeberg +# Managed by Ansible — runs daily at 04:00 + +CODEBERG_USER="coornhert" +CODEBERG_TOKEN_FILE="{{ app_dir }}/.codeberg-token" +MIRROR_DIR="{{ app_dir }}/mirrors" +LOG_PREFIX="[$(date '+%Y-%m-%d %H:%M:%S')]" + +REPOS=( + "wetgit/meta" + "wetgit/rijk" + # Add more as they are created: + # "wetgit/cvdr-noord-holland" + # "wetgit/eu" +) + +if [ ! -f "$CODEBERG_TOKEN_FILE" ]; then + echo "$LOG_PREFIX ERROR: Codeberg token not found at $CODEBERG_TOKEN_FILE" + exit 1 +fi + +CODEBERG_TOKEN=$(cat "$CODEBERG_TOKEN_FILE") +mkdir -p "$MIRROR_DIR" + +for REPO in "${REPOS[@]}"; do + REPO_NAME=$(basename "$REPO") + REPO_MIRROR_DIR="$MIRROR_DIR/$REPO_NAME.git" + FORGEJO_URL="https://{{ forgejo_domain }}/${REPO}.git" + CODEBERG_URL="https://${CODEBERG_USER}:${CODEBERG_TOKEN}@codeberg.org/${REPO}.git" + + echo "$LOG_PREFIX Mirroring $REPO..." + + if [ ! -d "$REPO_MIRROR_DIR" ]; then + echo "$LOG_PREFIX Initial clone from Forgejo..." + git clone --bare "$FORGEJO_URL" "$REPO_MIRROR_DIR" + cd "$REPO_MIRROR_DIR" + git remote add codeberg "$CODEBERG_URL" + else + cd "$REPO_MIRROR_DIR" + echo "$LOG_PREFIX Fetching from Forgejo..." + git fetch origin --prune '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' + fi + + echo "$LOG_PREFIX Pushing to Codeberg..." + git push codeberg --mirror --force 2>&1 || { + echo "$LOG_PREFIX WARNING: Push to Codeberg failed for $REPO (non-fatal)" + } + + echo "$LOG_PREFIX Done: $REPO" +done + +echo "$LOG_PREFIX Mirror complete." diff --git a/ansible/roles/wetgit-nginx/handlers/main.yml b/ansible/roles/wetgit-nginx/handlers/main.yml new file mode 100644 index 0000000..4e0a6ca --- /dev/null +++ b/ansible/roles/wetgit-nginx/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: reload nginx + systemd: + name: nginx + state: reloaded diff --git a/ansible/roles/wetgit-nginx/tasks/main.yml b/ansible/roles/wetgit-nginx/tasks/main.yml new file mode 100644 index 0000000..ce9035f --- /dev/null +++ b/ansible/roles/wetgit-nginx/tasks/main.yml @@ -0,0 +1,118 @@ +--- +# Nginx vhosts for WetGIT +# IMPORTANT: Only adds vhost configs. Does NOT touch global nginx.conf +# (managed by dt-platform's nginx role). +# +# Strategy: Deploy HTTP-only first → get SSL certs → deploy full HTTPS config. + +# --- Step 1: Check existing SSL certificates --- + +- name: Check if API SSL certificate exists + stat: + path: "/etc/letsencrypt/live/{{ server_name }}/fullchain.pem" + register: ssl_cert_api + +- name: Check if Forgejo SSL certificate exists + stat: + path: "/etc/letsencrypt/live/{{ forgejo_domain }}/fullchain.pem" + register: ssl_cert_git + +# --- Step 2: Deploy HTTP-only configs for domains without certs --- + +- name: Deploy API HTTP-only vhost (pre-SSL) + copy: + content: | + # Temporary HTTP-only config for SSL provisioning — managed by Ansible + server { + listen 80; + listen [::]:80; + server_name {{ server_name }}; + location /.well-known/acme-challenge/ { root /var/www/certbot; } + location / { return 503; } + } + dest: /etc/nginx/sites-available/wetgit-api.conf + owner: root + group: root + mode: "0644" + when: not ssl_cert_api.stat.exists + notify: reload nginx + +- name: Deploy Forgejo HTTP-only vhost (pre-SSL) + copy: + content: | + # Temporary HTTP-only config for SSL provisioning — managed by Ansible + server { + listen 80; + listen [::]:80; + server_name {{ forgejo_domain }}; + location /.well-known/acme-challenge/ { root /var/www/certbot; } + location / { return 503; } + } + dest: /etc/nginx/sites-available/wetgit-git.conf + owner: root + group: root + mode: "0644" + when: not ssl_cert_git.stat.exists + notify: reload nginx + +# --- Step 3: Enable vhosts and reload nginx --- + +- name: Enable API vhost + file: + src: /etc/nginx/sites-available/wetgit-api.conf + dest: /etc/nginx/sites-enabled/wetgit-api.conf + state: link + notify: reload nginx + +- name: Enable Forgejo vhost + file: + src: /etc/nginx/sites-available/wetgit-git.conf + dest: /etc/nginx/sites-enabled/wetgit-git.conf + state: link + notify: reload nginx + +# Force handler to run now so nginx has the HTTP configs before certbot +- name: Flush handlers (reload nginx for certbot) + meta: flush_handlers + +# --- Step 4: Obtain SSL certificates via webroot --- + +- name: Obtain SSL certificate for {{ server_name }} + command: > + certbot certonly --webroot + -w /var/www/certbot + -d {{ server_name }} + --non-interactive --agree-tos + --email coornhert@wetgit.nl + when: not ssl_cert_api.stat.exists + register: certbot_api + +- name: Obtain SSL certificate for {{ forgejo_domain }} + command: > + certbot certonly --webroot + -w /var/www/certbot + -d {{ forgejo_domain }} + --non-interactive --agree-tos + --email coornhert@wetgit.nl + when: not ssl_cert_git.stat.exists + register: certbot_git + +# --- Step 5: Deploy full HTTPS configs --- + +- name: Deploy API nginx vhost (full HTTPS) + template: + src: wetgit-api.conf.j2 + dest: /etc/nginx/sites-available/wetgit-api.conf + owner: root + group: root + mode: "0644" + notify: reload nginx + +- name: Deploy Forgejo nginx vhost (full HTTPS) + template: + src: wetgit-git.conf.j2 + dest: /etc/nginx/sites-available/wetgit-git.conf + owner: root + group: root + mode: "0644" + notify: reload nginx diff --git a/ansible/roles/wetgit-nginx/templates/wetgit-api.conf.j2 b/ansible/roles/wetgit-nginx/templates/wetgit-api.conf.j2 new file mode 100644 index 0000000..858112d --- /dev/null +++ b/ansible/roles/wetgit-nginx/templates/wetgit-api.conf.j2 @@ -0,0 +1,51 @@ +# WetGIT API — managed by WetGIT Ansible (not dt-platform) +# Do NOT edit manually + +server { + listen 80; + listen [::]:80; + server_name {{ server_name }}; + + # ACME challenge (reuse existing certbot webroot) + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name {{ server_name }}; + + ssl_certificate /etc/letsencrypt/live/{{ server_name }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ server_name }}/privkey.pem; + + # Security headers + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # API proxy + location / { + proxy_pass http://{{ backend_host }}:{{ backend_port }}; + 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; + + # Timeouts for long-running legislation processing + proxy_read_timeout 120s; + proxy_connect_timeout 10s; + } + + # Health check (no rate limit) + location = /health { + proxy_pass http://{{ backend_host }}:{{ backend_port }}/health; + access_log off; + } +} diff --git a/ansible/roles/wetgit-nginx/templates/wetgit-git.conf.j2 b/ansible/roles/wetgit-nginx/templates/wetgit-git.conf.j2 new file mode 100644 index 0000000..a1d9014 --- /dev/null +++ b/ansible/roles/wetgit-nginx/templates/wetgit-git.conf.j2 @@ -0,0 +1,52 @@ +# Forgejo (git.wetgit.nl) — managed by WetGIT Ansible (not dt-platform) +# Do NOT edit manually + +server { + listen 80; + listen [::]:80; + server_name {{ forgejo_domain }}; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name {{ forgejo_domain }}; + + ssl_certificate /etc/letsencrypt/live/{{ forgejo_domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ forgejo_domain }}/privkey.pem; + + # Security headers + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Large body size for git pushes (law datasets can be large) + client_max_body_size 512M; + + # Timeouts for large git operations + proxy_connect_timeout 300; + proxy_send_timeout 300; + proxy_read_timeout 300; + + location / { + proxy_pass http://{{ backend_host }}:{{ forgejo_port }}; + 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; + + # WebSocket support (Forgejo live features) + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} diff --git a/ansible/site.yml b/ansible/site.yml new file mode 100644 index 0000000..165034d --- /dev/null +++ b/ansible/site.yml @@ -0,0 +1,27 @@ +--- +# WetGIT - Nederlandse wetgeving als code +# +# Usage: +# ansible-playbook ansible/site.yml +# ansible-playbook ansible/site.yml --tags forgejo +# ansible-playbook ansible/site.yml --tags app +# ansible-playbook ansible/site.yml --tags nginx +# ansible-playbook ansible/site.yml --check (dry-run) +# +# NOTE: This server is shared with dt-platform. +# This playbook only manages WetGIT resources. +# System-level config (users, packages, firewall) is managed by dt-platform. + +- name: Deploy WetGIT + hosts: wetgit + become: yes + + roles: + - role: wetgit-forgejo + tags: [forgejo, docker] + + - role: wetgit-app + tags: [app] + + - role: wetgit-nginx + tags: [nginx]