Compare commits
43 Commits
9771fc620e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2281ebcb6d | ||
|
|
2942ff15bc | ||
|
|
24e80de43c | ||
|
|
cfc8b61f98 | ||
|
|
b063128049 | ||
|
|
a07adedd00 | ||
|
|
31705ad888 | ||
|
|
b5c5c11114 | ||
|
|
926766346c | ||
|
|
c736c23e9a | ||
|
|
a02f33e96e | ||
|
|
d62b627093 | ||
|
|
fb1de4f079 | ||
|
|
3bf80f6940 | ||
|
|
1c2fb3c807 | ||
|
|
b918e713e5 | ||
|
|
ac3bff9351 | ||
|
|
0088c11d5e | ||
|
|
4f3f4b0487 | ||
|
|
a51f86ea0a | ||
|
|
22198784d3 | ||
|
|
0a305a47b9 | ||
|
|
d88a8db9f1 | ||
|
|
995dfcc099 | ||
|
|
4329cfd3f2 | ||
|
|
c0c20a42ed | ||
|
|
a17c63a39b | ||
|
|
cdec4e3e22 | ||
|
|
0e0a6ff1eb | ||
|
|
7225f526da | ||
|
|
8b5c9bdbfc | ||
|
|
770081397c | ||
|
|
8f5b73dffc | ||
|
|
5e57d5258a | ||
|
|
522207b9d9 | ||
|
|
09aee112da | ||
|
|
158a8e6eb4 | ||
|
|
f3eea007f7 | ||
|
|
1fed3dde51 | ||
|
|
89b806fd5b | ||
|
|
caa1c7f471 | ||
|
|
0f12c5f5a8 | ||
|
|
ce9dba4923 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.env
|
||||
.idea/
|
||||
*.iml
|
||||
.claude/settings.local.json
|
||||
|
||||
19
CLAUDE.md
19
CLAUDE.md
@@ -8,7 +8,7 @@ GitOps-style Docker Compose setup for a self-hosted VPS running Nextcloud, Gitea
|
||||
|
||||
## Architecture
|
||||
|
||||
Four independent service stacks, each with its own `docker-compose.yml`:
|
||||
A root `docker-compose.yml` uses `include` to compose four service stacks, each with its own `docker-compose.yml`:
|
||||
|
||||
- **caddy/** — Reverse proxy with auto HTTPS. All services route through the shared `proxy` Docker network.
|
||||
- **nextcloud/** — Nextcloud 29 + PostgreSQL 16 + Redis 7 + cron container. Has its own `.env` for DB credentials and Nextcloud config. Uses internal `nextcloud-internal` network for DB/Redis isolation.
|
||||
@@ -16,8 +16,9 @@ Four independent service stacks, each with its own `docker-compose.yml`:
|
||||
- **monitoring/** — Grafana Alloy collecting Docker logs (Loki) and node metrics (Prometheus) to Grafana Cloud. Has its own `.env` for cloud credentials.
|
||||
|
||||
Key design patterns:
|
||||
- Root `docker-compose.yml` includes all stacks via `include:` — single command to manage everything
|
||||
- All stacks share the external `proxy` Docker network for Caddy routing
|
||||
- Each service's compose file requires `--env-file .env` (root-level) for `DATA_ROOT` and `DOMAIN`
|
||||
- Root `.env` provides `DATA_ROOT` and `DOMAIN` (pass via `--env-file .env`)
|
||||
- Service-specific secrets live in per-service `.env` files (loaded via `env_file:` in compose)
|
||||
- All persistent data under `${DATA_ROOT}` (default `/opt/docker-data/`)
|
||||
|
||||
@@ -27,6 +28,11 @@ Key design patterns:
|
||||
# Deploy everything (installs Docker if needed, creates dirs, starts all stacks)
|
||||
./scripts/deploy.sh
|
||||
|
||||
# Manage all services
|
||||
docker compose --env-file .env up -d
|
||||
docker compose --env-file .env logs -f
|
||||
docker compose --env-file .env down
|
||||
|
||||
# Manage individual services
|
||||
docker compose -f <service>/docker-compose.yml --env-file .env up -d
|
||||
docker compose -f <service>/docker-compose.yml --env-file .env logs -f
|
||||
@@ -43,10 +49,11 @@ docker exec caddy caddy reload --config /etc/caddy/Caddyfile
|
||||
## Adding a New Service
|
||||
|
||||
1. Create `myapp/docker-compose.yml` joining the `proxy` external network, with data under `${DATA_ROOT}/myapp/`
|
||||
2. Add reverse proxy entry in `caddy/Caddyfile`
|
||||
3. Add data directory creation to `scripts/deploy.sh`
|
||||
4. Add backup steps to `scripts/backup.sh` if it has persistent data
|
||||
5. Create DNS A record for the subdomain
|
||||
2. Add `- path: myapp/docker-compose.yml` to root `docker-compose.yml`
|
||||
3. Add reverse proxy entry in `caddy/Caddyfile`
|
||||
4. Add data directory creation to `scripts/deploy.sh`
|
||||
5. Add backup steps to `scripts/backup.sh` if it has persistent data
|
||||
6. Create DNS A record for the subdomain
|
||||
|
||||
## Environment Files
|
||||
|
||||
|
||||
293
README.md
293
README.md
@@ -2,9 +2,51 @@
|
||||
|
||||
Docker Compose setup for a self-hosted VPS running Nextcloud, Gitea, and monitoring — managed as a GitOps-style repo.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Internet([Internet])
|
||||
|
||||
subgraph VPS["VPS (t-gstone.de)"]
|
||||
subgraph proxy_net["proxy network"]
|
||||
Caddy["Caddy<br/>reverse proxy + auto HTTPS"]
|
||||
end
|
||||
|
||||
subgraph nc_stack["Nextcloud Stack"]
|
||||
Nginx["Nginx<br/>static files + FastCGI proxy"]
|
||||
NC["Nextcloud FPM<br/>PHP processing"]
|
||||
Cron["Cron<br/>background jobs"]
|
||||
PG["PostgreSQL 17"]
|
||||
Redis["Redis 8"]
|
||||
end
|
||||
|
||||
subgraph gitea_stack["Gitea Stack"]
|
||||
Gitea["Gitea<br/>rootless, SQLite"]
|
||||
end
|
||||
|
||||
subgraph mon_stack["Monitoring Stack"]
|
||||
Alloy["Grafana Alloy"]
|
||||
end
|
||||
end
|
||||
|
||||
GrafanaCloud([Grafana Cloud])
|
||||
|
||||
Internet -->|":443 HTTPS"| Caddy
|
||||
Internet -->|":2222 SSH"| Gitea
|
||||
Caddy -->|"nextcloud.t-gstone.de"| Nginx
|
||||
Caddy -->|"git.t-gstone.de"| Gitea
|
||||
Nginx -->|":9000 FastCGI"| NC
|
||||
NC --> PG
|
||||
NC --> Redis
|
||||
Cron --> PG
|
||||
Cron --> Redis
|
||||
Alloy -->|"logs + metrics"| GrafanaCloud
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A VPS with SSH access
|
||||
- A VPS with SSH access (minimum 1 core, 3 GB RAM)
|
||||
- Domain `t-gstone.de` with DNS control
|
||||
- Git installed locally
|
||||
|
||||
@@ -14,6 +56,24 @@ Check your VPS OS:
|
||||
cat /etc/os-release
|
||||
```
|
||||
|
||||
### Swap (recommended)
|
||||
|
||||
Check current memory and swap:
|
||||
|
||||
```bash
|
||||
free -h
|
||||
```
|
||||
|
||||
If swap shows `0B`, add a 2 GB swapfile to prevent OOM kills:
|
||||
|
||||
```bash
|
||||
sudo fallocate -l 2G /swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
|
||||
```
|
||||
|
||||
## DNS Setup
|
||||
|
||||
Create these A records pointing to your VPS IP:
|
||||
@@ -23,6 +83,93 @@ Create these A records pointing to your VPS IP:
|
||||
| `nextcloud.t-gstone.de` | `<VPS_IP>` |
|
||||
| `git.t-gstone.de` | `<VPS_IP>` |
|
||||
|
||||
## Local Setup
|
||||
|
||||
### SSH Access
|
||||
|
||||
Add this to `~/.ssh/config` on your local machine:
|
||||
|
||||
```
|
||||
Host t-gstone.de
|
||||
HostName t-gstone.de
|
||||
User gstone
|
||||
Port 55
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
UseKeychain yes
|
||||
AddKeysToAgent yes
|
||||
```
|
||||
|
||||
Generate a key and copy it to the VPS:
|
||||
|
||||
```bash
|
||||
# Generate key (skip if you already have ~/.ssh/id_ed25519)
|
||||
ssh-keygen -t ed25519
|
||||
|
||||
# Copy it to the VPS (will ask for your password once)
|
||||
ssh-copy-id -p 55 gstone@t-gstone.de
|
||||
|
||||
# Store passphrase in macOS Keychain
|
||||
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
After this, `ssh t-gstone.de` connects without any password prompts.
|
||||
|
||||
## Syncing to the VPS
|
||||
|
||||
If the VPS doesn't have a git remote set up yet, use `rsync` over SSH to push the repo:
|
||||
|
||||
```bash
|
||||
# Sync the repo to the VPS (dry-run first to check what would be sent)
|
||||
rsync -avz --dry-run -e 'ssh -p 55' \
|
||||
--exclude '.env' --exclude '*/.env' --exclude '.git' \
|
||||
./ gstone@t-gstone.de:~/nextcloud-selfhosted/
|
||||
|
||||
# Run for real (remove --dry-run)
|
||||
rsync -avz -e 'ssh -p 55' \
|
||||
--exclude '.env' --exclude '*/.env' --exclude '.git' \
|
||||
./ gstone@t-gstone.de:~/nextcloud-selfhosted/
|
||||
```
|
||||
|
||||
The `.env` files are excluded because they contain secrets and should be created directly on the VPS from the `.env.example` templates.
|
||||
|
||||
## Syncing via Git (recommended)
|
||||
|
||||
Now that Gitea is running, you can clone the repo directly on the VPS instead of using rsync.
|
||||
|
||||
### First-time setup on the VPS
|
||||
|
||||
```bash
|
||||
# 1. Add your SSH key to Gitea (via https://git.t-gstone.de user settings)
|
||||
|
||||
# 2. Clone the repo
|
||||
cd ~
|
||||
git clone ssh://git@git.t-gstone.de:2222/gstone/nextcloud-selfhosted.git
|
||||
cd nextcloud-selfhosted
|
||||
|
||||
# 3. Create .env files from examples
|
||||
cp .env.example .env
|
||||
cp nextcloud/.env.example nextcloud/.env
|
||||
cp gitea/.env.example gitea/.env
|
||||
cp monitoring/.env.example monitoring/.env
|
||||
|
||||
# 4. Edit each .env file with real values
|
||||
nano .env
|
||||
nano nextcloud/.env
|
||||
nano gitea/.env
|
||||
nano monitoring/.env
|
||||
|
||||
# 5. Deploy
|
||||
sudo ./scripts/deploy.sh
|
||||
```
|
||||
|
||||
### Updating the VPS after pushing changes
|
||||
|
||||
```bash
|
||||
cd ~/nextcloud-selfhosted
|
||||
git pull
|
||||
sudo docker compose --env-file .env up -d
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
@@ -69,19 +216,44 @@ All persistent data lives under `/opt/docker-data/`:
|
||||
|
||||
## Managing Services
|
||||
|
||||
Each service has its own compose file and can be managed independently:
|
||||
A root `docker-compose.yml` includes all stacks, so you can manage everything with one command:
|
||||
|
||||
```bash
|
||||
# Restart just Nextcloud
|
||||
docker compose -f nextcloud/docker-compose.yml --env-file .env up -d
|
||||
# Start / restart all services
|
||||
docker compose --env-file .env up -d
|
||||
|
||||
# View logs for Gitea
|
||||
docker compose -f gitea/docker-compose.yml --env-file .env logs -f
|
||||
# View logs for all services
|
||||
docker compose --env-file .env logs -f
|
||||
|
||||
# Stop everything
|
||||
for svc in monitoring gitea nextcloud caddy; do
|
||||
docker compose -f $svc/docker-compose.yml --env-file .env down
|
||||
done
|
||||
docker compose --env-file .env down
|
||||
```
|
||||
|
||||
You can still target individual services via their compose file:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -f caddy/docker-compose.yml up -d
|
||||
docker compose --env-file .env -f gitea/docker-compose.yml up -d
|
||||
docker compose --env-file .env -f monitoring/docker-compose.yml up -d
|
||||
docker compose --env-file .env -f nextcloud/docker-compose.yml up -d
|
||||
|
||||
docker compose --env-file .env -f gitea/docker-compose.yml logs -f
|
||||
```
|
||||
|
||||
## Running Nextcloud OCC Commands
|
||||
|
||||
Nextcloud's `occ` command-line tool must run as the `www-data` user inside the container:
|
||||
|
||||
```bash
|
||||
# General syntax
|
||||
sudo docker exec -u www-data nextcloud php occ <command>
|
||||
|
||||
# Examples
|
||||
sudo docker exec -u www-data nextcloud php occ status
|
||||
sudo docker exec -u www-data nextcloud php occ config:list
|
||||
sudo docker exec -u www-data nextcloud php occ app:list
|
||||
sudo docker exec -u www-data nextcloud php occ db:add-missing-indices
|
||||
sudo docker exec -u www-data nextcloud php occ maintenance:repair --include-expensive
|
||||
```
|
||||
|
||||
## Adding a New Service
|
||||
@@ -91,16 +263,17 @@ done
|
||||
- Join the `proxy` external network
|
||||
- Bind mount data to `${DATA_ROOT}/myapp/`
|
||||
- Add `myapp/.env.example` if the service needs secrets
|
||||
3. Add a reverse proxy entry in `caddy/Caddyfile`:
|
||||
3. Add `- path: myapp/docker-compose.yml` to root `docker-compose.yml`
|
||||
4. Add a reverse proxy entry in `caddy/Caddyfile`:
|
||||
```
|
||||
myapp.t-gstone.de {
|
||||
reverse_proxy myapp:8080
|
||||
}
|
||||
```
|
||||
4. Reload Caddy: `docker exec caddy caddy reload --config /etc/caddy/Caddyfile`
|
||||
5. Add a DNS A record for `myapp.t-gstone.de` -> VPS IP
|
||||
6. Add data directory creation to `scripts/deploy.sh`
|
||||
7. Add backup steps to `scripts/backup.sh` if the service has persistent data
|
||||
5. Reload Caddy: `docker exec caddy caddy reload --config /etc/caddy/Caddyfile`
|
||||
6. Add a DNS A record for `myapp.t-gstone.de` -> VPS IP
|
||||
7. Add data directory creation to `scripts/deploy.sh`
|
||||
8. Add backup steps to `scripts/backup.sh` if the service has persistent data
|
||||
|
||||
## Backup & Restore
|
||||
|
||||
@@ -153,19 +326,95 @@ or low cost, and Restic handles encryption + deduplication automatically. A cron
|
||||
2. Go to **My Account** -> **Grafana Cloud** -> your stack
|
||||
3. Find your Loki and Prometheus endpoints + credentials
|
||||
4. Fill in `monitoring/.env` with those values
|
||||
5. Start the monitoring stack: `docker compose -f monitoring/docker-compose.yml --env-file .env up -d`
|
||||
5. Start the monitoring stack: `docker compose --env-file .env up -d`
|
||||
|
||||
### Recommended Alerts
|
||||
|
||||
Set these up in Grafana Cloud UI (**Alerting** -> **Alert rules**):
|
||||
Set these up in Grafana Cloud UI (**Alerting** -> **Alert rules** -> **New alert rule**). Choose **Grafana-managed rule**
|
||||
and select the appropriate data source (Prometheus or Loki).
|
||||
|
||||
| Alert | Condition | Severity |
|
||||
|----------------------|-----------------------------------------------------------------------|----------|
|
||||
| Disk usage high | `node_filesystem_avail_bytes` / `node_filesystem_size_bytes` < 0.2 | Critical |
|
||||
| Container restarting | Container restart count > 3 in 10 min | Warning |
|
||||
| High memory usage | `node_memory_MemAvailable_bytes` / `node_memory_MemTotal_bytes` < 0.1 | Warning |
|
||||
| High CPU usage | `node_cpu_seconds_total` idle < 10% sustained 5 min | Warning |
|
||||
| Nextcloud cron stale | No log line from `nextcloud-cron` in 15 min | Warning |
|
||||
|----------------------|--------------------------------------|----------|
|
||||
| Disk usage high | Available disk < 20% | Critical |
|
||||
| Container restarting | Restart count > 3 in 10 min | Warning |
|
||||
| High memory usage | Available memory < 10% | Warning |
|
||||
| High CPU usage | CPU usage > 90% sustained 5 min | Warning |
|
||||
| Nextcloud cron stale | No cron log lines in 15 min | Warning |
|
||||
|
||||
#### Disk usage high
|
||||
|
||||
Fires when any filesystem drops below 20% free space.
|
||||
|
||||
- **Data source:** Prometheus
|
||||
- **Query (A):**
|
||||
```promql
|
||||
node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} * 100
|
||||
```
|
||||
- **Expression (B):** Threshold — `A IS BELOW 20`
|
||||
- **Evaluate every:** `1m`
|
||||
- **Pending period (For):** `5m`
|
||||
- **Labels:** `severity: critical`
|
||||
|
||||
#### Container restarting
|
||||
|
||||
Fires when any container restarts more than 3 times in 10 minutes, indicating a crash loop.
|
||||
Detects both in-place restarts (`docker restart`) and ID-changing restarts (`docker compose down/up`).
|
||||
Requires cAdvisor (included in the monitoring stack).
|
||||
|
||||
- **Data source:** Prometheus
|
||||
- **Query (A):**
|
||||
```promql
|
||||
sum by (name) (changes(container_start_time_seconds{name!=""}[10m]))
|
||||
+
|
||||
count by (name) (count_over_time(container_start_time_seconds{name!=""}[10m])) - 1
|
||||
```
|
||||
- **Expression (B):** Threshold — `A IS ABOVE 3`
|
||||
- **Evaluate every:** `1m`
|
||||
- **Pending period (For):** `0s`
|
||||
- **Labels:** `severity: warning`
|
||||
|
||||
#### High memory usage
|
||||
|
||||
Fires when available memory drops below 10% of total.
|
||||
|
||||
- **Data source:** Prometheus
|
||||
- **Query (A):**
|
||||
```promql
|
||||
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100
|
||||
```
|
||||
- **Expression (B):** Threshold — `A IS BELOW 10`
|
||||
- **Evaluate every:** `1m`
|
||||
- **Pending period (For):** `5m`
|
||||
- **Labels:** `severity: warning`
|
||||
|
||||
#### High CPU usage
|
||||
|
||||
Fires when average CPU usage exceeds 90% for 5 minutes.
|
||||
|
||||
- **Data source:** Prometheus
|
||||
- **Query (A):**
|
||||
```promql
|
||||
avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100
|
||||
```
|
||||
- **Expression (B):** Threshold — `A IS BELOW 10`
|
||||
- **Evaluate every:** `1m`
|
||||
- **Pending period (For):** `5m`
|
||||
- **Labels:** `severity: warning`
|
||||
|
||||
#### Nextcloud cron stale
|
||||
|
||||
Fires when no log output from the `nextcloud-cron` container appears for 15 minutes, indicating background jobs have stopped.
|
||||
|
||||
- **Data source:** Loki
|
||||
- **Query (A):**
|
||||
```logql
|
||||
count_over_time({container="/nextcloud-cron"}[15m])
|
||||
```
|
||||
- **Expression (B):** Threshold — `A IS BELOW 1`
|
||||
- **Alert condition:** also trigger on **No Data**
|
||||
- **Evaluate every:** `5m`
|
||||
- **Pending period (For):** `0s`
|
||||
- **Labels:** `severity: warning`
|
||||
|
||||
### Recommended Dashboards
|
||||
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
{
|
||||
servers {
|
||||
timeouts {
|
||||
read_header 10s
|
||||
idle 60s
|
||||
}
|
||||
max_header_size 16KB
|
||||
}
|
||||
}
|
||||
|
||||
nextcloud.t-gstone.de {
|
||||
reverse_proxy nextcloud:80
|
||||
reverse_proxy nextcloud-nginx:80
|
||||
|
||||
header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
|
||||
redir /.well-known/carddav /remote.php/dav/ 301
|
||||
redir /.well-known/caldav /remote.php/dav/ 301
|
||||
header Referrer-Policy "no-referrer"
|
||||
header X-Content-Type-Options "nosniff"
|
||||
header X-Frame-Options "SAMEORIGIN"
|
||||
header X-Permitted-Cross-Domain-Policies "none"
|
||||
header X-Robots-Tag "noindex, nofollow"
|
||||
|
||||
request_body {
|
||||
max_size 10G
|
||||
|
||||
@@ -3,6 +3,8 @@ services:
|
||||
image: caddy:2-alpine
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- alloy
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
@@ -13,6 +15,16 @@ services:
|
||||
- ${DATA_ROOT}/caddy/config:/config
|
||||
networks:
|
||||
- proxy
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
healthcheck:
|
||||
test: ["CMD", "caddy", "validate", "--config", "/etc/caddy/Caddyfile"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
|
||||
5
docker-compose.yml
Normal file
5
docker-compose.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
include:
|
||||
- path: caddy/docker-compose.yml
|
||||
- path: nextcloud/docker-compose.yml
|
||||
- path: gitea/docker-compose.yml
|
||||
- path: monitoring/docker-compose.yml
|
||||
@@ -1,8 +1,10 @@
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:latest-rootless
|
||||
image: gitea/gitea:1.25.5-rootless
|
||||
container_name: gitea
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- alloy
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ${DATA_ROOT}/gitea/data:/var/lib/gitea
|
||||
@@ -11,6 +13,16 @@ services:
|
||||
- "2222:2222"
|
||||
networks:
|
||||
- proxy
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/healthz"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
|
||||
73
migration.md
Normal file
73
migration.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Migration Plan: Bare-Metal Nextcloud to Docker
|
||||
|
||||
Fresh Docker install with manual re-upload via Nextcloud client.
|
||||
Old setup: bare-metal Nextcloud (MySQL) at `t-gstone.de/nextcloud`.
|
||||
New setup: Docker-based Nextcloud (PostgreSQL) at `nextcloud.t-gstone.de`.
|
||||
|
||||
## Before Migration
|
||||
|
||||
### 1. Export Calendars and Contacts from Old Instance
|
||||
|
||||
These live in the database and won't carry over automatically:
|
||||
|
||||
- **Calendars**: Go to Calendar app > Settings (bottom-left) > click `...` next to each calendar > Export (downloads `.ics`)
|
||||
- **Contacts**: Go to Contacts app > Settings (bottom-left) > click `...` next to each address book > Export (downloads `.vcf`)
|
||||
|
||||
Also export any other DB-only app data you care about (Notes, Deck boards, Bookmarks, etc.).
|
||||
|
||||
### 2. Create DNS Record
|
||||
|
||||
Add an A record for `nextcloud.t-gstone.de` pointing to your VPS IP. Do this early so DNS propagates.
|
||||
|
||||
### 3. Deploy Fresh Docker Setup
|
||||
|
||||
```bash
|
||||
# Clone repo on VPS, configure .env files
|
||||
cp .env.example .env # set DOMAIN and DATA_ROOT
|
||||
cp nextcloud/.env.example nextcloud/.env # set DB creds, admin user, redis password
|
||||
cp gitea/.env.example gitea/.env
|
||||
cp monitoring/.env.example monitoring/.env
|
||||
|
||||
# Deploy
|
||||
./scripts/deploy.sh
|
||||
```
|
||||
|
||||
Verify the fresh instance works at `https://nextcloud.t-gstone.de`.
|
||||
|
||||
### 4. Re-Upload Files via Nextcloud Client
|
||||
|
||||
- Install the Nextcloud desktop client
|
||||
- Point it to `https://nextcloud.t-gstone.de`
|
||||
- Sync your files from your local machine
|
||||
|
||||
### 5. Re-Import Calendars and Contacts
|
||||
|
||||
- **Calendars**: Calendar app > Settings > Import > select the `.ics` files
|
||||
- **Contacts**: Contacts app > Settings > Import > select the `.vcf` files
|
||||
|
||||
### 6. Verify
|
||||
|
||||
- [ ] Files are complete and accessible
|
||||
- [ ] Calendars show all events
|
||||
- [ ] Contacts are intact
|
||||
- [ ] Sharing works
|
||||
- [ ] Mobile apps connect successfully
|
||||
|
||||
### 7. Decommission Old Instance
|
||||
|
||||
Once satisfied:
|
||||
|
||||
1. Shut down old bare-metal Nextcloud
|
||||
2. Optionally redirect `t-gstone.de/nextcloud` to `nextcloud.t-gstone.de`
|
||||
3. Update all Nextcloud clients on your devices to the new URL
|
||||
4. Keep the old data/DB dump as a backup for a few weeks before deleting
|
||||
|
||||
## What Won't Carry Over (DB-only data)
|
||||
|
||||
These are stored in the MySQL database, not in files. Export before shutting down the old instance if you need them:
|
||||
|
||||
- Calendars / Contacts (CalDAV/CardDAV) — export as `.ics`/`.vcf`
|
||||
- Share links and shared folder structures
|
||||
- Notes, Deck boards, Bookmarks, Talk history
|
||||
- App settings and configurations
|
||||
- Activity log / file versioning metadata
|
||||
@@ -3,7 +3,7 @@
|
||||
// ============================================================
|
||||
|
||||
discovery.docker "containers" {
|
||||
host = "unix:///var/run/docker.sock"
|
||||
host = "http://docker-socket-proxy:2375"
|
||||
}
|
||||
|
||||
discovery.relabel "containers" {
|
||||
@@ -21,7 +21,7 @@ discovery.relabel "containers" {
|
||||
}
|
||||
|
||||
loki.source.docker "containers" {
|
||||
host = "unix:///var/run/docker.sock"
|
||||
host = "http://docker-socket-proxy:2375"
|
||||
targets = discovery.relabel.containers.output
|
||||
forward_to = [loki.write.grafana_cloud.receiver]
|
||||
}
|
||||
@@ -54,6 +54,18 @@ prometheus.scrape "node" {
|
||||
scrape_interval = "60s"
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// cAdvisor container metrics -> Grafana Cloud Prometheus
|
||||
// ============================================================
|
||||
|
||||
prometheus.scrape "cadvisor" {
|
||||
targets = [{"__address__" = "cadvisor:8080"}]
|
||||
forward_to = [prometheus.remote_write.grafana_cloud.receiver]
|
||||
|
||||
scrape_interval = "60s"
|
||||
metrics_path = "/metrics"
|
||||
}
|
||||
|
||||
prometheus.remote_write "grafana_cloud" {
|
||||
endpoint {
|
||||
url = env("GRAFANA_CLOUD_PROMETHEUS_URL")
|
||||
|
||||
@@ -1,12 +1,70 @@
|
||||
services:
|
||||
docker-socket-proxy:
|
||||
image: tecnativa/docker-socket-proxy:0.3
|
||||
container_name: docker-socket-proxy
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- CONTAINERS=1
|
||||
- LOG=1
|
||||
- POST=0
|
||||
- BUILD=0
|
||||
- COMMIT=0
|
||||
- CONFIGS=0
|
||||
- DISTRIBUTION=0
|
||||
- EXEC=0
|
||||
- IMAGES=0
|
||||
- INFO=0
|
||||
- NETWORKS=1
|
||||
- NODES=0
|
||||
- PLUGINS=0
|
||||
- SERVICES=0
|
||||
- SESSION=0
|
||||
- SWARM=0
|
||||
- SYSTEM=0
|
||||
- TASKS=0
|
||||
- VOLUMES=0
|
||||
networks:
|
||||
- monitoring
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
cadvisor:
|
||||
image: gcr.io/cadvisor/cadvisor:v0.54.1
|
||||
container_name: cadvisor
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- /run/containerd/containerd.sock:/run/containerd/containerd.sock:ro
|
||||
- /sys:/sys:ro
|
||||
- /var/lib/docker/:/var/lib/docker:ro
|
||||
command:
|
||||
- --docker_only=true
|
||||
- --housekeeping_interval=30s
|
||||
- --containerd=/run/containerd/containerd.sock
|
||||
- --disable_metrics=cpu_topology,disk,diskIO,hugetlb,memory_numa,network,oom_event,percpu,perf_event,process,referenced_memory,resctrl,sched,tcp,udp
|
||||
networks:
|
||||
- monitoring
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
alloy:
|
||||
image: grafana/alloy:latest
|
||||
image: grafana/alloy:v1.14.1
|
||||
container_name: alloy
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- docker-socket-proxy
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./config.alloy:/etc/alloy/config.alloy:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/host/root:ro
|
||||
@@ -17,6 +75,11 @@ services:
|
||||
pid: host
|
||||
networks:
|
||||
- monitoring
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
monitoring:
|
||||
|
||||
@@ -11,3 +11,6 @@ NEXTCLOUD_ADMIN_PASSWORD=CHANGE_ME_admin_password
|
||||
NEXTCLOUD_TRUSTED_DOMAINS=nextcloud.t-gstone.de
|
||||
OVERWRITEPROTOCOL=https
|
||||
OVERWRITECLIURL=https://nextcloud.t-gstone.de
|
||||
|
||||
# Redis
|
||||
REDIS_PASSWORD=CHANGE_ME_redis_password
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
nextcloud:
|
||||
image: nextcloud:29-apache
|
||||
image: nextcloud:33-fpm
|
||||
container_name: nextcloud
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
@@ -12,17 +12,53 @@ services:
|
||||
environment:
|
||||
- POSTGRES_HOST=postgres
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_HOST_PASSWORD=${REDIS_PASSWORD}
|
||||
- TRUSTED_PROXIES=172.18.0.0/16
|
||||
volumes:
|
||||
- ${DATA_ROOT}/nextcloud/html:/var/www/html
|
||||
- ${DATA_ROOT}/nextcloud/data:/var/www/html/data
|
||||
- ./hooks/post-installation.sh:/docker-entrypoint-hooks.d/post-installation/post-installation.sh:ro
|
||||
- ./hooks/post-upgrade.sh:/docker-entrypoint-hooks.d/post-upgrade/post-upgrade.sh:ro
|
||||
- ./fpm-tuning.conf:/usr/local/etc/php-fpm.d/zz-tuning.conf:ro
|
||||
- ./php-tuning.ini:/usr/local/etc/php/conf.d/zz-tuning.ini:ro
|
||||
networks:
|
||||
- nextcloud-internal
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: nextcloud-nginx
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- nextcloud
|
||||
volumes:
|
||||
- ${DATA_ROOT}/nextcloud/html:/var/www/html:ro
|
||||
- ${DATA_ROOT}/nextcloud/data:/var/www/html/data:ro
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost/status.php || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
networks:
|
||||
- proxy
|
||||
- nextcloud-internal
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
image: postgres:17-alpine
|
||||
container_name: nextcloud-postgres
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- alloy
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ${DATA_ROOT}/nextcloud/db:/var/lib/postgresql/data
|
||||
@@ -33,16 +69,30 @@ services:
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
image: redis:8-alpine
|
||||
container_name: nextcloud-redis
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- alloy
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
env_file: .env
|
||||
networks:
|
||||
- nextcloud-internal
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
cron:
|
||||
image: nextcloud:29-apache
|
||||
image: nextcloud:33-fpm
|
||||
container_name: nextcloud-cron
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
@@ -53,6 +103,11 @@ services:
|
||||
- ${DATA_ROOT}/nextcloud/data:/var/www/html/data
|
||||
networks:
|
||||
- nextcloud-internal
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
|
||||
7
nextcloud/fpm-tuning.conf
Normal file
7
nextcloud/fpm-tuning.conf
Normal file
@@ -0,0 +1,7 @@
|
||||
[www]
|
||||
pm = dynamic
|
||||
pm.max_children = 5
|
||||
pm.start_servers = 2
|
||||
pm.min_spare_servers = 1
|
||||
pm.max_spare_servers = 3
|
||||
pm.max_requests = 500
|
||||
21
nextcloud/hooks/post-installation.sh
Executable file
21
nextcloud/hooks/post-installation.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
echo "==> Post-installation: setting maintenance window start to 01:00 UTC..."
|
||||
php occ config:system:set maintenance_window_start --type=integer --value=1
|
||||
|
||||
echo "==> Post-installation: setting default phone region to DE..."
|
||||
php occ config:system:set default_phone_region --value="DE"
|
||||
|
||||
echo "==> Post-installation: adding missing DB indices..."
|
||||
php occ db:add-missing-indices
|
||||
|
||||
echo "==> Post-installation: running MIME type migrations..."
|
||||
php occ maintenance:repair --include-expensive
|
||||
|
||||
echo "==> Post-installation: configuring Redis caching and file locking..."
|
||||
php occ config:system:set memcache.local --value="\\OC\\Memcache\\APCu"
|
||||
php occ config:system:set memcache.distributed --value="\\OC\\Memcache\\Redis"
|
||||
php occ config:system:set memcache.locking --value="\\OC\\Memcache\\Redis"
|
||||
|
||||
echo "==> Post-installation: done."
|
||||
10
nextcloud/hooks/post-upgrade.sh
Executable file
10
nextcloud/hooks/post-upgrade.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
echo "==> Post-upgrade: adding missing DB indices..."
|
||||
php occ db:add-missing-indices
|
||||
|
||||
echo "==> Post-upgrade: running MIME type migrations..."
|
||||
php occ maintenance:repair --include-expensive
|
||||
|
||||
echo "==> Post-upgrade: done."
|
||||
87
nextcloud/nginx.conf
Normal file
87
nextcloud/nginx.conf
Normal file
@@ -0,0 +1,87 @@
|
||||
upstream php-handler {
|
||||
server nextcloud:9000;
|
||||
}
|
||||
|
||||
map $uri $nonce_uri {
|
||||
default "";
|
||||
}
|
||||
|
||||
map $arg_v $asset_immutable {
|
||||
"" "";
|
||||
default ", immutable";
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
include mime.types;
|
||||
types {
|
||||
application/javascript mjs;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_comp_level 4;
|
||||
gzip_min_length 256;
|
||||
gzip_proxied any;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
|
||||
|
||||
client_max_body_size 10G;
|
||||
client_body_timeout 300s;
|
||||
fastcgi_buffers 64 4K;
|
||||
|
||||
root /var/www/html;
|
||||
index index.php index.html /index.php$request_uri;
|
||||
|
||||
# Redirect well-known URLs
|
||||
location ^~ /.well-known {
|
||||
location = /.well-known/carddav { return 301 /remote.php/dav/; }
|
||||
location = /.well-known/caldav { return 301 /remote.php/dav/; }
|
||||
location /.well-known/acme-challenge { try_files $uri $uri/ =404; }
|
||||
location /.well-known/pki-validation { try_files $uri $uri/ =404; }
|
||||
return 301 /index.php$request_uri;
|
||||
}
|
||||
|
||||
# Deny access to internal paths
|
||||
location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { return 404; }
|
||||
location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; }
|
||||
|
||||
# PHP handling (must be before static file locations so that internal
|
||||
# redirects like /index.php/apps/theming/theme/dark.css match here
|
||||
# instead of cycling back into the static file try_files)
|
||||
location ~ \.php(?:$|/) {
|
||||
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
|
||||
set $path_info $fastcgi_path_info;
|
||||
try_files $fastcgi_script_name =404;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $path_info;
|
||||
fastcgi_param HTTPS on;
|
||||
fastcgi_param modHeadersAvailable true;
|
||||
fastcgi_param front_controller_active true;
|
||||
fastcgi_pass php-handler;
|
||||
fastcgi_intercept_errors on;
|
||||
fastcgi_hide_header X-Powered-By;
|
||||
fastcgi_request_buffering off;
|
||||
fastcgi_max_temp_file_size 0;
|
||||
}
|
||||
|
||||
# Serve static files directly, fall through to PHP for dynamic assets (e.g. theming)
|
||||
location ~ \.(?:css|js|mjs|svg|gif|ico|jpg|png|webp|wasm|tflite|map|ogg|flac|mp4|webm)$ {
|
||||
try_files $uri /index.php$request_uri;
|
||||
add_header Cache-Control "public, max-age=15778463$asset_immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location ~ \.woff2?$ {
|
||||
try_files $uri /index.php$request_uri;
|
||||
expires 7d;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Default handler — route everything else through PHP front controller
|
||||
location / {
|
||||
rewrite ^ /index.php$request_uri last;
|
||||
}
|
||||
}
|
||||
10
nextcloud/php-tuning.ini
Normal file
10
nextcloud/php-tuning.ini
Normal file
@@ -0,0 +1,10 @@
|
||||
; OPcache tuning for Nextcloud
|
||||
opcache.interned_strings_buffer=16
|
||||
opcache.max_accelerated_files=10000
|
||||
opcache.revalidate_freq=60
|
||||
opcache.save_comments=1
|
||||
opcache.enable_file_override=1
|
||||
|
||||
; APCu local cache
|
||||
apc.shm_size=64M
|
||||
apc.enable_cli=1
|
||||
28
review.md
28
review.md
@@ -1,14 +1,16 @@
|
||||
# Code Review Issues
|
||||
# Repo Review — nextcloud-selfhosted
|
||||
|
||||
| # | Severity | File | Issue | Status |
|
||||
|----|----------|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|
|
||||
| 1 | Critical | `scripts/deploy.sh` | `SCRIPT_DIR` resolves to `scripts/` but paths assume repo root (e.g. `$SCRIPT_DIR/caddy/docker-compose.yml`). All scripts broken after move to `scripts/`. Fix: use `REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"` | DONE |
|
||||
| 2 | Critical | `scripts/backup.sh` | Same broken `SCRIPT_DIR` path issue | DONE |
|
||||
| 3 | Critical | `scripts/restore.sh` | Same broken `SCRIPT_DIR` path issue | DONE |
|
||||
| 4 | High | `scripts/backup.sh:20` | `pg_dumpall -U nextcloud` hardcodes DB username instead of reading from env | DONE |
|
||||
| 5 | High | `scripts/restore.sh:68` | `psql -U nextcloud` hardcodes DB username instead of reading from env | DONE |
|
||||
| 6 | High | `scripts/deploy.sh:13` | `source .env` in a root-privileged script can execute arbitrary commands. Consider safer parsing or variable validation | DONE |
|
||||
| 7 | Medium | `monitoring/docker-compose.yml` | Docker socket + `/proc` + `/sys` + `/` mounted into Alloy container. Consider using a Docker socket proxy to limit API access | TODO |
|
||||
| 8 | Medium | `caddy/Caddyfile` | No rate limiting configured at the reverse proxy layer | TODO |
|
||||
| 9 | Low | `gitea/docker-compose.yml` | `gitea/gitea:latest-rootless` unpinned — pin to specific version like Nextcloud does | TODO |
|
||||
| 10 | Low | `monitoring/docker-compose.yml` | `grafana/alloy:latest` unpinned — pin to specific version | TODO |
|
||||
| # | Priority | Category | Issue | Location | Suggestion | Status |
|
||||
|----|----------|-------------|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|---------|
|
||||
| 1 | High | Security | `backup.sh` and `restore.sh` use `source` to load `.env` files, which executes arbitrary shell code | `scripts/backup.sh:5-6`, `scripts/restore.sh:5-6` | Replace `source` with the safe `eval "$(grep ...)"` parser used in `deploy.sh:14` | DONE |
|
||||
| 2 | High | Correctness | Cron path hint is wrong — says `$REPO_ROOT/backup.sh` instead of `$REPO_ROOT/scripts/backup.sh` | `scripts/backup.sh:49` | Change to `$REPO_ROOT/scripts/backup.sh` | DONE |
|
||||
| 3 | Medium | Correctness | Postgres readiness check uses `sleep 5` instead of a proper wait | `scripts/restore.sh:66` | Use `docker compose up -d --wait postgres` or poll with `pg_isready` in a loop | DONE |
|
||||
| 4 | Medium | Correctness | `pg_dumpall` output restored with `psql -U $POSTGRES_USER` — role creation statements may fail | `scripts/restore.sh:69` | Restore against the `postgres` database: `psql -U "$POSTGRES_USER" -d postgres` | DONE |
|
||||
| 5 | Medium | Reliability | No Docker log rotation — JSON log driver can fill disk | All `docker-compose.yml` files | Add `logging: { driver: json-file, options: { max-size: "10m", max-file: "3" } }` to each service, or configure in `/etc/docker/daemon.json` | DONE |
|
||||
| 6 | Medium | Security | Alloy container mounts entire root filesystem (`/:/host/root:ro`) — exposes secrets in `.env` files | `monitoring/docker-compose.yml:42` | Mount only needed paths (e.g., `/etc:/host/etc:ro`) or use a more restrictive bind | SKIPPED |
|
||||
| 7 | Medium | Reliability | Rate limits mentioned in commit `0f12c5f` but not present in Caddyfile | `caddy/Caddyfile` | Add `rate_limit` directive or verify the commit wasn't partially reverted | SKIPPED |
|
||||
| 8 | Low | Backup | Caddy TLS certificates (`${DATA_ROOT}/caddy/data/`) not included in backup | `scripts/backup.sh` | Add a `tar` step for `caddy/data` — avoids Let's Encrypt rate limits on restore | DONE |
|
||||
| 9 | Low | Reliability | `deploy.sh` doesn't pull latest images before starting | `scripts/deploy.sh:78-88` | Add `docker compose pull` before each `up -d` call | DONE |
|
||||
| 10 | Low | Security | Redis has no password — reachable from any container on `nextcloud-internal` network | `nextcloud/docker-compose.yml:38-42` | Add `command: redis-server --requirepass $REDIS_PASSWORD` and pass the password to Nextcloud via `REDIS_HOST_PASSWORD` | DONE |
|
||||
| 11 | Low | Reliability | No healthchecks on Nextcloud, Gitea, or Caddy containers | `nextcloud/docker-compose.yml`, `gitea/docker-compose.yml`, `caddy/docker-compose.yml` | Add `healthcheck` blocks (e.g., `curl -f http://localhost` for Nextcloud, `caddy validate` for Caddy) | DONE |
|
||||
| 12 | Low | Reliability | No container resource limits — a runaway process can OOM the VPS | All `docker-compose.yml` files | Add `mem_limit` and `cpus` to at least Nextcloud, Postgres, and Alloy | SKIPPED |
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
source "$REPO_ROOT/.env"
|
||||
source "$REPO_ROOT/nextcloud/.env"
|
||||
set -a
|
||||
eval "$(grep -v '^#' "$REPO_ROOT/.env" | grep -v '^$' | grep '^[A-Za-z_][A-Za-z_0-9]*=' )"
|
||||
set +a
|
||||
set -a
|
||||
eval "$(grep -v '^#' "$REPO_ROOT/nextcloud/.env" | grep -v '^$' | grep '^[A-Za-z_][A-Za-z_0-9]*=' )"
|
||||
set +a
|
||||
DATA_ROOT="${DATA_ROOT:-/opt/docker-data}"
|
||||
|
||||
BACKUP_DIR="/opt/backups"
|
||||
@@ -34,6 +38,13 @@ echo " -> Archiving Gitea data..."
|
||||
tar -czf "$BACKUP_DIR/gitea-$DATE.tar.gz" \
|
||||
-C "$DATA_ROOT" gitea/data gitea/config
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Caddy TLS certificates
|
||||
# ------------------------------------------------------------------
|
||||
echo " -> Archiving Caddy TLS data..."
|
||||
tar -czf "$BACKUP_DIR/caddy-$DATE.tar.gz" \
|
||||
-C "$DATA_ROOT" caddy/data
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Rotate old backups
|
||||
# ------------------------------------------------------------------
|
||||
@@ -46,4 +57,4 @@ ls -lh "$BACKUP_DIR"/*"$DATE"* 2>/dev/null || echo " (no files found)"
|
||||
|
||||
echo ""
|
||||
echo "To schedule daily backups, add to crontab (crontab -e):"
|
||||
echo " 0 3 * * * $REPO_ROOT/backup.sh >> /var/log/backup.log 2>&1"
|
||||
echo " 0 3 * * * $REPO_ROOT/scripts/backup.sh >> /var/log/backup.log 2>&1"
|
||||
|
||||
@@ -61,6 +61,7 @@ echo "==> Creating data directories under $DATA_ROOT..."
|
||||
mkdir -p "$DATA_ROOT"/{caddy/data,caddy/config}
|
||||
mkdir -p "$DATA_ROOT"/{nextcloud/html,nextcloud/data,nextcloud/db}
|
||||
mkdir -p "$DATA_ROOT"/{gitea/data,gitea/config}
|
||||
chown -R 1000:1000 "$DATA_ROOT"/gitea
|
||||
mkdir -p /opt/backups
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -73,19 +74,19 @@ for svc in nextcloud gitea monitoring; do
|
||||
done
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Start stacks in order
|
||||
# Lock down .env files (readable only by root)
|
||||
# ------------------------------------------------------------------
|
||||
echo "==> Starting Caddy..."
|
||||
docker compose -f "$REPO_ROOT/caddy/docker-compose.yml" --env-file "$REPO_ROOT/.env" up -d
|
||||
echo "==> Securing .env files..."
|
||||
for envfile in "$REPO_ROOT"/.env "$REPO_ROOT"/*/.env; do
|
||||
[ -f "$envfile" ] && chmod 600 "$envfile" && chown root:root "$envfile"
|
||||
done
|
||||
|
||||
echo "==> Starting Nextcloud..."
|
||||
docker compose -f "$REPO_ROOT/nextcloud/docker-compose.yml" --env-file "$REPO_ROOT/.env" up -d
|
||||
|
||||
echo "==> Starting Gitea..."
|
||||
docker compose -f "$REPO_ROOT/gitea/docker-compose.yml" --env-file "$REPO_ROOT/.env" up -d
|
||||
|
||||
echo "==> Starting Monitoring..."
|
||||
docker compose -f "$REPO_ROOT/monitoring/docker-compose.yml" --env-file "$REPO_ROOT/.env" up -d
|
||||
# ------------------------------------------------------------------
|
||||
# Start all stacks
|
||||
# ------------------------------------------------------------------
|
||||
echo "==> Pulling and starting all services..."
|
||||
docker compose --env-file "$REPO_ROOT/.env" --project-directory "$REPO_ROOT" pull
|
||||
docker compose --env-file "$REPO_ROOT/.env" --project-directory "$REPO_ROOT" up -d
|
||||
|
||||
echo ""
|
||||
echo "==> All services started. Verify with: docker ps"
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
source "$REPO_ROOT/.env"
|
||||
source "$REPO_ROOT/nextcloud/.env"
|
||||
set -a
|
||||
eval "$(grep -v '^#' "$REPO_ROOT/.env" | grep -v '^$' | grep '^[A-Za-z_][A-Za-z_0-9]*=' )"
|
||||
set +a
|
||||
set -a
|
||||
eval "$(grep -v '^#' "$REPO_ROOT/nextcloud/.env" | grep -v '^$' | grep '^[A-Za-z_][A-Za-z_0-9]*=' )"
|
||||
set +a
|
||||
DATA_ROOT="${DATA_ROOT:-/opt/docker-data}"
|
||||
|
||||
BACKUP_DIR="/opt/backups"
|
||||
@@ -63,10 +67,12 @@ tar -xzf "$GITEA_ARCHIVE" -C "$DATA_ROOT"
|
||||
echo "==> Starting Postgres for DB restore..."
|
||||
docker compose -f "$REPO_ROOT/nextcloud/docker-compose.yml" --env-file "$REPO_ROOT/.env" up -d postgres
|
||||
echo " -> Waiting for Postgres to be ready..."
|
||||
sleep 5
|
||||
until docker exec nextcloud-postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" &>/dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "==> Restoring Nextcloud database..."
|
||||
docker exec -i nextcloud-postgres psql -U "$POSTGRES_USER" < "$DB_DUMP"
|
||||
docker exec -i nextcloud-postgres psql -U "$POSTGRES_USER" -d postgres < "$DB_DUMP"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Start all services
|
||||
|
||||
Reference in New Issue
Block a user