Initial infra setup: Terraform, Ansible, backup roles

This commit is contained in:
Ryan Moon
2026-03-31 08:11:12 -05:00
commit d6ff4746d0
31 changed files with 792 additions and 0 deletions

7
ansible/gitea.yml Normal file
View File

@@ -0,0 +1,7 @@
---
- name: Set up Gitea
hosts: gitea
become: true
roles:
- gitea

View File

@@ -0,0 +1,12 @@
# Copy to vault.yml and encrypt with: ansible-vault encrypt vault.yml
# Reference in playbook with: ansible-playbook --ask-vault-pass gitea.yml
---
cf_origin_cert: |
-----BEGIN CERTIFICATE-----
<paste cert from Cloudflare here>
-----END CERTIFICATE-----
cf_origin_key: |
-----BEGIN PRIVATE KEY-----
<paste key from Cloudflare here>
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,6 @@
---
gitea_domain: "git.lunarfront.tech"
gitea_instance_url: "https://git.lunarfront.tech"
vaultwarden_domain: "vault.lunarfront.tech"
vaultwarden_signups_allowed: false
gitea_db_type: postgres

10
ansible/infra.yml Normal file
View File

@@ -0,0 +1,10 @@
---
- name: Set up infra server
hosts: infra
become: true
roles:
- gitea
- gitea-runner
- vaultwarden
- backup

View File

@@ -0,0 +1,2 @@
[gitea]
<GITEA_IP> ansible_user=root ansible_ssh_private_key_file=~/.ssh/id_ed25519

View File

@@ -0,0 +1,14 @@
---
backup_bucket: "lunarfront-infra"
backup_spaces_region: "nyc3"
backup_schedule: "0 3 * * *" # 3am daily
backup_dirs:
- src: /var/lib/gitea/data
dest: backups/gitea
- src: /var/lib/vaultwarden/data
dest: backups/vaultwarden
# Set in vault
spaces_access_key: ""
spaces_secret_key: ""

View File

@@ -0,0 +1,38 @@
---
- name: Install rclone
apt:
name: rclone
state: present
update_cache: true
- name: Create rclone config directory
file:
path: /root/.config/rclone
state: directory
owner: root
group: root
mode: "0700"
- name: Deploy rclone config
template:
src: rclone.conf.j2
dest: /root/.config/rclone/rclone.conf
owner: root
group: root
mode: "0600"
- name: Deploy backup script
template:
src: backup.sh.j2
dest: /usr/local/bin/backup.sh
owner: root
group: root
mode: "0700"
- name: Schedule daily backup cron job
cron:
name: "lunarfront backup"
job: "/usr/local/bin/backup.sh >> /var/log/backup.log 2>&1"
minute: "0"
hour: "3"
user: root

View File

@@ -0,0 +1,34 @@
#!/bin/bash
set -euo pipefail
DATE=$(date +%Y-%m-%d)
MONTH=$(date +%Y-%m)
DAY_OF_MONTH=$(date +%d)
echo "[$(date)] Starting backup"
{% for item in backup_dirs %}
echo "[$(date)] Backing up {{ item.src }}"
# Daily backup — kept for 30 days
rclone sync {{ item.src }} spaces://{{ backup_bucket }}/{{ item.dest }}/daily/${DATE} \
--s3-acl private
# Monthly backup on the 1st — kept for 12 months
if [ "${DAY_OF_MONTH}" = "01" ]; then
rclone sync {{ item.src }} spaces://{{ backup_bucket }}/{{ item.dest }}/monthly/${MONTH} \
--s3-acl private
echo "[$(date)] Monthly backup complete for {{ item.src }}"
fi
# Prune daily backups older than 30 days
rclone delete spaces://{{ backup_bucket }}/{{ item.dest }}/daily \
--min-age 30d --s3-acl private
# Prune monthly backups older than 12 months
rclone delete spaces://{{ backup_bucket }}/{{ item.dest }}/monthly \
--min-age 365d --s3-acl private
{% endfor %}
echo "[$(date)] Backup complete"

View File

@@ -0,0 +1,7 @@
[spaces]
type = s3
provider = DigitalOcean
access_key_id = {{ spaces_access_key }}
secret_access_key = {{ spaces_secret_key }}
endpoint = nyc3.digitaloceanspaces.com
acl = private

View File

@@ -0,0 +1,10 @@
---
gitea_runner_version: "0.2.11"
gitea_runner_data_dir: /var/lib/gitea-runner
gitea_instance_url: "https://git.example.com"
# Generate in Gitea: Site Admin → Actions → Runners → Create new runner
gitea_runner_token: ""
gitea_runner_name: "{{ inventory_hostname }}"
gitea_runner_labels: "ubuntu-latest:docker://node:20"

View File

@@ -0,0 +1,6 @@
---
- name: Restart gitea-runner
community.docker.docker_compose_v2:
project_src: "{{ gitea_runner_data_dir }}"
state: present
recreate: always

