Compare commits

...

43 Commits

Author SHA1 Message Date
Thomas Gräfenstein
2281ebcb6d improved container restart alert 2026-03-22 23:56:27 +01:00
Thomas Gräfenstein
2942ff15bc remove unused /dev/kmsg device mount from cAdvisor (oom_event is disabled) 2026-03-22 23:34:29 +01:00
Thomas Gräfenstein
24e80de43c upgrade cAdvisor to v0.54.1 for Docker 29 containerd image store support 2026-03-22 23:30:24 +01:00
Thomas Gräfenstein
cfc8b61f98 connect cAdvisor to containerd socket for Docker 29 image store compatibility 2026-03-22 23:24:50 +01:00
Thomas Gräfenstein
b063128049 grant cAdvisor privileged access for cgroup v2 container discovery 2026-03-22 23:17:33 +01:00
Thomas Gräfenstein
a07adedd00 fix cAdvisor container discovery by mounting /sys and /var/lib/docker correctly 2026-03-22 23:14:32 +01:00
Thomas Gräfenstein
31705ad888 fix cAdvisor crash by removing unsupported accelerator metric group 2026-03-22 23:06:34 +01:00
Thomas Gräfenstein
b5c5c11114 ensure monitoring stack starts before all other services 2026-03-22 22:55:42 +01:00
Thomas Gräfenstein
926766346c add cAdvisor and document detailed alert queries in README
Add cAdvisor container to the monitoring stack for container-level
metrics. Configure Alloy to scrape cAdvisor. Expand the README
Recommended Alerts section with exact PromQL/LogQL queries, thresholds,
and Grafana alert rule configuration for all five alerts.
2026-03-22 22:51:22 +01:00
Thomas Gräfenstein
c736c23e9a enable NETWORKS in docker-socket-proxy for Alloy container discovery 2026-03-22 21:27:26 +01:00
Thomas Gräfenstein
a02f33e96e move text compression from Caddy to nginx for lower latency
Nginx is closer to the origin, so compressing there avoids an
extra hop. Removes the Caddy encode block for Nextcloud and adds
gzip in nginx with level 4 targeting text, CSS, JS, JSON, XML, SVG.
2026-03-22 21:08:40 +01:00
Thomas Gräfenstein
d62b627093 add .mjs MIME type to nginx to fix NS_ERROR_CORRUPTED_CONTENT
nginx doesn't know .mjs by default and serves it as
application/octet-stream, which breaks ES module loading
and causes Caddy compression mismatches.
2026-03-22 20:56:10 +01:00
Thomas Gräfenstein
fb1de4f079 limit Caddy compression to text content types to fix slow file downloads
Caddy was compressing all responses including binary file downloads
(PDFs, images, videos), which severely throttled download speed to
~130KB/s despite 30MB/s VPS bandwidth. Now only compresses text-based
types (HTML, CSS, JS, JSON, XML, SVG) where compression actually helps.
2026-03-22 20:26:03 +01:00
Thomas Gräfenstein
3bf80f6940 disable file compression temporary 2026-03-22 20:20:37 +01:00
Thomas Gräfenstein
1c2fb3c807 fix nginx redirect loop 2026-03-22 18:12:18 +01:00
Thomas Gräfenstein
b918e713e5 align nginx and Caddy config with official Nextcloud docs
Move security headers to Caddy (edge proxy), remove nginx gzip
(Caddy already compresses), add asset_immutable map for versioned
cache control, add missing static file extensions, fix .well-known
block, and hide X-Powered-By header.
2026-03-22 17:58:26 +01:00
Thomas Gräfenstein
ac3bff9351 fix nginx to fall through to PHP for dynamic assets like theming CSS
Static file locations were returning hard 404s instead of falling
through to PHP, which broke dynamically generated assets like
theming CSS files.
2026-03-22 17:49:45 +01:00
Thomas Gräfenstein
0088c11d5e enable Caddy response compression to fix slow page loads
Caddy was decompressing nginx's gzip responses and sending them
uncompressed to the browser, causing core-common.js (5.7MB) to
take 25s to download. Adding encode zstd gzip compresses it to
1.3MB at the edge.
2026-03-22 17:43:24 +01:00
Thomas Gräfenstein
4f3f4b0487 add swap check command before setup instructions 2026-03-22 17:33:11 +01:00
Thomas Gräfenstein
a51f86ea0a add swap setup instructions to README prerequisites 2026-03-22 17:32:48 +01:00
Thomas Gräfenstein
22198784d3 tune PHP and FPM for 1-core/3GB VPS performance
Reduce FPM workers from 12 to 5 max to stop memory thrashing on
a single-core VPS with 3GB RAM. Add OPcache and APCu tuning to
reduce filesystem stat calls and improve cache hit rates.
2026-03-22 17:31:14 +01:00
Thomas Gräfenstein
0a305a47b9 gitignore claude local settings 2026-03-22 17:21:13 +01:00
Thomas Gräfenstein
d88a8db9f1 fix nginx rewrite loop causing slow page loads and 500 errors
Static file locations now return 404 instead of falling through to
index.php, and the default location uses a clean rewrite to prevent
/index.php/index.php redirect cycles.
2026-03-22 17:19:34 +01:00
Thomas Gräfenstein
995dfcc099 add FPM worker tuning and architecture diagram
Increase PHP-FPM max_children from 5 to 12 to handle concurrent
requests without queuing, sized for a ~3GB VPS. Add Mermaid
architecture diagram to README.
2026-03-22 17:07:43 +01:00
Thomas Gräfenstein
4329cfd3f2 switch nextcloud to FPM + Nginx for better static file performance
Replace the all-in-one Apache image with nextcloud:33-fpm and an Nginx
sidecar that serves static assets directly with gzip compression and
cache headers, avoiding the prefork concurrency bottleneck.
2026-03-22 17:00:33 +01:00
Thomas Gräfenstein
c0c20a42ed add gzip/zstd compression and Redis caching for Nextcloud performance 2026-03-22 16:47:02 +01:00
Thomas Gräfenstein
a17c63a39b remove nextcloud review, all issues fixed 2026-03-22 16:44:15 +01:00
Thomas Gräfenstein
cdec4e3e22 fix trusted_proxies to use CIDR instead of hostname 2026-03-22 16:39:54 +01:00
Thomas Gräfenstein
0e0a6ff1eb add trusted proxy, post-install/upgrade hooks, occ docs and admin review
- Add TRUSTED_PROXIES=caddy to fix reverse proxy header warning
- Add post-installation hook: maintenance window, phone region, DB indices, MIME migrations
- Add post-upgrade hook: DB indices and MIME migrations
- Add occ commands section to README
- Add nextcloud-review.md with admin warning fixes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 16:33:40 +01:00
Thomas Gräfenstein
7225f526da enhanced readme 2026-03-22 16:14:19 +01:00
Thomas Gräfenstein
8b5c9bdbfc bump nextcloud versions 2026-03-22 16:07:18 +01:00
Thomas Gräfenstein
770081397c enhanced readme 2026-03-22 16:00:55 +01:00
Thomas Gräfenstein
8f5b73dffc fix readme and script 2026-03-22 15:46:29 +01:00
Thomas Gräfenstein
5e57d5258a add migration plan 2026-03-22 13:09:13 +01:00
Thomas Gräfenstein
522207b9d9 add claude permissions 2026-03-22 13:09:02 +01:00
Thomas Gräfenstein
09aee112da add local setup doc 2026-03-22 13:02:11 +01:00
Thomas Gräfenstein
158a8e6eb4 update readme 2026-03-22 12:38:24 +01:00
Thomas Gräfenstein
f3eea007f7 improve .env handling 2026-03-22 12:38:17 +01:00
Thomas Gräfenstein
1fed3dde51 simplified docker compose setup 2026-03-22 12:32:37 +01:00
Thomas Gräfenstein
89b806fd5b fix more issues 2026-03-22 12:29:58 +01:00
Thomas Gräfenstein
caa1c7f471 pin versions 2026-03-22 12:23:52 +01:00
Thomas Gräfenstein
0f12c5f5a8 added basic caddy rate limits 2026-03-22 12:22:00 +01:00
Thomas Gräfenstein
ce9dba4923 limit docker socket api access to alloy 2026-03-22 12:19:10 +01:00
21 changed files with 732 additions and 73 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.env .env
.idea/ .idea/
*.iml *.iml
.claude/settings.local.json

