--- - name: Server Initialization Playbook hosts: localhost connection: local become: yes vars: base_domain: pangolin_domain: "pangolin.{{ base_domain }}" forgejo_domain: "forgejo.{{ base_domain }}" opencode_domain: "opencode.{{ base_domain }}" acme_email: "admin@{{ base_domain }}" docker_hosting_dir: "/home/{{ login_user }}/docker_hosting" resources_dir: "{{ playbook_dir }}/resources" login_user: "{{ ansible_env.SUDO_USER | default(ansible_user_id) }}" deb_architecture: "{{ 'amd64' if ansible_architecture == 'x86_64' else 'arm64' if ansible_architecture == 'aarch64' else ansible_architecture }}" tasks: - name: Update apt cache ansible.builtin.apt: update_cache: yes cache_valid_time: 3600 - name: Install prerequisites for Docker repository ansible.builtin.apt: name: - ca-certificates - curl - gnupg - lsb-release state: present - name: Install common dev dependencies ansible.builtin.apt: name: - git - vim - wget - htop - tmux - unzip - zip - build-essential - gcc - make - pkg-config - libssl-dev - libffi-dev - python3 - python3-pip - python3-venv - nodejs - npm - jq state: present # --- Server hardening --- - name: Install security packages ansible.builtin.apt: name: - ufw - fail2ban - unattended-upgrades - apt-listchanges state: present - name: Disable root SSH login ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^#?PermitRootLogin' line: 'PermitRootLogin no' validate: 'sshd -t -f %s' notify: restart sshd - name: Disable SSH password authentication ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^#?PasswordAuthentication' line: 'PasswordAuthentication no' validate: 'sshd -t -f %s' notify: restart sshd - name: Disable SSH challenge-response authentication ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^#?KbdInteractiveAuthentication' line: 'KbdInteractiveAuthentication no' validate: 'sshd -t -f %s' notify: restart sshd - name: Set SSH MaxAuthTries ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^#?MaxAuthTries' line: 'MaxAuthTries 3' validate: 'sshd -t -f %s' notify: restart sshd - name: UFW - Allow forwarding for Docker ansible.builtin.lineinfile: path: /etc/default/ufw regexp: '^DEFAULT_FORWARD_POLICY=' line: 'DEFAULT_FORWARD_POLICY="ACCEPT"' - name: UFW - Set default deny incoming community.general.ufw: direction: incoming default: deny - name: UFW - Set default allow outgoing community.general.ufw: direction: outgoing default: allow - name: UFW - Allow SSH community.general.ufw: rule: allow port: '22' proto: tcp - name: UFW - Allow HTTP community.general.ufw: rule: allow port: '80' proto: tcp - name: UFW - Allow HTTPS community.general.ufw: rule: allow port: '443' proto: tcp - name: UFW - Allow WireGuard community.general.ufw: rule: allow port: '51820' proto: udp - name: UFW - Allow WireGuard clients community.general.ufw: rule: allow port: '21820' proto: udp - name: UFW - Allow Docker to reach opencode on host community.general.ufw: rule: allow from_ip: 172.18.0.0/16 to_ip: any port: '3000' proto: tcp - name: UFW - Enable firewall community.general.ufw: state: enabled - name: Configure fail2ban for SSH ansible.builtin.copy: dest: /etc/fail2ban/jail.local mode: '0644' content: | [DEFAULT] bantime = 1h findtime = 10m maxretry = 5 [sshd] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log - name: Start and enable fail2ban ansible.builtin.service: name: fail2ban state: started enabled: yes - name: Enable unattended security upgrades ansible.builtin.copy: dest: /etc/apt/apt.conf.d/20auto-upgrades mode: '0644' content: | APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; APT::Periodic::AutocleanInterval "7"; - name: Create keyrings directory ansible.builtin.file: path: /etc/apt/keyrings state: directory mode: '0755' - name: Add Docker GPG key ansible.builtin.shell: cmd: curl -fsSL https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg -o /etc/apt/keyrings/docker.asc args: creates: /etc/apt/keyrings/docker.asc - name: Add Docker repository ansible.builtin.apt_repository: repo: "deb [arch={{ deb_architecture }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable" state: present - name: Install Docker ansible.builtin.apt: name: - docker-ce - docker-ce-cli - containerd.io - docker-compose-plugin state: present update_cache: yes - name: Start and enable Docker ansible.builtin.service: name: docker state: started enabled: yes - name: Add user to docker group ansible.builtin.user: name: "{{ login_user }}" groups: docker append: yes - name: Install Rust via rustup for login user ansible.builtin.shell: cmd: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y args: creates: "/home/{{ login_user }}/.cargo/bin/rustup" become_user: "{{ login_user }}" - name: Install uv (Python package manager) ansible.builtin.shell: cmd: curl -LsSf https://astral.sh/uv/install.sh | sh args: creates: "/home/{{ login_user }}/.local/bin/uv" become_user: "{{ login_user }}" # --- Forgejo SSH passthrough --- - name: Create forgejo-shell directory ansible.builtin.file: path: /etc/forgejo/bin state: directory mode: '0755' - name: Create forgejo-shell script ansible.builtin.copy: dest: /etc/forgejo/bin/forgejo-shell mode: '0755' content: | #!/bin/sh /usr/bin/docker exec -i -u git --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" forgejo sh "$@" - name: Create git user for Forgejo ansible.builtin.user: name: git system: yes shell: /etc/forgejo/bin/forgejo-shell home: /home/git create_home: yes register: git_user - name: Set git user UID/GID facts ansible.builtin.set_fact: git_uid: "{{ git_user.uid }}" git_gid: "{{ git_user.group }}" - name: Add git user to docker group ansible.builtin.user: name: git groups: docker append: yes - name: Configure SSH passthrough for git user ansible.builtin.copy: dest: /etc/ssh/sshd_config.d/forgejo-ssh-passthrough.conf mode: '0644' content: | Match User git AuthorizedKeysCommandUser git AuthorizedKeysCommand /usr/bin/docker exec -i -u git forgejo /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k notify: restart sshd - name: Generate Pangolin secret ansible.builtin.shell: cmd: openssl rand -base64 48 register: pangolin_secret_result changed_when: false - name: Set Pangolin secret fact ansible.builtin.set_fact: pangolin_secret: "{{ pangolin_secret_result.stdout }}" # --- Directory structure --- - name: Create docker_hosting directory structure ansible.builtin.file: path: "{{ docker_hosting_dir }}/{{ item }}" state: directory owner: "{{ login_user }}" group: "{{ login_user }}" mode: '0755' loop: - docker_data/pangolin - docker_data/pangolin/db - docker_data/pangolin/letsencrypt - docker_data/pangolin/logs - docker_data/pangolin/traefik - docker_data/pangolin/traefik/logs - name: Create Forgejo data directory ansible.builtin.file: path: "{{ docker_hosting_dir }}/docker_data/forgejo/data" state: directory owner: git group: git mode: '0755' # --- Pangolin config files --- - name: Copy Pangolin config.yml ansible.builtin.template: src: "{{ resources_dir }}/pangolin/config.yml.j2" dest: "{{ docker_hosting_dir }}/docker_data/pangolin/config.yml" owner: "{{ login_user }}" group: "{{ login_user }}" mode: '0600' force: no - name: Copy Traefik static config ansible.builtin.template: src: "{{ resources_dir }}/pangolin/traefik_config.yml.j2" dest: "{{ docker_hosting_dir }}/docker_data/pangolin/traefik/traefik_config.yml" owner: "{{ login_user }}" group: "{{ login_user }}" mode: '0644' - name: Copy Traefik dynamic config ansible.builtin.template: src: "{{ resources_dir }}/pangolin/dynamic_config.yml.j2" dest: "{{ docker_hosting_dir }}/docker_data/pangolin/traefik/dynamic_config.yml" owner: "{{ login_user }}" group: "{{ login_user }}" mode: '0644' - name: Copy compose.yml ansible.builtin.template: src: "{{ resources_dir }}/compose.yml.j2" dest: "{{ docker_hosting_dir }}/compose.yml" owner: "{{ login_user }}" group: "{{ login_user }}" mode: '0644' # --- OpenCode --- - name: Install opencode ansible.builtin.shell: cmd: curl -fsSL https://opencode.ai/install | bash args: creates: /home/{{ login_user }}/.opencode/bin/opencode become_user: "{{ login_user }}" - name: Create opencode systemd service ansible.builtin.copy: dest: /etc/systemd/system/opencode.service mode: '0644' content: | [Unit] Description=OpenCode Server After=network.target [Service] Type=simple User={{ login_user }} WorkingDirectory=/home/{{ login_user }} ExecStart=/home/{{ login_user }}/.opencode/bin/opencode serve --port 3000 --hostname 0.0.0.0 Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target - name: Start and enable opencode service ansible.builtin.systemd: name: opencode state: started enabled: yes daemon_reload: yes # --- Start services --- - name: Start all services via docker compose ansible.builtin.shell: cmd: docker compose up -d chdir: "{{ docker_hosting_dir }}" handlers: - name: restart sshd ansible.builtin.service: name: ssh state: restarted