vibecarbon 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/carbon/.env.example +3 -2
- package/carbon/DEVELOPMENT.md +18 -3
- package/carbon/PRODUCTION.md +1 -1
- package/carbon/content/docs/cli.mdx +3 -3
- package/carbon/content/docs/deployment.mdx +1 -1
- package/carbon/content/docs/development.mdx +1 -1
- package/carbon/content/docs/getting-started.mdx +0 -1
- package/carbon/scripts/dev-init.js +1 -1
- package/carbon/scripts/generate-dev-configs.sh +5 -7
- package/carbon/scripts/validate-dev-configs.sh +5 -7
- package/carbon/src/client/components/TechStackSection.tsx +0 -68
- package/carbon/src/client/index.css +0 -14
- package/carbon/src/client/locales/de.json +0 -18
- package/carbon/src/client/locales/en.json +86 -36
- package/carbon/src/client/locales/es.json +0 -18
- package/carbon/src/client/locales/fr.json +0 -18
- package/carbon/src/client/locales/pt.json +0 -18
- package/carbon/src/client/pages/Home.tsx +0 -3
- package/package.json +4 -2
- package/services/index.json +29 -0
- package/services/metabase/compose/docker-compose.override.yml +14 -0
- package/services/metabase/compose/docker-compose.prod.yml +29 -0
- package/services/metabase/compose/docker-compose.yml +68 -0
- package/services/metabase/db/init.sh +29 -0
- package/services/metabase/docs/integration.md +191 -0
- package/services/metabase/k8s/deployment.yaml +88 -0
- package/services/metabase/k8s/ingressroute.yaml +22 -0
- package/services/metabase/k8s/kustomization.yaml +9 -0
- package/services/metabase/k8s/network-policy.yaml +74 -0
- package/services/metabase/k8s/pvc.yaml +12 -0
- package/services/metabase/k8s/service.yaml +15 -0
- package/services/metabase/manifest.json +34 -0
- package/services/metabase/scripts/setup.sh +152 -0
- package/services/n8n/compose/docker-compose.override.yml +14 -0
- package/services/n8n/compose/docker-compose.prod.yml +32 -0
- package/services/n8n/compose/docker-compose.yml +99 -0
- package/services/n8n/db/init.sh +25 -0
- package/services/n8n/docs/SSO.md +124 -0
- package/services/n8n/docs/integration.md +155 -0
- package/services/n8n/hooks/hooks.js +131 -0
- package/services/n8n/k8s/deployment.yaml +116 -0
- package/services/n8n/k8s/ingressroute.yaml +24 -0
- package/services/n8n/k8s/kustomization.yaml +9 -0
- package/services/n8n/k8s/network-policy.yaml +82 -0
- package/services/n8n/k8s/pvc.yaml +14 -0
- package/services/n8n/k8s/service.yaml +14 -0
- package/services/n8n/manifest.json +36 -0
- package/services/n8n/scripts/setup.sh +42 -0
- package/{carbon/docker-compose.observability.prod.yml → services/observability/compose/docker-compose.prod.yml} +8 -0
- package/services/observability/docs/integration.md +31 -0
- package/services/observability/k8s/grafana-deployment.yaml +107 -0
- package/services/observability/k8s/grafana-provisioning-configmap.yaml +65 -0
- package/services/observability/k8s/grafana-pvc.yaml +14 -0
- package/services/observability/k8s/grafana-service.yaml +16 -0
- package/services/observability/k8s/ingressroute.yaml +22 -0
- package/services/observability/k8s/kustomization.yaml +24 -0
- package/services/observability/k8s/loki-configmap.yaml +74 -0
- package/services/observability/k8s/loki-deployment.yaml +72 -0
- package/services/observability/k8s/loki-pvc.yaml +14 -0
- package/services/observability/k8s/loki-service.yaml +16 -0
- package/services/observability/k8s/network-policy.yaml +141 -0
- package/services/observability/k8s/prometheus-configmap.yaml +80 -0
- package/services/observability/k8s/prometheus-deployment.yaml +78 -0
- package/services/observability/k8s/prometheus-pvc.yaml +14 -0
- package/services/observability/k8s/prometheus-rbac.yaml +55 -0
- package/services/observability/k8s/prometheus-service.yaml +16 -0
- package/services/observability/manifest.json +28 -0
- package/services/redis/compose/docker-compose.prod.yml +10 -0
- package/services/redis/compose/docker-compose.yml +24 -0
- package/services/redis/docs/integration.md +148 -0
- package/services/redis/k8s/deployment.yaml +70 -0
- package/services/redis/k8s/kustomization.yaml +8 -0
- package/services/redis/k8s/network-policy.yaml +20 -0
- package/services/redis/k8s/pvc.yaml +12 -0
- package/services/redis/k8s/service.yaml +15 -0
- package/services/redis/manifest.json +30 -0
- package/services/s3/compose/docker-compose.s3-ha.yml +86 -0
- package/services/s3/compose/docker-compose.s3.yml +35 -0
- package/services/s3/manifest.json +87 -0
- package/src/add.js +57 -89
- package/src/cli.js +1 -1
- package/src/create.js +3 -87
- package/src/lib/deploy/bundle.js +4 -2
- package/src/lib/deploy/compose/index.js +45 -17
- package/src/lib/deploy/orchestrator.js +13 -1
- package/src/remove.js +9 -6
- package/carbon/content/docs/optional-services.mdx +0 -160
- package/carbon/src/client/components/MetricsStrip.tsx +0 -48
- /package/{carbon/docker-compose.observability.override.yml → services/observability/compose/docker-compose.override.yml} +0 -0
- /package/{carbon/docker-compose.observability.yml → services/observability/compose/docker-compose.yml} +0 -0
- /package/{carbon → services/observability}/volumes/grafana/dashboards/logs.json +0 -0
- /package/{carbon → services/observability}/volumes/grafana/dashboards/overview.json +0 -0
- /package/{carbon → services/observability}/volumes/grafana/dashboards/postgresql.json +0 -0
- /package/{carbon → services/observability}/volumes/grafana/dashboards.dev/logs.json +0 -0
- /package/{carbon → services/observability}/volumes/grafana/dashboards.dev/overview.json +0 -0
- /package/{carbon → services/observability}/volumes/grafana/dashboards.dev/postgresql.json +0 -0
- /package/{carbon → services/observability}/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +0 -0
- /package/{carbon → services/observability}/volumes/grafana/provisioning/dashboards/dashboards.yml +0 -0
- /package/{carbon → services/observability}/volumes/grafana/provisioning/datasources/datasources.yml +0 -0
- /package/{carbon → services/observability}/volumes/loki/loki-config.yml +0 -0
- /package/{carbon → services/observability}/volumes/prometheus/prometheus.yml +0 -0
- /package/{carbon → services/observability}/volumes/promtail/promtail-config.yml +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Redis - In-memory data store
|
|
2
|
+
# Added via: vibecarbon add redis
|
|
3
|
+
|
|
4
|
+
services:
|
|
5
|
+
redis:
|
|
6
|
+
image: redis:alpine
|
|
7
|
+
container_name: {{PROJECT_NAME}}-redis
|
|
8
|
+
restart: unless-stopped
|
|
9
|
+
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
|
|
10
|
+
ports:
|
|
11
|
+
- "6379:6379"
|
|
12
|
+
volumes:
|
|
13
|
+
- redis_data:/data
|
|
14
|
+
networks:
|
|
15
|
+
- vibecarbon-network
|
|
16
|
+
healthcheck:
|
|
17
|
+
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
|
|
18
|
+
interval: 10s
|
|
19
|
+
timeout: 5s
|
|
20
|
+
retries: 5
|
|
21
|
+
|
|
22
|
+
volumes:
|
|
23
|
+
redis_data:
|
|
24
|
+
driver: local
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Redis Integration Guide
|
|
2
|
+
|
|
3
|
+
Redis is an in-memory data store used for caching, session management, rate limiting, and pub/sub messaging.
|
|
4
|
+
|
|
5
|
+
## Connection Details
|
|
6
|
+
|
|
7
|
+
- **Host**: `redis` (internal) / `localhost` (external)
|
|
8
|
+
- **Port**: `6379`
|
|
9
|
+
- **Password**: See `REDIS_PASSWORD` in `.env.local`
|
|
10
|
+
|
|
11
|
+
## Usage in Your Application
|
|
12
|
+
|
|
13
|
+
### Install Redis Client
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add ioredis
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Server-Side Integration
|
|
20
|
+
|
|
21
|
+
Create `src/server/lib/redis.ts`:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import Redis from 'ioredis';
|
|
25
|
+
import { env } from './env';
|
|
26
|
+
|
|
27
|
+
export const redis = new Redis({
|
|
28
|
+
host: env.REDIS_HOST || 'redis',
|
|
29
|
+
port: parseInt(env.REDIS_PORT || '6379'),
|
|
30
|
+
password: env.REDIS_PASSWORD,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Connection event handlers
|
|
34
|
+
redis.on('connect', () => console.log('Redis connected'));
|
|
35
|
+
redis.on('error', (err) => console.error('Redis error:', err));
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Caching Example
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { redis } from './lib/redis';
|
|
42
|
+
|
|
43
|
+
// Cache a value for 1 hour
|
|
44
|
+
await redis.setex('user:123', 3600, JSON.stringify(userData));
|
|
45
|
+
|
|
46
|
+
// Retrieve cached value
|
|
47
|
+
const cached = await redis.get('user:123');
|
|
48
|
+
if (cached) {
|
|
49
|
+
return JSON.parse(cached);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Session Storage
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { redis } from './lib/redis';
|
|
57
|
+
|
|
58
|
+
// Store session
|
|
59
|
+
await redis.hset(`session:${sessionId}`, {
|
|
60
|
+
userId: user.id,
|
|
61
|
+
email: user.email,
|
|
62
|
+
createdAt: Date.now(),
|
|
63
|
+
});
|
|
64
|
+
await redis.expire(`session:${sessionId}`, 86400); // 24 hours
|
|
65
|
+
|
|
66
|
+
// Retrieve session
|
|
67
|
+
const session = await redis.hgetall(`session:${sessionId}`);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Rate Limiting
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { redis } from './lib/redis';
|
|
74
|
+
|
|
75
|
+
async function rateLimit(key: string, limit: number, windowSecs: number): Promise<boolean> {
|
|
76
|
+
const current = await redis.incr(key);
|
|
77
|
+
if (current === 1) {
|
|
78
|
+
await redis.expire(key, windowSecs);
|
|
79
|
+
}
|
|
80
|
+
return current <= limit;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Usage: 100 requests per minute per IP
|
|
84
|
+
const allowed = await rateLimit(`ratelimit:${ip}`, 100, 60);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Pub/Sub Example
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import Redis from 'ioredis';
|
|
91
|
+
|
|
92
|
+
const subscriber = new Redis({ /* config */ });
|
|
93
|
+
const publisher = new Redis({ /* config */ });
|
|
94
|
+
|
|
95
|
+
// Subscribe to channel
|
|
96
|
+
subscriber.subscribe('notifications', (err, count) => {
|
|
97
|
+
console.log(`Subscribed to ${count} channels`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
subscriber.on('message', (channel, message) => {
|
|
101
|
+
console.log(`Received ${message} from ${channel}`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Publish message
|
|
105
|
+
await publisher.publish('notifications', JSON.stringify({ type: 'alert', data: '...' }));
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Environment Variables
|
|
109
|
+
|
|
110
|
+
Add to your `.env.local`:
|
|
111
|
+
|
|
112
|
+
```env
|
|
113
|
+
REDIS_HOST=redis
|
|
114
|
+
REDIS_PORT=6379
|
|
115
|
+
REDIS_PASSWORD=your-generated-password
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Docker Commands
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# View Redis logs
|
|
122
|
+
pnpm docker:logs redis
|
|
123
|
+
|
|
124
|
+
# Connect to Redis CLI
|
|
125
|
+
docker exec -it {{PROJECT_NAME}}-redis redis-cli -a $REDIS_PASSWORD
|
|
126
|
+
|
|
127
|
+
# Monitor Redis commands in real-time
|
|
128
|
+
docker exec -it {{PROJECT_NAME}}-redis redis-cli -a $REDIS_PASSWORD MONITOR
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Performance Tips
|
|
132
|
+
|
|
133
|
+
1. **Use pipelining** for multiple commands:
|
|
134
|
+
```typescript
|
|
135
|
+
const pipeline = redis.pipeline();
|
|
136
|
+
pipeline.get('key1');
|
|
137
|
+
pipeline.get('key2');
|
|
138
|
+
const results = await pipeline.exec();
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
2. **Set appropriate TTLs** to prevent memory bloat
|
|
142
|
+
|
|
143
|
+
3. **Use Redis data structures** (hashes, sets, sorted sets) instead of serialized JSON when possible
|
|
144
|
+
|
|
145
|
+
4. **Monitor memory usage**:
|
|
146
|
+
```bash
|
|
147
|
+
redis-cli -a $REDIS_PASSWORD INFO memory
|
|
148
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
apiVersion: apps/v1
|
|
2
|
+
kind: Deployment
|
|
3
|
+
metadata:
|
|
4
|
+
name: redis
|
|
5
|
+
labels:
|
|
6
|
+
app: redis
|
|
7
|
+
spec:
|
|
8
|
+
replicas: 1
|
|
9
|
+
selector:
|
|
10
|
+
matchLabels:
|
|
11
|
+
app: redis
|
|
12
|
+
template:
|
|
13
|
+
metadata:
|
|
14
|
+
labels:
|
|
15
|
+
app: redis
|
|
16
|
+
spec:
|
|
17
|
+
containers:
|
|
18
|
+
- name: redis
|
|
19
|
+
image: redis:alpine
|
|
20
|
+
command:
|
|
21
|
+
- redis-server
|
|
22
|
+
- --appendonly
|
|
23
|
+
- 'yes'
|
|
24
|
+
- --requirepass
|
|
25
|
+
- $(REDIS_PASSWORD)
|
|
26
|
+
ports:
|
|
27
|
+
- containerPort: 6379
|
|
28
|
+
env:
|
|
29
|
+
- name: REDIS_PASSWORD
|
|
30
|
+
valueFrom:
|
|
31
|
+
secretKeyRef:
|
|
32
|
+
name: vibecarbon-secrets
|
|
33
|
+
key: REDIS_PASSWORD
|
|
34
|
+
volumeMounts:
|
|
35
|
+
- name: redis-data
|
|
36
|
+
mountPath: /data
|
|
37
|
+
resources:
|
|
38
|
+
requests:
|
|
39
|
+
memory: 128Mi
|
|
40
|
+
cpu: 100m
|
|
41
|
+
limits:
|
|
42
|
+
memory: 512Mi
|
|
43
|
+
cpu: 500m
|
|
44
|
+
livenessProbe:
|
|
45
|
+
exec:
|
|
46
|
+
command:
|
|
47
|
+
- redis-cli
|
|
48
|
+
- -a
|
|
49
|
+
- $(REDIS_PASSWORD)
|
|
50
|
+
- ping
|
|
51
|
+
initialDelaySeconds: 10
|
|
52
|
+
periodSeconds: 10
|
|
53
|
+
readinessProbe:
|
|
54
|
+
exec:
|
|
55
|
+
command:
|
|
56
|
+
- redis-cli
|
|
57
|
+
- -a
|
|
58
|
+
- $(REDIS_PASSWORD)
|
|
59
|
+
- ping
|
|
60
|
+
initialDelaySeconds: 5
|
|
61
|
+
periodSeconds: 5
|
|
62
|
+
securityContext:
|
|
63
|
+
allowPrivilegeEscalation: false
|
|
64
|
+
volumes:
|
|
65
|
+
- name: redis-data
|
|
66
|
+
persistentVolumeClaim:
|
|
67
|
+
claimName: redis-pvc
|
|
68
|
+
securityContext:
|
|
69
|
+
seccompProfile:
|
|
70
|
+
type: RuntimeDefault
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
apiVersion: networking.k8s.io/v1
|
|
2
|
+
kind: NetworkPolicy
|
|
3
|
+
metadata:
|
|
4
|
+
name: redis-policy
|
|
5
|
+
namespace: vibecarbon
|
|
6
|
+
spec:
|
|
7
|
+
podSelector:
|
|
8
|
+
matchLabels:
|
|
9
|
+
app: redis
|
|
10
|
+
policyTypes:
|
|
11
|
+
- Ingress
|
|
12
|
+
ingress:
|
|
13
|
+
# Allow connections from the application server only
|
|
14
|
+
- from:
|
|
15
|
+
- podSelector:
|
|
16
|
+
matchLabels:
|
|
17
|
+
app: vibecarbon-app
|
|
18
|
+
ports:
|
|
19
|
+
- protocol: TCP
|
|
20
|
+
port: 6379
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "redis",
|
|
3
|
+
"description": "In-memory data store for caching and sessions",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"port": 6379,
|
|
6
|
+
"url": "redis://localhost:6379",
|
|
7
|
+
|
|
8
|
+
"files": {
|
|
9
|
+
"compose/docker-compose.yml": "docker-compose.redis.yml",
|
|
10
|
+
"compose/docker-compose.prod.yml": "docker-compose.redis.prod.yml",
|
|
11
|
+
"k8s/": "k8s/base/redis/",
|
|
12
|
+
"docs/integration.md": "redis-integration.md"
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
"envVars": {
|
|
16
|
+
"REDIS_PASSWORD": { "generate": "password", "length": 32 }
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
"kustomization": {
|
|
20
|
+
"entry": "redis/"
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
"credentials": {
|
|
24
|
+
"title": "Redis",
|
|
25
|
+
"fields": [
|
|
26
|
+
{ "label": "Host", "value": "localhost:6379" },
|
|
27
|
+
{ "label": "Credentials", "value": "Stored in .env.local (REDIS_PASSWORD)" }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# S3 HA Storage Backend Configuration
|
|
2
|
+
# Overlay for Supabase Storage with cross-region replication via rclone
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.s3-ha.yml up -d
|
|
6
|
+
#
|
|
7
|
+
# Required environment variables in .env:
|
|
8
|
+
# S3_ACCESS_KEY - Hetzner S3 access key
|
|
9
|
+
# S3_SECRET_KEY - Hetzner S3 secret key
|
|
10
|
+
# S3_PRIMARY_BUCKET - Primary region bucket name
|
|
11
|
+
# S3_STANDBY_BUCKET - Standby region bucket name
|
|
12
|
+
# S3_PRIMARY_ENDPOINT - Primary region endpoint
|
|
13
|
+
# S3_STANDBY_ENDPOINT - Standby region endpoint
|
|
14
|
+
# S3_PRIMARY_REGION - Primary region (e.g., fsn1)
|
|
15
|
+
# S3_STANDBY_REGION - Standby region (e.g., hel1)
|
|
16
|
+
# S3_SYNC_INTERVAL - Sync interval in minutes (default: 5)
|
|
17
|
+
|
|
18
|
+
services:
|
|
19
|
+
storage:
|
|
20
|
+
environment:
|
|
21
|
+
# Override storage backend from 'file' to 's3'
|
|
22
|
+
STORAGE_BACKEND: s3
|
|
23
|
+
# S3 bucket configuration (uses primary bucket)
|
|
24
|
+
GLOBAL_S3_BUCKET: ${S3_PRIMARY_BUCKET}
|
|
25
|
+
GLOBAL_S3_ENDPOINT: ${S3_PRIMARY_ENDPOINT}
|
|
26
|
+
GLOBAL_S3_FORCE_PATH_STYLE: "true"
|
|
27
|
+
REGION: ${S3_PRIMARY_REGION}
|
|
28
|
+
# S3 credentials
|
|
29
|
+
AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
|
|
30
|
+
AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
|
|
31
|
+
|
|
32
|
+
imgproxy:
|
|
33
|
+
environment:
|
|
34
|
+
# Enable S3 backend for image transformations
|
|
35
|
+
IMGPROXY_USE_S3: "true"
|
|
36
|
+
AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
|
|
37
|
+
AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
|
|
38
|
+
AWS_REGION: ${S3_PRIMARY_REGION}
|
|
39
|
+
IMGPROXY_S3_ENDPOINT: ${S3_PRIMARY_ENDPOINT}
|
|
40
|
+
|
|
41
|
+
# Cross-region S3 synchronization service
|
|
42
|
+
s3-sync:
|
|
43
|
+
image: rclone/rclone:latest
|
|
44
|
+
restart: unless-stopped
|
|
45
|
+
environment:
|
|
46
|
+
# Primary bucket configuration
|
|
47
|
+
RCLONE_CONFIG_PRIMARY_TYPE: s3
|
|
48
|
+
RCLONE_CONFIG_PRIMARY_PROVIDER: Other
|
|
49
|
+
RCLONE_CONFIG_PRIMARY_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
|
|
50
|
+
RCLONE_CONFIG_PRIMARY_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
|
|
51
|
+
RCLONE_CONFIG_PRIMARY_ENDPOINT: ${S3_PRIMARY_ENDPOINT}
|
|
52
|
+
RCLONE_CONFIG_PRIMARY_ACL: private
|
|
53
|
+
# Standby bucket configuration
|
|
54
|
+
RCLONE_CONFIG_STANDBY_TYPE: s3
|
|
55
|
+
RCLONE_CONFIG_STANDBY_PROVIDER: Other
|
|
56
|
+
RCLONE_CONFIG_STANDBY_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
|
|
57
|
+
RCLONE_CONFIG_STANDBY_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
|
|
58
|
+
RCLONE_CONFIG_STANDBY_ENDPOINT: ${S3_STANDBY_ENDPOINT}
|
|
59
|
+
RCLONE_CONFIG_STANDBY_ACL: private
|
|
60
|
+
command: |
|
|
61
|
+
sh -c '
|
|
62
|
+
echo "Starting S3 cross-region sync..."
|
|
63
|
+
echo "Primary: ${S3_PRIMARY_BUCKET} @ ${S3_PRIMARY_ENDPOINT}"
|
|
64
|
+
echo "Standby: ${S3_STANDBY_BUCKET} @ ${S3_STANDBY_ENDPOINT}"
|
|
65
|
+
echo "Sync interval: ${S3_SYNC_INTERVAL:-5} minutes"
|
|
66
|
+
while true; do
|
|
67
|
+
echo "[$(date)] Starting sync from primary to standby..."
|
|
68
|
+
rclone sync primary:${S3_PRIMARY_BUCKET} standby:${S3_STANDBY_BUCKET} \
|
|
69
|
+
--transfers 8 \
|
|
70
|
+
--checkers 16 \
|
|
71
|
+
--fast-list \
|
|
72
|
+
--stats 30s \
|
|
73
|
+
--stats-one-line
|
|
74
|
+
echo "[$(date)] Sync complete. Sleeping for ${S3_SYNC_INTERVAL:-5} minutes..."
|
|
75
|
+
sleep ${S3_SYNC_INTERVAL:-5}m
|
|
76
|
+
done
|
|
77
|
+
'
|
|
78
|
+
healthcheck:
|
|
79
|
+
test: ["CMD", "rclone", "version"]
|
|
80
|
+
interval: 60s
|
|
81
|
+
timeout: 10s
|
|
82
|
+
retries: 3
|
|
83
|
+
deploy:
|
|
84
|
+
resources:
|
|
85
|
+
limits:
|
|
86
|
+
memory: 256M
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# S3 Storage Backend Configuration
|
|
2
|
+
# Overlay for Supabase Storage to use Hetzner Object Storage (S3-compatible)
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
|
|
6
|
+
#
|
|
7
|
+
# Required environment variables in .env:
|
|
8
|
+
# S3_ACCESS_KEY - Hetzner S3 access key
|
|
9
|
+
# S3_SECRET_KEY - Hetzner S3 secret key
|
|
10
|
+
# S3_BUCKET - S3 bucket name
|
|
11
|
+
# S3_ENDPOINT - S3 endpoint URL (e.g., https://fsn1.your-objectstorage.com)
|
|
12
|
+
# S3_REGION - S3 region (e.g., fsn1)
|
|
13
|
+
|
|
14
|
+
services:
|
|
15
|
+
storage:
|
|
16
|
+
environment:
|
|
17
|
+
# Override storage backend from 'file' to 's3'
|
|
18
|
+
STORAGE_BACKEND: s3
|
|
19
|
+
# S3 bucket configuration
|
|
20
|
+
GLOBAL_S3_BUCKET: ${S3_BUCKET}
|
|
21
|
+
GLOBAL_S3_ENDPOINT: ${S3_ENDPOINT}
|
|
22
|
+
GLOBAL_S3_FORCE_PATH_STYLE: "true"
|
|
23
|
+
REGION: ${S3_REGION}
|
|
24
|
+
# S3 credentials
|
|
25
|
+
AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
|
|
26
|
+
AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
|
|
27
|
+
|
|
28
|
+
imgproxy:
|
|
29
|
+
environment:
|
|
30
|
+
# Enable S3 backend for image transformations
|
|
31
|
+
IMGPROXY_USE_S3: "true"
|
|
32
|
+
AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
|
|
33
|
+
AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
|
|
34
|
+
AWS_REGION: ${S3_REGION}
|
|
35
|
+
IMGPROXY_S3_ENDPOINT: ${S3_ENDPOINT}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "s3",
|
|
3
|
+
"displayName": "Hetzner S3 Object Storage",
|
|
4
|
+
"description": "S3-compatible object storage for Supabase file storage backend",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"type": "infrastructure",
|
|
7
|
+
"requiresCredentials": true,
|
|
8
|
+
|
|
9
|
+
"envVars": {
|
|
10
|
+
"S3_ACCESS_KEY": {
|
|
11
|
+
"prompt": true,
|
|
12
|
+
"secure": true,
|
|
13
|
+
"description": "Hetzner S3 access key"
|
|
14
|
+
},
|
|
15
|
+
"S3_SECRET_KEY": {
|
|
16
|
+
"prompt": true,
|
|
17
|
+
"secure": true,
|
|
18
|
+
"description": "Hetzner S3 secret key"
|
|
19
|
+
},
|
|
20
|
+
"S3_BUCKET": {
|
|
21
|
+
"generate": "from-project-name",
|
|
22
|
+
"description": "S3 bucket name"
|
|
23
|
+
},
|
|
24
|
+
"S3_ENDPOINT": {
|
|
25
|
+
"generate": "from-region",
|
|
26
|
+
"description": "S3 endpoint URL"
|
|
27
|
+
},
|
|
28
|
+
"S3_REGION": {
|
|
29
|
+
"prompt": true,
|
|
30
|
+
"options": ["fsn1", "nbg1", "hel1"],
|
|
31
|
+
"default": "fsn1",
|
|
32
|
+
"description": "S3 storage region"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
"haEnvVars": {
|
|
37
|
+
"S3_PRIMARY_BUCKET": {
|
|
38
|
+
"generate": "from-project-name-primary",
|
|
39
|
+
"description": "Primary region bucket name"
|
|
40
|
+
},
|
|
41
|
+
"S3_STANDBY_BUCKET": {
|
|
42
|
+
"generate": "from-project-name-standby",
|
|
43
|
+
"description": "Standby region bucket name"
|
|
44
|
+
},
|
|
45
|
+
"S3_PRIMARY_ENDPOINT": {
|
|
46
|
+
"generate": "from-region",
|
|
47
|
+
"description": "Primary region endpoint"
|
|
48
|
+
},
|
|
49
|
+
"S3_STANDBY_ENDPOINT": {
|
|
50
|
+
"generate": "from-region",
|
|
51
|
+
"description": "Standby region endpoint"
|
|
52
|
+
},
|
|
53
|
+
"S3_PRIMARY_REGION": {
|
|
54
|
+
"prompt": true,
|
|
55
|
+
"options": ["fsn1", "nbg1", "hel1"],
|
|
56
|
+
"default": "fsn1",
|
|
57
|
+
"description": "Primary storage region"
|
|
58
|
+
},
|
|
59
|
+
"S3_STANDBY_REGION": {
|
|
60
|
+
"prompt": true,
|
|
61
|
+
"options": ["fsn1", "nbg1", "hel1"],
|
|
62
|
+
"default": "hel1",
|
|
63
|
+
"description": "Standby storage region"
|
|
64
|
+
},
|
|
65
|
+
"S3_SYNC_INTERVAL": {
|
|
66
|
+
"default": "5",
|
|
67
|
+
"description": "Cross-region sync interval in minutes"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
"compose": {
|
|
72
|
+
"standard": "compose/docker-compose.s3.yml",
|
|
73
|
+
"ha": "compose/docker-compose.s3-ha.yml"
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
"instructions": {
|
|
77
|
+
"title": "Hetzner Object Storage Setup",
|
|
78
|
+
"url": "https://console.hetzner.cloud/",
|
|
79
|
+
"steps": [
|
|
80
|
+
"Go to https://console.hetzner.cloud/",
|
|
81
|
+
"Select your project (or create one)",
|
|
82
|
+
"Click 'Object Storage' in the sidebar",
|
|
83
|
+
"Click 'Create Credentials'",
|
|
84
|
+
"Copy the Access Key and Secret Key (secret shown only once!)"
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
}
|