Skip to content

Commit 3892fc4

Browse files
baijumclaude
andcommitted
feat: implement Phase 6 self-hosting ecosystem
Parameterize infrastructure scripts to remove all hardcoded values (anulectra.com, towlion/platform). Bootstrap script now accepts ACME_EMAIL, OPS_DOMAIN, and ALERT_REPO as env vars. Fix documentation to list correct 4 required secrets (not 7). Add critical missing step to tutorial (pre-clone app on server). Update roadmap status. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6c9c160 commit 3892fc4

6 files changed

Lines changed: 123 additions & 58 deletions

File tree

docs/roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Enable self-hosting through repository forks.
9393

9494
**Done when:** A person who has never seen the project can fork an app repo, configure 4-5 secrets, run the bootstrap script on a fresh server, push to `main`, and have a working deployment. Tested by someone other than the author.
9595

96-
**Status:** Model is conceptually sound but impractical without bootstrap automation.
96+
**Status:** Infrastructure parameterized (ACME_EMAIL, OPS_DOMAIN, ALERT_REPO replace all hardcoded values). Documentation corrected (self-hosting.md, tutorial.md list correct 4 secrets, tutorial uses bootstrap script). App-template README updated with deployment secrets and self-hosting links. Awaiting external validation (tested by someone other than the author).
9797

9898
## Phase 7 — Application Development
9999

docs/self-hosting.md

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,17 @@ Fork users must configure these GitHub Actions secrets:
5656
| Secret | Purpose |
5757
|---|---|
5858
| `SERVER_HOST` | Server IP address |
59-
| `SERVER_USER` | SSH user |
59+
| `SERVER_USER` | SSH user (typically `deploy`) |
6060
| `SERVER_SSH_KEY` | SSH private key for deployment |
61-
| `APP_DOMAIN` | Application domain name |
62-
| `DATABASE_PASSWORD` | PostgreSQL password |
63-
| `MINIO_ROOT_USER` | MinIO admin username |
64-
| `MINIO_ROOT_PASSWORD` | MinIO admin password |
65-
| `EMAIL_API_KEY` | Transactional email API key |
61+
| `APP_DOMAIN` | Application domain name (e.g., `app.example.com`) |
62+
63+
> **Note:** Database and storage credentials are auto-generated by the bootstrap script on the server. They are not GitHub secrets.
6664
6765
Optional secrets:
6866

6967
| Secret | Purpose |
7068
|---|---|
71-
| `SENTRY_DSN` | Error tracking |
72-
| `REDIS_PASSWORD` | Redis authentication |
69+
| `PREVIEW_DOMAIN` | Base domain for preview environments (e.g., `example.com`) |
7370

7471
## Bootstrap Process
7572

@@ -87,21 +84,46 @@ To deploy an application from a fork:
8784
The repository includes a single bootstrap script that transforms a fresh Debian 12 server into a ready-to-deploy platform:
8885

8986
```bash
90-
sudo bash infrastructure/bootstrap-server.sh
87+
sudo ACME_EMAIL=you@example.com OPS_DOMAIN=ops.example.com bash infrastructure/bootstrap-server.sh
9188
```
9289

9390
The script is idempotent — safe to re-run on an already-bootstrapped server. It performs:
9491