View File

@@ -8,7 +8,7 @@ GitOps-style Docker Compose setup for a self-hosted VPS running Nextcloud, Gitea
## Architecture ## 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. - **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. - **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. - **monitoring/** — Grafana Alloy collecting Docker logs (Loki) and node metrics (Prometheus) to Grafana Cloud. Has its own `.env` for cloud credentials.
Key design patterns: 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 - 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) - 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/`) - 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) # Deploy everything (installs Docker if needed, creates dirs, starts all stacks)
./scripts/deploy.sh ./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 # 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 up -d
docker compose -f <service>/docker-compose.yml --env-file .env logs -f 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 ## Adding a New Service
1. Create `myapp/docker-compose.yml` joining the `proxy` external network, with data under `${DATA_ROOT}/myapp/` 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` 2. Add `- path: myapp/docker-compose.yml` to root `docker-compose.yml`
3. Add data directory creation to `scripts/deploy.sh` 3. Add reverse proxy entry in `caddy/Caddyfile`
4. Add backup steps to `scripts/backup.sh` if it has persistent data 4. Add data directory creation to `scripts/deploy.sh`
5. Create DNS A record for the subdomain 5. Add backup steps to `scripts/backup.sh` if it has persistent data
6. Create DNS A record for the subdomain
## Environment Files ## Environment Files

