improve: best-practice configs for all projects, CLI UX overhaul

Compose improvements:
- forgejo: add healthcheck (/api/healthz), ROOT_URL + SSH_PORT env, LFS
- tailscale: drop redundant privileged (use cap_add only), use devices
  for /dev/net/tun, mount /lib/modules, reliable healthcheck (tailscale
  status), profiles for opt-in DERP, headscale comment in .env.example
- uptime-kuma: add built-in healthcheck (extra/healthcheck)
- filesuite: add healthchecks for both cloudreve and qbittorrent
- minecraft: add mc-health check (built into itzg image), simplify volumes
- teamspeak: add healthcheck via ServerQuery (nc localhost 10011)
- nextcloud: add healthchecks for all 3 services, depends_on with
  service_healthy conditions so startup order is correct

CLI improvements:
- Fix docker compose detection (was broken with space in arg)
- Use global array for project discovery (no word-splitting bugs)
- Empty selection no longer defaults to "all" (safety)
- Show .env.example comments as hints during interactive configure
- Required fields (empty default) loop until user provides a value
- Disable colors when stdout is not a terminal
- compose() wrapper auto-adds --env-file
- Deduplicate project_exists / project_dir helpers
This commit is contained in:
m1ngsama 2026-04-15 10:02:41 +08:00
parent 48b32d46b2
commit 1ef24b3be8
16 changed files with 267 additions and 124 deletions

View file

@ -14,11 +14,11 @@ cd ~/automa
```bash ```bash
./automa deploy # interactive project selection ./automa deploy # interactive project selection
./automa deploy forgejo monitoring # deploy specific projects ./automa deploy forgejo filesuite # deploy specific projects
./automa status # check all project status ./automa status # check all project status
./automa logs forgejo # follow logs ./automa logs forgejo # follow logs
./automa stop forgejo # stop a project ./automa stop forgejo # stop a project
./automa update monitoring # pull latest images & recreate ./automa update nextcloud # pull latest images & recreate
./automa config tailscale # reconfigure .env ./automa config tailscale # reconfigure .env
./automa list # list available projects ./automa list # list available projects
``` ```
@ -29,15 +29,11 @@ cd ~/automa
|---------|-------------| |---------|-------------|
| `forgejo` | Self-hosted Git (Gitea fork) | | `forgejo` | Self-hosted Git (Gitea fork) |
| `uptime-kuma` | Uptime monitoring dashboard | | `uptime-kuma` | Uptime monitoring dashboard |
| `tailscale` | Tailscale client + DERP relay server | | `tailscale` | Tailscale client + DERP relay server (profiles) |
| `monitoring` | Prometheus + Grafana + Blackbox + Node Exporter |
| `filesuite` | Cloudreve cloud storage + qBittorrent | | `filesuite` | Cloudreve cloud storage + qBittorrent |
| `minecraft` | Fabric Minecraft server | | `minecraft` | Fabric Minecraft server |
| `teamspeak` | TeamSpeak voice server | | `teamspeak` | TeamSpeak voice server |
| `nextcloud` | Nextcloud with MariaDB + Redis | | `nextcloud` | Nextcloud with MariaDB + Redis |
| `huajibot` | HuaJi Bot |
| `dockge` | Docker Compose stack manager |
| `notification-center` | Webhook notification service |
## Structure ## Structure
@ -46,7 +42,7 @@ Each project is a self-contained directory:
``` ```
project-name/ project-name/
├── compose.yaml # Docker Compose definition ├── compose.yaml # Docker Compose definition
├── .env.example # Template with default values ├── .env.example # Template — comments shown during setup
└── .env # Your config (gitignored, created by CLI) └── .env # Your config (gitignored, created by CLI)
``` ```

248
automa
View file

