Skip to content

About

Foundation roles for VPS setup - system hardening, Docker, and common services.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

98 Commits

Folders and files

Repository files navigation

Ansible Collection - softure.vps_foundation

Foundation roles for VPS setup - system hardening, Docker, and common services.

Installation

ansible-galaxy collection install git+https://github.com/softure/VPS_FOUNDATION.git#vps_foundation

Or add to requirements.yml:

collections:
  - name: softure.vps_foundation
    source: https://github.com/softure/VPS_FOUNDATION.git#vps_foundation
    type: git

Roles

Role Description
system_hardening System hardening (SSH, firewall, fail2ban, users)
docker_engine Docker and Docker Compose installation
postgres PostgreSQL container deployment (with optional PgAdmin and init scripts)
mssql Microsoft SQL Server container deployment
rabbitmq RabbitMQ container with management UI (with optional config)
caddy Caddy reverse proxy (with optional Cloudflare)
seq Seq logging server
typesense Typesense search engine
portainer Portainer CE for Docker management
supertokens SuperTokens authentication server
sqlpad SQL query tool with web UI
backrest Backrest backup manager
cronicle Cronicle task scheduler with web UI
redis Redis container with password auth and AOF persistence
usertour Self-hosted UserTour (product tours, checklists, tooltips)
traefik_edge Shared Traefik edge: its own compose project and network, Docker label routing, ACME, trusted proxy ranges
cloudflare_origin Only Cloudflare reaches published ports 80/443 (DOCKER-USER rules), ranges refreshed by a systemd timer
deploy_user Non-sudo deploy user whose authorized_keys holds one forced-command release key per app

Custom container images

Every service uses a sensible default image (e.g. postgres:16-alpine). Each image can be overridden with an optional <name>_image_override variable. When set, it fully replaces the default image — provide the complete reference including registry and tag; when left empty (the default), the built-in image is used.

Variable Overrides
postgres_image_override PostgreSQL image
pgadmin_image_override PgAdmin image
mssql_image_override MSSQL image
rabbitmq_image_override RabbitMQ image
caddy_image_override Caddy image (wins over both the default and Cloudflare variants)
seq_image_override Seq image
typesense_image_override Typesense image
portainer_image_override Portainer image
supertokens_image_override SuperTokens image
sqlpad_image_override SQLPad image
backrest_image_override Backrest image
cronicle_image_override Cronicle image
redis_image_override Redis image
usertour_image_override UserTour image
vars:
  # Pull from a private registry, or pin/bump to a different tag
  postgres_image_override: myregistry.io/library/postgres:17-alpine
  # Just change the tag on the default image
  typesense_image_override: typesense/typesense:0.26.0

Published ports and the firewall

Ports published by Docker bypass UFW. On a host where nothing should be reachable from outside except through a reverse proxy (e.g. a CI runner box), set docker_default_bind_ip: 127.0.0.1: ports published without an address ("5432:5432") then listen on localhost only. Leave it empty (the default) on hosts whose reverse proxy publishes "80:80"/"443:443", or give those ports an explicit 0.0.0.0: address.

Playbooks

VPS Bootstrap

- import_playbook: softure.vps_foundation.vps_bootstrap
  vars:
    vps_app_user: myproject
    vps_app_group: myproject
    vps_timezone: "Europe/Warsaw"
    vps_locale: "en_US.UTF-8"
    vps_github_actions_ssh_required: true  # prompts for GitHub Actions SSH key

VPS Deploy Infrastructure

- import_playbook: softure.vps_foundation.vps_deploy_infrastructure
  vars:
    services_network: myproject-network
    services_postgres_enabled: true
    services_pgadmin_enabled: true
    postgres_db: myproject_db
    postgres_user: myproject
    postgres_password: "{{ lookup('env', 'POSTGRES_PASSWORD') }}"
    postgres_init_script_path: "{{ playbook_dir }}/files/init.sql"
    rabbitmq_config_path: "{{ playbook_dir }}/files/rabbitmq.conf"
    services_mssql_enabled: true
    mssql_sa_password: "{{ lookup('env', 'MSSQL_SA_PASSWORD') }}"
    services_seq_enabled: true
    services_typesense_enabled: true
    services_portainer_enabled: false
    services_supertokens_enabled: true
    services_cronicle_enabled: true
    cronicle_secret_key: "{{ lookup('env', 'CRONICLE_SECRET_KEY') }}"

Shared app edge

Three roles for a server that hosts several apps, each deployed by its own release pipeline. None of them names an app: every shared name is a variable with a generic default.