295
README.md
View File

@@ -2,9 +2,51 @@
Docker Compose setup for a self-hosted VPS running Nextcloud, Gitea, and monitoring — managed as a GitOps-style repo. 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 ## Prerequisites
- A VPS with SSH access - A VPS with SSH access (minimum 1 core, 3 GB RAM)
- Domain `t-gstone.de` with DNS control - Domain `t-gstone.de` with DNS control
- Git installed locally - Git installed locally
@@ -14,6 +56,24 @@ Check your VPS OS:
cat /etc/os-release 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 ## DNS Setup
Create these A records pointing to your VPS IP: 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>` | | `nextcloud.t-gstone.de` | `<VPS_IP>` |
| `git.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 ## Quick Start
```bash ```bash
@@ -69,19 +216,44 @@ All persistent data lives under `/opt/docker-data/`:
## Managing Services ## 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 ```bash
# Restart just Nextcloud # Start / restart all services
docker compose -f nextcloud/docker-compose.yml --env-file .env up -d docker compose --env-file .env up -d
# View logs for Gitea # View logs for all services
docker compose -f gitea/docker-compose.yml --env-file .env logs -f docker compose --env-file .env logs -f
# Stop everything # Stop everything
for svc in monitoring gitea nextcloud caddy; do docker compose --env-file .env down
docker compose -f $svc/docker-compose.yml --env-file .env down ```
done
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 ## Adding a New Service
@@ -91,16 +263,17 @@ done
- Join the `proxy` external network - Join the `proxy` external network
- Bind mount data to `${DATA_ROOT}/myapp/` - Bind mount data to `${DATA_ROOT}/myapp/`
- Add `myapp/.env.example` if the service needs secrets - 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 { myapp.t-gstone.de {
reverse_proxy myapp:8080 reverse_proxy myapp:8080
} }
``` ```
4. Reload Caddy: `docker exec caddy caddy reload --config /etc/caddy/Caddyfile` 5. 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 a DNS A record for `myapp.t-gstone.de` -> VPS IP
6. Add data directory creation to `scripts/deploy.sh` 7. Add data directory creation to `scripts/deploy.sh`
7. Add backup steps to `scripts/backup.sh` if the service has persistent data 8. Add backup steps to `scripts/backup.sh` if the service has persistent data
## Backup & Restore ## 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 2. Go to **My Account** -> **Grafana Cloud** -> your stack
3. Find your Loki and Prometheus endpoints + credentials 3. Find your Loki and Prometheus endpoints + credentials
4. Fill in `monitoring/.env` with those values 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 ### 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 | | Alert | Condition | Severity |
|----------------------|-----------------------------------------------------------------------|----------| |----------------------|--------------------------------------|----------|
| Disk usage high | `node_filesystem_avail_bytes` / `node_filesystem_size_bytes` < 0.2 | Critical | | Disk usage high | Available disk < 20% | Critical |
| Container restarting | Container restart count > 3 in 10 min | Warning | | Container restarting | Restart count > 3 in 10 min | Warning |
| High memory usage | `node_memory_MemAvailable_bytes` / `node_memory_MemTotal_bytes` < 0.1 | Warning | | High memory usage | Available memory < 10% | Warning |
| High CPU usage | `node_cpu_seconds_total` idle < 10% sustained 5 min | Warning | | High CPU usage | CPU usage > 90% sustained 5 min | Warning |
| Nextcloud cron stale | No log line from `nextcloud-cron` in 15 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 ### Recommended Dashboards

View File

@@ -1,10 +1,22 @@
{
servers {
timeouts {
read_header 10s
idle 60s
}
max_header_size 16KB
}
}
nextcloud.t-gstone.de { nextcloud.t-gstone.de {
reverse_proxy nextcloud:80 reverse_proxy nextcloud-nginx:80
header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
header Referrer-Policy "no-referrer"
redir /.well-known/carddav /remote.php/dav/ 301 header X-Content-Type-Options "nosniff"
redir /.well-known/caldav /remote.php/dav/ 301 header X-Frame-Options "SAMEORIGIN"
header X-Permitted-Cross-Domain-Policies "none"
header X-Robots-Tag "noindex, nofollow"
request_body { request_body {
max_size 10G max_size 10G

View File

@@ -3,6 +3,8 @@ services:
image: caddy:2-alpine image: caddy:2-alpine
container_name: caddy container_name: caddy
restart: unless-stopped restart: unless-stopped
depends_on:
- alloy
ports: ports:
- "80:80" - "80:80"
- "443:443" - "443:443"
@@ -13,6 +15,16 @@ services:
- ${DATA_ROOT}/caddy/config:/config - ${DATA_ROOT}/caddy/config:/config
networks: networks:
- proxy - 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: networks:
proxy: proxy:

5
docker-compose.yml Normal file
View 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

View File

@@ -1,8 +1,10 @@
services: services:
gitea: gitea:
image: gitea/gitea:latest-rootless image: gitea/gitea:1.25.5-rootless
container_name: gitea container_name: gitea
restart: unless-stopped restart: unless-stopped
depends_on:
- alloy
env_file: .env env_file: .env
volumes: volumes:
- ${DATA_ROOT}/gitea/data:/var/lib/gitea - ${DATA_ROOT}/gitea/data:/var/lib/gitea
@@ -11,6 +13,16 @@ services:
- "2222:2222" - "2222:2222"
networks: networks:
- proxy - 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: networks:
proxy: proxy:

73
migration.md Normal file
View 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

View File

@@ -3,7 +3,7 @@
// ============================================================ // ============================================================
discovery.docker "containers" { discovery.docker "containers" {
host = "unix:///var/run/docker.sock" host = "http://docker-socket-proxy:2375"
} }
discovery.relabel "containers" { discovery.relabel "containers" {
@@ -21,7 +21,7 @@ discovery.relabel "containers" {
} }
loki.source.docker "containers" { loki.source.docker "containers" {
host = "unix:///var/run/docker.sock" host = "http://docker-socket-proxy:2375"
targets = discovery.relabel.containers.output targets = discovery.relabel.containers.output
forward_to = [loki.write.grafana_cloud.receiver] forward_to = [loki.write.grafana_cloud.receiver]
} }
@@ -54,6 +54,18 @@ prometheus.scrape "node" {
scrape_interval = "60s" 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" { prometheus.remote_write "grafana_cloud" {
endpoint { endpoint {
url = env("GRAFANA_CLOUD_PROMETHEUS_URL") url = env("GRAFANA_CLOUD_PROMETHEUS_URL")

View File

@@ -1,12 +1,70 @@
services: 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: alloy:
image: grafana/alloy:latest image: grafana/alloy:v1.14.1
container_name: alloy container_name: alloy
restart: unless-stopped restart: unless-stopped
depends_on:
- docker-socket-proxy
env_file: .env env_file: .env
volumes: volumes:
- ./config.alloy:/etc/alloy/config.alloy:ro - ./config.alloy:/etc/alloy/config.alloy:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /proc:/host/proc:ro - /proc:/host/proc:ro
- /sys:/host/sys:ro - /sys:/host/sys:ro
- /:/host/root:ro - /:/host/root:ro
@@ -17,6 +75,11 @@ services:
pid: host pid: host
networks: networks:
- monitoring - monitoring
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks: networks:
monitoring: monitoring:

View File

@@ -11,3 +11,6 @@ NEXTCLOUD_ADMIN_PASSWORD=CHANGE_ME_admin_password
NEXTCLOUD_TRUSTED_DOMAINS=nextcloud.t-gstone.de NEXTCLOUD_TRUSTED_DOMAINS=nextcloud.t-gstone.de
OVERWRITEPROTOCOL=https OVERWRITEPROTOCOL=https
OVERWRITECLIURL=https://nextcloud.t-gstone.de OVERWRITECLIURL=https://nextcloud.t-gstone.de
# Redis
REDIS_PASSWORD=CHANGE_ME_redis_password

View File

@@ -1,6 +1,6 @@
services: services:
nextcloud: nextcloud:
image: nextcloud:29-apache image: nextcloud:33-fpm
container_name: nextcloud container_name: nextcloud
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
@@ -12,17 +12,53 @@ services:
environment: environment:
- POSTGRES_HOST=postgres - POSTGRES_HOST=postgres
- REDIS_HOST=redis - REDIS_HOST=redis
- REDIS_HOST_PASSWORD=${REDIS_PASSWORD}
- TRUSTED_PROXIES=172.18.0.0/16
volumes: volumes:
- ${DATA_ROOT}/nextcloud/html:/var/www/html - ${DATA_ROOT}/nextcloud/html:/var/www/html
- ${DATA_ROOT}/nextcloud/data:/var/www/html/data - ${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: networks:
- proxy - proxy
- nextcloud-internal - nextcloud-internal
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
postgres: postgres:
image: postgres:16-alpine image: postgres:17-alpine
container_name: nextcloud-postgres container_name: nextcloud-postgres
restart: unless-stopped restart: unless-stopped
depends_on:
- alloy
env_file: .env env_file: .env
volumes: volumes:
- ${DATA_ROOT}/nextcloud/db:/var/lib/postgresql/data - ${DATA_ROOT}/nextcloud/db:/var/lib/postgresql/data
@@ -33,16 +69,30 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
redis: redis:
image: redis:7-alpine image: redis:8-alpine
container_name: nextcloud-redis container_name: nextcloud-redis
restart: unless-stopped restart: unless-stopped
depends_on:
- alloy
command: redis-server --requirepass ${REDIS_PASSWORD}
env_file: .env
networks: networks:
- nextcloud-internal - nextcloud-internal
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
cron: cron:
image: nextcloud:29-apache image: nextcloud:33-fpm
container_name: nextcloud-cron container_name: nextcloud-cron
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
@@ -53,6 +103,11 @@ services:
- ${DATA_ROOT}/nextcloud/data:/var/www/html/data - ${DATA_ROOT}/nextcloud/data:/var/www/html/data
networks: networks:
- nextcloud-internal - nextcloud-internal
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks: networks:
proxy: proxy:

View 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

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

View File

@@ -1,14 +1,16 @@
# Code Review Issues # Repo Review — nextcloud-selfhosted
| # | Severity | File | Issue | Status | | # | Priority | Category | Issue | Location | Suggestion | 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 | | 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 | Critical | `scripts/backup.sh` | Same broken `SCRIPT_DIR` path issue | 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 | Critical | `scripts/restore.sh` | Same broken `SCRIPT_DIR` path issue | 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 | High | `scripts/backup.sh:20` | `pg_dumpall -U nextcloud` hardcodes DB username instead of reading from env | 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 | High | `scripts/restore.sh:68` | `psql -U nextcloud` hardcodes DB username instead of reading from env | 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 | High | `scripts/deploy.sh:13` | `source .env` in a root-privileged script can execute arbitrary commands. Consider safer parsing or variable validation | 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 | `monitoring/docker-compose.yml` | Docker socket + `/proc` + `/sys` + `/` mounted into Alloy container. Consider using a Docker socket proxy to limit API access | TODO | | 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 | Medium | `caddy/Caddyfile` | No rate limiting configured at the reverse proxy layer | TODO | | 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 | `gitea/docker-compose.yml` | `gitea/gitea:latest-rootless` unpinned — pin to specific version like Nextcloud does | TODO | | 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 | `monitoring/docker-compose.yml` | `grafana/alloy:latest` unpinned — pin to specific version | TODO | | 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 |

View File

@@ -2,8 +2,12 @@
set -euo pipefail set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$REPO_ROOT/.env" set -a
source "$REPO_ROOT/nextcloud/.env" 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}" DATA_ROOT="${DATA_ROOT:-/opt/docker-data}"
BACKUP_DIR="/opt/backups" BACKUP_DIR="/opt/backups"
@@ -34,6 +38,13 @@ echo " -> Archiving Gitea data..."
tar -czf "$BACKUP_DIR/gitea-$DATE.tar.gz" \ tar -czf "$BACKUP_DIR/gitea-$DATE.tar.gz" \
-C "$DATA_ROOT" gitea/data gitea/config -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 # Rotate old backups
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -46,4 +57,4 @@ ls -lh "$BACKUP_DIR"/*"$DATE"* 2>/dev/null || echo " (no files found)"
echo "" echo ""
echo "To schedule daily backups, add to crontab (crontab -e):" 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"

View File

@@ -61,6 +61,7 @@ echo "==> Creating data directories under $DATA_ROOT..."
mkdir -p "$DATA_ROOT"/{caddy/data,caddy/config} mkdir -p "$DATA_ROOT"/{caddy/data,caddy/config}
mkdir -p "$DATA_ROOT"/{nextcloud/html,nextcloud/data,nextcloud/db} mkdir -p "$DATA_ROOT"/{nextcloud/html,nextcloud/data,nextcloud/db}
mkdir -p "$DATA_ROOT"/{gitea/data,gitea/config} mkdir -p "$DATA_ROOT"/{gitea/data,gitea/config}
chown -R 1000:1000 "$DATA_ROOT"/gitea
mkdir -p /opt/backups mkdir -p /opt/backups
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -73,19 +74,19 @@ for svc in nextcloud gitea monitoring; do
done done
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Start stacks in order # Lock down .env files (readable only by root)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
echo "==> Starting Caddy..." echo "==> Securing .env files..."
docker compose -f "$REPO_ROOT/caddy/docker-compose.yml" --env-file "$REPO_ROOT/.env" up -d 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 # Start all stacks
# ------------------------------------------------------------------
echo "==> Starting Gitea..." echo "==> Pulling and starting all services..."
docker compose -f "$REPO_ROOT/gitea/docker-compose.yml" --env-file "$REPO_ROOT/.env" up -d 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 "==> Starting Monitoring..."
docker compose -f "$REPO_ROOT/monitoring/docker-compose.yml" --env-file "$REPO_ROOT/.env" up -d
echo "" echo ""
echo "==> All services started. Verify with: docker ps" echo "==> All services started. Verify with: docker ps"

View File

@@ -2,8 +2,12 @@
set -euo pipefail set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$REPO_ROOT/.env" set -a
source "$REPO_ROOT/nextcloud/.env" 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}" DATA_ROOT="${DATA_ROOT:-/opt/docker-data}"
BACKUP_DIR="/opt/backups" BACKUP_DIR="/opt/backups"
@@ -63,10 +67,12 @@ tar -xzf "$GITEA_ARCHIVE" -C "$DATA_ROOT"
echo "==> Starting Postgres for DB restore..." echo "==> Starting Postgres for DB restore..."
docker compose -f "$REPO_ROOT/nextcloud/docker-compose.yml" --env-file "$REPO_ROOT/.env" up -d postgres docker compose -f "$REPO_ROOT/nextcloud/docker-compose.yml" --env-file "$REPO_ROOT/.env" up -d postgres
echo " -> Waiting for Postgres to be ready..." 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..." 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 # Start all services