View File

@@ -0,0 +1,27 @@
---
- name: Create gitea-runner data directory
file:
path: "{{ gitea_runner_data_dir }}"
state: directory
owner: root
group: root
mode: "0700"
- name: Deploy docker-compose file
template:
src: docker-compose.yml.j2
dest: "{{ gitea_runner_data_dir }}/docker-compose.yml"
mode: "0600"
notify: Restart gitea-runner
- name: Deploy runner config
template:
src: config.yml.j2
dest: "{{ gitea_runner_data_dir }}/config.yml"
mode: "0600"
notify: Restart gitea-runner
- name: Start gitea-runner
community.docker.docker_compose_v2:
project_src: "{{ gitea_runner_data_dir }}"
state: present

View File

@@ -0,0 +1,17 @@
log:
level: info
runner:
file: /data/.runner
capacity: 2 # max concurrent jobs — lower if droplet is under load
labels:
- "ubuntu-latest:docker://node:20"
- "ubuntu-22.04:docker://node:20"
cache:
enabled: true
dir: /data/cache
container:
network: bridge
force_pull: false # reuse cached images to speed up builds

View File

@@ -0,0 +1,14 @@
services:
gitea-runner:
image: gitea/act_runner:{{ gitea_runner_version }}
container_name: gitea-runner
restart: unless-stopped
volumes:
- {{ gitea_runner_data_dir }}/config.yml:/config.yml
- {{ gitea_runner_data_dir }}/data:/data
- /var/run/docker.sock:/var/run/docker.sock # allows runner to spin up job containers
environment:
CONFIG_FILE: /config.yml
GITEA_INSTANCE_URL: "{{ gitea_instance_url }}"
GITEA_RUNNER_REGISTRATION_TOKEN: "{{ gitea_runner_token }}"
GITEA_RUNNER_NAME: "{{ gitea_runner_name }}"

View File

@@ -0,0 +1,10 @@
---
gitea_version: "1.22.3"
gitea_domain: "git.example.com"
gitea_http_port: 3000
gitea_ssh_port: 2222
gitea_data_dir: /var/lib/gitea
# Cloudflare Origin Certificate
cf_origin_cert: ""
cf_origin_key: ""

View File

@@ -0,0 +1,11 @@
---
- name: Restart gitea
community.docker.docker_compose_v2:
project_src: "{{ gitea_data_dir }}"
state: present
recreate: always
- name: Reload nginx
systemd:
name: nginx
state: reloaded

View File

@@ -0,0 +1,88 @@
---
- name: Install dependencies
apt:
name: [nginx, docker.io, docker-compose-v2]
state: present
update_cache: true
- name: Enable and start Docker
systemd:
name: docker
enabled: true
state: started
- name: Create gitea data directory
file:
path: "{{ gitea_data_dir }}"
state: directory
owner: root
group: root
mode: "0700"
- name: Deploy docker-compose file
template:
src: docker-compose.yml.j2
dest: "{{ gitea_data_dir }}/docker-compose.yml"
mode: "0600"
notify: Restart gitea
- name: Start gitea
community.docker.docker_compose_v2:
project_src: "{{ gitea_data_dir }}"
state: present
# ─── Cloudflare Origin Certificate ───────────────────────────────────────────
- name: Create SSL directory
file:
path: /etc/nginx/ssl
state: directory
owner: root
group: root
mode: "0700"
- name: Install Cloudflare origin certificate
copy:
content: "{{ cf_origin_cert }}"
dest: /etc/nginx/ssl/cf-origin.pem
owner: root
group: root
mode: "0600"
notify: Reload nginx
- name: Install Cloudflare origin key
copy:
content: "{{ cf_origin_key }}"
dest: /etc/nginx/ssl/cf-origin.key
owner: root
group: root
mode: "0600"
notify: Reload nginx
# ─── nginx ────────────────────────────────────────────────────────────────────
- name: Deploy nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/gitea
mode: "0644"
notify: Reload nginx
- name: Enable nginx site
file:
src: /etc/nginx/sites-available/gitea
dest: /etc/nginx/sites-enabled/gitea
state: link
notify: Reload nginx
- name: Remove nginx default site
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: Reload nginx
- name: Ensure nginx is started
systemd:
name: nginx
enabled: true
state: started

View File

