Compare commits

...

8 commits

Author SHA1 Message Date
23b8d1abd8
Merge pull request #10 from m1ngsama/fix/deploy-idempotent-binary-check
fix: idempotent binary install, bundled templates, envsubst export
2026-02-28 02:13:47 +08:00
2ae28fb0a7 fix: export .env vars with set -a so envsubst can see them
source without set -a sets vars in current shell but does not export them.
Child processes like envsubst cannot see unexported vars, causing all
template substitutions to produce empty strings.

Fix: set -a before source, set +a after — auto-exports every assigned var.
2026-02-28 02:04:26 +08:00
929c527ad0 fix: bundle config templates, add find_template fallback
Deploy scripts now look for templates in INFRA_DIR first, then fall back
to the bundled copies in automa. This makes automa fully self-contained:
a .env with filled values is all that is needed — no infra checkout required.

Bundle: config.json.example, privoxy.conf.example, shadowsocks-client.service,
shadowsocks-rust.service, frps.toml.example, frps.service,
frpc.toml.example, frpc.service
2026-02-28 02:00:52 +08:00
19b3e5035c fix: skip binary download if already installed, symlink from existing path
Resolves bootstrapping deadlock where downloading sslocal requires internet
access via the very proxy being deployed. Also handles distros (Arch) where
shadowsocks-rust is installed via package manager to /usr/bin instead of
/usr/local/bin.

Priority:
  1. /usr/local/bin/sslocal exists → skip download
  2. sslocal found in PATH elsewhere → symlink to /usr/local/bin/sslocal
  3. not found → download from GitHub releases

Same logic applied to ssserver-rust in server/deploy.sh.
Also: stop conflicting shadowsocks.service before starting shadowsocks-client,
and detect pacman vs apt for privoxy install.
2026-02-28 01:55:47 +08:00
990e0f93be
Merge pull request #9 from m1ngsama/feature/interactive-setup
Add interactive setup.sh wizard
2026-02-28 01:47:13 +08:00
1356348d79 feat: add interactive setup.sh wizard
Discovers all deployable modules from services/ automatically.
Grouped menu by role (vps / homeserver / any) with descriptions.

Env resolution priority:
  1. pre-filled .env in local infra checkout (--infra-dir)
  2. .env.example from infra (interactive fill)
  3. .env.example bundled in automa (interactive fill, no infra needed)

Usage:
  ./setup.sh                           # fully interactive
  ./setup.sh --infra-dir /path/to/infra # use pre-filled .env files
  ./setup.sh --dry-run                 # preview without deploying

Also add .env.example with role/description metadata to each service
module so setup.sh can build the menu and prompt for values without
requiring an infra checkout.
2026-02-28 01:42:50 +08:00
50ecd7c814
Merge pull request #7 from m1ngsama/feature/infra-service-deploy-scripts
Add infra service deploy scripts
2026-02-28 01:17:32 +08:00
f82cd2d956 feat: add infra service deploy scripts
Add services/ directory with deploy scripts for system-level infrastructure
services. Each script reads INFRA_DIR pointing to the corresponding infra
module, sources its .env, substitutes config templates via envsubst, and
installs/enables systemd services. Zero hardcoded values — public-safe.

New scripts:
- services/email/deploy.sh      (Postfix + Dovecot + OpenDKIM + SpamAssassin)
- services/nginx/deploy.sh      (Nginx vhosts via envsubst)
- services/shadowsocks/server/deploy.sh  (shadowsocks-rust server)
- services/shadowsocks/client/deploy.sh  (sslocal + privoxy chain)
- services/frp/server/deploy.sh (frps)
- services/frp/client/deploy.sh (frpc)

README: add "Relationship with infra" section explaining the two-repo workflow
Makefile: add deploy-email, deploy-nginx, deploy-ss-{server,client},
          deploy-frp-{server,client} targets

Closes #6
2026-02-28 01:09:36 +08:00
24 changed files with 921 additions and 8 deletions

View file

@ -4,6 +4,7 @@
.PHONY: help all status up down logs restart clean minecraft teamspeak nextcloud
.PHONY: health health-minecraft health-teamspeak health-nextcloud
.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
help:
@ -21,6 +22,14 @@ help:
@echo " backup-list List available backups"
@echo " backup-cleanup Remove old backups"
@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 " Minecraft:"
@echo " minecraft-up Start Minecraft server"
@ -55,6 +64,36 @@ help:
@echo " check Check prerequisites"
@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:
@echo "Checking prerequisites..."

View file

@ -1,6 +1,27 @@
# 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
@ -10,7 +31,49 @@ This project embraces Unix principles:
- **Composability**: Tools work together through standard interfaces
- **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.
```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
```
### 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
```
## Home Services
Docker-based services with their own config.
### Minecraft Server
Automated Minecraft Fabric server deployment with mod management.
@ -72,12 +135,21 @@ Batch clone all repositories from a GitHub organization.
```
automa/
├── bin/ # Utility scripts
│ └── org-clone.sh # GitHub org repo cloner
├── minecraft/ # Minecraft server setup
├── teamspeak/ # TeamSpeak server setup
├── nextcloud/ # Nextcloud setup
└── README.md # This file
├── bin/ # Utility scripts
│ └── lib/common.sh # Shared logging + env helpers
├── services/ # Infrastructure deploy scripts (reads infra .env)
│ ├── email/deploy.sh
│ ├── nginx/deploy.sh
│ ├── shadowsocks/
│ │ ├── server/deploy.sh
│ │ └── client/deploy.sh
│ └── frp/
│ ├── server/deploy.sh
│ └── client/deploy.sh
├── minecraft/ # Minecraft server (Docker)
├── teamspeak/ # TeamSpeak server (Docker)
├── nextcloud/ # Nextcloud (Docker)
└── README.md
```
## Common Operations

View 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
View 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)"

View 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
View 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}"

View 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

View 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

View file

@ -0,0 +1,6 @@
# role: vps
# description: FRP server (frps) — public entry point for reverse tunnels
FRP_TOKEN=
FRP_WEB_PASSWORD=
FRP_BIND_PORT=7000

52
services/frp/server/deploy.sh Executable file
View 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_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}"

View 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

View 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 = "root"
webServer.password = "${FRP_WEB_PASSWORD}"
log.to = "./frps.log"
log.level = "info"
log.maxDays = 3
auth.method = "token"
auth.token = "${FRP_TOKEN}"

View 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
View 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 < "$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"

View 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

View 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"
}

View 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"

View 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 .

View 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

View 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

View 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"
}

View 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}"

View 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

337
setup.sh Executable file
View 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."