95-
- System packages (git, curl, ufw)
92+
- System packages (git, curl, ufw, vnstat, unattended-upgrades)
9693
- Firewall configuration (ports 22, 80, 443)
9794
- Docker and Compose plugin installation
9895
- `deploy` user creation with SSH directory
99-
- Directory structure (`/data/postgres`, `/data/redis`, `/data/minio`, `/data/caddy`, `/opt/apps`, `/opt/platform`)
96+
- Directory structure (`/data/*`, `/opt/apps`, `/opt/platform`)
10097
- Docker network (`towlion`) for cross-container communication
101-
- Credential generation (PostgreSQL and MinIO passwords in `/opt/platform/.env`)
98+
- Credential generation (PostgreSQL, MinIO, and Grafana passwords in `/opt/platform/.env`)
10299
- Platform Caddyfile with per-app import pattern
103-
- Platform `docker-compose.yml` (PostgreSQL 16, Redis 7, MinIO, Caddy 2)
100+
- Platform `docker-compose.yml` (PostgreSQL 16, Redis 7, MinIO, Caddy 2, Loki 3.0, Promtail 3.0, Grafana 11.0)
104101
- Service startup and verification
102+
- Infrastructure scripts and cron jobs (backups, alerts, image updates)
103+
104+
### Post-Bootstrap Verification
105+
106+
After bootstrapping, run the verification script to confirm everything is working:
107+
108+
```bash
109+
bash infrastructure/verify-server.sh
110+
```
111+
112+
This performs a read-only check of all services, directories, and firewall rules.
113+
114+
### Per-App Credentials
115+
116+
For each application, provision isolated database and storage credentials:
117+
118+
```bash
119+
bash /opt/platform/infrastructure/create-app-credentials.sh <app-name>
120+
```
121+
122+
This creates a dedicated PostgreSQL user and MinIO bucket. Credentials are written to `/opt/platform/credentials/<app-name>.env` and automatically picked up by the deploy workflow.
123+
124+
### Monitoring (Optional)
125+
126+
If you set `OPS_DOMAIN` during bootstrap (or add it to `/opt/platform/.env`), Grafana is accessible at `https://OPS_DOMAIN`. The default admin credentials are in `/opt/platform/.env`. Configure DNS for the ops domain the same way as app domains.
105127

106128
## DNS Configuration
107129