Role What it owns Main variables (defaults)
traefik_edge /srv/softure-edge/docker-compose.yml (compose project softure-edge) and the network softure-edge. Apps join that network as external and route with labels (traefik.enable=true, tls.certresolver=letsencrypt); exposedByDefault is off. HTTP redirects to HTTPS, --ping health check, JSON access log without client addresses traefik_edge_name, traefik_edge_network, traefik_edge_cert_resolver (letsencrypt), traefik_edge_acme_email (required), traefik_edge_acme_challenge (http, or dns with traefik_edge_cloudflare_dns_token), traefik_edge_trusted_ips
cloudflare_origin /usr/local/sbin/softure-cloudflare-only with its .service (runs on every Docker start) and .path (re-runs when the ranges change), /usr/local/sbin/softure-cloudflare-only-refresh with its daily .timer, the ranges file (seeded from the role's list) cloudflare_origin_name, cloudflare_origin_ranges_file (/srv/softure-edge/cloudflare-ips.txt), cloudflare_origin_ports ([80, 443]), cloudflare_origin_interface (default route's), cloudflare_origin_refresh_calendar (daily)
deploy_user the user deploy (no sudo, in docker) and one line per app in its authorized_keys: command="/srv/<app>/deploy.sh",restrict <key> <app>-release. A line is added or replaced by its comment, never duplicated; other lines stay; state: absent removes it deploy_user_name, deploy_user_apps: [{ name, key?, command?, state? }], deploy_user_app_root (/srv)

UFW does not filter traffic to published container ports (Docker's DNAT runs before UFW's chains), which is why cloudflare_origin works in DOCKER-USER. The deploy user must also be in vps_ssh_allowed_users, or sshd's AllowUsers turns it away. deploy_user runs after docker_engine (it joins the docker group).

- import_playbook: softure.vps_foundation.vps_bootstrap
  vars:
    vps_app_user: admin
    vps_ssh_allowed_users: [admin, deploy]
    vps_github_actions_ssh_required: false

- name: Shared app edge
  hosts: all
  become: true
  roles:
    - role: softure.vps_foundation.deploy_user
      vars:
        deploy_user_apps:
          - { name: myapp, key: "ssh-ed25519 AAAA... any-comment" }
    - role: softure.vps_foundation.traefik_edge
      vars:
        traefik_edge_acme_email: ops@example.com
    - role: softure.vps_foundation.cloudflare_origin

Quick Start

# playbooks/setup.yml
---
- name: "Phase 1: Bootstrap VPS"
  ansible.builtin.import_playbook: softure.vps_foundation.vps_bootstrap
  vars:
    vps_app_user: deploy
    vps_app_group: deploy
    vps_timezone: "Europe/Warsaw"
  tags: [bootstrap]

- name: "Phase 2: Deploy Infrastructure"
  ansible.builtin.import_playbook: softure.vps_foundation.vps_deploy_infrastructure
  vars:
    services_network: app-network
    services_postgres_enabled: true
    services_pgadmin_enabled: true
    services_caddy_enabled: true
    services_seq_enabled: true
  tags: [infrastructure]

Run from the root of your project (where inventory/ and playbooks/ directories are located):

# Install collection and dependencies
ansible-galaxy collection install -r requirements.yml

# Full setup (bootstrap + infrastructure)
ansible-playbook -i inventory/hosts.yml playbooks/setup.yml

# Bootstrap only
ansible-playbook -i inventory/hosts.yml playbooks/setup.yml --tags bootstrap

# Infrastructure only
ansible-playbook -i inventory/hosts.yml playbooks/setup.yml --tags infrastructure

# Single service
ansible-playbook -i inventory/hosts.yml playbooks/setup.yml --tags postgres

One-off bootstrap (no inventory files)

To harden a fresh server and install Docker without creating an inventory — services deployed separately — use the wrapper script:

# Install the collection first
ansible-galaxy collection install . --force

./scripts/bootstrap.sh \
  --host 203.0.113.10 \
  --user deploy \
  --pubkey ~/.ssh/id_ed25519.pub

The script verifies key-based SSH access before running, then passes everything through --extra-vars — nothing is written to disk. Options: --ssh-user, --ssh-port, --timezone, --locale, --github-actions, --check. Arguments after -- are forwarded to ansible-playbook.

The public key must already be authorized on the server (ssh-copy-id first) — Ansible cannot log in with a password without sshpass.

See the example/ directory for a complete working setup including inventory, group vars, playbooks, and sample config files.

Requirements

  • Ansible >= 2.14
  • Ubuntu 22.04+ (jammy, noble)
  • Collections: community.docker, community.general, ansible.posix

Important

  • Bootstrap playbook validates Ubuntu 22.04+ and waits for system readiness before proceeding
  • After running vps_bootstrap, login using the vps_app_user account instead of the default ubuntu or root from your provider

License

MIT

About

Foundation roles for VPS setup - system hardening, Docker, and common services.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages