Compare commits
12 Commits
780007c1d3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
202a48f322 | ||
|
|
215fef2be3 | ||
|
|
7a1d031bc8 | ||
|
|
83843a6647 | ||
|
|
71a1386f13 | ||
|
|
4b1c2d1509 | ||
|
|
08fd5e82c4 | ||
|
|
e8fb7eef38 | ||
|
|
0d65ed73f7 | ||
|
|
012aa13f3e | ||
|
|
90258d8b98 | ||
|
|
99e65eab50 |
79
README.md
Normal file
79
README.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# server-infra
|
||||||
|
|
||||||
|
Shared Docker services for the Hetzner server: nginx proxy manager, umami analytics, Gitea + Actions runner.
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
| Service | Port (host) | Description |
|
||||||
|
|---------|-------------|-------------|
|
||||||
|
| nginx proxy manager | 80, 443, 81 (admin) | Reverse proxy + SSL termination |
|
||||||
|
| umami | internal | Web analytics |
|
||||||
|
| gitea | 127.0.0.1:3100 → nginx, 2222 (SSH) | Self-hosted git at `git.padelnomics.io` |
|
||||||
|
| act_runner | — | Gitea Actions CI runner |
|
||||||
|
|
||||||
|
## Directory layout
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/server-infra/ # this repo (owned by infra_service)
|
||||||
|
├── gitea/docker-compose.yml
|
||||||
|
├── reverse-proxy/docker-compose.yml # recovered from running container
|
||||||
|
├── umami/docker-compose.yml # recovered from running container
|
||||||
|
├── setup.sh # phase 1: user + dirs + uv
|
||||||
|
└── bootstrap.sh # phase 2: recover compose files, start Gitea
|
||||||
|
|
||||||
|
/data/server-infra/
|
||||||
|
├── gitea/ # Gitea data volume
|
||||||
|
└── act_runner/ # Runner data volume
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup (new server)
|
||||||
|
|
||||||
|
### Phase 1 — user, dirs, uv
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@<server-ip> 'bash -s' < setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates `infra_service` system user (docker group), `/opt/server-infra/`, `/data/server-infra/`, installs uv.
|
||||||
|
|
||||||
|
### Phase 2 — sync repo + recover
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsync -av --chown=infra_service:infra_service \
|
||||||
|
~/Projects/server-infra/ root@<server-ip>:/opt/server-infra/
|
||||||
|
|
||||||
|
ssh root@<server-ip> 'bash -s' < bootstrap.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Recovers `umami/docker-compose.yml` and `reverse-proxy/docker-compose.yml` from running containers, creates data dirs, sets ownership.
|
||||||
|
|
||||||
|
### Phase 3 — start Gitea
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh hetzner_root 'sudo -u infra_service docker compose \
|
||||||
|
-f /opt/server-infra/gitea/docker-compose.yml up -d'
|
||||||
|
```
|
||||||
|
|
||||||
|
Web installer at `http://<server-ip>:3100`. Set ROOT_URL to `https://git.padelnomics.io`.
|
||||||
|
After setup, add a proxy host in nginx proxy manager → `127.0.0.1:3100`.
|
||||||
|
|
||||||
|
### Phase 4 — start act_runner
|
||||||
|
|
||||||
|
1. Generate a runner token in Gitea: Site Administration → Actions → Runners → Create runner token
|
||||||
|
2. Create `/opt/server-infra/gitea/.env`:
|
||||||
|
```
|
||||||
|
GITEA_RUNNER_TOKEN=<token>
|
||||||
|
```
|
||||||
|
3. Restart with the env file:
|
||||||
|
```bash
|
||||||
|
ssh hetzner_root 'sudo -u infra_service docker compose \
|
||||||
|
-f /opt/server-infra/gitea/docker-compose.yml up -d'
|
||||||
|
```
|
||||||
|
|
||||||
|
## DNS
|
||||||
|
|
||||||
|
`git.padelnomics.io` must be DNS-only (grey cloud) in Cloudflare — **not** proxied — so that SSH on port 2222 reaches the server directly.
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
The `infra_service` user owns all compose files. Secrets (runner token) go in `/opt/server-infra/gitea/.env` — never committed.
|
||||||
223
bootstrap.sh
Normal file
223
bootstrap.sh
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Phase 2: Recover compose files, deploy services, start Gitea.
|
||||||
|
# Pipeable — run after setup.sh and after rsync'ing the repo.
|
||||||
|
#
|
||||||
|
# Usage (from workstation):
|
||||||
|
# ssh root@<server-ip> 'bash -s' < bootstrap.sh
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - setup.sh already run (infra_service user, dirs, uv)
|
||||||
|
# - Repo rsync'd to /opt/server-infra/
|
||||||
|
#
|
||||||
|
# What it does:
|
||||||
|
# 1. Validates prerequisites
|
||||||
|
# 2. Recovers compose files from running containers (umami, reverse-proxy)
|
||||||
|
# 3. Creates data directories
|
||||||
|
# 4. Sets correct ownership
|
||||||
|
# 5. Prints next steps (start Gitea, add proxy host, push repo to Gitea)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SERVICE_USER="infra_service"
|
||||||
|
REPO_DIR="/opt/server-infra"
|
||||||
|
DATA_DIR="/data/server-infra"
|
||||||
|
UV="/home/${SERVICE_USER}/.local/bin/uv"
|
||||||
|
|
||||||
|
[ "$(id -u)" = "0" ] || { echo "ERROR: Run as root"; exit 1; }
|
||||||
|
|
||||||
|
log() { echo "$(date '+%H:%M:%S') ==> $*"; }
|
||||||
|
warn() { echo "$(date '+%H:%M:%S') ==> WARNING: $*" >&2; }
|
||||||
|
|
||||||
|
# ── Preflight checks ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! id "${SERVICE_USER}" >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: ${SERVICE_USER} user not found. Run setup.sh first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "${REPO_DIR}/.git" ]; then
|
||||||
|
echo "ERROR: Repo not found at ${REPO_DIR}. Rsync the repo first:"
|
||||||
|
echo " rsync -av --chown=root:root ~/Projects/server-infra/ root@<server-ip>:${REPO_DIR}/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 || { echo "ERROR: docker not found"; exit 1; }
|
||||||
|
|
||||||
|
[ -f "${UV}" ] || { echo "ERROR: uv not found at ${UV}. Run setup.sh first."; exit 1; }
|
||||||
|
|
||||||
|
# ── Inline autocompose script (replaces abandoned docker-autocompose package) ──
|
||||||
|
# Uses modern docker SDK. Written to /tmp, run via uv (fetches deps on demand).
|
||||||
|
|
||||||
|
cat > /tmp/server-infra-autocompose.py << 'PYEOF'
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.10"
|
||||||
|
# dependencies = ["docker>=7.0", "pyyaml>=6.0"]
|
||||||
|
# ///
|
||||||
|
"""Generate a docker-compose services block from running containers."""
|
||||||
|
import sys
|
||||||
|
import docker
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def container_to_service(container):
|
||||||
|
attrs = container.attrs
|
||||||
|
config = attrs["Config"]
|
||||||
|
host_config = attrs["HostConfig"]
|
||||||
|
|
||||||
|
service = {"image": config["Image"]}
|
||||||
|
|
||||||
|
restart = (host_config.get("RestartPolicy") or {}).get("Name", "no")
|
||||||
|
if restart and restart not in ("no", ""):
|
||||||
|
service["restart"] = restart
|
||||||
|
|
||||||
|
env = config.get("Env") or []
|
||||||
|
if env:
|
||||||
|
service["environment"] = env
|
||||||
|
|
||||||
|
port_bindings = host_config.get("PortBindings") or {}
|
||||||
|
if port_bindings:
|
||||||
|
ports = []
|
||||||
|
for container_port, bindings in port_bindings.items():
|
||||||
|
for binding in (bindings or []):
|
||||||
|
host_ip = binding.get("HostIp", "")
|
||||||
|
host_port = binding.get("HostPort", "")
|
||||||
|
if host_ip and host_ip not in ("0.0.0.0", "::"):
|
||||||
|
ports.append(f"{host_ip}:{host_port}:{container_port}")
|
||||||
|
elif host_port:
|
||||||
|
ports.append(f"{host_port}:{container_port}")
|
||||||
|
else:
|
||||||
|
ports.append(container_port)
|
||||||
|
if ports:
|
||||||
|
service["ports"] = ports
|
||||||
|
|
||||||
|
volumes = []
|
||||||
|
for mount in attrs.get("Mounts") or []:
|
||||||
|
dst = mount["Destination"]
|
||||||
|
if mount["Type"] == "bind":
|
||||||
|
src = mount["Source"]
|
||||||
|
mode = mount.get("Mode", "rw")
|
||||||
|
volumes.append(f"{src}:{dst}" if mode == "rw" else f"{src}:{dst}:{mode}")
|
||||||
|
elif mount["Type"] == "volume":
|
||||||
|
name = mount.get("Name", "")
|
||||||
|
if name:
|
||||||
|
volumes.append(f"{name}:{dst}")
|
||||||
|
if volumes:
|
||||||
|
service["volumes"] = volumes
|
||||||
|
|
||||||
|
return service
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: autocompose.py <container_name> [...]", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
client = docker.from_env()
|
||||||
|
services = {}
|
||||||
|
|
||||||
|
for name in sys.argv[1:]:
|
||||||
|
try:
|
||||||
|
container = client.containers.get(name)
|
||||||
|
labels = (container.attrs["Config"].get("Labels") or {})
|
||||||
|
service_name = labels.get("com.docker.compose.service", name)
|
||||||
|
services[service_name] = container_to_service(container)
|
||||||
|
except docker.errors.NotFound:
|
||||||
|
print(f"WARNING: Container {name} not found", file=sys.stderr)
|
||||||
|
|
||||||
|
print(yaml.dump({"services": services}, default_flow_style=False, sort_keys=False))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# ── Recover compose files from running containers ──────────────────────────────
|
||||||
|
|
||||||
|
recover_project() {
|
||||||
|
local outfile="$1"
|
||||||
|
shift
|
||||||
|
local containers=("$@")
|
||||||
|
|
||||||
|
local running=0
|
||||||
|
for c in "${containers[@]}"; do
|
||||||
|
if docker inspect "${c}" >/dev/null 2>&1; then
|
||||||
|
running=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${running}" = "0" ]; then
|
||||||
|
warn "None of [${containers[*]}] are running — skipping recovery."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Recovering: ${containers[*]}"
|
||||||
|
(cd /tmp && sudo -u "${SERVICE_USER}" \
|
||||||
|
"${UV}" run /tmp/server-infra-autocompose.py "${containers[@]}") > "${outfile}"
|
||||||
|
log " Saved to ${outfile}"
|
||||||
|
}
|
||||||
|
|
||||||
|
log "Recovering compose files from running containers..."
|
||||||
|
|
||||||
|
recover_project \
|
||||||
|
"${REPO_DIR}/umami/docker-compose.yml" \
|
||||||
|
umami-umami-1 umami-db-1
|
||||||
|
|
||||||
|
recover_project \
|
||||||
|
"${REPO_DIR}/reverse-proxy/docker-compose.yml" \
|
||||||
|
reverse_proxy-app-1
|
||||||
|
|
||||||
|
# ── Data directories ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log "Creating data directories..."
|
||||||
|
mkdir -p "${DATA_DIR}/gitea" "${DATA_DIR}/act_runner"
|
||||||
|
|
||||||
|
# ── Ownership ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log "Setting ownership..."
|
||||||
|
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${REPO_DIR}"
|
||||||
|
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${DATA_DIR}"
|
||||||
|
|
||||||
|
# ── Summary ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
SERVER_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=================================================================="
|
||||||
|
echo ""
|
||||||
|
echo " Bootstrap complete."
|
||||||
|
echo ""
|
||||||
|
echo " Services ready in ${REPO_DIR}/:"
|
||||||
|
[ -f "${REPO_DIR}/umami/docker-compose.yml" ] && echo " umami (recovered)"
|
||||||
|
[ -f "${REPO_DIR}/reverse-proxy/docker-compose.yml" ] && echo " reverse-proxy (recovered)"
|
||||||
|
[ -f "${REPO_DIR}/gitea/docker-compose.yml" ] && echo " gitea (from repo)"
|
||||||
|
echo ""
|
||||||
|
echo " Service user: ${SERVICE_USER}"
|
||||||
|
echo " Data dir: ${DATA_DIR}/"
|
||||||
|
echo ""
|
||||||
|
echo "=================================================================="
|
||||||
|
echo ""
|
||||||
|
echo " Next steps (run from workstation):"
|
||||||
|
echo ""
|
||||||
|
echo " 1. Pull recovered compose files back to workstation:"
|
||||||
|
echo " rsync -av hetzner_root:${REPO_DIR}/umami/docker-compose.yml ~/Projects/server-infra/umami/"
|
||||||
|
echo " rsync -av hetzner_root:${REPO_DIR}/reverse-proxy/docker-compose.yml ~/Projects/server-infra/reverse-proxy/"
|
||||||
|
echo ""
|
||||||
|
echo " 2. Commit recovered files:"
|
||||||
|
echo " cd ~/Projects/server-infra"
|
||||||
|
echo " git add umami/docker-compose.yml reverse-proxy/docker-compose.yml"
|
||||||
|
echo " git commit -m 'chore: recover compose files from running containers'"
|
||||||
|
echo ""
|
||||||
|
echo " 3. Start Gitea:"
|
||||||
|
echo " ssh hetzner_root 'sudo -u ${SERVICE_USER} docker compose -f ${REPO_DIR}/gitea/docker-compose.yml up -d'"
|
||||||
|
echo " # Web installer at http://${SERVER_IP}:3000"
|
||||||
|
echo " # Set ROOT_URL to your public domain (e.g. https://git.yourdomain.com)"
|
||||||
|
echo ""
|
||||||
|
echo " 4. Add proxy host in nginx proxy manager → ${SERVER_IP}:3000"
|
||||||
|
echo ""
|
||||||
|
echo " 5. Push repo to Gitea:"
|
||||||
|
echo " ssh hetzner_root 'sudo -u ${SERVICE_USER} git -C ${REPO_DIR} remote add origin https://git.yourdomain.com/youruser/server-infra.git'"
|
||||||
|
echo " ssh hetzner_root 'sudo -u ${SERVICE_USER} git -C ${REPO_DIR} push -u origin master'"
|
||||||
|
echo ""
|
||||||
|
echo "=================================================================="
|
||||||
|
echo ""
|
||||||
@@ -4,11 +4,26 @@ services:
|
|||||||
container_name: gitea
|
container_name: gitea
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "127.0.0.1:3100:3000"
|
||||||
|
- "2222:22"
|
||||||
volumes:
|
volumes:
|
||||||
- /data/server-infra/gitea:/data
|
- /data/server-infra/gitea:/data
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
environment:
|
environment:
|
||||||
- GITEA__database__DB_TYPE=sqlite3
|
- GITEA__database__DB_TYPE=sqlite3
|
||||||
- GITEA__server__SSH_DISABLED=true
|
- GITEA__service__DISABLE_REGISTRATION=true
|
||||||
|
|
||||||
|
act_runner:
|
||||||
|
image: gitea/act_runner:latest
|
||||||
|
container_name: act_runner
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- gitea
|
||||||
|
volumes:
|
||||||
|
- /data/server-infra/act_runner:/data
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
- GITEA_INSTANCE_URL=https://git.padelnomics.io
|
||||||
|
- GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_TOKEN}
|
||||||
|
- GITEA_RUNNER_NAME=hetzner
|
||||||
|
|||||||
26
reverse-proxy/docker-compose.yml
Normal file
26
reverse-proxy/docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: jc21/nginx-proxy-manager:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Berlin
|
||||||
|
- PATH=/opt/certbot/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
- SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||||
|
- OPENRESTY_VERSION=1.27.1.2
|
||||||
|
- CROWDSEC_OPENRESTY_BOUNCER_VERSION=0.1.7
|
||||||
|
- CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||||
|
- SUPPRESS_NO_CONFIG_WARNING=1
|
||||||
|
- S6_BEHAVIOUR_IF_STAGE2_FAILS=1
|
||||||
|
- S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0
|
||||||
|
- S6_FIX_ATTRS_HIDDEN=1
|
||||||
|
- S6_KILL_FINISH_MAXTIME=10000
|
||||||
|
- S6_VERBOSITY=1
|
||||||
|
- NODE_ENV=production
|
||||||
|
- NPM_BUILD_VERSION=2.13.6
|
||||||
|
- NPM_BUILD_COMMIT=f3efaae
|
||||||
|
- NPM_BUILD_DATE=2026-01-14 04:42:42 UTC
|
||||||
|
- NODE_OPTIONS=--openssl-legacy-provider
|
||||||
|
volumes:
|
||||||
|
- /home/beanflows_service_user/base_services/reverse_proxy/letsencrypt:/etc/letsencrypt
|
||||||
|
- /home/beanflows_service_user/base_services/reverse_proxy/data:/data
|
||||||
|
|
||||||
166
setup.sh
166
setup.sh
@@ -1,104 +1,28 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Server shared-infra setup: recover compose files, create service user, deploy.
|
# Phase 1: Create infra_service user, directories, and install uv.
|
||||||
# Run as root on the server.
|
# Pipeable — no repo needed on the server.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage (from workstation):
|
||||||
# sudo bash setup.sh
|
# ssh root@<server-ip> 'bash -s' < setup.sh
|
||||||
#
|
#
|
||||||
# What it does:
|
# What it does:
|
||||||
# 1. Recovers docker-compose.yml files from already-running containers (one-time)
|
# 1. Creates infra_service system user (nologin, docker group)
|
||||||
# 2. Creates infra_service system user (docker group, nologin)
|
# 2. Creates /opt/server-infra/ and /data/server-infra/
|
||||||
# 3. Creates /data/server-infra/{service}/ data directories
|
# 3. Installs uv as infra_service
|
||||||
# 4. Deploys all compose files from this repo to /opt/server-infra/ (idempotent)
|
|
||||||
#
|
#
|
||||||
# Services managed by this repo:
|
# After this script: rsync the repo, then run bootstrap.sh.
|
||||||
# umami — analytics (recovered from running containers)
|
|
||||||
# reverse-proxy — nginx proxy manager (recovered from running containers)
|
|
||||||
# gitea — self-hosted git (new, compose file already in repo)
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
SERVICE_USER="infra_service"
|
SERVICE_USER="infra_service"
|
||||||
DEPLOY_DIR="/opt/server-infra"
|
REPO_DIR="/opt/server-infra"
|
||||||
|
DATA_DIR="/data/server-infra"
|
||||||
|
|
||||||
[ "$(id -u)" = "0" ] || { echo "ERROR: Run as root: sudo bash setup.sh"; exit 1; }
|
[ "$(id -u)" = "0" ] || { echo "ERROR: Run as root"; exit 1; }
|
||||||
|
|
||||||
log() { echo "$(date '+%H:%M:%S') ==> $*"; }
|
log() { echo "$(date '+%H:%M:%S') ==> $*"; }
|
||||||
warn() { echo "$(date '+%H:%M:%S') ==> WARNING: $*" >&2; }
|
|
||||||
|
|
||||||
# ── Preflight checks ───────────────────────────────────────────────────────────
|
# ── Service user ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
command -v docker >/dev/null 2>&1 || { echo "ERROR: docker not found"; exit 1; }
|
|
||||||
|
|
||||||
# Find uvx: prefer the installing user's local bin, fall back to PATH
|
|
||||||
UVX=""
|
|
||||||
for candidate in /root/.local/bin/uvx /home/Deeman/.local/bin/uvx /usr/local/bin/uvx "$(command -v uvx 2>/dev/null || true)"; do
|
|
||||||
if [ -n "${candidate}" ] && [ -x "${candidate}" ]; then
|
|
||||||
UVX="${candidate}"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
[ -n "${UVX}" ] || { echo "ERROR: uvx not found — install uv first: curl -LsSf https://astral.sh/uv/install.sh | sh"; exit 1; }
|
|
||||||
|
|
||||||
log "Using uvx at ${UVX}"
|
|
||||||
|
|
||||||
# ── Helper: recover one compose project ───────────────────────────────────────
|
|
||||||
# Usage: recover_project <outfile> <container_name> [<container_name> ...]
|
|
||||||
recover_project() {
|
|
||||||
local outfile="$1"
|
|
||||||
shift
|
|
||||||
local containers=("$@")
|
|
||||||
|
|
||||||
# Check at least one container from the list is running
|
|
||||||
local running=0
|
|
||||||
for c in "${containers[@]}"; do
|
|
||||||
if docker inspect "${c}" >/dev/null 2>&1; then
|
|
||||||
running=1
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "${running}" = "0" ]; then
|
|
||||||
warn "None of [${containers[*]}] are running — skipping recovery."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Recovering compose file for: ${containers[*]}"
|
|
||||||
"${UVX}" docker-autocompose "${containers[@]}" > "${outfile}"
|
|
||||||
log "Saved to ${outfile}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Recover compose files ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
log "Recovering compose files from running containers..."
|
|
||||||
mkdir -p /tmp/server-infra-recovery
|
|
||||||
|
|
||||||
recover_project \
|
|
||||||
/tmp/server-infra-recovery/umami.yml \
|
|
||||||
umami-umami-1 umami-db-1
|
|
||||||
|
|
||||||
recover_project \
|
|
||||||
/tmp/server-infra-recovery/reverse-proxy.yml \
|
|
||||||
reverse_proxy-app-1
|
|
||||||
|
|
||||||
# ── Copy recovered files into this repo ───────────────────────────────────────
|
|
||||||
|
|
||||||
log "Copying recovered files into repo (${SCRIPT_DIR})..."
|
|
||||||
|
|
||||||
for pair in \
|
|
||||||
"/tmp/server-infra-recovery/umami.yml:${SCRIPT_DIR}/umami/docker-compose.yml" \
|
|
||||||
"/tmp/server-infra-recovery/reverse-proxy.yml:${SCRIPT_DIR}/reverse-proxy/docker-compose.yml"; do
|
|
||||||
src="${pair%%:*}"
|
|
||||||
dst="${pair##*:}"
|
|
||||||
if [ -f "${src}" ]; then
|
|
||||||
mkdir -p "$(dirname "${dst}")"
|
|
||||||
cp "${src}" "${dst}"
|
|
||||||
log " ${dst}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# ── Service account ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
log "Creating service user ${SERVICE_USER}..."
|
log "Creating service user ${SERVICE_USER}..."
|
||||||
if ! id "${SERVICE_USER}" >/dev/null 2>&1; then
|
if ! id "${SERVICE_USER}" >/dev/null 2>&1; then
|
||||||
@@ -109,69 +33,45 @@ else
|
|||||||
fi
|
fi
|
||||||
usermod -aG docker "${SERVICE_USER}"
|
usermod -aG docker "${SERVICE_USER}"
|
||||||
|
|
||||||
# ── Data directories ───────────────────────────────────────────────────────────
|
# ── Directories ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
log "Creating data directories..."
|
log "Creating directories..."
|
||||||
mkdir -p /data/server-infra/gitea
|
mkdir -p "${REPO_DIR}" "${DATA_DIR}"
|
||||||
chown -R "${SERVICE_USER}:${SERVICE_USER}" /data/server-infra
|
chown "${SERVICE_USER}:${SERVICE_USER}" "${REPO_DIR}"
|
||||||
|
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${DATA_DIR}"
|
||||||
|
|
||||||
# ── Deploy: copy all compose files from repo to deploy dir (idempotent) ────────
|
# ── uv ────────────────────────────────────────────────────────────────────────
|
||||||
#
|
|
||||||
# Recovered files (umami, reverse-proxy) are already in ${SCRIPT_DIR} after
|
|
||||||
# the recovery section above. Gitea compose is committed directly in the repo.
|
|
||||||
# This section deploys all of them to DEPLOY_DIR.
|
|
||||||
|
|
||||||
log "Deploying compose files to ${DEPLOY_DIR}..."
|
if [ ! -f "/home/${SERVICE_USER}/.local/bin/uv" ]; then
|
||||||
|
log "Installing uv..."
|
||||||
for service in umami reverse-proxy gitea; do
|
sudo -u "${SERVICE_USER}" bash -c 'curl -LsSf https://astral.sh/uv/install.sh | sh'
|
||||||
src="${SCRIPT_DIR}/${service}/docker-compose.yml"
|
log " uv installed."
|
||||||
dst="${DEPLOY_DIR}/${service}/docker-compose.yml"
|
|
||||||
if [ -f "${src}" ]; then
|
|
||||||
mkdir -p "${DEPLOY_DIR}/${service}"
|
|
||||||
cp "${src}" "${dst}"
|
|
||||||
log " ${service} → ${dst}"
|
|
||||||
else
|
else
|
||||||
warn " ${service}/docker-compose.yml not in repo — skipping deploy"
|
log " uv already installed — skipping."
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
|
|
||||||
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${DEPLOY_DIR}"
|
|
||||||
|
|
||||||
# ── Summary ────────────────────────────────────────────────────────────────────
|
# ── Summary ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
SERVER_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=================================================================="
|
echo "=================================================================="
|
||||||
echo ""
|
echo ""
|
||||||
echo " Setup complete."
|
echo " Phase 1 complete."
|
||||||
echo ""
|
|
||||||
echo " Deployed services:"
|
|
||||||
[ -f "${DEPLOY_DIR}/umami/docker-compose.yml" ] && echo " umami → ${DEPLOY_DIR}/umami/docker-compose.yml"
|
|
||||||
[ -f "${DEPLOY_DIR}/reverse-proxy/docker-compose.yml" ] && echo " reverse-proxy → ${DEPLOY_DIR}/reverse-proxy/docker-compose.yml"
|
|
||||||
[ -f "${DEPLOY_DIR}/gitea/docker-compose.yml" ] && echo " gitea → ${DEPLOY_DIR}/gitea/docker-compose.yml"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " Service user: ${SERVICE_USER} (docker group)"
|
echo " Service user: ${SERVICE_USER} (docker group)"
|
||||||
echo " Deploy dir: ${DEPLOY_DIR}/"
|
echo " Repo dir: ${REPO_DIR}/"
|
||||||
echo " Data dir: /data/server-infra/"
|
echo " Data dir: ${DATA_DIR}/"
|
||||||
echo ""
|
echo ""
|
||||||
echo "=================================================================="
|
echo "=================================================================="
|
||||||
echo ""
|
echo ""
|
||||||
echo " Next steps:"
|
echo " Next step — rsync repo from workstation, then run bootstrap:"
|
||||||
echo ""
|
echo ""
|
||||||
echo " 1. Commit recovered compose files to git:"
|
echo " 1. On your workstation:"
|
||||||
echo " cd ${SCRIPT_DIR}"
|
echo " rsync -av --chown=root:root ~/Projects/server-infra/ root@${SERVER_IP}:${REPO_DIR}/"
|
||||||
echo " git add umami/docker-compose.yml reverse-proxy/docker-compose.yml"
|
|
||||||
echo " git commit -m 'chore: recover compose files from running containers'"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " 2. Start Gitea:"
|
echo " 2. Then run bootstrap:"
|
||||||
echo " sudo -u ${SERVICE_USER} docker compose -f ${DEPLOY_DIR}/gitea/docker-compose.yml up -d"
|
echo " ssh root@${SERVER_IP} 'bash -s' < bootstrap.sh"
|
||||||
echo " # Then open http://<server-ip>:3000 to complete the web installer"
|
|
||||||
echo " # Set ROOT_URL to your public domain (e.g. https://git.yourdomain.com)"
|
|
||||||
echo ""
|
|
||||||
echo " 3. Add Gitea proxy host in nginx proxy manager:"
|
|
||||||
echo " Forward to <server-ip>:3000"
|
|
||||||
echo ""
|
|
||||||
echo " 4. To restart any service if containers go down:"
|
|
||||||
echo " sudo -u ${SERVICE_USER} docker compose -f ${DEPLOY_DIR}/<service>/docker-compose.yml up -d"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=================================================================="
|
echo "=================================================================="
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
38
umami/docker-compose.yml
Normal file
38
umami/docker-compose.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
services:
|
||||||
|
umami:
|
||||||
|
image: ghcr.io/umami-software/umami:latest
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=postgresql://umami:umami@db:5432/umami
|
||||||
|
- APP_SECRET=V896IlIKJUiwZrUMq6AbKETbBVIZqMFT8dmWBQdC0e
|
||||||
|
- TRACKER_SCRIPT_NAME=Z.js
|
||||||
|
- COLLECT_API_ENDPOINT=/api/Z
|
||||||
|
- TOKEN_EXPIRATION_TIME=365d
|
||||||
|
- PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
- NODE_VERSION=22.21.1
|
||||||
|
- YARN_VERSION=1.22.22
|
||||||
|
- NODE_ENV=production
|
||||||
|
- NEXT_TELEMETRY_DISABLED=1
|
||||||
|
- NODE_OPTIONS=
|
||||||
|
- HOSTNAME=0.0.0.0
|
||||||
|
- PORT=3000
|
||||||
|
ports:
|
||||||
|
- 8300:3000/tcp
|
||||||
|
db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=umami
|
||||||
|
- POSTGRES_DB=umami
|
||||||
|
- POSTGRES_USER=umami
|
||||||
|
- PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
- GOSU_VERSION=1.19
|
||||||
|
- LANG=en_US.utf8
|
||||||
|
- PG_MAJOR=15
|
||||||
|
- PG_VERSION=15.16
|
||||||
|
- PG_SHA256=695ee29a77be1f5010e10f3667696f29871587f7aa311eadc1f809bea287cf48
|
||||||
|
- "DOCKER_PG_LLVM_DEPS=llvm19-dev \t\tclang19"
|
||||||
|
- PGDATA=/var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
- umami_umami-db-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
Reference in New Issue
Block a user