docs/server-contract.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This document defines the contract between the platform infrastructure (bootstra
77
```
88
/opt/platform/ # Platform root (created by bootstrap-server.sh)
99
docker-compose.yml # Platform services (postgres, redis, minio, caddy, loki, promtail, grafana)
10-
.env # Platform credentials (POSTGRES_PASSWORD, MINIO_ROOT_*, GRAFANA_ADMIN_PASSWORD, ACME_EMAIL)
10+
.env # Platform credentials (POSTGRES_PASSWORD, MINIO_ROOT_*, GRAFANA_ADMIN_PASSWORD, ACME_EMAIL, OPS_DOMAIN, ALERT_REPO)
1111
.bootstrapped # Timestamp marker from last bootstrap run
1212
Caddyfile # Global Caddyfile — imports /etc/caddy/apps/*.caddy
1313
caddy-apps/ # Per-app Caddyfile fragments (written by deploy/preview workflows)

docs/tutorial.md

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -51,51 +51,61 @@ You should see a shell prompt. If this works, you are ready to bootstrap.
5151

5252
## Step 3: Bootstrap the server
5353

54-
SSH into your server and install Docker:
54+
SSH into your server as root and run the bootstrap script. This installs Docker, creates the `deploy` user, starts all platform services, and generates credentials:
5555

5656
```bash
57-
ssh deploy@YOUR_SERVER_IP
57+
ssh root@YOUR_SERVER_IP
58+
git clone https://github.com/towlion/platform.git /tmp/platform
59+
sudo ACME_EMAIL=you@example.com bash /tmp/platform/infrastructure/bootstrap-server.sh
5860
```
5961

60-
Install Docker using the official convenience script:
62+
!!! tip
63+
Set `OPS_DOMAIN=ops.example.com` to also enable the Grafana monitoring dashboard:
64+
```bash
65+
sudo ACME_EMAIL=you@example.com OPS_DOMAIN=ops.example.com bash /tmp/platform/infrastructure/bootstrap-server.sh
66+
```
67+
68+
Verify the bootstrap was successful:
6169

6270
```bash
63-
curl -fsSL https://get.docker.com | sudo sh
64-
sudo usermod -aG docker $USER
71+
bash /tmp/platform/infrastructure/verify-server.sh
6572
```
6673

67-
!!! warning
68-
Log out and back in after adding yourself to the `docker` group, or the next commands will fail with a permission error.
74+
All checks should pass. The script creates:
75+
76+
```
77+
/data/ # Persistent data (postgres, redis, minio, caddy, loki, grafana)
78+
/opt/platform/ # Platform services and config
79+
/opt/apps/ # Application deployments
80+
```
81+
82+
## Step 3.5: Clone your app on the server
83+
84+
The deploy workflow runs `git pull` (not `git clone`), so the app must be pre-cloned on the server. SSH in as the `deploy` user:
6985

7086
```bash
71-
exit
7287
ssh deploy@YOUR_SERVER_IP
88+
cd /opt/apps
89+
git clone git@github.com:YOUR_USERNAME/app-template.git my-app
90+
cd my-app
7391
```
7492

75-
Verify Docker is working:
93+
Provision per-app database and storage credentials:
7694

7795
```bash
78-
docker run --rm hello-world
96+
sudo /opt/platform/infrastructure/create-app-credentials.sh my-app
7997
```
8098

81-
You should see `Hello from Docker!` in the output.
82-
83-
Create the data directory structure:
99+
Create the deploy environment file from the template:
84100

85101
```bash
86-
sudo mkdir -p /data/{postgres,redis,minio,caddy}
87-
sudo chown -R $USER:$USER /data
102+
cp deploy/env.template deploy/.env
88103
```
89104

90-
This is where persistent data lives across deployments. The directory layout:
105+
The deploy workflow will auto-update `deploy/.env` with the correct credentials on the next push.
91106

92-
```
93-
/data/
94-
postgres/ # Database files
95-
redis/ # Cache and queue data
96-
minio/ # Object storage
97-
caddy/ # TLS certificates and config
98-
```
107+
!!! warning
108+
The `deploy` user needs SSH access to your GitHub repo to `git pull`. Add the deploy user's public key (`/home/deploy/.ssh/id_ed25519.pub`) as a deploy key on your GitHub repository, or use HTTPS cloning with a personal access token.
99109

100110
## Step 4: Configure DNS
101111

@@ -139,9 +149,10 @@ In your forked repository on GitHub, go to **Settings > Secrets and variables >
139149
| `SERVER_USER` | `deploy` | SSH username on the server |
140150
| `SERVER_SSH_KEY` | *(private key contents)* | SSH private key for deployment |
141151
| `APP_DOMAIN` | `app.example.com` | Domain pointing to your server |
142-
| `DATABASE_PASSWORD` | *(strong password)* | PostgreSQL password |
143-
| `MINIO_ROOT_USER` | `minio-admin` | MinIO admin username |
144-
| `MINIO_ROOT_PASSWORD` | *(strong password)* | MinIO admin password |
152+
153+
> **Note:** Database and storage credentials are auto-generated by the bootstrap script on the server. You do not need to create them as GitHub secrets.
154+
155+
Optionally, add `PREVIEW_DOMAIN` (e.g., `example.com`) to enable preview environments for pull requests.
145156

146157
### Generate a deploy SSH key
147158

infrastructure/bootstrap-server.sh

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
1313

1414
MARKER="/opt/platform/.bootstrapped"
1515

16+
# Optional environment variables:
17+
# ACME_EMAIL - Email for Let's Encrypt TLS certificates (required for production)
18+
# OPS_DOMAIN - Domain for Grafana dashboard (e.g., ops.example.com)
19+
# ALERT_REPO - GitHub repo for alert issues (e.g., youruser/platform)
20+
1621
# --- Preflight ---
1722

1823
if [[ $EUID -ne 0 ]]; then
@@ -177,12 +182,18 @@ POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
177182
MINIO_ROOT_USER=${MINIO_ROOT_USER}
178183
MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
179184
GRAFANA_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
180-
ACME_EMAIL=admin@localhost
185+
ACME_EMAIL=${ACME_EMAIL:-admin@localhost}
186+
OPS_DOMAIN=${OPS_DOMAIN:-localhost}
187+
ALERT_REPO=${ALERT_REPO:-}
181188
EOF
182189

183190
chmod 600 "$ENV_FILE"
184191
chown deploy:deploy "$ENV_FILE"
185192
info "Credentials generated and written to $ENV_FILE"
193+
194+
if [[ "${ACME_EMAIL:-admin@localhost}" == "admin@localhost" ]]; then
195+
warn "ACME_EMAIL is admin@localhost — TLS certificates will fail. Re-run with ACME_EMAIL=you@example.com"
196+
fi
186197
fi
187198

188199
# --- Caddyfile ---
@@ -400,7 +411,7 @@ else
400411
"gridPos": { "h": 4, "w": 24, "x": 0, "y": 20 },
401412
"options": {
402413
"mode": "markdown",
403-
"content": "## Towlion Platform Overview\n\nThis dashboard shows logs from all containers on the platform.\n\n- **Log Stream**: All container logs (filter by service label)\n- **Error Rate**: Count of ERROR lines per service over 5-minute windows\n- **Container Logs by App**: Select an app from the dropdown to filter logs\n\nAlerts are managed by `check-alerts.sh` (cron every 5 min) and create GitHub Issues on the `towlion/platform` repo."
414+
"content": "## Towlion Platform Overview\n\nThis dashboard shows logs from all containers on the platform.\n\n- **Log Stream**: All container logs (filter by service label)\n- **Error Rate**: Count of ERROR lines per service over 5-minute windows\n- **Container Logs by App**: Select an app from the dropdown to filter logs\n\nAlerts are managed by `check-alerts.sh` (cron every 5 min) and create GitHub Issues when ALERT_REPO is configured."
404415
}
405416
}
406417
],
@@ -435,15 +446,17 @@ fi
435446
OPS_CADDY="/opt/platform/caddy-apps/ops.caddy"
436447
if [[ -f "$OPS_CADDY" ]]; then
437448
info "Grafana Caddy route already exists"
438-
else
439-
cat > "$OPS_CADDY" <<'EOF'
440-
ops.anulectra.com {
449+
elif [[ -n "${OPS_DOMAIN:-}" ]]; then
450+
cat > "$OPS_CADDY" <<EOF
451+
${OPS_DOMAIN} {
441452
reverse_proxy grafana:3000
442453
}
443454
EOF
444455

445456
chown deploy:deploy "$OPS_CADDY"
446457
info "Grafana Caddy route created at $OPS_CADDY"
458+
else
459+
info "OPS_DOMAIN not set — skipping Grafana Caddy route (set OPS_DOMAIN to enable)"
447460
fi
448461

449462
# --- Platform Compose File ---
@@ -606,7 +619,7 @@ services:
606619
environment:
607620
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
608621
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
609-
GF_SERVER_ROOT_URL: https://ops.anulectra.com
622+
GF_SERVER_ROOT_URL: https://${OPS_DOMAIN:-localhost}
610623
volumes:
611624
- /data/grafana:/var/lib/grafana
612625
- ./grafana/provisioning:/etc/grafana/provisioning:ro
@@ -687,7 +700,7 @@ else
687700
fi
688701

689702
# Alert check every 5 minutes
690-
ALERT_CRON="*/5 * * * * /opt/platform/infrastructure/check-alerts.sh >> /var/log/towlion-alerts.log 2>&1"
703+
ALERT_CRON="*/5 * * * * . /opt/platform/.env && /opt/platform/infrastructure/check-alerts.sh >> /var/log/towlion-alerts.log 2>&1"
691704
if echo "$DEPLOY_CRON" | grep -q "check-alerts"; then
692705
info "Alert cron job already exists"
693706
else
@@ -746,10 +759,26 @@ echo
746759
echo "Next steps:"
747760
echo " 1. Add your SSH public key to /home/deploy/.ssh/authorized_keys"
748761
echo " 2. Configure DNS — point your domain to this server's IP"
749-
echo " 3. Update ACME_EMAIL in $ENV_FILE to a real email for TLS certificates"
750-
echo " 4. Set GitHub Actions secrets on your app repo:"
762+
echo " 3. Set GitHub Actions secrets on your app repo:"
751763
echo " SERVER_HOST, SERVER_USER (deploy), SERVER_SSH_KEY, APP_DOMAIN"
752-
echo " 5. Run create-app-credentials.sh <app-name> for per-app credentials"
753-
echo " 6. Create app dir, clone repo, create deploy/.env from template"
754-
echo " 7. Push to main — deployment runs automatically"
755-
echo " 8. Set GITHUB_TOKEN in $ENV_FILE for alert issue creation (optional)"
764+
echo " 4. Run create-app-credentials.sh <app-name> for per-app credentials"
765+
echo " 5. Clone app repo to /opt/apps/<name>, create deploy/.env from template"
766+
echo " 6. Push to main — deployment runs automatically"
767+
768+
if [[ "${ACME_EMAIL:-admin@localhost}" == "admin@localhost" ]]; then
769+
echo
770+
echo -e "${YELLOW} ACTION REQUIRED: Set ACME_EMAIL in $ENV_FILE to a real email for TLS certificates.${NC}"
771+
echo " Or re-run: sudo ACME_EMAIL=you@example.com bash bootstrap-server.sh"
772+
fi
773+
774+
if [[ "${OPS_DOMAIN:-localhost}" == "localhost" ]]; then
775+
echo
776+
echo -e "${YELLOW} OPTIONAL: Set OPS_DOMAIN in $ENV_FILE for Grafana dashboard access.${NC}"
777+
echo " Or re-run: sudo OPS_DOMAIN=ops.example.com bash bootstrap-server.sh"
778+
fi
779+
780+
if [[ -z "${ALERT_REPO:-}" ]]; then
781+
echo
782+
echo -e "${YELLOW} OPTIONAL: Set ALERT_REPO in $ENV_FILE (e.g., youruser/platform) for GitHub issue alerts.${NC}"
783+
echo " Also set GITHUB_TOKEN for issue creation."
784+
fi

infrastructure/check-alerts.sh

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ set -euo pipefail
88
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
99
ALERTS=()
1010

11-
# Check if GITHUB_TOKEN is set (required for issue creation)
12-
if [[ -z "${GITHUB_TOKEN:-}" ]]; then
11+
# Check if ALERT_REPO and GITHUB_TOKEN are set (required for issue creation)
12+
if [[ -z "${ALERT_REPO:-}" ]]; then
13+
echo "[$TIMESTAMP] WARNING: ALERT_REPO not set — alerts will be logged but not posted to GitHub"
14+
CREATE_ISSUES=false
15+
elif [[ -z "${GITHUB_TOKEN:-}" ]]; then
1316
echo "[$TIMESTAMP] WARNING: GITHUB_TOKEN not set, alerts will only be logged (not created as issues)"
1417
CREATE_ISSUES=false
1518
else
@@ -55,7 +58,7 @@ if [[ ${#ALERTS[@]} -gt 0 ]]; then
5558

5659
if [[ "$CREATE_ISSUES" == true ]]; then
5760
# Check if issue already exists (dedup)
58-
EXISTING=$(gh issue list --repo towlion/platform \
61+
EXISTING=$(gh issue list --repo "${ALERT_REPO}" \
5962
--search "Alert: $alert in:title" \
6063
--state open \
6164
--limit 1 \
@@ -85,7 +88,7 @@ This issue was automatically created by \`infrastructure/check-alerts.sh\`."
8588

8689
echo "[$TIMESTAMP] Creating issue..."
8790
gh issue create \
88-
--repo towlion/platform \
91+
--repo "${ALERT_REPO}" \
8992
--title "$ISSUE_TITLE" \
9093
--body "$ISSUE_BODY" \
9194
--label "alert" || echo "[$TIMESTAMP] Failed to create issue"

0 commit comments

Comments
 (0)