Initial server init setup with Ansible playbook
Automated server provisioning with Pangolin reverse proxy, Forgejo git server with SSH passthrough, and OpenCode dev environment. Includes server hardening (UFW, fail2ban, SSH lockdown), Docker, Rust, Python/uv, and unattended security upgrades. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
c34d2b4849
42
init.sh
Executable file
42
init.sh
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "=== Server Init ==="
|
||||
echo ""
|
||||
|
||||
read -p "Username [ubuntu]: " OLD_USER
|
||||
OLD_USER="${OLD_USER:-ubuntu}"
|
||||
read -p "Host: " HOST
|
||||
read -p "SSH public key path [~/.ssh/id_ed25519.pub]: " PUBKEY_PATH
|
||||
PUBKEY_PATH="${PUBKEY_PATH:-$HOME/.ssh/id_ed25519.pub}"
|
||||
read -p "New username: " NEW_USER
|
||||
|
||||
SSH_TARGET="${OLD_USER}@pangolin.${HOST}"
|
||||
|
||||
echo ""
|
||||
echo "Set a password for '$NEW_USER' (used for sudo):"
|
||||
read -s -p "Password: " USER_PASSWORD
|
||||
echo
|
||||
read -s -p "Confirm: " USER_PASSWORD_CONFIRM
|
||||
echo
|
||||
|
||||
if [ "$USER_PASSWORD" != "$USER_PASSWORD_CONFIRM" ]; then
|
||||
echo "Error: Passwords do not match"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$PUBKEY_PATH" ]; then
|
||||
echo "Error: Public key file not found: $PUBKEY_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PUBKEY=$(cat "$PUBKEY_PATH")
|
||||
|
||||
export SCRIPT_DIR HOST OLD_USER NEW_USER SSH_TARGET PUBKEY USER_PASSWORD
|
||||
|
||||
source "$SCRIPT_DIR/scripts/01_create_user.sh"
|
||||
source "$SCRIPT_DIR/scripts/02_remove_old_user.sh"
|
||||
source "$SCRIPT_DIR/scripts/03_run_playbook.sh"
|
||||
source "$SCRIPT_DIR/scripts/04_show_setup_info.sh"
|
||||
404
playbook.yml
Normal file
404
playbook.yml
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
---
|
||||
- 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 --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 forgejo /usr/local/bin/gitea keys -c /etc/gitea/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
|
||||
74
resources/compose.yml.j2
Normal file
74
resources/compose.yml.j2
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
name: pangolin
|
||||
|
||||
services:
|
||||
pangolin:
|
||||
image: docker.io/fosrl/pangolin:latest
|
||||
container_name: pangolin
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./docker_data/pangolin:/app/config
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||
interval: "10s"
|
||||
timeout: "10s"
|
||||
retries: 15
|
||||
|
||||
gerbil:
|
||||
image: docker.io/fosrl/gerbil:latest
|
||||
container_name: gerbil
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --reachableAt=http://gerbil:3004
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||
volumes:
|
||||
- ./docker_data/pangolin:/var/config
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
ports:
|
||||
- "51820:51820/udp"
|
||||
- "21820:21820/udp"
|
||||
- "443:443"
|
||||
- "80:80"
|
||||
|
||||
traefik:
|
||||
image: docker.io/traefik:v3.6
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
network_mode: service:gerbil
|
||||
depends_on:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --configFile=/etc/traefik/traefik_config.yml
|
||||
volumes:
|
||||
- ./docker_data/pangolin/traefik:/etc/traefik:ro
|
||||
- ./docker_data/pangolin/letsencrypt:/letsencrypt
|
||||
- ./docker_data/pangolin/traefik/logs:/var/log/traefik
|
||||
|
||||
forgejo:
|
||||
image: codeberg.org/forgejo/forgejo:7
|
||||
container_name: forgejo
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./docker_data/forgejo/data:/data
|
||||
environment:
|
||||
- USER_UID={{ git_uid }}
|
||||
- USER_GID={{ git_gid }}
|
||||
- FORGEJO__service__DISABLE_REGISTRATION=true
|
||||
- FORGEJO__server__ROOT_URL=https://{{ forgejo_domain }}
|
||||
- FORGEJO__server__HTTP_PORT=3001
|
||||
- FORGEJO__server__DISABLE_SSH=false
|
||||
- FORGEJO__server__START_SSH_SERVER=false
|
||||
- FORGEJO__server__SSH_PORT=22
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
name: pangolin
|
||||
48
resources/pangolin/config.yml.j2
Normal file
48
resources/pangolin/config.yml.j2
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
app:
|
||||
dashboard_url: "https://{{ pangolin_domain }}"
|
||||
log_level: "info"
|
||||
save_logs: true
|
||||
|
||||
server:
|
||||
external_port: 3000
|
||||
internal_port: 3001
|
||||
next_port: 3002
|
||||
internal_hostname: "pangolin"
|
||||
secret: "{{ pangolin_secret }}"
|
||||
dashboard_session_length_hours: 720
|
||||
resource_session_length_hours: 720
|
||||
cors:
|
||||
origins:
|
||||
- "https://{{ pangolin_domain }}"
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||
credentials: false
|
||||
|
||||
domains:
|
||||
domain1:
|
||||
base_domain: "{{ base_domain }}"
|
||||
cert_resolver: "letsencrypt"
|
||||
|
||||
traefik:
|
||||
http_entrypoint: "web"
|
||||
https_entrypoint: "websecure"
|
||||
cert_resolver: "letsencrypt"
|
||||
|
||||
gerbil:
|
||||
base_endpoint: "{{ pangolin_domain }}"
|
||||
start_port: 51820
|
||||
clients_start_port: 21820
|
||||
|
||||
rate_limits:
|
||||
global:
|
||||
window_minutes: 1
|
||||
max_requests: 100
|
||||
auth:
|
||||
window_minutes: 1
|
||||
max_requests: 10
|
||||
|
||||
flags:
|
||||
require_email_verification: false
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: false
|
||||
allow_raw_resources: true
|
||||
92
resources/pangolin/dynamic_config.yml.j2
Normal file
92
resources/pangolin/dynamic_config.yml.j2
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
http:
|
||||
middlewares:
|
||||
badger:
|
||||
plugin:
|
||||
badger:
|
||||
disableForwardAuth: true
|
||||
redirect-to-https:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
|
||||
routers:
|
||||
# --- Pangolin dashboard ---
|
||||
main-app-router-redirect:
|
||||
rule: "Host(`{{ pangolin_domain }}`)"
|
||||
service: next-service
|
||||
entryPoints:
|
||||
- web
|
||||
middlewares:
|
||||
- redirect-to-https
|
||||
- badger
|
||||
|
||||
next-router:
|
||||
rule: "Host(`{{ pangolin_domain }}`) && !PathPrefix(`/api/v1`)"
|
||||
service: next-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
api-router:
|
||||
rule: "Host(`{{ pangolin_domain }}`) && PathPrefix(`/api/v1`)"
|
||||
service: api-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
ws-router:
|
||||
rule: "Host(`{{ pangolin_domain }}`)"
|
||||
service: api-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
# --- Forgejo (public, no auth) ---
|
||||
forgejo-redirect:
|
||||
rule: "Host(`{{ forgejo_domain }}`)"
|
||||
service: forgejo-service
|
||||
entryPoints:
|
||||
- web
|
||||
middlewares:
|
||||
- redirect-to-https
|
||||
|
||||
forgejo-router:
|
||||
rule: "Host(`{{ forgejo_domain }}`)"
|
||||
service: forgejo-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
services:
|
||||
next-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3002"
|
||||
|
||||
api-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3000"
|
||||
|
||||
forgejo-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://forgejo:3001"
|
||||
|
||||
tcp:
|
||||
serversTransports:
|
||||
pp-transport-v1:
|
||||
proxyProtocol:
|
||||
version: 1
|
||||
pp-transport-v2:
|
||||
proxyProtocol:
|
||||
version: 2
|
||||
54
resources/pangolin/traefik_config.yml.j2
Normal file
54
resources/pangolin/traefik_config.yml.j2
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
api:
|
||||
insecure: true
|
||||
dashboard: true
|
||||
|
||||
providers:
|
||||
http:
|
||||
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||
pollInterval: "5s"
|
||||
file:
|
||||
filename: "/etc/traefik/dynamic_config.yml"
|
||||
|
||||
experimental:
|
||||
plugins:
|
||||
badger:
|
||||
moduleName: "github.com/fosrl/badger"
|
||||
version: "v1.3.1"
|
||||
|
||||
log:
|
||||
level: "INFO"
|
||||
format: "common"
|
||||
maxSize: 100
|
||||
maxBackups: 3
|
||||
maxAge: 3
|
||||
compress: true
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
email: "{{ acme_email }}"
|
||||
storage: "/letsencrypt/acme.json"
|
||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
websecure:
|
||||
address: ":443"
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
ping:
|
||||
entryPoint: "web"
|
||||
24
scripts/01_create_user.sh
Executable file
24
scripts/01_create_user.sh
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Phase 1: Creating user '$NEW_USER' on $SSH_TARGET ==="
|
||||
|
||||
PUBKEY_B64=$(echo "$PUBKEY" | base64 -w 0)
|
||||
PASS_B64=$(echo "$USER_PASSWORD" | base64 -w 0)
|
||||
|
||||
ssh -o StrictHostKeyChecking=accept-new "$SSH_TARGET" bash -s -- "$NEW_USER" "$PUBKEY_B64" "$PASS_B64" <<'REMOTE'
|
||||
set -e
|
||||
NEW_USER="$1"
|
||||
PUBKEY=$(echo "$2" | base64 -d)
|
||||
USER_PASSWORD=$(echo "$3" | base64 -d)
|
||||
|
||||
sudo adduser --disabled-password --gecos "" "$NEW_USER"
|
||||
echo "$NEW_USER:$USER_PASSWORD" | sudo chpasswd
|
||||
sudo usermod -aG sudo "$NEW_USER"
|
||||
|
||||
sudo mkdir -p /home/$NEW_USER/.ssh
|
||||
echo "$PUBKEY" | sudo tee /home/$NEW_USER/.ssh/authorized_keys > /dev/null
|
||||
sudo chmod 700 /home/$NEW_USER/.ssh
|
||||
sudo chmod 600 /home/$NEW_USER/.ssh/authorized_keys
|
||||
sudo chown -R $NEW_USER:$NEW_USER /home/$NEW_USER/.ssh
|
||||
REMOTE
|
||||
7
scripts/02_remove_old_user.sh
Executable file
7
scripts/02_remove_old_user.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Phase 2: Removing default user '$OLD_USER' ==="
|
||||
|
||||
ssh -t -o StrictHostKeyChecking=accept-new "$NEW_USER@pangolin.$HOST" \
|
||||
"sudo pkill -u $OLD_USER 2>/dev/null || true; sleep 1; sudo deluser --remove-home $OLD_USER 2>/dev/null || sudo userdel -r $OLD_USER; echo 'Removed user: $OLD_USER'"
|
||||
9
scripts/03_run_playbook.sh
Executable file
9
scripts/03_run_playbook.sh
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Phase 3: Copying files and running playbook ==="
|
||||
|
||||
rsync -az --exclude '.git' "$SCRIPT_DIR/" "$NEW_USER@pangolin.$HOST:~/server_init/"
|
||||
|
||||
ssh -t "$NEW_USER@pangolin.$HOST" \
|
||||
"bash -c 'sudo locale-gen en_US.UTF-8 && if ! command -v ansible &>/dev/null; then echo \"Installing Ansible...\"; sudo apt-get update && sudo apt-get install -y ansible-core python3-pip; fi && echo \"Running Ansible playbook...\" && cd ~/server_init && LC_ALL=en_US.UTF-8 ansible-playbook -i localhost, -c local playbook.yml -e base_domain=\"$HOST\" --ask-become-pass'"
|
||||
27
scripts/04_show_setup_info.sh
Executable file
27
scripts/04_show_setup_info.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo ""
|
||||
echo "=== Waiting for Pangolin setup token ==="
|
||||
|
||||
SETUP_TOKEN=$(ssh "$NEW_USER@pangolin.$HOST" 'timeout 120 bash -c "until docker logs pangolin 2>&1 | grep -oP \"Token: \K.*\"; do sleep 2; done"')
|
||||
|
||||
echo ""
|
||||
echo "==========================================="
|
||||
echo " Server setup complete!"
|
||||
echo "==========================================="
|
||||
echo ""
|
||||
echo " SSH: ssh $NEW_USER@pangolin.$HOST"
|
||||
echo ""
|
||||
echo " Pangolin setup token: $SETUP_TOKEN"
|
||||
echo ""
|
||||
echo " Next steps:"
|
||||
echo " 1. Go to https://pangolin.$HOST/auth/initial-setup"
|
||||
echo " and enter the setup token above to create your admin account."
|
||||
echo ""
|
||||
echo " 2. In the Pangolin dashboard, add a local site, then create"
|
||||
echo " a public resource for opencode:"
|
||||
echo " - Target: host.docker.internal"
|
||||
echo " - Port: 3000"
|
||||
echo " - Domain: opencode.$HOST"
|
||||
echo "==========================================="
|
||||
Loading…
Reference in a new issue