@ -3,11 +3,7 @@
# #
# Install & run: # Install & run:
# curl -fsSL https://raw.githubusercontent.com/m1ngsama/automa/main/install.sh | bash # curl -fsSL https://raw.githubusercontent.com/m1ngsama/automa/main/install.sh | bash
# automa deploy # cd ~/automa && ./automa deploy
#
# Or clone and run directly:
# git clone https://github.com/m1ngsama/automa.git && cd automa
# ./automa deploy
set -euo pipefail set -euo pipefail
@ -17,15 +13,13 @@ set -euo pipefail
AUTOMA_VERSION="1.0.0" AUTOMA_VERSION="1.0.0"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Colors # Colors (disabled when not a terminal)
RED='\033[0;31m' if [[ -t 1 ]]; then
GREEN='\033[0;32m' RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m'
YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m'
BLUE='\033[0;34m' else
CYAN='\033[0;36m' RED='' GREEN='' YELLOW='' CYAN='' BOLD='' DIM='' NC=''
BOLD='\033[1m' fi
DIM='\033[2m'
NC='\033[0m'
# ============================================================================ # ============================================================================
# Helpers # Helpers
@ -33,7 +27,6 @@ NC='\033[0m'
info() { echo -e "${GREEN}[+]${NC} $*"; } info() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; }
error() { echo -e "${RED}[-]${NC} $*" >&2; } error() { echo -e "${RED}[-]${NC} $*" >&2; }
dim() { echo -e "${DIM}$*${NC}"; }
banner() { banner() {
echo "" echo ""
@ -42,47 +35,75 @@ banner() {
echo "" echo ""
} }
require_cmd() { check_docker() {
if ! command -v "$1" &>/dev/null; then if ! command -v docker &>/dev/null; then
error "$1 is required but not installed." error "docker is required but not installed."
[[ -n "${2:-}" ]] && dim " Install: $2" echo -e " ${DIM}Install: https://docs.docker.com/engine/install/${NC}"
exit 1
fi
if ! docker compose version &>/dev/null 2>&1; then
error "docker compose plugin is required."
echo -e " ${DIM}Install: https://docs.docker.com/compose/install/${NC}"
exit 1 exit 1
fi fi
} }
# ============================================================================ # ============================================================================
# Project discovery # Project discovery
# Scans for top-level dirs containing compose.yaml
# ============================================================================ # ============================================================================
PROJECTS=()
discover_projects() { discover_projects() {
local projects=() PROJECTS=()
for dir in "$SCRIPT_DIR"/*/; do for dir in "$SCRIPT_DIR"/*/; do
[[ -f "$dir/compose.yaml" ]] && projects+=("$(basename "$dir")") [[ -f "$dir/compose.yaml" ]] && PROJECTS+=("$(basename "$dir")")
done done
echo "${projects[@]}" }
project_exists() {
local name="$1"
[[ -f "$SCRIPT_DIR/$name/compose.yaml" ]]
}
project_dir() {
echo "$SCRIPT_DIR/$1"
}
# compose wrapper that auto-adds --env-file if .env exists
compose() {
local name="$1"; shift
local dir="$SCRIPT_DIR/$name"
local args=(-f "$dir/compose.yaml")
if [[ -f "$dir/.env" ]]; then
args+=(--env-file "$dir/.env")
fi
docker compose "${args[@]}" "$@"
} }
# ============================================================================ # ============================================================================
# Interactive .env configuration # Interactive .env configuration
# Reads .env.example, prompts user for each value, writes .env
# ============================================================================ # ============================================================================
configure_env() { configure_env() {
local project_dir="$1" local name="$1"
local env_example="$project_dir/.env.example" local dir="$SCRIPT_DIR/$name"
local env_file="$project_dir/.env" local env_example="$dir/.env.example"
local env_file="$dir/.env"
if [[ ! -f "$env_example" ]]; then if [[ ! -f "$env_example" ]]; then
warn "No .env.example found, skipping configuration" warn "No .env.example found, skipping configuration"
return 0 return 0
fi fi
# If .env already exists, ask what to do
if [[ -f "$env_file" ]]; then if [[ -f "$env_file" ]]; then
echo "" echo ""
echo -e " ${YELLOW}.env already exists.${NC}" echo -e " ${YELLOW}.env already exists for ${BOLD}${name}${NC}"
echo -e " ${DIM}[k]eep [r]econfigure [v]iew${NC}" echo -e " ${DIM}[k]eep current [r]econfigure [v]iew${NC}"
while true; do while true; do
read -rp " > " choice read -rp " > " choice
case "$choice" in case "${choice,,}" in
k|keep|"") info "Keeping existing .env"; return 0 ;; k|keep|"") info "Keeping existing .env"; return 0 ;;
r|reconfigure) break ;; r|reconfigure) break ;;
v|view) v|view)
@ -93,39 +114,66 @@ configure_env() {
echo "" echo ""
echo -e " ${DIM}[k]eep [r]econfigure${NC}" echo -e " ${DIM}[k]eep [r]econfigure${NC}"
;; ;;
*) echo -e " ${DIM}k/r/v${NC}" ;; *) echo -e " ${DIM}Enter k, r, or v${NC}" ;;
esac esac
done done
fi fi
echo "" echo ""
info "Configure ${BOLD}$(basename "$project_dir")${NC}" echo -e " ${BOLD}Configure: ${CYAN}${name}${NC}"
dim " Enter values (blank = use default shown in brackets)" echo ""
# Show header comments from .env.example
while IFS= read -r line; do
[[ "$line" =~ ^#.* ]] && echo -e " ${DIM}${line}${NC}" || break
done < "$env_example"
echo ""
echo -e " ${DIM}Enter values (blank = accept default in brackets)${NC}"
echo "" echo ""
local tmp_env local tmp_env
tmp_env="$(mktemp)" tmp_env="$(mktemp)"
local pending_comment=""
while IFS= read -r line; do while IFS= read -r line; do
# Skip comments and empty lines # Blank line
[[ "$line" =~ ^#.* ]] && continue if [[ -z "$line" ]]; then
[[ -z "$line" ]] && continue [[ -n "$pending_comment" ]] && { echo "" ; pending_comment=""; }
continue
fi
# Comment line — show as hint for next variable
if [[ "$line" =~ ^#.* ]]; then
echo -e " ${DIM}${line}${NC}"
pending_comment="$line"
continue
fi
local key="${line%%=*}" local key="${line%%=*}"
local default="${line#*=}" local default="${line#*=}"
pending_comment=""
local val
if [[ -n "$default" ]]; then if [[ -n "$default" ]]; then
read -rp " ${key} [${default}]: " val read -rp " ${key} [${default}]: " val
echo "${key}=${val:-$default}" >> "$tmp_env" echo "${key}=${val:-$default}" >> "$tmp_env"
else else
read -rp " ${key}: " val # Required field — no default
while true; do
read -rp " ${key} (required): " val
if [[ -n "$val" ]]; then
break
fi
echo -e " ${RED}This field cannot be empty${NC}"
done
echo "${key}=${val}" >> "$tmp_env" echo "${key}=${val}" >> "$tmp_env"
fi fi
done < "$env_example" done < "$env_example"
mv "$tmp_env" "$env_file" mv "$tmp_env" "$env_file"
chmod 600 "$env_file" chmod 600 "$env_file"
info ".env written" echo ""
info ".env saved (chmod 600)"
} }
# ============================================================================ # ============================================================================
@ -134,25 +182,24 @@ configure_env() {
cmd_list() { cmd_list() {
banner banner
local projects discover_projects
read -ra projects <<< "$(discover_projects)"
if [[ ${#projects[@]} -eq 0 ]]; then if [[ ${#PROJECTS[@]} -eq 0 ]]; then
warn "No projects found" warn "No projects found"
return 1 return 1
fi fi
echo -e " ${BOLD}Available projects:${NC}" echo -e " ${BOLD}Available projects:${NC}"
echo "" echo ""
local i=1 local i=1
for p in "${projects[@]}"; do for p in "${PROJECTS[@]}"; do
local status="${DIM}not deployed${NC}" local status="${DIM}not configured${NC}"
if [[ -f "$SCRIPT_DIR/$p/.env" ]]; then if [[ -f "$SCRIPT_DIR/$p/.env" ]]; then
# Check if compose is running if compose "$p" ps --status running 2>/dev/null | grep -q .; then
if docker compose -f "$SCRIPT_DIR/$p/compose.yaml" ps --status running 2>/dev/null | grep -q .; then
status="${GREEN}running${NC}" status="${GREEN}running${NC}"
else else
status="${YELLOW}configured${NC}" status="${YELLOW}stopped${NC}"
fi fi
fi fi
printf " ${BOLD}%2d${NC} %-24s %b\n" "$i" "$p" "$status" printf " ${BOLD}%2d${NC} %-24s %b\n" "$i" "$p" "$status"
@ -163,13 +210,10 @@ cmd_list() {
cmd_deploy() { cmd_deploy() {
banner banner
require_cmd docker "https://docs.docker.com/engine/install/" check_docker
require_cmd "docker compose" || require_cmd docker-compose discover_projects
local projects if [[ ${#PROJECTS[@]} -eq 0 ]]; then
read -ra projects <<< "$(discover_projects)"
if [[ ${#projects[@]} -eq 0 ]]; then
error "No projects found" error "No projects found"
return 1 return 1
fi fi
@ -186,36 +230,35 @@ cmd_deploy() {
echo -e " ${BOLD}Select projects to deploy:${NC}" echo -e " ${BOLD}Select projects to deploy:${NC}"
echo "" echo ""
local i=1 local i=1
for p in "${projects[@]}"; do for p in "${PROJECTS[@]}"; do
printf " ${BOLD}%2d${NC} %s\n" "$i" "$p" printf " ${BOLD}%2d${NC} %s\n" "$i" "$p"
((i++)) ((i++))
done done
echo "" echo ""
echo -e " ${DIM}Enter numbers (space-separated), 'all', or 'q' to quit${NC}" echo -e " ${DIM}Enter numbers (space-separated), or 'q' to quit${NC}"
read -rp " > " selection read -rp " > " selection
[[ "$selection" == "q" ]] && return 0 [[ "$selection" == "q" || -z "$selection" ]] && return 0
local selected=() local selected=()
if [[ "$selection" == "all" || -z "$selection" ]]; then if [[ "$selection" == "all" ]]; then
selected=("${projects[@]}") selected=("${PROJECTS[@]}")
else else
for num in $selection; do for num in $selection; do
if [[ "$num" =~ ^[0-9]+$ ]] && ((num > 0 && num <= ${#projects[@]})); then if [[ "$num" =~ ^[0-9]+$ ]] && ((num > 0 && num <= ${#PROJECTS[@]})); then
selected+=("${projects[$((num-1))]}") selected+=("${PROJECTS[$((num-1))]}")
else else
warn "Invalid selection: $num" warn "Invalid: $num (skipped)"
fi fi
done done
fi fi
if [[ ${#selected[@]} -eq 0 ]]; then if [[ ${#selected[@]} -eq 0 ]]; then
error "No projects selected" return 0
return 1
fi fi
echo "" echo ""
info "Deploying: ${selected[*]}" info "Will deploy: ${selected[*]}"
echo "" echo ""
local ok=0 fail=0 local ok=0 fail=0
@ -225,76 +268,78 @@ cmd_deploy() {
else else
((fail++)) ((fail++))
fi fi
echo ""
done done
echo "" echo -e " ${BOLD}Done.${NC} ${GREEN}${ok} deployed${NC}" \
echo -e " ${BOLD}Summary${NC}" "$( ((fail > 0)) && echo -e ", ${RED}${fail} failed${NC}" )"
[[ $ok -gt 0 ]] && info "Deployed: $ok"
[[ $fail -gt 0 ]] && error "Failed: $fail"
} }
deploy_project() { deploy_project() {
local name="$1" local name="$1"
local project_dir="$SCRIPT_DIR/$name"
if [[ ! -f "$project_dir/compose.yaml" ]]; then if ! project_exists "$name"; then
error "Project not found: $name" error "Project not found: $name"
return 1 return 1
fi fi
echo -e " ${CYAN}── ${BOLD}${name}${NC} ${CYAN}──${NC}" echo -e " ${CYAN}── ${BOLD}${name}${NC} ${CYAN}──${NC}"
configure_env "$project_dir" configure_env "$name"
if [[ ! -f "$SCRIPT_DIR/$name/.env" ]]; then
error "No .env file — run: automa config $name"
return 1
fi
info "Starting containers..." info "Starting containers..."
if docker compose -f "$project_dir/compose.yaml" --env-file "$project_dir/.env" up -d 2>&1; then if compose "$name" up -d 2>&1; then
info "${name} deployed" info "${name} is up"
return 0 return 0
else else
error "${name} deployment failed" error "${name} failed to start"
return 1 return 1
fi fi
} }
cmd_stop() { cmd_stop() {
local name="${1:?Usage: automa stop <project>}" local name="${1:?Usage: automa stop <project>}"
local project_dir="$SCRIPT_DIR/$name"
if [[ ! -f "$project_dir/compose.yaml" ]]; then if ! project_exists "$name"; then
error "Project not found: $name" error "Project not found: $name"
return 1 return 1
fi fi
info "Stopping ${name}..." info "Stopping ${name}..."
docker compose -f "$project_dir/compose.yaml" down compose "$name" down
info "${name} stopped" info "${name} stopped"
} }
cmd_logs() { cmd_logs() {
local name="${1:?Usage: automa logs <project>}" local name="${1:?Usage: automa logs <project>}"
shift shift
local project_dir="$SCRIPT_DIR/$name"
if [[ ! -f "$project_dir/compose.yaml" ]]; then if ! project_exists "$name"; then
error "Project not found: $name" error "Project not found: $name"
return 1 return 1
fi fi
docker compose -f "$project_dir/compose.yaml" logs -f "$@" compose "$name" logs -f "$@"
} }
cmd_status() { cmd_status() {
banner banner
local projects check_docker
read -ra projects <<< "$(discover_projects)" discover_projects
for p in "${projects[@]}"; do for p in "${PROJECTS[@]}"; do
local dir="$SCRIPT_DIR/$p"
echo -e " ${BOLD}${p}${NC}" echo -e " ${BOLD}${p}${NC}"
if docker compose -f "$dir/compose.yaml" ps --format "table {{.Name}}\t{{.Status}}" 2>/dev/null | tail -n +2 | grep -q .; then local output
docker compose -f "$dir/compose.yaml" ps --format "table {{.Name}}\t{{.Status}}" 2>/dev/null | tail -n +2 | while IFS= read -r line; do output=$(compose "$p" ps --format "table {{.Name}}\t{{.Status}}" 2>/dev/null | tail -n +2) || true
if [[ -n "$output" ]]; then
while IFS= read -r line; do
echo -e " ${line}" echo -e " ${line}"
done done <<< "$output"
else else
echo -e " ${DIM}not running${NC}" echo -e " ${DIM}not running${NC}"
fi fi
@ -304,43 +349,40 @@ cmd_status() {
cmd_restart() { cmd_restart() {
local name="${1:?Usage: automa restart <project>}" local name="${1:?Usage: automa restart <project>}"
local project_dir="$SCRIPT_DIR/$name"
if [[ ! -f "$project_dir/compose.yaml" ]]; then if ! project_exists "$name"; then
error "Project not found: $name" error "Project not found: $name"
return 1 return 1
fi fi
info "Restarting ${name}..." info "Restarting ${name}..."
docker compose -f "$project_dir/compose.yaml" restart compose "$name" restart
info "${name} restarted" info "${name} restarted"
} }
cmd_config() { cmd_config() {
local name="${1:?Usage: automa config <project>}" local name="${1:?Usage: automa config <project>}"
local project_dir="$SCRIPT_DIR/$name"
if [[ ! -f "$project_dir/compose.yaml" ]]; then if ! project_exists "$name"; then
error "Project not found: $name" error "Project not found: $name"
return 1 return 1
fi fi
configure_env "$project_dir" configure_env "$name"
} }
cmd_update() { cmd_update() {
local name="${1:?Usage: automa update <project>}" local name="${1:?Usage: automa update <project>}"
local project_dir="$SCRIPT_DIR/$name"
if [[ ! -f "$project_dir/compose.yaml" ]]; then if ! project_exists "$name"; then
error "Project not found: $name" error "Project not found: $name"
return 1 return 1
fi fi
info "Pulling latest images for ${name}..." info "Pulling latest images for ${name}..."
docker compose -f "$project_dir/compose.yaml" pull compose "$name" pull
info "Recreating containers..." info "Recreating containers..."
docker compose -f "$project_dir/compose.yaml" --env-file "$project_dir/.env" up -d compose "$name" up -d
info "${name} updated" info "${name} updated"
} }
@ -351,25 +393,25 @@ cmd_help() {
${BOLD}Commands:${NC} ${BOLD}Commands:${NC}
deploy [project...] Interactive deploy (or specify project names) deploy [project...] Interactive deploy (or specify project names)
list List all available projects list List all available projects and status
status Show running status of all projects status Show running containers per project
stop <project> Stop a project stop <project> Stop a project (docker compose down)
restart <project> Restart a project restart <project> Restart a project
logs <project> Follow logs of a project logs <project> Follow logs of a project
config <project> Configure .env for a project config <project> (Re)configure .env for a project
update <project> Pull latest images and recreate update <project> Pull latest images and recreate
help Show this help help Show this help
${BOLD}Examples:${NC} ${BOLD}Examples:${NC}
automa deploy # interactive project selection automa deploy # interactive project selection
automa deploy forgejo monitoring # deploy specific projects automa deploy forgejo filesuite # deploy specific projects
automa status # check all project status automa status # check all project status
automa logs forgejo # follow forgejo logs automa logs forgejo # follow forgejo logs
automa update monitoring # pull & restart monitoring automa update nextcloud # pull & restart nextcloud
${BOLD}Quick start:${NC} ${BOLD}Quick start:${NC}
curl -fsSL https://raw.githubusercontent.com/m1ngsama/automa/main/install.sh | bash curl -fsSL https://raw.githubusercontent.com/m1ngsama/automa/main/install.sh | bash
cd automa && ./automa deploy cd ~/automa && ./automa deploy
EOF EOF
} }

View file

@ -1,7 +1,17 @@
# Filesuite - Cloudreve cloud storage + qBittorrent
# Both services share the same downloads directory
TZ=Asia/Shanghai TZ=Asia/Shanghai
PUID=1000 PUID=1000
PGID=1000 PGID=1000
# Shared downloads path (absolute path recommended)
DOWNLOADS_DIR=./downloads DOWNLOADS_DIR=./downloads
# Cloudreve
CLOUDREVE_PORT=5212 CLOUDREVE_PORT=5212
CR_ENABLE_ARIA2=0
# qBittorrent
QB_WEBUI_PORT=8090 QB_WEBUI_PORT=8090
QB_BT_PORT=44773 QB_BT_PORT=44773

View file

@ -4,11 +4,18 @@ services:
container_name: cloudreve container_name: cloudreve
environment: environment:
TZ: "${TZ:-Asia/Shanghai}" TZ: "${TZ:-Asia/Shanghai}"
CR_ENABLE_ARIA2: "${CR_ENABLE_ARIA2:-0}"
volumes: volumes:
- ./cloudreve-data:/cloudreve/data - ./cloudreve-data:/cloudreve/data
- ${DOWNLOADS_DIR:-./downloads}:/data/downloads - ${DOWNLOADS_DIR:-./downloads}:/data/downloads
ports: ports:
- "${CLOUDREVE_PORT:-5212}:5212" - "${CLOUDREVE_PORT:-5212}:5212"
healthcheck:
test: ["CMD-SHELL", "curl -fSs http://localhost:5212/ || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
restart: unless-stopped restart: unless-stopped
qbittorrent: qbittorrent:
@ -26,4 +33,10 @@ services:
- "${QB_WEBUI_PORT:-8090}:${QB_WEBUI_PORT:-8090}" - "${QB_WEBUI_PORT:-8090}:${QB_WEBUI_PORT:-8090}"
- "${QB_BT_PORT:-44773}:${QB_BT_PORT:-44773}" - "${QB_BT_PORT:-44773}:${QB_BT_PORT:-44773}"
- "${QB_BT_PORT:-44773}:${QB_BT_PORT:-44773}/udp" - "${QB_BT_PORT:-44773}:${QB_BT_PORT:-44773}/udp"
healthcheck:
test: ["CMD-SHELL", "curl -fSs http://localhost:${QB_WEBUI_PORT:-8090}/ || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
restart: unless-stopped restart: unless-stopped

View file

@ -1,2 +1,8 @@
# Forgejo - self-hosted Git service
# Docs: https://forgejo.org/docs/latest/admin/config-cheat-sheet/
FORGEJO_HTTP_PORT=3000 FORGEJO_HTTP_PORT=3000
FORGEJO_SSH_PORT=2223 FORGEJO_SSH_PORT=2223
# Set this to your public URL when behind a reverse proxy
FORGEJO_ROOT_URL=http://localhost:3000

View file

@ -3,8 +3,11 @@ services:
image: codeberg.org/forgejo/forgejo:9 image: codeberg.org/forgejo/forgejo:9
container_name: forgejo container_name: forgejo
environment: environment:
USER_UID: 1000 USER_UID: "${FORGEJO_UID:-1000}"
USER_GID: 1000 USER_GID: "${FORGEJO_GID:-1000}"
FORGEJO__server__ROOT_URL: "${FORGEJO_ROOT_URL:-http://localhost:3000}"
FORGEJO__server__SSH_PORT: "${FORGEJO_SSH_PORT:-2223}"
FORGEJO__server__LFS_START_SERVER: "true"
ports: ports:
- "${FORGEJO_HTTP_PORT:-3000}:3000" - "${FORGEJO_HTTP_PORT:-3000}:3000"
- "${FORGEJO_SSH_PORT:-2223}:22" - "${FORGEJO_SSH_PORT:-2223}:22"
@ -12,4 +15,10 @@ services:
- ./data:/data - ./data:/data
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
healthcheck:
test: ["CMD", "curl", "-fSs", "http://localhost:3000/api/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
restart: unless-stopped restart: unless-stopped

View file

@ -1,3 +1,6 @@
# Minecraft server (itzg/minecraft-server)
# Docs: https://docker-minecraft-server.readthedocs.io/
TZ=Asia/Shanghai TZ=Asia/Shanghai
MC_TYPE=FABRIC MC_TYPE=FABRIC
MC_VERSION=1.21.1 MC_VERSION=1.21.1

View file

@ -17,8 +17,12 @@ services:
TZ: "${TZ:-Asia/Shanghai}" TZ: "${TZ:-Asia/Shanghai}"
volumes: volumes:
- ./data:/data - ./data:/data
- ./mods:/data/mods healthcheck:
- ./configs:/configs:ro test: mc-health
interval: 30s
timeout: 10s
retries: 5
start_period: 120s
restart: unless-stopped restart: unless-stopped
tty: true tty: true
stdin_open: true stdin_open: true

View file

@ -1,10 +1,17 @@
# Nextcloud with MariaDB + Redis
# Docs: https://hub.docker.com/_/nextcloud
TZ=Asia/Shanghai TZ=Asia/Shanghai
NC_PORT=8080 NC_PORT=8080
NC_ADMIN_USER=admin NC_ADMIN_USER=admin
NC_ADMIN_PASSWORD= NC_ADMIN_PASSWORD=
NC_TRUSTED_DOMAINS=localhost NC_TRUSTED_DOMAINS=localhost
# MariaDB
MYSQL_DATABASE=nextcloud MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud MYSQL_USER=nextcloud
MYSQL_PASSWORD= MYSQL_PASSWORD=
MYSQL_ROOT_PASSWORD= MYSQL_ROOT_PASSWORD=
# Redis
REDIS_PASSWORD= REDIS_PASSWORD=

View file

@ -3,8 +3,10 @@ services:
image: nextcloud:stable-apache image: nextcloud:stable-apache
container_name: nextcloud container_name: nextcloud
depends_on: depends_on:
- db db:
- redis condition: service_healthy
redis:
condition: service_healthy
ports: ports:
- "${NC_PORT:-8080}:80" - "${NC_PORT:-8080}:80"
environment: environment:
@ -23,6 +25,12 @@ services:
- nc-data:/var/www/html/data - nc-data:/var/www/html/data
- nc-config:/var/www/html/config - nc-config:/var/www/html/config
- nc-apps:/var/www/html/custom_apps - nc-apps:/var/www/html/custom_apps
healthcheck:
test: ["CMD", "curl", "-fSs", "http://localhost/status.php"]
interval: 30s
timeout: 5s
retries: 5
start_period: 60s
restart: unless-stopped restart: unless-stopped
db: db:
@ -37,6 +45,12 @@ services:
MYSQL_PASSWORD: "${MYSQL_PASSWORD}" MYSQL_PASSWORD: "${MYSQL_PASSWORD}"
volumes: volumes:
- nc-db:/var/lib/mysql - nc-db:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped restart: unless-stopped
redis: redis:
@ -45,6 +59,11 @@ services:
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"] command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
volumes: volumes:
- nc-redis:/data - nc-redis:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 3
restart: unless-stopped restart: unless-stopped
volumes: volumes:

View file

@ -1,9 +1,19 @@
# Tailscale + DERP relay server
#
# Deploy tailscale only: docker compose --profile tailscale up -d
# Deploy with DERP: docker compose --profile derp up -d
TZ=Asia/Shanghai TZ=Asia/Shanghai
TS_HOSTNAME= TS_HOSTNAME=
TS_AUTHKEY= TS_AUTHKEY=
# For headscale: --advertise-tags=tag:container --login-server=https://your.headscale.host
TS_EXTRA_ARGS=--advertise-tags=tag:container TS_EXTRA_ARGS=--advertise-tags=tag:container
TS_USERSPACE=false TS_USERSPACE=false
TS_FIREWALL_MODE=nftables TS_FIREWALL_MODE=nftables
# DERP relay (only needed with --profile derp)
DERP_HOST= DERP_HOST=
DERP_PORT=443 DERP_PORT=443
STUN_PORT=3478 STUN_PORT=3478

View file

@ -3,15 +3,13 @@ services:
image: tailscale/tailscale:latest image: tailscale/tailscale:latest
container_name: tailscale container_name: tailscale
hostname: "${TS_HOSTNAME}" hostname: "${TS_HOSTNAME}"
volumes: profiles: ["tailscale", "derp"]
- ./tailscale-data:/var/lib/tailscale
- /dev/net/tun:/dev/net/tun
- /var/run/tailscale:/var/run/tailscale
privileged: true
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
- SYS_MODULE - SYS_MODULE
- NET_RAW - NET_RAW
devices:
- /dev/net/tun:/dev/net/tun
network_mode: host network_mode: host
environment: environment:
TS_AUTHKEY: "${TS_AUTHKEY}" TS_AUTHKEY: "${TS_AUTHKEY}"
@ -22,13 +20,22 @@ services:
TS_DEBUG_FIREWALL_MODE: "${TS_FIREWALL_MODE:-nftables}" TS_DEBUG_FIREWALL_MODE: "${TS_FIREWALL_MODE:-nftables}"
TS_HOSTNAME: "${TS_HOSTNAME}" TS_HOSTNAME: "${TS_HOSTNAME}"
TZ: "${TZ:-Asia/Shanghai}" TZ: "${TZ:-Asia/Shanghai}"
volumes:
- ./tailscale-data:/var/lib/tailscale
- /var/run/tailscale:/var/run/tailscale
- /lib/modules:/lib/modules:ro
healthcheck: healthcheck:
test: ["CMD-SHELL", "tailscale status --json | grep -q '\"BackendState\": \"Running\"'"] test: ["CMD-SHELL", "tailscale status"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
restart: unless-stopped restart: unless-stopped
derp-server: derp-server:
image: ghcr.io/nbtca/tailscale-derp:edge image: ghcr.io/nbtca/tailscale-derp:edge
container_name: tailscale-derp container_name: tailscale-derp
profiles: ["derp"]
network_mode: host network_mode: host
depends_on: depends_on:
tailscale: tailscale:

View file

@ -1 +1,3 @@
# TeamSpeak voice server
TS3_ADMIN_PASSWORD= TS3_ADMIN_PASSWORD=

View file

@ -11,6 +11,12 @@ services:
TS3SERVER_SERVERADMIN_PASSWORD: "${TS3_ADMIN_PASSWORD}" TS3SERVER_SERVERADMIN_PASSWORD: "${TS3_ADMIN_PASSWORD}"
volumes: volumes:
- ts-data:/var/ts3server/ - ts-data:/var/ts3server/
healthcheck:
test: ["CMD-SHELL", "echo quit | nc localhost 10011 | grep -q TS3"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
restart: unless-stopped restart: unless-stopped
volumes: volumes:

View file

@ -1 +1,4 @@
# Uptime Kuma - uptime monitoring
# Default binds to localhost only; change to 0.0.0.0:3001 for external access
KUMA_PORT=127.0.0.1:3001 KUMA_PORT=127.0.0.1:3001

View file

@ -6,4 +6,10 @@ services:
- ./data:/app/data - ./data:/app/data
ports: ports:
- "${KUMA_PORT:-127.0.0.1:3001}:3001" - "${KUMA_PORT:-127.0.0.1:3001}:3001"
healthcheck:
test: ["CMD-SHELL", "extra/healthcheck"]
interval: 60s
timeout: 30s
retries: 5
start_period: 180s
restart: unless-stopped restart: unless-stopped