@@ -0,0 +1,29 @@
services:
gitea:
image: gitea/gitea:{{ gitea_version }}
container_name: gitea
restart: unless-stopped
volumes:
- {{ gitea_data_dir }}/data:/data
environment:
USER_UID: 1000
USER_GID: 1000
GITEA__server__DOMAIN: "{{ gitea_domain }}"
GITEA__server__ROOT_URL: "https://{{ gitea_domain }}/"
GITEA__server__HTTP_PORT: 3000
GITEA__server__SSH_DOMAIN: "{{ gitea_domain }}"
GITEA__server__SSH_PORT: "{{ gitea_ssh_port }}"
GITEA__server__SSH_LISTEN_PORT: 2222
GITEA__server__START_SSH_SERVER: "true"
GITEA__service__DISABLE_REGISTRATION: "true"
{% if gitea_db_url is defined and gitea_db_url %}
GITEA__database__DB_TYPE: "postgres"
GITEA__database__HOST: "{{ gitea_db_host }}"
GITEA__database__NAME: "gitea"
GITEA__database__USER: "{{ gitea_db_user }}"
GITEA__database__PASSWD: "{{ gitea_db_password }}"
GITEA__database__SSL_MODE: "require"
{% endif %}
ports:
- "127.0.0.1:{{ gitea_http_port }}:3000"
- "{{ gitea_ssh_port }}:2222"

View File

@@ -0,0 +1,24 @@
server {
listen 80;
server_name {{ gitea_domain }};
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name {{ gitea_domain }};
ssl_certificate /etc/nginx/ssl/cf-origin.pem;
ssl_certificate_key /etc/nginx/ssl/cf-origin.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://127.0.0.1:{{ gitea_http_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;
}
}

View File

@@ -0,0 +1,18 @@
---
vaultwarden_domain: "vault.example.com"
vaultwarden_port: 8080
vaultwarden_data_dir: /var/lib/vaultwarden
# Set to your postgres connection string or leave as default for SQLite
vaultwarden_database_url: "" # e.g. postgresql://user:pass@host/vaultwarden
# Restrict signups after first admin account is created
vaultwarden_signups_allowed: "true"
# Admin token — set this to a strong random string
# Generate with: openssl rand -base64 48
vaultwarden_admin_token: ""
# Cloudflare Origin Certificate
cf_origin_cert: ""
cf_origin_key: ""

View File

@@ -0,0 +1,11 @@
---
- name: Restart vaultwarden
community.docker.docker_compose_v2:
project_src: "{{ vaultwarden_data_dir }}"
state: present
recreate: always
- name: Reload nginx
systemd:
name: nginx
state: reloaded

View File

@@ -0,0 +1,76 @@
---
- name: Install dependencies
apt:
name: [docker.io, docker-compose-v2]
state: present
update_cache: true
- name: Enable and start Docker
systemd:
name: docker
enabled: true
state: started
- name: Create vaultwarden data directory
file:
path: "{{ vaultwarden_data_dir }}"
state: directory
owner: root
group: root
mode: "0700"
- name: Deploy docker-compose file
template:
src: docker-compose.yml.j2
dest: "{{ vaultwarden_data_dir }}/docker-compose.yml"
mode: "0600"
notify: Restart vaultwarden
- name: Start vaultwarden
community.docker.docker_compose_v2:
project_src: "{{ vaultwarden_data_dir }}"
state: present
# ─── Cloudflare Origin Certificate ───────────────────────────────────────────
- name: Create SSL directory
file:
path: /etc/nginx/ssl
state: directory
owner: root
group: root
mode: "0700"
- name: Install Cloudflare origin certificate
copy:
content: "{{ cf_origin_cert }}"
dest: /etc/nginx/ssl/cf-origin.pem
owner: root
group: root
mode: "0600"
notify: Reload nginx
- name: Install Cloudflare origin key
copy:
content: "{{ cf_origin_key }}"
dest: /etc/nginx/ssl/cf-origin.key
owner: root
group: root
mode: "0600"
notify: Reload nginx
# ─── nginx ────────────────────────────────────────────────────────────────────
- name: Deploy nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/vaultwarden
mode: "0644"
notify: Reload nginx
- name: Enable nginx site
file:
src: /etc/nginx/sites-available/vaultwarden
dest: /etc/nginx/sites-enabled/vaultwarden
state: link
notify: Reload nginx

View File

@@ -0,0 +1,16 @@
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
volumes:
- {{ vaultwarden_data_dir }}/data:/data
environment:
DOMAIN: "https://{{ vaultwarden_domain }}"
SIGNUPS_ALLOWED: "{{ vaultwarden_signups_allowed | lower }}"
ADMIN_TOKEN: "{{ vaultwarden_admin_token }}"
{% if vaultwarden_database_url %}
DATABASE_URL: "{{ vaultwarden_database_url }}"
{% endif %}
ports:
- "127.0.0.1:{{ vaultwarden_port }}:80"

View File

@@ -0,0 +1,27 @@
server {
listen 80;
server_name {{ vaultwarden_domain }};
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name {{ vaultwarden_domain }};
ssl_certificate /etc/nginx/ssl/cf-origin.pem;
ssl_certificate_key /etc/nginx/ssl/cf-origin.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Required for Bitwarden web vault
client_max_body_size 525m;
location / {
proxy_pass http://127.0.0.1:{{ vaultwarden_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;
}
}