mirror of
https://oauth2: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@github.com/m1ngsama/automa.git
synced 2026-06-26 11:14:38 +08:00
Compare commits
12 commits
ec63b8d1c5
...
917f29cf9f
| Author | SHA1 | Date | |
|---|---|---|---|
| 917f29cf9f | |||
| b29abcec8a | |||
| b91512e97f | |||
| 9d8a08900d | |||
| 23b8d1abd8 | |||
| 2ae28fb0a7 | |||
| 929c527ad0 | |||
| 19b3e5035c | |||
| 990e0f93be | |||
| 1356348d79 | |||
| 50ecd7c814 | |||
| f82cd2d956 |
38 changed files with 1491 additions and 8 deletions
39
Makefile
39
Makefile
|
|
@ -4,6 +4,7 @@
|
||||||
.PHONY: help all status up down logs restart clean minecraft teamspeak nextcloud
|
.PHONY: help all status up down logs restart clean minecraft teamspeak nextcloud
|
||||||
.PHONY: health health-minecraft health-teamspeak health-nextcloud
|
.PHONY: health health-minecraft health-teamspeak health-nextcloud
|
||||||
.PHONY: backup backup-minecraft backup-teamspeak backup-nextcloud backup-list backup-cleanup
|
.PHONY: backup backup-minecraft backup-teamspeak backup-nextcloud backup-list backup-cleanup
|
||||||
|
.PHONY: deploy-email deploy-nginx deploy-ss-server deploy-ss-client deploy-frp-server deploy-frp-client
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
help:
|
help:
|
||||||
|
|
@ -21,6 +22,14 @@ help:
|
||||||
@echo " backup-list List available backups"
|
@echo " backup-list List available backups"
|
||||||
@echo " backup-cleanup Remove old backups"
|
@echo " backup-cleanup Remove old backups"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@echo "Infrastructure Deploy (set INFRA_DIR first):"
|
||||||
|
@echo " deploy-email Deploy Postfix+Dovecot+OpenDKIM+SpamAssassin"
|
||||||
|
@echo " deploy-nginx Deploy Nginx vhosts"
|
||||||
|
@echo " deploy-ss-server Deploy Shadowsocks server"
|
||||||
|
@echo " deploy-ss-client Deploy Shadowsocks client + privoxy"
|
||||||
|
@echo " deploy-frp-server Deploy FRP server (frps)"
|
||||||
|
@echo " deploy-frp-client Deploy FRP client (frpc)"
|
||||||
|
@echo ""
|
||||||
@echo "Service-specific Commands:"
|
@echo "Service-specific Commands:"
|
||||||
@echo " Minecraft:"
|
@echo " Minecraft:"
|
||||||
@echo " minecraft-up Start Minecraft server"
|
@echo " minecraft-up Start Minecraft server"
|
||||||
|
|
@ -55,6 +64,36 @@ help:
|
||||||
@echo " check Check prerequisites"
|
@echo " check Check prerequisites"
|
||||||
@echo " clean Remove stopped containers and unused volumes"
|
@echo " clean Remove stopped containers and unused volumes"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Infrastructure Service Targets
|
||||||
|
# Requires INFRA_DIR pointing to the corresponding infra module directory.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# deploy-email: INFRA_DIR=/path/to/infra/services/email make deploy-email
|
||||||
|
deploy-email:
|
||||||
|
@[ -n "$(INFRA_DIR)" ] || { echo "Set INFRA_DIR=/path/to/infra/services/email"; exit 1; }
|
||||||
|
INFRA_DIR=$(INFRA_DIR) ./services/email/deploy.sh
|
||||||
|
|
||||||
|
deploy-nginx:
|
||||||
|
@[ -n "$(INFRA_DIR)" ] || { echo "Set INFRA_DIR=/path/to/infra/services/nginx"; exit 1; }
|
||||||
|
INFRA_DIR=$(INFRA_DIR) ./services/nginx/deploy.sh
|
||||||
|
|
||||||
|
deploy-ss-server:
|
||||||
|
@[ -n "$(INFRA_DIR)" ] || { echo "Set INFRA_DIR=/path/to/infra/services/shadowsocks/server"; exit 1; }
|
||||||
|
INFRA_DIR=$(INFRA_DIR) ./services/shadowsocks/server/deploy.sh
|
||||||
|
|
||||||
|
deploy-ss-client:
|
||||||
|
@[ -n "$(INFRA_DIR)" ] || { echo "Set INFRA_DIR=/path/to/infra/services/shadowsocks/client"; exit 1; }
|
||||||
|
INFRA_DIR=$(INFRA_DIR) ./services/shadowsocks/client/deploy.sh
|
||||||
|
|
||||||
|
deploy-frp-server:
|
||||||
|
@[ -n "$(INFRA_DIR)" ] || { echo "Set INFRA_DIR=/path/to/infra/services/frp/server"; exit 1; }
|
||||||
|
INFRA_DIR=$(INFRA_DIR) ./services/frp/server/deploy.sh
|
||||||
|
|
||||||
|
deploy-frp-client:
|
||||||
|
@[ -n "$(INFRA_DIR)" ] || { echo "Set INFRA_DIR=/path/to/infra/services/frp/client"; exit 1; }
|
||||||
|
INFRA_DIR=$(INFRA_DIR) ./services/frp/client/deploy.sh
|
||||||
|
|
||||||
# Check prerequisites
|
# Check prerequisites
|
||||||
check:
|
check:
|
||||||
@echo "Checking prerequisites..."
|
@echo "Checking prerequisites..."
|
||||||
|
|
|
||||||
127
README.md
127
README.md
|
|
@ -1,6 +1,27 @@
|
||||||
# Automa
|
# Automa
|
||||||
|
|
||||||
A collection of self-hosted service automation tools following the Unix philosophy: do one thing well, be composable, and stay simple.
|
Deployment scripts for self-hosted infrastructure. Pairs with [infra](https://github.com/m1ngsama/infra) (private) for configuration.
|
||||||
|
|
||||||
|
```
|
||||||
|
infra/services/<name>/.env → automa/services/<name>/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Relationship with infra
|
||||||
|
|
||||||
|
**infra** (private) holds config templates and `.env.example` files — the "what" and "how to configure".
|
||||||
|
**automa** (public) holds deployment scripts — the "how to deploy". Zero hardcoded values, zero domain names.
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. Clone infra (private), fill in `.env` files for each service you want
|
||||||
|
2. Clone automa (public), run the matching deploy script
|
||||||
|
3. Each script reads `INFRA_DIR` to locate the corresponding `.env`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
cd infra/services/email && cp .env.example .env && $EDITOR .env
|
||||||
|
cd automa/services/email
|
||||||
|
INFRA_DIR=../../infra/services/email ./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
## Philosophy
|
## Philosophy
|
||||||
|
|
||||||
|
|
@ -10,7 +31,82 @@ This project embraces Unix principles:
|
||||||
- **Composability**: Tools work together through standard interfaces
|
- **Composability**: Tools work together through standard interfaces
|
||||||
- **Transparency**: Plain text configuration, readable scripts
|
- **Transparency**: Plain text configuration, readable scripts
|
||||||
|
|
||||||
## Services
|
## Infrastructure Services
|
||||||
|
|
||||||
|
System services deployed from infra module configs.
|
||||||
|
|
||||||
|
### Email
|
||||||
|
Postfix + Dovecot + OpenDKIM + SpamAssassin.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
INFRA_DIR=/path/to/infra/services/email ./services/email/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nginx
|
||||||
|
Web server and reverse proxy vhosts.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
INFRA_DIR=/path/to/infra/services/nginx ./services/nginx/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shadowsocks
|
||||||
|
GFW-resistant proxy (legacy; new deployments should use sing-box).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Server (VPS)
|
||||||
|
INFRA_DIR=/path/to/infra/services/shadowsocks/server ./services/shadowsocks/server/deploy.sh
|
||||||
|
|
||||||
|
# Client (home machine)
|
||||||
|
INFRA_DIR=/path/to/infra/services/shadowsocks/client ./services/shadowsocks/client/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sing-box
|
||||||
|
Multi-protocol proxy (VLESS/Reality, VMess/WS, Hysteria2). Config generated once
|
||||||
|
by [sing-box-yg](https://github.com/yonggekkk/sing-box-yg), then stored in infra.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Server (VPS)
|
||||||
|
INFRA_DIR=/path/to/infra/services/sing-box/server ./services/sing-box/server/deploy.sh
|
||||||
|
|
||||||
|
# Client (home machine)
|
||||||
|
INFRA_DIR=/path/to/infra/services/sing-box/client ./services/sing-box/client/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### FRP
|
||||||
|
Reverse tunnel — expose home services through VPS.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Server (VPS)
|
||||||
|
INFRA_DIR=/path/to/infra/services/frp/server ./services/frp/server/deploy.sh
|
||||||
|
|
||||||
|
# Client (home machine)
|
||||||
|
INFRA_DIR=/path/to/infra/services/frp/client ./services/frp/client/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### TNT
|
||||||
|
SSH-based terminal chat server.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
INFRA_DIR=/path/to/infra/services/tnt ./services/tnt/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### MinIO
|
||||||
|
S3-compatible object storage.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
INFRA_DIR=/path/to/infra/services/minio ./services/minio/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Galene
|
||||||
|
WebRTC video conferencing server.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
INFRA_DIR=/path/to/infra/services/galene ./services/galene/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Home Services
|
||||||
|
|
||||||
|
Docker-based services with their own config.
|
||||||
|
|
||||||
### Minecraft Server
|
### Minecraft Server
|
||||||
Automated Minecraft Fabric server deployment with mod management.
|
Automated Minecraft Fabric server deployment with mod management.
|
||||||
|
|
@ -72,12 +168,27 @@ Batch clone all repositories from a GitHub organization.
|
||||||
|
|
||||||
```
|
```
|
||||||
automa/
|
automa/
|
||||||
├── bin/ # Utility scripts
|
├── bin/ # Utility scripts
|
||||||
│ └── org-clone.sh # GitHub org repo cloner
|
│ └── lib/common.sh # Shared logging + env helpers
|
||||||
├── minecraft/ # Minecraft server setup
|
├── services/ # Infrastructure deploy scripts (reads infra .env)
|
||||||
├── teamspeak/ # TeamSpeak server setup
|
│ ├── email/deploy.sh
|
||||||
├── nextcloud/ # Nextcloud setup
|
│ ├── nginx/deploy.sh
|
||||||
└── README.md # This file
|
│ ├── shadowsocks/
|
||||||
|
│ │ ├── server/deploy.sh
|
||||||
|
│ │ └── client/deploy.sh
|
||||||
|
│ ├── sing-box/
|
||||||
|
│ │ ├── server/deploy.sh
|
||||||
|
│ │ └── client/deploy.sh
|
||||||
|
│ ├── frp/
|
||||||
|
│ │ ├── server/deploy.sh
|
||||||
|
│ │ └── client/deploy.sh
|
||||||
|
│ ├── tnt/deploy.sh
|
||||||
|
│ ├── minio/deploy.sh
|
||||||
|
│ └── galene/deploy.sh
|
||||||
|
├── minecraft/ # Minecraft server (Docker)
|
||||||
|
├── teamspeak/ # TeamSpeak server (Docker)
|
||||||
|
├── nextcloud/ # Nextcloud (Docker)
|
||||||
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Operations
|
## Common Operations
|
||||||
|
|
|
||||||
7
services/email/.env.example
Normal file
7
services/email/.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# role: vps
|
||||||
|
# description: Postfix + Dovecot + OpenDKIM + SpamAssassin full email stack
|
||||||
|
|
||||||
|
DOMAIN=your-domain.com
|
||||||
|
MAIL_HOST=mail.your-domain.com
|
||||||
|
SERVER_IP=x.x.x.x
|
||||||
|
MAIL_USER=contact
|
||||||
37
services/email/deploy.sh
Executable file
37
services/email/deploy.sh
Executable file
|
|
@ -0,0 +1,37 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploys Postfix + Dovecot + OpenDKIM + SpamAssassin email stack.
|
||||||
|
# Usage: INFRA_DIR=/path/to/infra/services/email ./deploy.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../bin/lib/common.sh"
|
||||||
|
|
||||||
|
ENV_FILE="${INFRA_DIR:-.}/.env"
|
||||||
|
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
require_env DOMAIN MAIL_HOST MAIL_USER
|
||||||
|
|
||||||
|
log_info "Installing packages..."
|
||||||
|
apt-get install -y postfix dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd \
|
||||||
|
dovecot-sieve opendkim opendkim-tools spamassassin spamc
|
||||||
|
|
||||||
|
log_info "Deploying Postfix config..."
|
||||||
|
envsubst < "${INFRA_DIR}/postfix/main.cf" > /etc/postfix/main.cf
|
||||||
|
cp "${INFRA_DIR}/postfix/aliases" /etc/aliases
|
||||||
|
newaliases
|
||||||
|
|
||||||
|
log_info "Deploying Dovecot config..."
|
||||||
|
cp "${INFRA_DIR}/dovecot/dovecot.conf" /etc/dovecot/dovecot.conf
|
||||||
|
cp "${INFRA_DIR}/dovecot/99-stats-fix.conf" /etc/dovecot/conf.d/99-stats-fix.conf
|
||||||
|
|
||||||
|
log_info "Adding postfix to dovecot group..."
|
||||||
|
usermod -aG dovecot postfix
|
||||||
|
|
||||||
|
log_info "Enabling services..."
|
||||||
|
systemctl enable --now postfix dovecot opendkim spamassassin
|
||||||
|
|
||||||
|
log_info "Email stack deployed. Remaining manual steps:"
|
||||||
|
echo " 1. Run certbot for mail.${DOMAIN}"
|
||||||
|
echo " 2. Generate DKIM key: opendkim-genkey -b 2048 -d ${DOMAIN} -s mail -D /etc/opendkim/keys/${DOMAIN}/"
|
||||||
|
echo " 3. Add DNS records (see services/email/README.md)"
|
||||||
10
services/forgejo/.env.example
Normal file
10
services/forgejo/.env.example
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# role: vps
|
||||||
|
# description: Forgejo self-hosted git service with optional GitHub mirror sync
|
||||||
|
|
||||||
|
GIT_DOMAIN=git.your-domain.com
|
||||||
|
|
||||||
|
# Optional: GitHub → Forgejo mirror sync (leave blank to skip cron setup)
|
||||||
|
GITHUB_USER=
|
||||||
|
GITHUB_TOKEN=
|
||||||
|
FORGEJO_URL=https://git.your-domain.com
|
||||||
|
FORGEJO_TOKEN=
|
||||||
67
services/forgejo/deploy.sh
Executable file
67
services/forgejo/deploy.sh
Executable file
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploys Forgejo (self-hosted git) via Docker on a VPS.
|
||||||
|
# Usage: INFRA_DIR=/path/to/infra/services/forgejo ./deploy.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../bin/lib/common.sh"
|
||||||
|
|
||||||
|
ENV_FILE="${INFRA_DIR:-.}/.env"
|
||||||
|
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
require_env GIT_DOMAIN
|
||||||
|
require_command docker "https://docs.docker.com/engine/install/"
|
||||||
|
|
||||||
|
find_template() {
|
||||||
|
local f="$1"
|
||||||
|
if [[ -n "${INFRA_DIR:-}" && -f "${INFRA_DIR}/$f" ]]; then
|
||||||
|
echo "${INFRA_DIR}/$f"
|
||||||
|
elif [[ -f "$SCRIPT_DIR/$f" ]]; then
|
||||||
|
echo "$SCRIPT_DIR/$f"
|
||||||
|
else
|
||||||
|
log_error "Template not found: $f"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
DEPLOY_DIR="/opt/forgejo"
|
||||||
|
log_info "Creating deploy directory $DEPLOY_DIR..."
|
||||||
|
mkdir -p "$DEPLOY_DIR/data"
|
||||||
|
|
||||||
|
log_info "Deploying docker-compose.yml..."
|
||||||
|
cp "$(find_template docker-compose.yml)" "$DEPLOY_DIR/docker-compose.yml"
|
||||||
|
|
||||||
|
log_info "Starting Forgejo container..."
|
||||||
|
docker compose -f "$DEPLOY_DIR/docker-compose.yml" up -d
|
||||||
|
|
||||||
|
log_info "Deploying nginx vhost for ${GIT_DOMAIN}..."
|
||||||
|
envsubst '${GIT_DOMAIN}' < "$(find_template nginx.conf.example)" > "/etc/nginx/sites-available/forgejo"
|
||||||
|
ln -sf /etc/nginx/sites-available/forgejo /etc/nginx/sites-enabled/forgejo
|
||||||
|
nginx -t
|
||||||
|
systemctl reload nginx
|
||||||
|
|
||||||
|
# Optional: set up GitHub mirror sync cron
|
||||||
|
if [[ -n "${GITHUB_USER:-}" && -n "${GITHUB_TOKEN:-}" && -n "${FORGEJO_URL:-}" && -n "${FORGEJO_TOKEN:-}" ]]; then
|
||||||
|
SYNC_SCRIPT="$(find_template migrate_github_to_forgejo.py 2>/dev/null || true)"
|
||||||
|
if [[ -n "$SYNC_SCRIPT" ]]; then
|
||||||
|
log_info "Installing GitHub mirror sync script..."
|
||||||
|
cp "$SYNC_SCRIPT" "$DEPLOY_DIR/migrate_github_to_forgejo.py"
|
||||||
|
mkdir -p "$DEPLOY_DIR/logs"
|
||||||
|
|
||||||
|
CRON_LINE="0 3 * * * cd $DEPLOY_DIR && GITHUB_USER=${GITHUB_USER} GITHUB_TOKEN=${GITHUB_TOKEN} FORGEJO_URL=${FORGEJO_URL} FORGEJO_TOKEN=${FORGEJO_TOKEN} python3 migrate_github_to_forgejo.py >> $DEPLOY_DIR/logs/mirror-sync.log 2>&1"
|
||||||
|
(crontab -l 2>/dev/null | grep -v "migrate_github_to_forgejo"; echo "$CRON_LINE") | crontab -
|
||||||
|
log_info "Cron sync installed (daily 03:00)"
|
||||||
|
else
|
||||||
|
log_warn "migrate_github_to_forgejo.py not found in INFRA_DIR — skipping cron setup"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_info "GitHub sync vars not set — skipping cron setup"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Forgejo deployed at http://localhost:3000"
|
||||||
|
echo ""
|
||||||
|
echo "Remaining manual steps:"
|
||||||
|
echo " 1. Get TLS cert: certbot --nginx -d ${GIT_DOMAIN}"
|
||||||
|
echo " 2. Complete Forgejo initial setup at https://${GIT_DOMAIN}"
|
||||||
|
echo " 3. Generate Forgejo API token: https://${GIT_DOMAIN}/user/settings/applications"
|
||||||
23
services/forgejo/docker-compose.yml
Normal file
23
services/forgejo/docker-compose.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
forgejo:
|
||||||
|
external: false
|
||||||
|
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
image: codeberg.org/forgejo/forgejo:9
|
||||||
|
container_name: forgejo
|
||||||
|
environment:
|
||||||
|
- USER_UID=1000
|
||||||
|
- USER_GID=1000
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- forgejo
|
||||||
|
volumes:
|
||||||
|
- /opt/forgejo/data:/data
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
- "2223:22"
|
||||||
26
services/forgejo/nginx.conf.example
Normal file
26
services/forgejo/nginx.conf.example
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ${GIT_DOMAIN};
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
server_name ${GIT_DOMAIN};
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/${GIT_DOMAIN}/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/${GIT_DOMAIN}/privkey.pem;
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:3000;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
client_max_body_size 512M;
|
||||||
|
}
|
||||||
6
services/frp/client/.env.example
Normal file
6
services/frp/client/.env.example
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# role: homeserver
|
||||||
|
# description: FRP client (frpc) — tunnels local services through VPS
|
||||||
|
|
||||||
|
FRP_SERVER_ADDR=your-vps-ip
|
||||||
|
FRP_SERVER_PORT=7000
|
||||||
|
FRP_TOKEN=
|
||||||
52
services/frp/client/deploy.sh
Executable file
52
services/frp/client/deploy.sh
Executable file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploys frpc (FRP client) on home machine.
|
||||||
|
# Usage: INFRA_DIR=/path/to/infra/services/frp/client ./deploy.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../../bin/lib/common.sh"
|
||||||
|
|
||||||
|
ENV_FILE="${INFRA_DIR:-.}/.env"
|
||||||
|
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
require_env FRP_SERVER_ADDR FRP_SERVER_PORT FRP_TOKEN
|
||||||
|
|
||||||
|
find_template() {
|
||||||
|
local f="$1"
|
||||||
|
if [[ -n "${INFRA_DIR:-}" && -f "${INFRA_DIR}/$f" ]]; then
|
||||||
|
echo "${INFRA_DIR}/$f"
|
||||||
|
elif [[ -f "$SCRIPT_DIR/$f" ]]; then
|
||||||
|
echo "$SCRIPT_DIR/$f"
|
||||||
|
else
|
||||||
|
log_error "Template not found: $f"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
FRPC_BIN="/opt/frp/frpc"
|
||||||
|
|
||||||
|
if [[ -x "$FRPC_BIN" ]]; then
|
||||||
|
log_info "frpc already at $FRPC_BIN, skipping download"
|
||||||
|
else
|
||||||
|
log_info "Downloading FRP..."
|
||||||
|
VERSION=$(curl -s https://api.github.com/repos/fatedier/frp/releases/latest \
|
||||||
|
| python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'][1:])")
|
||||||
|
ARCHIVE="frp_${VERSION}_linux_amd64.tar.gz"
|
||||||
|
wget -q "https://github.com/fatedier/frp/releases/download/v${VERSION}/${ARCHIVE}"
|
||||||
|
tar -xf "$ARCHIVE"
|
||||||
|
mkdir -p /opt/frp
|
||||||
|
cp "frp_${VERSION}_linux_amd64/frpc" /opt/frp/
|
||||||
|
chmod +x /opt/frp/frpc
|
||||||
|
rm -rf "$ARCHIVE" "frp_${VERSION}_linux_amd64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Deploying config..."
|
||||||
|
envsubst < "$(find_template frpc.toml.example)" > /opt/frp/frpc.toml
|
||||||
|
|
||||||
|
log_info "Installing service..."
|
||||||
|
cp "$(find_template frpc.service)" /etc/systemd/system/
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now frpc
|
||||||
|
|
||||||
|
log_info "FRP client deployed, connecting to ${FRP_SERVER_ADDR}:${FRP_SERVER_PORT}"
|
||||||
15
services/frp/client/frpc.service
Normal file
15
services/frp/client/frpc.service
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[Unit]
|
||||||
|
Description=FRP Client
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/opt/frp/frpc -c /opt/frp/frpc.toml
|
||||||
|
WorkingDirectory=/opt/frp
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
21
services/frp/client/frpc.toml.example
Normal file
21
services/frp/client/frpc.toml.example
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
serverAddr = "${FRP_SERVER_ADDR}"
|
||||||
|
serverPort = ${FRP_SERVER_PORT}
|
||||||
|
|
||||||
|
auth.method = "token"
|
||||||
|
auth.token = "${FRP_TOKEN}"
|
||||||
|
|
||||||
|
# Example: expose home SSH
|
||||||
|
[[proxies]]
|
||||||
|
name = "home-ssh"
|
||||||
|
type = "tcp"
|
||||||
|
localIP = "127.0.0.1"
|
||||||
|
localPort = 22
|
||||||
|
remotePort = 1234
|
||||||
|
|
||||||
|
# Example: expose Minecraft
|
||||||
|
[[proxies]]
|
||||||
|
name = "minecraft"
|
||||||
|
type = "tcp"
|
||||||
|
localIP = "127.0.0.1"
|
||||||
|
localPort = 25565
|
||||||
|
remotePort = 25565
|
||||||
7
services/frp/server/.env.example
Normal file
7
services/frp/server/.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# role: vps
|
||||||
|
# description: FRP server (frps) — public entry point for reverse tunnels
|
||||||
|
|
||||||
|
FRP_TOKEN=
|
||||||
|
FRP_WEB_USER=admin
|
||||||
|
FRP_WEB_PASSWORD=
|
||||||
|
FRP_BIND_PORT=7000
|
||||||
52
services/frp/server/deploy.sh
Executable file
52
services/frp/server/deploy.sh
Executable file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploys frps (FRP server) on VPS.
|
||||||
|
# Usage: INFRA_DIR=/path/to/infra/services/frp/server ./deploy.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../../bin/lib/common.sh"
|
||||||
|
|
||||||
|
ENV_FILE="${INFRA_DIR:-.}/.env"
|
||||||
|
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
require_env FRP_TOKEN FRP_WEB_USER FRP_WEB_PASSWORD FRP_BIND_PORT
|
||||||
|
|
||||||
|
find_template() {
|
||||||
|
local f="$1"
|
||||||
|
if [[ -n "${INFRA_DIR:-}" && -f "${INFRA_DIR}/$f" ]]; then
|
||||||
|
echo "${INFRA_DIR}/$f"
|
||||||
|
elif [[ -f "$SCRIPT_DIR/$f" ]]; then
|
||||||
|
echo "$SCRIPT_DIR/$f"
|
||||||
|
else
|
||||||
|
log_error "Template not found: $f"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
FRPS_BIN="/opt/frp/frps"
|
||||||
|
|
||||||
|
if [[ -x "$FRPS_BIN" ]]; then
|
||||||
|
log_info "frps already at $FRPS_BIN, skipping download"
|
||||||
|
else
|
||||||
|
log_info "Downloading FRP..."
|
||||||
|
VERSION=$(curl -s https://api.github.com/repos/fatedier/frp/releases/latest \
|
||||||
|
| python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'][1:])")
|
||||||
|
ARCHIVE="frp_${VERSION}_linux_amd64.tar.gz"
|
||||||
|
wget -q "https://github.com/fatedier/frp/releases/download/v${VERSION}/${ARCHIVE}"
|
||||||
|
tar -xf "$ARCHIVE"
|
||||||
|
mkdir -p /opt/frp
|
||||||
|
cp "frp_${VERSION}_linux_amd64/frps" /opt/frp/
|
||||||
|
chmod +x /opt/frp/frps
|
||||||
|
rm -rf "$ARCHIVE" "frp_${VERSION}_linux_amd64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Deploying config..."
|
||||||
|
envsubst < "$(find_template frps.toml.example)" > /opt/frp/frps.toml
|
||||||
|
|
||||||
|
log_info "Installing service..."
|
||||||
|
cp "$(find_template frps.service)" /etc/systemd/system/
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now frps
|
||||||
|
|
||||||
|
log_info "FRP server deployed on port ${FRP_BIND_PORT}"
|
||||||
15
services/frp/server/frps.service
Normal file
15
services/frp/server/frps.service
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[Unit]
|
||||||
|
Description=FRP Server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/opt/frp/frps -c /opt/frp/frps.toml
|
||||||
|
WorkingDirectory=/opt/frp
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
17
services/frp/server/frps.toml.example
Normal file
17
services/frp/server/frps.toml.example
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
bindAddr = "0.0.0.0"
|
||||||
|
bindPort = ${FRP_BIND_PORT}
|
||||||
|
|
||||||
|
vhostHTTPPort = 8080
|
||||||
|
vhostHTTPSPort = 8443
|
||||||
|
|
||||||
|
webServer.addr = "127.0.0.1"
|
||||||
|
webServer.port = 7500
|
||||||
|
webServer.user = "${FRP_WEB_USER}"
|
||||||
|
webServer.password = "${FRP_WEB_PASSWORD}"
|
||||||
|
|
||||||
|
log.to = "./frps.log"
|
||||||
|
log.level = "info"
|
||||||
|
log.maxDays = 3
|
||||||
|
|
||||||
|
auth.method = "token"
|
||||||
|
auth.token = "${FRP_TOKEN}"
|
||||||
6
services/galene/.env.example
Normal file
6
services/galene/.env.example
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# role: vps
|
||||||
|
# description: Galene video conferencing server
|
||||||
|
GALENE_VERSION=0.9.2
|
||||||
|
GALENE_HTTP_ADDR=127.0.0.1:8443
|
||||||
|
GALENE_TURN_ADDR=x.x.x.x:1194
|
||||||
|
GALENE_UDP_RANGE=10000-10100
|
||||||
81
services/galene/deploy.sh
Executable file
81
services/galene/deploy.sh
Executable file
|
|
@ -0,0 +1,81 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploys Galene video conferencing server.
|
||||||
|
# https://github.com/jech/galene
|
||||||
|
#
|
||||||
|
# Usage: INFRA_DIR=/path/to/infra/services/galene ./deploy.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../bin/lib/common.sh"
|
||||||
|
|
||||||
|
ENV_FILE="${INFRA_DIR:-.}/.env"
|
||||||
|
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
require_env GALENE_VERSION GALENE_HTTP_ADDR GALENE_TURN_ADDR
|
||||||
|
|
||||||
|
INSTALL_DIR="/opt/galene"
|
||||||
|
BIN="$INSTALL_DIR/galene"
|
||||||
|
|
||||||
|
if [[ -x "$BIN" ]]; then
|
||||||
|
log_info "galene already at $BIN, skipping download"
|
||||||
|
else
|
||||||
|
log_info "Downloading Galene ${GALENE_VERSION}..."
|
||||||
|
ARCH="$(uname -m)"
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64) ARCH="amd64" ;;
|
||||||
|
aarch64) ARCH="arm64" ;;
|
||||||
|
*) log_error "Unsupported arch: $ARCH"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
URL="https://github.com/jech/galene/releases/download/galene-${GALENE_VERSION}/galene-${GALENE_VERSION}-linux-${ARCH}.tar.gz"
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
wget -qO "$TMP/galene.tar.gz" "$URL"
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
tar -xf "$TMP/galene.tar.gz" -C "$INSTALL_DIR" --strip-components=1
|
||||||
|
chmod +x "$BIN"
|
||||||
|
rm -rf "$TMP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Creating directories..."
|
||||||
|
mkdir -p "$INSTALL_DIR"/{data,groups,static}
|
||||||
|
|
||||||
|
log_info "Deploying groups config from INFRA_DIR..."
|
||||||
|
if [[ -d "${INFRA_DIR}/groups" ]]; then
|
||||||
|
cp -r "${INFRA_DIR}/groups/." "$INSTALL_DIR/groups/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Installing systemd service..."
|
||||||
|
cat > /etc/systemd/system/galene.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Galene videoconference server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=$INSTALL_DIR
|
||||||
|
ExecStart=$BIN \\
|
||||||
|
-insecure \\
|
||||||
|
-http ${GALENE_HTTP_ADDR} \\
|
||||||
|
-static $INSTALL_DIR/static \\
|
||||||
|
-groups $INSTALL_DIR/groups \\
|
||||||
|
-data $INSTALL_DIR/data \\
|
||||||
|
-turn ${GALENE_TURN_ADDR} \\
|
||||||
|
-udp-range ${GALENE_UDP_RANGE:-10000-10100}
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
LimitNOFILE=65536
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now galene
|
||||||
|
|
||||||
|
log_info "Galene deployed"
|
||||||
|
echo " Listening: ${GALENE_HTTP_ADDR}"
|
||||||
|
echo ""
|
||||||
|
echo "Remaining manual steps:"
|
||||||
|
echo " 1. Configure nginx reverse proxy (see infra/services/nginx/sites/)"
|
||||||
|
echo " 2. Get TLS cert for frontend domain"
|
||||||
|
echo " 3. Open UDP ports ${GALENE_UDP_RANGE:-10000-10100} in firewall"
|
||||||
8
services/minio/.env.example
Normal file
8
services/minio/.env.example
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# role: vps
|
||||||
|
# description: MinIO object storage server
|
||||||
|
MINIO_ROOT_USER=minioadmin
|
||||||
|
MINIO_ROOT_PASSWORD=
|
||||||
|
MINIO_VOLUMES=/data/minio
|
||||||
|
MINIO_OPTS=--console-address :9001
|
||||||
|
MINIO_BROWSER_REDIRECT_URL=https://console.example.com
|
||||||
|
MINIO_SERVER_URL=https://oss.example.com
|
||||||
83
services/minio/deploy.sh
Executable file
83
services/minio/deploy.sh
Executable file
|
|
@ -0,0 +1,83 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploys MinIO object storage server.
|
||||||
|
# https://min.io/docs/minio/linux/index.html
|
||||||
|
#
|
||||||
|
# Usage: INFRA_DIR=/path/to/infra/services/minio ./deploy.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../bin/lib/common.sh"
|
||||||
|
|
||||||
|
ENV_FILE="${INFRA_DIR:-.}/.env"
|
||||||
|
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
require_env MINIO_ROOT_USER MINIO_ROOT_PASSWORD MINIO_VOLUMES
|
||||||
|
|
||||||
|
BIN="/usr/local/bin/minio"
|
||||||
|
|
||||||
|
if [[ -x "$BIN" ]]; then
|
||||||
|
log_info "minio already at $BIN, skipping download"
|
||||||
|
else
|
||||||
|
log_info "Downloading MinIO..."
|
||||||
|
wget -qO "$BIN" https://dl.min.io/server/minio/release/linux-amd64/minio
|
||||||
|
chmod +x "$BIN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Creating minio-user..."
|
||||||
|
if ! id minio-user &>/dev/null; then
|
||||||
|
useradd --system --no-create-home --shell /usr/sbin/nologin minio-user
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Creating data directory: ${MINIO_VOLUMES}..."
|
||||||
|
mkdir -p "${MINIO_VOLUMES}"
|
||||||
|
chown minio-user:minio-user "${MINIO_VOLUMES}"
|
||||||
|
|
||||||
|
log_info "Writing /etc/default/minio..."
|
||||||
|
cat > /etc/default/minio <<EOF
|
||||||
|
MINIO_ROOT_USER=${MINIO_ROOT_USER}
|
||||||
|
MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
|
||||||
|
MINIO_VOLUMES=${MINIO_VOLUMES}
|
||||||
|
MINIO_OPTS=${MINIO_OPTS:---console-address :9001}
|
||||||
|
MINIO_BROWSER_REDIRECT_URL=${MINIO_BROWSER_REDIRECT_URL:-}
|
||||||
|
MINIO_SERVER_URL=${MINIO_SERVER_URL:-}
|
||||||
|
EOF
|
||||||
|
chmod 640 /etc/default/minio
|
||||||
|
chown root:minio-user /etc/default/minio
|
||||||
|
|
||||||
|
log_info "Installing systemd service..."
|
||||||
|
cat > /etc/systemd/system/minio.service <<'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=MinIO
|
||||||
|
Documentation=https://min.io/docs/minio/linux/index.html
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
AssertFileIsExecutable=/usr/local/bin/minio
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/usr/local
|
||||||
|
User=minio-user
|
||||||
|
Group=minio-user
|
||||||
|
EnvironmentFile=/etc/default/minio
|
||||||
|
ExecStartPre=/bin/bash -c 'if [ -z "${MINIO_VOLUMES}" ]; then echo "MINIO_VOLUMES not set"; exit 1; fi'
|
||||||
|
ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES
|
||||||
|
Restart=always
|
||||||
|
LimitNOFILE=65536
|
||||||
|
TasksMax=infinity
|
||||||
|
TimeoutStopSec=infinity
|
||||||
|
SendSIGKILL=no
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now minio
|
||||||
|
|
||||||
|
log_info "MinIO deployed"
|
||||||
|
echo " API: http://localhost:9000"
|
||||||
|
echo " Console: http://localhost:9001"
|
||||||
|
echo ""
|
||||||
|
echo "Remaining manual steps:"
|
||||||
|
echo " 1. Configure nginx reverse proxy (see infra/services/nginx/sites/)"
|
||||||
|
echo " 2. Get TLS cert: certbot --nginx -d ${MINIO_SERVER_URL#https://}"
|
||||||
8
services/nginx/.env.example
Normal file
8
services/nginx/.env.example
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# role: vps
|
||||||
|
# description: Nginx web server and reverse proxy vhosts
|
||||||
|
|
||||||
|
DOMAIN=your-domain.com
|
||||||
|
BLOG_DOMAIN=blog.your-domain.com
|
||||||
|
CHAN_DOMAIN=chan.your-domain.com
|
||||||
|
MAIL_DOMAIN=mail.your-domain.com
|
||||||
|
GIT_DOMAIN=git.your-domain.com
|
||||||
36
services/nginx/deploy.sh
Executable file
36
services/nginx/deploy.sh
Executable file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploys Nginx web server and vhost configs.
|
||||||
|
# Usage: INFRA_DIR=/path/to/infra/services/nginx ./deploy.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../bin/lib/common.sh"
|
||||||
|
|
||||||
|
ENV_FILE="${INFRA_DIR:-.}/.env"
|
||||||
|
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
require_env DOMAIN
|
||||||
|
|
||||||
|
log_info "Installing nginx..."
|
||||||
|
apt-get install -y nginx certbot python3-certbot-nginx
|
||||||
|
|
||||||
|
log_info "Deploying nginx.conf..."
|
||||||
|
cp "${INFRA_DIR}/nginx.conf" /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
log_info "Deploying vhost configs..."
|
||||||
|
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||||
|
|
||||||
|
for conf in "${INFRA_DIR}/sites/"*.conf; do
|
||||||
|
name="$(basename "$conf" .conf)"
|
||||||
|
envsubst '${DOMAIN}${BLOG_DOMAIN}${CHAN_DOMAIN}${MAIL_DOMAIN}${GIT_DOMAIN}' < "$conf" > "/etc/nginx/sites-available/${name}"
|
||||||
|
ln -sf "/etc/nginx/sites-available/${name}" "/etc/nginx/sites-enabled/${name}"
|
||||||
|
log_info " Deployed ${name}"
|
||||||
|
done
|
||||||
|
|
||||||
|
log_info "Testing nginx config..."
|
||||||
|
nginx -t
|
||||||
|
|
||||||
|
log_info "Nginx deployed. Remaining manual steps:"
|
||||||
|
echo " 1. Get TLS certs: certbot --nginx -d ${DOMAIN} -d ${CHAN_DOMAIN:-chan.${DOMAIN}} -d ${BLOG_DOMAIN:-blog.${DOMAIN}}"
|
||||||
|
echo " 2. systemctl reload nginx"
|
||||||
7
services/shadowsocks/client/.env.example
Normal file
7
services/shadowsocks/client/.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# role: homeserver
|
||||||
|
# description: sslocal + privoxy proxy chain (SOCKS5 :1080, HTTP :8118)
|
||||||
|
|
||||||
|
SS_SERVER=your-vps-ip
|
||||||
|
SS_PORT=41268
|
||||||
|
SS_PASSWORD=
|
||||||
|
SS_METHOD=2022-blake3-aes-256-gcm
|
||||||
11
services/shadowsocks/client/config.json.example
Normal file
11
services/shadowsocks/client/config.json.example
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"server": "${SS_SERVER}",
|
||||||
|
"server_port": ${SS_PORT},
|
||||||
|
"password": "${SS_PASSWORD}",
|
||||||
|
"method": "${SS_METHOD}",
|
||||||
|
"local_address": "127.0.0.1",
|
||||||
|
"local_port": 1080,
|
||||||
|
"timeout": 300,
|
||||||
|
"fast_open": true,
|
||||||
|
"mode": "tcp_and_udp"
|
||||||
|
}
|
||||||
70
services/shadowsocks/client/deploy.sh
Executable file
70
services/shadowsocks/client/deploy.sh
Executable file
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Installs shadowsocks-rust client (sslocal) + privoxy proxy chain.
|
||||||
|
# Usage: INFRA_DIR=/path/to/infra/services/shadowsocks/client ./deploy.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../../bin/lib/common.sh"
|
||||||
|
|
||||||
|
ENV_FILE="${INFRA_DIR:-.}/.env"
|
||||||
|
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
require_env SS_SERVER SS_PORT SS_PASSWORD SS_METHOD
|
||||||
|
|
||||||
|
# Find a template file: prefer INFRA_DIR override, fall back to bundled default.
|
||||||
|
find_template() {
|
||||||
|
local f="$1"
|
||||||
|
if [[ -n "${INFRA_DIR:-}" && -f "${INFRA_DIR}/$f" ]]; then
|
||||||
|
echo "${INFRA_DIR}/$f"
|
||||||
|
elif [[ -f "$SCRIPT_DIR/$f" ]]; then
|
||||||
|
echo "$SCRIPT_DIR/$f"
|
||||||
|
else
|
||||||
|
log_error "Template not found: $f"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
SSLOCAL_BIN="/usr/local/bin/sslocal"
|
||||||
|
|
||||||
|
# Install sslocal — skip if already present, symlink if installed elsewhere.
|
||||||
|
if [[ -x "$SSLOCAL_BIN" ]]; then
|
||||||
|
log_info "sslocal already at $SSLOCAL_BIN ($($SSLOCAL_BIN --version 2>&1 | head -1)), skipping download"
|
||||||
|
elif command -v sslocal &>/dev/null; then
|
||||||
|
existing="$(command -v sslocal)"
|
||||||
|
log_info "sslocal found at $existing, symlinking to $SSLOCAL_BIN"
|
||||||
|
ln -sf "$existing" "$SSLOCAL_BIN"
|
||||||
|
else
|
||||||
|
log_info "Downloading shadowsocks-rust client..."
|
||||||
|
VERSION=$(curl -s https://api.github.com/repos/shadowsocks/shadowsocks-rust/releases/latest \
|
||||||
|
| python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])")
|
||||||
|
ARCHIVE="shadowsocks-${VERSION}.x86_64-unknown-linux-gnu.tar.xz"
|
||||||
|
wget -q "https://github.com/shadowsocks/shadowsocks-rust/releases/download/${VERSION}/${ARCHIVE}"
|
||||||
|
tar -xf "$ARCHIVE"
|
||||||
|
cp sslocal "$SSLOCAL_BIN"
|
||||||
|
chmod +x "$SSLOCAL_BIN"
|
||||||
|
rm -f "$ARCHIVE" ssserver sslocal ssurl ssmanager redir tunnel
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Installing privoxy..."
|
||||||
|
if command -v pacman &>/dev/null; then
|
||||||
|
pacman -S --noconfirm --needed privoxy
|
||||||
|
else
|
||||||
|
apt-get install -y privoxy
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Deploying configs..."
|
||||||
|
mkdir -p /etc/shadowsocks-rust
|
||||||
|
envsubst < "$(find_template config.json.example)" > /etc/shadowsocks-rust/client.json
|
||||||
|
cp "$(find_template privoxy.conf.example)" /etc/privoxy/config
|
||||||
|
|
||||||
|
log_info "Stopping any existing shadowsocks service on port 1080..."
|
||||||
|
systemctl stop shadowsocks 2>/dev/null || true
|
||||||
|
|
||||||
|
log_info "Installing service..."
|
||||||
|
cp "$(find_template shadowsocks-client.service)" /etc/systemd/system/
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now shadowsocks-client
|
||||||
|
systemctl enable --now privoxy
|
||||||
|
|
||||||
|
log_info "Proxy chain ready: SOCKS5 at 127.0.0.1:1080, HTTP at 127.0.0.1:8118"
|
||||||
5
services/shadowsocks/client/privoxy.conf.example
Normal file
5
services/shadowsocks/client/privoxy.conf.example
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Privoxy config — bridges SOCKS5 (sslocal) to HTTP proxy
|
||||||
|
# Listens on :8118, forwards to sslocal SOCKS5 on :1080
|
||||||
|
|
||||||
|
listen-address 127.0.0.1:8118
|
||||||
|
forward-socks5t / 127.0.0.1:1080 .
|
||||||
14
services/shadowsocks/client/shadowsocks-client.service
Normal file
14
services/shadowsocks/client/shadowsocks-client.service
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Shadowsocks-Rust Client
|
||||||
|
Documentation=https://github.com/shadowsocks/shadowsocks-rust
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/sslocal -c /etc/shadowsocks-rust/client.json
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
LimitNOFILE=51200
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
6
services/shadowsocks/server/.env.example
Normal file
6
services/shadowsocks/server/.env.example
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# role: vps
|
||||||
|
# description: Shadowsocks-Rust server (GFW-resistant proxy)
|
||||||
|
|
||||||
|
SS_PORT=41268
|
||||||
|
SS_PASSWORD=
|
||||||
|
SS_METHOD=2022-blake3-aes-256-gcm
|
||||||
10
services/shadowsocks/server/config.json.example
Normal file
10
services/shadowsocks/server/config.json.example
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"server": "0.0.0.0",
|
||||||
|
"server_port": ${SS_PORT},
|
||||||
|
"password": "${SS_PASSWORD}",
|
||||||
|
"method": "${SS_METHOD}",
|
||||||
|
"timeout": 300,
|
||||||
|
"fast_open": true,
|
||||||
|
"no_delay": true,
|
||||||
|
"mode": "tcp_and_udp"
|
||||||
|
}
|
||||||
56
services/shadowsocks/server/deploy.sh
Executable file
56
services/shadowsocks/server/deploy.sh
Executable file
|
|
@ -0,0 +1,56 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Installs shadowsocks-rust server and configures systemd service.
|
||||||
|
# Usage: INFRA_DIR=/path/to/infra/services/shadowsocks/server ./deploy.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../../bin/lib/common.sh"
|
||||||
|
|
||||||
|
ENV_FILE="${INFRA_DIR:-.}/.env"
|
||||||
|
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
require_env SS_PORT SS_PASSWORD SS_METHOD
|
||||||
|
|
||||||
|
find_template() {
|
||||||
|
local f="$1"
|
||||||
|
if [[ -n "${INFRA_DIR:-}" && -f "${INFRA_DIR}/$f" ]]; then
|
||||||
|
echo "${INFRA_DIR}/$f"
|
||||||
|
elif [[ -f "$SCRIPT_DIR/$f" ]]; then
|
||||||
|
echo "$SCRIPT_DIR/$f"
|
||||||
|
else
|
||||||
|
log_error "Template not found: $f"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
SSSERVER_BIN="/usr/local/bin/ssserver-rust"
|
||||||
|
|
||||||
|
if [[ -x "$SSSERVER_BIN" ]]; then
|
||||||
|
log_info "ssserver-rust already at $SSSERVER_BIN ($($SSSERVER_BIN --version 2>&1 | head -1)), skipping download"
|
||||||
|
elif command -v ssserver &>/dev/null; then
|
||||||
|
existing="$(command -v ssserver)"
|
||||||
|
log_info "ssserver found at $existing, symlinking to $SSSERVER_BIN"
|
||||||
|
ln -sf "$existing" "$SSSERVER_BIN"
|
||||||
|
else
|
||||||
|
log_info "Downloading shadowsocks-rust..."
|
||||||
|
VERSION=$(curl -s https://api.github.com/repos/shadowsocks/shadowsocks-rust/releases/latest \
|
||||||
|
| python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])")
|
||||||
|
ARCHIVE="shadowsocks-${VERSION}.x86_64-unknown-linux-gnu.tar.xz"
|
||||||
|
wget -q "https://github.com/shadowsocks/shadowsocks-rust/releases/download/${VERSION}/${ARCHIVE}"
|
||||||
|
tar -xf "$ARCHIVE"
|
||||||
|
cp ssserver "$SSSERVER_BIN"
|
||||||
|
chmod +x "$SSSERVER_BIN"
|
||||||
|
rm -f "$ARCHIVE" ssserver sslocal ssurl ssmanager redir tunnel
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Deploying config..."
|
||||||
|
mkdir -p /etc/shadowsocks-rust
|
||||||
|
envsubst < "$(find_template config.json.example)" > /etc/shadowsocks-rust/config.json
|
||||||
|
|
||||||
|
log_info "Installing service..."
|
||||||
|
cp "$(find_template shadowsocks-rust.service)" /etc/systemd/system/
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now shadowsocks-rust
|
||||||
|
|
||||||
|
log_info "Shadowsocks server deployed on port ${SS_PORT}"
|
||||||
14
services/shadowsocks/server/shadowsocks-rust.service
Normal file
14
services/shadowsocks/server/shadowsocks-rust.service
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Shadowsocks-Rust Server
|
||||||
|
Documentation=https://github.com/shadowsocks/shadowsocks-rust
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/ssserver-rust -c /etc/shadowsocks-rust/config.json
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
LimitNOFILE=51200
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
3
services/sing-box/client/.env.example
Normal file
3
services/sing-box/client/.env.example
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# role: homeserver
|
||||||
|
# description: sing-box transparent proxy client connecting to sing-box server
|
||||||
|
SING_BOX_VERSION=1.11.0
|
||||||
66
services/sing-box/client/deploy.sh
Executable file
66
services/sing-box/client/deploy.sh
Executable file
|
|
@ -0,0 +1,66 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploys sing-box as a transparent proxy client (home machine / LAN gateway).
|
||||||
|
#
|
||||||
|
# Usage: INFRA_DIR=/path/to/infra/services/sing-box/client ./deploy.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../../bin/lib/common.sh"
|
||||||
|
|
||||||
|
ENV_FILE="${INFRA_DIR:-.}/.env"
|
||||||
|
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
require_env SING_BOX_VERSION
|
||||||
|
|
||||||
|
INSTALL_DIR="/etc/sing-box"
|
||||||
|
BIN="$INSTALL_DIR/sing-box"
|
||||||
|
|
||||||
|
if [[ -x "$BIN" ]]; then
|
||||||
|
log_info "sing-box already at $BIN, skipping download"
|
||||||
|
else
|
||||||
|
log_info "Downloading sing-box ${SING_BOX_VERSION}..."
|
||||||
|
ARCH="$(uname -m)"
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64) ARCH="amd64" ;;
|
||||||
|
aarch64) ARCH="arm64" ;;
|
||||||
|
*) log_error "Unsupported arch: $ARCH"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
URL="https://github.com/SagerNet/sing-box/releases/download/v${SING_BOX_VERSION}/sing-box-${SING_BOX_VERSION}-linux-${ARCH}.tar.gz"
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
wget -qO "$TMP/sing-box.tar.gz" "$URL"
|
||||||
|
tar -xf "$TMP/sing-box.tar.gz" -C "$TMP"
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
install -m 755 "$TMP/sing-box-${SING_BOX_VERSION}-linux-${ARCH}/sing-box" "$BIN"
|
||||||
|
rm -rf "$TMP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Deploying client config..."
|
||||||
|
CONFIG_SRC="${INFRA_DIR}/sing_box_client.json"
|
||||||
|
[ -f "$CONFIG_SRC" ] || { log_error "sing_box_client.json not found in INFRA_DIR"; exit 1; }
|
||||||
|
cp "$CONFIG_SRC" "$INSTALL_DIR/config.json"
|
||||||
|
|
||||||
|
log_info "Installing systemd service..."
|
||||||
|
cat > /etc/systemd/system/sing-box-client.service <<'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=sing-box Client
|
||||||
|
After=network.target nss-lookup.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=root
|
||||||
|
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
|
||||||
|
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
|
||||||
|
ExecStart=/etc/sing-box/sing-box run -c /etc/sing-box/config.json
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
LimitNOFILE=65536
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now sing-box-client
|
||||||
|
|
||||||
|
log_info "sing-box client deployed"
|
||||||
3
services/sing-box/server/.env.example
Normal file
3
services/sing-box/server/.env.example
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# role: vps
|
||||||
|
# description: sing-box proxy server (VLESS/Reality, VMess/WS, Hysteria2 via sing-box-yg)
|
||||||
|
SING_BOX_VERSION=1.11.0
|
||||||
77
services/sing-box/server/deploy.sh
Executable file
77
services/sing-box/server/deploy.sh
Executable file
|
|
@ -0,0 +1,77 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploys sing-box proxy server on VPS.
|
||||||
|
#
|
||||||
|
# Config generated by https://github.com/yonggekkk/sing-box-yg — run that
|
||||||
|
# script once interactively to create /etc/s-box/sb.json, certs, and keys.
|
||||||
|
# Then commit the generated files into infra for future re-deployment.
|
||||||
|
#
|
||||||
|
# Usage: INFRA_DIR=/path/to/infra/services/sing-box/server ./deploy.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../../bin/lib/common.sh"
|
||||||
|
|
||||||
|
ENV_FILE="${INFRA_DIR:-.}/.env"
|
||||||
|
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
require_env SING_BOX_VERSION
|
||||||
|
|
||||||
|
INSTALL_DIR="/etc/s-box"
|
||||||
|
BIN="$INSTALL_DIR/sing-box"
|
||||||
|
|
||||||
|
if [[ -x "$BIN" ]]; then
|
||||||
|
log_info "sing-box already at $BIN, skipping download"
|
||||||
|
else
|
||||||
|
log_info "Downloading sing-box ${SING_BOX_VERSION}..."
|
||||||
|
ARCH="$(uname -m)"
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64) ARCH="amd64" ;;
|
||||||
|
aarch64) ARCH="arm64" ;;
|
||||||
|
*) log_error "Unsupported arch: $ARCH"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
URL="https://github.com/SagerNet/sing-box/releases/download/v${SING_BOX_VERSION}/sing-box-${SING_BOX_VERSION}-linux-${ARCH}.tar.gz"
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
wget -qO "$TMP/sing-box.tar.gz" "$URL"
|
||||||
|
tar -xf "$TMP/sing-box.tar.gz" -C "$TMP"
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
install -m 755 "$TMP/sing-box-${SING_BOX_VERSION}-linux-${ARCH}/sing-box" "$BIN"
|
||||||
|
rm -rf "$TMP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Deploying config from INFRA_DIR..."
|
||||||
|
for f in sb.json cert.pem private.key public.key; do
|
||||||
|
src="${INFRA_DIR}/$f"
|
||||||
|
if [[ -f "$src" ]]; then
|
||||||
|
cp "$src" "$INSTALL_DIR/$f"
|
||||||
|
log_info " copied $f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log_info "Installing systemd service..."
|
||||||
|
cat > /etc/systemd/system/sing-box.service <<'EOF'
|
||||||
|
[Unit]
|
||||||
|
After=network.target nss-lookup.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/root
|
||||||
|
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
|
||||||
|
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
|
||||||
|
ExecStart=/etc/s-box/sing-box run -c /etc/s-box/sb.json
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
LimitNOFILE=infinity
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now sing-box
|
||||||
|
|
||||||
|
log_info "sing-box server deployed"
|
||||||
|
echo ""
|
||||||
|
echo "Note: initial config must be generated via sing-box-yg:"
|
||||||
|
echo " bash <(curl -Ls https://raw.githubusercontent.com/yonggekkk/sing-box-yg/main/sb.sh)"
|
||||||
7
services/tnt/.env.example
Normal file
7
services/tnt/.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# role: any
|
||||||
|
# description: TNT terminal chat server (SSH-based, github.com/m1ngsama/TNT)
|
||||||
|
TNT_PORT=2222
|
||||||
|
TNT_ACCESS_TOKEN=
|
||||||
|
TNT_BIND_ADDR=0.0.0.0
|
||||||
|
TNT_MAX_CONNECTIONS=50
|
||||||
|
TNT_MAX_CONN_PER_IP=3
|
||||||
70
services/tnt/deploy.sh
Executable file
70
services/tnt/deploy.sh
Executable file
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploys TNT terminal chat server.
|
||||||
|
# https://github.com/m1ngsama/TNT
|
||||||
|
#
|
||||||
|
# Usage: INFRA_DIR=/path/to/infra/services/tnt ./deploy.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../bin/lib/common.sh"
|
||||||
|
|
||||||
|
ENV_FILE="${INFRA_DIR:-.}/.env"
|
||||||
|
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
require_env TNT_PORT TNT_ACCESS_TOKEN
|
||||||
|
|
||||||
|
BIN="/usr/local/bin/tnt"
|
||||||
|
DATA_DIR="/var/lib/tnt"
|
||||||
|
|
||||||
|
if [[ -x "$BIN" ]]; then
|
||||||
|
log_info "tnt already at $BIN, skipping download"
|
||||||
|
else
|
||||||
|
log_info "Installing tnt via official installer..."
|
||||||
|
curl -sSL https://raw.githubusercontent.com/m1ngsama/TNT/main/install.sh | sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Setting up data directory..."
|
||||||
|
mkdir -p "$DATA_DIR"
|
||||||
|
|
||||||
|
# Create unprivileged user if not exists
|
||||||
|
if ! id tnt &>/dev/null; then
|
||||||
|
useradd --system --no-create-home --shell /usr/sbin/nologin tnt
|
||||||
|
fi
|
||||||
|
chown tnt:tnt "$DATA_DIR"
|
||||||
|
|
||||||
|
log_info "Installing systemd service..."
|
||||||
|
cat > /etc/systemd/system/tnt.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=TNT Terminal Chat Server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=tnt
|
||||||
|
Group=tnt
|
||||||
|
WorkingDirectory=$DATA_DIR
|
||||||
|
ExecStart=$BIN -p ${TNT_PORT}
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
Environment="TNT_ACCESS_TOKEN=${TNT_ACCESS_TOKEN}"
|
||||||
|
Environment="TNT_BIND_ADDR=${TNT_BIND_ADDR:-0.0.0.0}"
|
||||||
|
Environment="TNT_MAX_CONNECTIONS=${TNT_MAX_CONNECTIONS:-50}"
|
||||||
|
Environment="TNT_MAX_CONN_PER_IP=${TNT_MAX_CONN_PER_IP:-3}"
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=$DATA_DIR
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now tnt
|
||||||
|
|
||||||
|
log_info "TNT deployed on port ${TNT_PORT}"
|
||||||
|
echo " Connect: ssh -p ${TNT_PORT} <host>"
|
||||||
337
setup.sh
Executable file
337
setup.sh
Executable file
|
|
@ -0,0 +1,337 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Interactive installer for infra services.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./setup.sh # fully interactive
|
||||||
|
# ./setup.sh --infra-dir PATH # use pre-filled .env files from local infra checkout
|
||||||
|
# ./setup.sh --dry-run # show what would be deployed, no changes
|
||||||
|
# INFRA_DIR=/path/to/infra ./setup.sh # same as --infra-dir via env var
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/bin/lib/common.sh"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Args
|
||||||
|
# ============================================================================
|
||||||
|
INFRA_DIR="${INFRA_DIR:-}"
|
||||||
|
DRY_RUN=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--infra-dir) INFRA_DIR="$2"; shift 2 ;;
|
||||||
|
--dry-run) DRY_RUN=1; shift ;;
|
||||||
|
*) log_error "Unknown argument: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Banner
|
||||||
|
# ============================================================================
|
||||||
|
echo ""
|
||||||
|
echo " ┌─────────────────────────────────────┐"
|
||||||
|
echo " │ automa setup │"
|
||||||
|
echo " │ interactive infrastructure deploy │"
|
||||||
|
echo " └─────────────────────────────────────┘"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Prerequisites (skipped in dry-run)
|
||||||
|
# ============================================================================
|
||||||
|
check_prerequisites() {
|
||||||
|
log_info "Checking prerequisites..."
|
||||||
|
local missing=0
|
||||||
|
for cmd in curl wget systemctl envsubst; do
|
||||||
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
|
log_warn " missing: $cmd"
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$missing" -eq 1 ]]; then
|
||||||
|
log_error "Install missing tools before continuing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_info "Prerequisites OK."
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ "$DRY_RUN" -eq 0 ]] && check_prerequisites
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Infra dir resolution
|
||||||
|
# ============================================================================
|
||||||
|
if [[ -n "$INFRA_DIR" ]]; then
|
||||||
|
if [[ ! -d "$INFRA_DIR" ]]; then
|
||||||
|
log_error "INFRA_DIR does not exist: $INFRA_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_info "Using infra dir: $INFRA_DIR"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo " No --infra-dir specified."
|
||||||
|
echo " Point to a local infra checkout (with pre-filled .env files) for auto-config,"
|
||||||
|
echo " or leave blank to enter all values interactively."
|
||||||
|
echo ""
|
||||||
|
read -rp " Path to local infra repo [blank = interactive mode]: " infra_input
|
||||||
|
if [[ -n "$infra_input" ]]; then
|
||||||
|
infra_input="${infra_input/#\~/$HOME}"
|
||||||
|
if [[ -d "$infra_input" ]]; then
|
||||||
|
INFRA_DIR="$infra_input"
|
||||||
|
log_info "Using infra dir: $INFRA_DIR"
|
||||||
|
else
|
||||||
|
log_warn "Directory not found, continuing in interactive mode."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Module discovery
|
||||||
|
# Parallel arrays indexed by position: MODULES[i], ROLES[i], DESCS[i]
|
||||||
|
# MENU_MODS[menu_number] = module_path (1-based; index 0 = "")
|
||||||
|
# ============================================================================
|
||||||
|
MODULES=()
|
||||||
|
ROLES=()
|
||||||
|
DESCS=()
|
||||||
|
MENU_MODS=("") # index 0 unused — menu numbers start at 1
|
||||||
|
|
||||||
|
discover_modules() {
|
||||||
|
while IFS= read -r deploy; do
|
||||||
|
local mod env_ex role desc r d
|
||||||
|
mod="$(dirname "$deploy" | sed "s|^$SCRIPT_DIR/services/||")"
|
||||||
|
env_ex="$SCRIPT_DIR/services/$mod/.env.example"
|
||||||
|
|
||||||
|
role="any"
|
||||||
|
desc="$mod"
|
||||||
|
|
||||||
|
if [[ -f "$env_ex" ]]; then
|
||||||
|
# grep returns 1 if no match — use || true to avoid pipefail exit
|
||||||
|
r="$(grep '^# role:' "$env_ex" | head -1 | sed 's/^# role: *//' || true)"
|
||||||
|
d="$(grep '^# description:' "$env_ex" | head -1 | sed 's/^# description: *//' || true)"
|
||||||
|
[[ -n "$r" ]] && role="$r"
|
||||||
|
[[ -n "$d" ]] && desc="$d"
|
||||||
|
fi
|
||||||
|
|
||||||
|
MODULES+=("$mod")
|
||||||
|
ROLES+=("$role")
|
||||||
|
DESCS+=("$desc")
|
||||||
|
done < <(find "$SCRIPT_DIR/services" -name "deploy.sh" | sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
discover_modules
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Interactive menu (grouped by role)
|
||||||
|
# Populates MENU_MODS as a side effect.
|
||||||
|
# ============================================================================
|
||||||
|
print_menu() {
|
||||||
|
local printed_any=0
|
||||||
|
|
||||||
|
for role_group in "vps" "homeserver" "any"; do
|
||||||
|
local label header_printed=0
|
||||||
|
case "$role_group" in
|
||||||
|
vps) label="VPS services" ;;
|
||||||
|
homeserver) label="Home server services" ;;
|
||||||
|
any) label="Any machine" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
for i in "${!MODULES[@]}"; do
|
||||||
|
[[ "${ROLES[$i]}" != "$role_group" ]] && continue
|
||||||
|
|
||||||
|
if [[ "$header_printed" -eq 0 ]]; then
|
||||||
|
echo " $label:"
|
||||||
|
header_printed=1
|
||||||
|
printed_any=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local menu_idx=${#MENU_MODS[@]}
|
||||||
|
MENU_MODS+=("${MODULES[$i]}")
|
||||||
|
printf " [%2d] %-30s %s\n" "$menu_idx" "${MODULES[$i]}" "${DESCS[$i]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ "$header_printed" -eq 1 ]] && echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$printed_any" -eq 0 ]]; then
|
||||||
|
log_error "No deployable modules found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_menu
|
||||||
|
|
||||||
|
echo " Enter module numbers to deploy (space-separated, e.g. \"1 3\")."
|
||||||
|
echo " Press Enter to select all, or type \"none\" to exit."
|
||||||
|
echo ""
|
||||||
|
read -rp " > " selection
|
||||||
|
|
||||||
|
if [[ "$selection" == "none" ]]; then
|
||||||
|
echo " Exiting."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SELECTED_MODULES=()
|
||||||
|
|
||||||
|
if [[ -z "$selection" ]]; then
|
||||||
|
SELECTED_MODULES=("${MODULES[@]}")
|
||||||
|
else
|
||||||
|
for num in $selection; do
|
||||||
|
if [[ "$num" =~ ^[0-9]+$ ]] \
|
||||||
|
&& [[ "$num" -gt 0 ]] \
|
||||||
|
&& [[ "$num" -lt "${#MENU_MODS[@]}" ]]; then
|
||||||
|
SELECTED_MODULES+=("${MENU_MODS[$num]}")
|
||||||
|
else
|
||||||
|
log_warn "Unknown module number: $num (skipped)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#SELECTED_MODULES[@]} -eq 0 ]]; then
|
||||||
|
log_error "No valid modules selected."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Selected: ${SELECTED_MODULES[*]}"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Env resolution
|
||||||
|
# Priority:
|
||||||
|
# 1. $INFRA_DIR/services/$mod/.env (pre-filled, use as-is)
|
||||||
|
# 2. $INFRA_DIR/services/$mod/.env.example (prompt, fill from infra template)
|
||||||
|
# 3. $SCRIPT_DIR/services/$mod/.env.example (prompt, fill from automa template)
|
||||||
|
# Prints the resolved .env path to stdout.
|
||||||
|
# ============================================================================
|
||||||
|
fill_env_interactive() {
|
||||||
|
# All user-visible output → stderr so caller can capture stdout for the path.
|
||||||
|
local env_example="$1"
|
||||||
|
local output="$2"
|
||||||
|
|
||||||
|
printf "" > "$output"
|
||||||
|
echo " Fill in values (Enter = accept default shown in [brackets]):" >&2
|
||||||
|
echo "" >&2
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^#\ (role|description): ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ -z "$line" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$line" =~ ^# ]]; then
|
||||||
|
printf " %s\n" "$line" >&2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local key default hint val
|
||||||
|
key="${line%%=*}"
|
||||||
|
default="${line#*=}"
|
||||||
|
hint=""
|
||||||
|
|
||||||
|
if [[ -n "$default" && "$default" != "your-"* && "$default" != "x.x.x.x" ]]; then
|
||||||
|
hint=" [$default]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -rp " $key$hint: " val <&0
|
||||||
|
printf "%s=%s\n" "$key" "${val:-$default}" >> "$output"
|
||||||
|
done < "$env_example"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_env_for_module() {
|
||||||
|
# Prints resolved .env path to stdout; all messages → stderr.
|
||||||
|
local mod="$1"
|
||||||
|
local dry_run="${2:-0}"
|
||||||
|
local infra_mod_dir=""
|
||||||
|
|
||||||
|
if [[ -n "$INFRA_DIR" ]]; then
|
||||||
|
infra_mod_dir="$INFRA_DIR/services/$mod"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Case 1: pre-filled .env in infra — use directly
|
||||||
|
if [[ -n "$infra_mod_dir" && -f "$infra_mod_dir/.env" ]]; then
|
||||||
|
log_info " Using pre-filled .env from infra" >&2
|
||||||
|
printf "%s" "$infra_mod_dir/.env"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine .env.example template source
|
||||||
|
local env_example=""
|
||||||
|
if [[ -n "$infra_mod_dir" && -f "$infra_mod_dir/.env.example" ]]; then
|
||||||
|
env_example="$infra_mod_dir/.env.example"
|
||||||
|
elif [[ -f "$SCRIPT_DIR/services/$mod/.env.example" ]]; then
|
||||||
|
env_example="$SCRIPT_DIR/services/$mod/.env.example"
|
||||||
|
else
|
||||||
|
log_error " No .env.example found for: $mod" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Dry-run: skip interactive fill, just return path to the example
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
printf "%s" "$env_example"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmp_env
|
||||||
|
tmp_env="$(mktemp /tmp/automa-env-XXXXXX)"
|
||||||
|
fill_env_interactive "$env_example" "$tmp_env"
|
||||||
|
printf "%s" "$tmp_env"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Deployment
|
||||||
|
# ============================================================================
|
||||||
|
DEPLOYED=()
|
||||||
|
FAILED=()
|
||||||
|
|
||||||
|
for mod in "${SELECTED_MODULES[@]}"; do
|
||||||
|
echo ""
|
||||||
|
echo " ── $mod ──"
|
||||||
|
|
||||||
|
local_env=""
|
||||||
|
if ! local_env="$(resolve_env_for_module "$mod" "$DRY_RUN")"; then
|
||||||
|
FAILED+=("$mod")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
deploy_script="$SCRIPT_DIR/services/$mod/deploy.sh"
|
||||||
|
if [[ ! -x "$deploy_script" ]]; then
|
||||||
|
log_error " deploy.sh not found or not executable: $deploy_script"
|
||||||
|
FAILED+=("$mod")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||||
|
log_info " [dry-run] $mod → $deploy_script (env: $local_env)"
|
||||||
|
DEPLOYED+=("$mod")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if INFRA_DIR="$(dirname "$local_env")" bash "$deploy_script"; then
|
||||||
|
DEPLOYED+=("$mod")
|
||||||
|
else
|
||||||
|
log_error " Deployment failed: $mod"
|
||||||
|
FAILED+=("$mod")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Summary
|
||||||
|
# ============================================================================
|
||||||
|
echo ""
|
||||||
|
echo " ┌─────────────────────────────────────┐"
|
||||||
|
echo " │ Summary │"
|
||||||
|
echo " └─────────────────────────────────────┘"
|
||||||
|
|
||||||
|
if [[ ${#DEPLOYED[@]} -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
log_info "Deployed (${#DEPLOYED[@]}):"
|
||||||
|
for m in "${DEPLOYED[@]}"; do echo " ✓ $m"; done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#FAILED[@]} -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
log_error "Failed (${#FAILED[@]}):"
|
||||||
|
for m in "${FAILED[@]}"; do echo " ✗ $m"; done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Done."
|
||||||
Loading…
Reference in a new issue