Compare commits
4 Commits
c75d03a548
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a617fdf1ec | |||
| b615fef334 | |||
| 2eadd81d6e | |||
| 5511ed5d91 |
11
.env.example
11
.env.example
@@ -1,2 +1,13 @@
|
|||||||
NEXT_HOST=east-guilan-ce.ir
|
NEXT_HOST=east-guilan-ce.ir
|
||||||
LETSENCRYPT_EMAIL=admin@east-guilan-ce.ir
|
LETSENCRYPT_EMAIL=admin@east-guilan-ce.ir
|
||||||
|
|
||||||
|
# Backups
|
||||||
|
S3_BACKUP_BUCKET=replace-with-bucket
|
||||||
|
S3_BACKUP_PREFIX=backup/guilan-ace
|
||||||
|
S3_BACKUP_REGION=us-east-1
|
||||||
|
S3_BACKUP_ENDPOINT_URL=https://replace-with-s3-endpoint.example.com
|
||||||
|
S3_BACKUP_ACCESS_KEY_ID=replace-with-access-key
|
||||||
|
S3_BACKUP_SECRET_ACCESS_KEY=replace-with-secret-key
|
||||||
|
BACKUP_ENCRYPTION_PASSPHRASE=replace-with-long-random-passphrase
|
||||||
|
BACKUP_LOCAL_KEEP_LATEST=3
|
||||||
|
BACKUP_REMOTE_KEEP_LATEST=10
|
||||||
|
|||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.sh text eol=lf
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,5 +1,9 @@
|
|||||||
.env
|
.env
|
||||||
data/
|
data/
|
||||||
|
backups/
|
||||||
certs/fullchain.pem
|
certs/fullchain.pem
|
||||||
certs/privateKey.pem
|
certs/privateKey.pem
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
backend/guilan-ace-backend
|
||||||
|
frontend/guilan-ace-frontend
|
||||||
|
|||||||
88
README.md
88
README.md
@@ -24,9 +24,95 @@ parent-directory/
|
|||||||
3. On the deployment server, place the backend repo at `guilan-ace-deployment/backend/guilan-ace-backend`.
|
3. On the deployment server, place the backend repo at `guilan-ace-deployment/backend/guilan-ace-backend`.
|
||||||
4. On the deployment server, place the frontend repo at `guilan-ace-deployment/frontend/guilan-ace-frontend`.
|
4. On the deployment server, place the frontend repo at `guilan-ace-deployment/frontend/guilan-ace-frontend`.
|
||||||
5. Create `backend/guilan-ace-backend/.env` from the backend repo sample.
|
5. Create `backend/guilan-ace-backend/.env` from the backend repo sample.
|
||||||
6. Run `docker compose up -d --build`.
|
6. Create `frontend/guilan-ace-frontend/.env` from the frontend repo sample.
|
||||||
|
7. Run `docker compose up -d --build`.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- Traefik terminates TLS and routes frontend, API, admin, static, media, Grafana, Prometheus, and Uptime Kuma.
|
- Traefik terminates TLS and routes frontend, API, admin, static, media, Grafana, Prometheus, and Uptime Kuma.
|
||||||
|
- The frontend is a Next.js standalone Node runtime behind Traefik on internal port `3000`.
|
||||||
- Alertmanager has been removed from this stack. Prometheus scraping and Grafana provisioning remain intact.
|
- Alertmanager has been removed from this stack. Prometheus scraping and Grafana provisioning remain intact.
|
||||||
- Dockerfiles live only in this deployment repository, and compose is intentionally wired only for the nested production layout.
|
- Dockerfiles live only in this deployment repository, and compose is intentionally wired only for the nested production layout.
|
||||||
|
|
||||||
|
## Backups and restores
|
||||||
|
The deployment repo owns backup automation in `scripts/`. Backups include:
|
||||||
|
- PostgreSQL dump from the `db` service.
|
||||||
|
- Django media files from the `django_media` Docker volume.
|
||||||
|
- Deployment, backend, and frontend `.env` files when present.
|
||||||
|
|
||||||
|
The scripts expect this production layout:
|
||||||
|
```text
|
||||||
|
guilan-ace-deployment/
|
||||||
|
.env
|
||||||
|
backend/guilan-ace-backend/.env
|
||||||
|
frontend/guilan-ace-frontend/.env
|
||||||
|
docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Add these backup variables to `guilan-ace-deployment/.env`:
|
||||||
|
```env
|
||||||
|
S3_BACKUP_BUCKET=c284984
|
||||||
|
S3_BACKUP_PREFIX=backup/guilan-ace
|
||||||
|
S3_BACKUP_REGION=us-east-1
|
||||||
|
S3_BACKUP_ENDPOINT_URL=https://c284984.parspack.net
|
||||||
|
S3_BACKUP_ACCESS_KEY_ID=replace-with-access-key
|
||||||
|
S3_BACKUP_SECRET_ACCESS_KEY=replace-with-secret-key
|
||||||
|
BACKUP_ENCRYPTION_PASSPHRASE=replace-with-long-random-passphrase
|
||||||
|
BACKUP_LOCAL_KEEP_LATEST=3
|
||||||
|
BACKUP_REMOTE_KEEP_LATEST=10
|
||||||
|
```
|
||||||
|
|
||||||
|
Install script dependencies on the server:
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y docker-compose-plugin rclone openssl
|
||||||
|
chmod +x ~/guilan-ace-deployment/scripts/*.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a manual local backup:
|
||||||
|
```bash
|
||||||
|
cd ~/guilan-ace-deployment
|
||||||
|
./scripts/backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Create, encrypt, upload, and rotate an S3 backup:
|
||||||
|
```bash
|
||||||
|
cd ~/guilan-ace-deployment
|
||||||
|
./scripts/backup-upload-s3.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Restore from a local backup archive:
|
||||||
|
```bash
|
||||||
|
cd ~/guilan-ace-deployment
|
||||||
|
./scripts/restore.sh backups/guilan-ace-backup-YYYYMMDD-HHMMSS.tar.gz
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Restore the latest encrypted S3 backup:
|
||||||
|
```bash
|
||||||
|
cd ~/guilan-ace-deployment
|
||||||
|
./scripts/restore-from-s3.sh latest
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Restore only selected parts by setting skip flags:
|
||||||
|
```bash
|
||||||
|
RESTORE_SKIP_ENV=1 ./scripts/restore.sh backups/guilan-ace-backup-YYYYMMDD-HHMMSS.tar.gz
|
||||||
|
RESTORE_SKIP_DB=1 RESTORE_SKIP_MEDIA=1 ./scripts/restore-from-s3.sh latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Database restore recreates the configured database and is destructive unless `RESTORE_SKIP_DB=1` is set.
|
||||||
|
|
||||||
|
Run encrypted S3 backups periodically with cron:
|
||||||
|
```bash
|
||||||
|
crontab -e
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: run every day at `03:30` server time and write logs to `backups/backup.log`:
|
||||||
|
```cron
|
||||||
|
30 3 * * * cd /home/ubuntu/guilan-ace-deployment && ./scripts/backup-upload-s3.sh >> /home/ubuntu/guilan-ace-deployment/backups/backup.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: run every 6 hours:
|
||||||
|
```cron
|
||||||
|
0 */6 * * * cd /home/ubuntu/guilan-ace-deployment && ./scripts/backup-upload-s3.sh >> /home/ubuntu/guilan-ace-deployment/backups/backup.log 2>&1
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,64 +1,6 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIGEzCCBPugAwIBAgISBZf+U3m3Aftq6nyZTh1KZD6tMA0GCSqGSIb3DQEBCwUA
|
...
|
||||||
MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD
|
|
||||||
EwNSMTIwHhcNMjYwNTE4MTQyOTI0WhcNMjYwODE2MTQyOTIzWjAeMRwwGgYDVQQD
|
|
||||||
DBMqLmVhc3QtZ3VpbGFuLWNlLmlyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
|
|
||||||
CgKCAgEAx0eyNPO1YVzOZGiC19l5IEedFeMe1Yf2T1srJUr9MSpDKKiE8n3AY0Jq
|
|
||||||
9WgUou9E8ZViu3cYjE8UEuP6s4W+U1iXYWMwqc6hAkSejn8mb4vSdIO1dVEW9BNM
|
|
||||||
spDgXZvbwSs6UWm+sUpxwot4hV9RlzIdSoZ4nrLRJnu7OSW/fO7xU8UNPAgPHarH
|
|
||||||
e/xBPPTeYKq+CcDb7HJUdJMYxDd1oRtZQm/Uz5rDrqf0R4DxAhUBcZXgp8zn4yH1
|
|
||||||
nPjIR+2XUCB2n4QlOfNqhiPa9JwD6ZVrUImaFBdDTZjenE/HHVJ94k1LzMTGgHRv
|
|
||||||
Cp/avx5fw4lYlY8J72eKKFK77fHQpUUk7F7klZ+CHDlrLi+RDyqiJezhVJlPuXXn
|
|
||||||
ivEWvrrVN0EZ+1pO+Xn1xqBDjJ2KP+VbGzSveGgxsO5XHy2BaGumYx7nQwhqynXk
|
|
||||||
BMKC5PCufu8zfvsd2ODSGUvIhihgsXbFRaWEWOQiLqFq3z/4GORnn09DvBqnIsKQ
|
|
||||||
LGI4xDrnb/JYmkCs5zxLey1kDWOtqRjzZ/aVzSUM8h1UOmofXKPGg/ucM1SMEqe5
|
|
||||||
qLyifgYsxZ/OUnrx6OZt1xW4eROGxovyehscaCIIMeIAhKZ4oiJhWfZ4mgb9teiF
|
|
||||||
RylJST+YXqN9/DydVe6AyIUInKAQZL5PsY36B/QK6Fckb2GnOK8CAwEAAaOCAjQw
|
|
||||||
ggIwMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMB
|
|
||||||
Af8EAjAAMB0GA1UdDgQWBBTU6TZIKc65xcaDLlCRk8padrq7yjAfBgNVHSMEGDAW
|
|
||||||
gBQAtSnyLY5vMeibTK14Pvrc6QzR0jAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUH
|
|
||||||
MAKGF2h0dHA6Ly9yMTIuaS5sZW5jci5vcmcvMDEGA1UdEQQqMCiCEyouZWFzdC1n
|
|
||||||
dWlsYW4tY2UuaXKCEWVhc3QtZ3VpbGFuLWNlLmlyMBMGA1UdIAQMMAowCAYGZ4EM
|
|
||||||
AQIBMC4GA1UdHwQnMCUwI6AhoB+GHWh0dHA6Ly9yMTIuYy5sZW5jci5vcmcvNDYu
|
|
||||||
Y3JsMIIBDAYKKwYBBAHWeQIEAgSB/QSB+gD4AHcAwjF+V0UZo0XufzjespBB68fC
|
|
||||||
IVoiv3/Vta12mtkOUs0AAAGeO7NpVgAABAMASDBGAiEAmxz+oc1QeAR1J/yEe1jZ
|
|
||||||
W2hT/U3XF+5q63O+kRjQWO0CIQCGI+xwY/hhPjJr9HkPRTI5NXGt9EeVe0vMAu1s
|
|
||||||
+TMtxgB9ABqLnWsP/r+BtHk5xtIxCobW0QLU8EbiGCyd419eJiXvAAABnjuza0wA
|
|
||||||
CAAABQAUIto1BAMARjBEAiAPAyeQN4zkD/vUAAqo/8sF5uKicaul9fS9y0bUv+8d
|
|
||||||
hgIgQxClI5FycnYL6GTwBxpNWS0uWbDhoTAtj+Mw6NOGOD8wDQYJKoZIhvcNAQEL
|
|
||||||
BQADggEBAHt+R9Da16AbLPZqC5BelK6prKdmeaqkDIDO6aE1aZyuS0xxK228fPAr
|
|
||||||
zcopyWI4Onm29bAYxaeFtUwZurDyqb+jHf0AD8PC2zOFxQCvDyEO9l28yQ51hSR2
|
|
||||||
6KlTNfsUaBAdZctSseZI1wcqJ3cwZLGAJjrTXaMDDDc83UeG/bJ8syU+iGlUhhWu
|
|
||||||
e/hEFTtIQ14U6lvNLwgtsC3Ptwe5gGIauJ+wtTIkavulGrWpGLgEUQPqNFz/NmwM
|
|
||||||
+Nis+oB2qKMJfQBoQgNIVFahsnOvUTJZbx3nEKQfI/OAGQHH2fr6xpg09zbi0j/5
|
|
||||||
Y8q58R3hzVyBdJHytdcI5W/pEJmYzSE=
|
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIFBjCCAu6gAwIBAgIRAMISMktwqbSRcdxA9+KFJjwwDQYJKoZIhvcNAQELBQAw
|
...
|
||||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
|
||||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
|
|
||||||
WhcNMjcwMzEyMjM1OTU5WjAzMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
|
||||||
RW5jcnlwdDEMMAoGA1UEAxMDUjEyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
|
||||||
CgKCAQEA2pgodK2+lP474B7i5Ut1qywSf+2nAzJ+Npfs6DGPpRONC5kuHs0BUT1M
|
|
||||||
5ShuCVUxqqUiXXL0LQfCTUA83wEjuXg39RplMjTmhnGdBO+ECFu9AhqZ66YBAJpz
|
|
||||||
kG2Pogeg0JfT2kVhgTU9FPnEwF9q3AuWGrCf4yrqvSrWmMebcas7dA8827JgvlpL
|
|
||||||
Thjp2ypzXIlhZZ7+7Tymy05v5J75AEaz/xlNKmOzjmbGGIVwx1Blbzt05UiDDwhY
|
|
||||||
XS0jnV6j/ujbAKHS9OMZTfLuevYnnuXNnC2i8n+cF63vEzc50bTILEHWhsDp7CH4
|
|
||||||
WRt/uTp8n1wBnWIEwii9Cq08yhDsGwIDAQABo4H4MIH1MA4GA1UdDwEB/wQEAwIB
|
|
||||||
hjAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwEgYDVR0TAQH/BAgwBgEB
|
|
||||||
/wIBADAdBgNVHQ4EFgQUALUp8i2ObzHom0yteD763OkM0dIwHwYDVR0jBBgwFoAU
|
|
||||||
ebRZ5nu25eQBc4AIiMgaWPbpm24wMgYIKwYBBQUHAQEEJjAkMCIGCCsGAQUFBzAC
|
|
||||||
hhZodHRwOi8veDEuaS5sZW5jci5vcmcvMBMGA1UdIAQMMAowCAYGZ4EMAQIBMCcG
|
|
||||||
A1UdHwQgMB4wHKAaoBiGFmh0dHA6Ly94MS5jLmxlbmNyLm9yZy8wDQYJKoZIhvcN
|
|
||||||
AQELBQADggIBAI910AnPanZIZTKS3rVEyIV29BWEjAK/duuz8eL5boSoVpHhkkv3
|
|
||||||
4eoAeEiPdZLj5EZ7G2ArIK+gzhTlRQ1q4FKGpPPaFBSpqV/xbUb5UlAXQOnkHn3m
|
|
||||||
FVj+qYv87/WeY+Bm4sN3Ox8BhyaU7UAQ3LeZ7N1X01xxQe4wIAAE3JVLUCiHmZL+
|
|
||||||
qoCUtgYIFPgcg350QMUIWgxPXNGEncT921ne7nluI02V8pLUmClqXOsCwULw+PVO
|
|
||||||
ZCB7qOMxxMBoCUeL2Ll4oMpOSr5pJCpLN3tRA2s6P1KLs9TSrVhOk+7LX28NMUlI
|
|
||||||
usQ/nxLJID0RhAeFtPjyOCOscQBA53+NRjSCak7P4A5jX7ppmkcJECL+S0i3kXVU
|
|
||||||
y5Me5BbrU8973jZNv/ax6+ZK6TM8jWmimL6of6OrX7ZU6E2WqazzsFrLG3o2kySb
|
|
||||||
zlhSgJ81Cl4tv3SbYiYXnJExKQvzf83DYotox3f0fwv7xln1A2ZLplCb0O+l/AK0
|
|
||||||
YE0DS2FPxSAHi0iwMfW2nNHJrXcY3LLHD77gRgje4Eveubi2xxa+Nmk/hmhLdIET
|
|
||||||
iVDFanoCrMVIpQ59XWHkzdFmoHXHBV7oibVjGSO7ULSQ7MJ1Nz51phuDJSgAIU7A
|
|
||||||
0zrLnOrAj/dfrlEWRhCvAgbuwLZX1A2sjNjXoPOHbsPiy+lO1KF8/XY7
|
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
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
|
|
||||||
Fsu0vT7dhy5GJP6Qmrt7ogjJ9K4grQ==
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
3
certs/privateKey.example.pem
Normal file
3
certs/privateKey.example.pem
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
...
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -66,7 +66,7 @@ services:
|
|||||||
web:
|
web:
|
||||||
build:
|
build:
|
||||||
context: ./backend/guilan-ace-backend
|
context: ./backend/guilan-ace-backend
|
||||||
dockerfile: ./backend/Dockerfile
|
dockerfile: ../Dockerfile
|
||||||
env_file:
|
env_file:
|
||||||
- ./backend/guilan-ace-backend/.env
|
- ./backend/guilan-ace-backend/.env
|
||||||
environment:
|
environment:
|
||||||
@@ -102,7 +102,7 @@ services:
|
|||||||
worker:
|
worker:
|
||||||
build:
|
build:
|
||||||
context: ./backend/guilan-ace-backend
|
context: ./backend/guilan-ace-backend
|
||||||
dockerfile: ./backend/Dockerfile
|
dockerfile: ../Dockerfile
|
||||||
env_file:
|
env_file:
|
||||||
- ./backend/guilan-ace-backend/.env
|
- ./backend/guilan-ace-backend/.env
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -117,7 +117,7 @@ services:
|
|||||||
beat:
|
beat:
|
||||||
build:
|
build:
|
||||||
context: ./backend/guilan-ace-backend
|
context: ./backend/guilan-ace-backend
|
||||||
dockerfile: ./backend/Dockerfile
|
dockerfile: ../Dockerfile
|
||||||
env_file:
|
env_file:
|
||||||
- ./backend/guilan-ace-backend/.env
|
- ./backend/guilan-ace-backend/.env
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -129,13 +129,15 @@ services:
|
|||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend/guilan-ace-frontend
|
context: ./frontend/guilan-ace-frontend
|
||||||
dockerfile: ./frontend/Dockerfile
|
dockerfile: ../Dockerfile
|
||||||
|
env_file:
|
||||||
|
- ./frontend/guilan-ace-frontend/.env
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.http.routers.frontend.rule=Host(`${NEXT_HOST}`)
|
- traefik.http.routers.frontend.rule=Host(`${NEXT_HOST}`)
|
||||||
- traefik.http.routers.frontend.entrypoints=websecure
|
- traefik.http.routers.frontend.entrypoints=websecure
|
||||||
- traefik.http.routers.frontend.tls.certresolver=le
|
- traefik.http.routers.frontend.tls.certresolver=le
|
||||||
- traefik.http.services.frontend.loadbalancer.server.port=80
|
- traefik.http.services.frontend.loadbalancer.server.port=3000
|
||||||
|
|
||||||
static:
|
static:
|
||||||
image: nginx:1.27-alpine
|
image: nginx:1.27-alpine
|
||||||
|
|||||||
@@ -1,30 +1,36 @@
|
|||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS deps
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN npm config set registry https://package-mirror.liara.ir/repository/npm/ --global
|
RUN npm config set registry https://package-mirror.liara.ir/repository/npm/ --global
|
||||||
|
|
||||||
# Copy package files
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install dependencies
|
RUN npm ci
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# Copy source code
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build the application
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Production image
|
FROM node:20-alpine AS runner
|
||||||
FROM nginx:alpine
|
|
||||||
|
|
||||||
# Copy built files to nginx
|
WORKDIR /app
|
||||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
|
||||||
|
|
||||||
# Copy nginx configuration
|
ENV NODE_ENV=production
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
EXPOSE 80
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/.next/standalone ./
|
||||||
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
215
scripts/backup-upload-s3.sh
Normal file
215
scripts/backup-upload-s3.sh
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
backup-upload-s3.sh
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
DEPLOY_ROOT Deployment directory. Defaults to ~/guilan-ace-deployment.
|
||||||
|
|
||||||
|
S3_BACKUP_ENDPOINT_URL S3 endpoint URL.
|
||||||
|
S3_BACKUP_ACCESS_KEY_ID S3 access key.
|
||||||
|
S3_BACKUP_SECRET_ACCESS_KEY S3 secret key.
|
||||||
|
S3_BACKUP_BUCKET Rclone bucket/root name, e.g. c284984.
|
||||||
|
S3_BACKUP_PREFIX Backup folder path. Defaults to backup/guilan-ace.
|
||||||
|
S3_BACKUP_REGION S3 region. Defaults to us-east-1.
|
||||||
|
|
||||||
|
BACKUP_ENCRYPTION_PASSPHRASE Encryption passphrase.
|
||||||
|
BACKUP_LOCAL_KEEP_LATEST Number of latest local plaintext backups to keep. Defaults to 3.
|
||||||
|
BACKUP_REMOTE_KEEP_LATEST Number of latest remote encrypted backups to keep. Defaults to 7.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[backup-rclone] %s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf '[backup-rclone] %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_var() {
|
||||||
|
local name="$1"
|
||||||
|
[[ -n "${!name:-}" ]] || fail "$name is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env() {
|
||||||
|
local env_path="$1"
|
||||||
|
|
||||||
|
[[ -f "$env_path" ]] || fail "Deployment env file not found: $env_path"
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$env_path"
|
||||||
|
set +a
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_path() {
|
||||||
|
local value="${1:-backup/guilan-ace}"
|
||||||
|
|
||||||
|
value="${value#/}"
|
||||||
|
value="${value%/}"
|
||||||
|
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
positive_integer_or_default() {
|
||||||
|
local value="${1:-}"
|
||||||
|
local fallback="$2"
|
||||||
|
|
||||||
|
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
||||||
|
printf '%s' "$value"
|
||||||
|
else
|
||||||
|
printf '%s' "$fallback"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
rclone_remote_path() {
|
||||||
|
printf 'parspack:%s/%s' "$S3_BACKUP_BUCKET" "$S3_BACKUP_PREFIX"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_local_backups() {
|
||||||
|
local keep_count="$1"
|
||||||
|
local latest_dir="$2"
|
||||||
|
|
||||||
|
[[ "$keep_count" -gt 0 ]] || {
|
||||||
|
rm -rf "$latest_dir"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "$latest_dir"
|
||||||
|
find "$latest_dir" -maxdepth 1 -type f -name '*.tar.gz.enc' -delete
|
||||||
|
cp "$ARCHIVE_PATH" "$latest_dir/$ARCHIVE_NAME"
|
||||||
|
|
||||||
|
find "$latest_dir" -maxdepth 1 -type f -name '*.tar.gz' -printf '%T@ %p\n' \
|
||||||
|
| sort -nr \
|
||||||
|
| awk -v keep="$keep_count" 'NR > keep {print $2}' \
|
||||||
|
| xargs -r rm -f
|
||||||
|
|
||||||
|
log "Kept latest $keep_count plaintext backup(s) locally in: $latest_dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_remote_backups() {
|
||||||
|
local keep_count="$1"
|
||||||
|
local remote_path="$2"
|
||||||
|
local stale_files
|
||||||
|
|
||||||
|
[[ "$keep_count" -gt 0 ]] || {
|
||||||
|
log "Skipping remote cleanup because BACKUP_REMOTE_KEEP_LATEST is $keep_count"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stale_files="$(
|
||||||
|
rclone lsf "$remote_path" \
|
||||||
|
--config "$RCLONE_CONFIG" \
|
||||||
|
--s3-no-check-bucket \
|
||||||
|
--files-only \
|
||||||
|
| grep '\.tar\.gz\.enc$' \
|
||||||
|
| sort -r \
|
||||||
|
| awk -v keep="$keep_count" 'NR > keep' \
|
||||||
|
|| true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -z "$stale_files" ]]; then
|
||||||
|
log "No remote backups need cleanup"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r stale_file; do
|
||||||
|
[[ -n "$stale_file" ]] || continue
|
||||||
|
log "Deleting old remote backup: $remote_path/$stale_file"
|
||||||
|
rclone deletefile "$remote_path/$stale_file" \
|
||||||
|
--config "$RCLONE_CONFIG" \
|
||||||
|
--s3-no-check-bucket
|
||||||
|
done <<< "$stale_files"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v rclone >/dev/null 2>&1 || fail "rclone is required"
|
||||||
|
command -v openssl >/dev/null 2>&1 || fail "openssl is required"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DEPLOY_ROOT="${DEPLOY_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
||||||
|
|
||||||
|
DEPLOY_ENV="$DEPLOY_ROOT/.env"
|
||||||
|
BACKUP_SCRIPT="$SCRIPT_DIR/backup.sh"
|
||||||
|
|
||||||
|
[[ -x "$BACKUP_SCRIPT" ]] || fail "Backup script is not executable: $BACKUP_SCRIPT"
|
||||||
|
|
||||||
|
load_env "$DEPLOY_ENV"
|
||||||
|
|
||||||
|
require_var S3_BACKUP_ENDPOINT_URL
|
||||||
|
require_var S3_BACKUP_ACCESS_KEY_ID
|
||||||
|
require_var S3_BACKUP_SECRET_ACCESS_KEY
|
||||||
|
require_var S3_BACKUP_BUCKET
|
||||||
|
require_var BACKUP_ENCRYPTION_PASSPHRASE
|
||||||
|
|
||||||
|
S3_BACKUP_PREFIX="$(normalize_path "${S3_BACKUP_PREFIX:-backup/guilan-ace}")"
|
||||||
|
BACKUP_LOCAL_KEEP_LATEST="$(positive_integer_or_default "${BACKUP_LOCAL_KEEP_LATEST:-3}" 3)"
|
||||||
|
BACKUP_REMOTE_KEEP_LATEST="$(positive_integer_or_default "${BACKUP_REMOTE_KEEP_LATEST:-7}" 7)"
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
PLAIN_DIR="$WORK_DIR/plain"
|
||||||
|
mkdir -p "$PLAIN_DIR"
|
||||||
|
|
||||||
|
log "Creating local backup archive"
|
||||||
|
|
||||||
|
DEPLOY_ROOT="$DEPLOY_ROOT" "$BACKUP_SCRIPT" "$PLAIN_DIR"
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
archives=("$PLAIN_DIR"/*.tar.gz)
|
||||||
|
shopt -u nullglob
|
||||||
|
|
||||||
|
[[ "${#archives[@]}" -eq 1 ]] || fail "Expected exactly one backup archive, found ${#archives[@]}"
|
||||||
|
|
||||||
|
ARCHIVE_PATH="${archives[0]}"
|
||||||
|
ARCHIVE_NAME="$(basename "$ARCHIVE_PATH")"
|
||||||
|
|
||||||
|
ENCRYPTED_NAME="$ARCHIVE_NAME.enc"
|
||||||
|
ENCRYPTED_PATH="$WORK_DIR/$ENCRYPTED_NAME"
|
||||||
|
|
||||||
|
log "Encrypting backup archive"
|
||||||
|
|
||||||
|
openssl enc -aes-256-cbc -salt -pbkdf2 -iter 200000 \
|
||||||
|
-in "$ARCHIVE_PATH" \
|
||||||
|
-out "$ENCRYPTED_PATH" \
|
||||||
|
-pass env:BACKUP_ENCRYPTION_PASSPHRASE
|
||||||
|
|
||||||
|
RCLONE_CONFIG="$WORK_DIR/rclone.conf"
|
||||||
|
|
||||||
|
cat > "$RCLONE_CONFIG" <<EOF
|
||||||
|
[parspack]
|
||||||
|
type = s3
|
||||||
|
provider = Other
|
||||||
|
access_key_id = $S3_BACKUP_ACCESS_KEY_ID
|
||||||
|
secret_access_key = $S3_BACKUP_SECRET_ACCESS_KEY
|
||||||
|
endpoint = $S3_BACKUP_ENDPOINT_URL
|
||||||
|
region = ${S3_BACKUP_REGION:-us-east-1}
|
||||||
|
acl = private
|
||||||
|
force_path_style = true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
REMOTE_PATH="$(rclone_remote_path)"
|
||||||
|
|
||||||
|
log "Uploading encrypted backup to $REMOTE_PATH/$ENCRYPTED_NAME"
|
||||||
|
|
||||||
|
rclone copy \
|
||||||
|
"$ENCRYPTED_PATH" \
|
||||||
|
"$REMOTE_PATH" \
|
||||||
|
--config "$RCLONE_CONFIG" \
|
||||||
|
--s3-no-check-bucket \
|
||||||
|
--progress
|
||||||
|
|
||||||
|
cleanup_local_backups "$BACKUP_LOCAL_KEEP_LATEST" "$DEPLOY_ROOT/backups/latest"
|
||||||
|
cleanup_remote_backups "$BACKUP_REMOTE_KEEP_LATEST" "$REMOTE_PATH"
|
||||||
|
|
||||||
|
log "Backup upload completed successfully"
|
||||||
127
scripts/backup.sh
Normal file
127
scripts/backup.sh
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
backup.sh [output-directory]
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
DEPLOY_ROOT Deployment directory. Defaults to ~/guilan-ace-deployment.
|
||||||
|
|
||||||
|
Creates a timestamped .tar.gz archive containing:
|
||||||
|
- PostgreSQL dump from the db service
|
||||||
|
- media files from the Django media volume
|
||||||
|
- deployment, backend, and frontend .env files when present
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[backup] %s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf '[backup] %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_file() {
|
||||||
|
local path="$1"
|
||||||
|
[[ -f "$path" ]] || fail "Required file not found: $path"
|
||||||
|
}
|
||||||
|
|
||||||
|
compose() {
|
||||||
|
docker compose --env-file "$DEPLOY_ENV" -f "$DEPLOY_ROOT/docker-compose.yml" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_db() {
|
||||||
|
compose exec -T db sh -c '
|
||||||
|
set -eu
|
||||||
|
: "${DB_USER:?DB_USER is missing}"
|
||||||
|
: "${DB_NAME:?DB_NAME is missing}"
|
||||||
|
until pg_isready --username="$DB_USER" --dbname="$DB_NAME" --host=127.0.0.1; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
' >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_env_if_exists() {
|
||||||
|
local source_path="$1"
|
||||||
|
local target_path="$2"
|
||||||
|
|
||||||
|
if [[ -f "$source_path" ]]; then
|
||||||
|
cp "$source_path" "$target_path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/guilan-ace-deployment}"
|
||||||
|
OUTPUT_DIR="${1:-$DEPLOY_ROOT/backups}"
|
||||||
|
BACKEND_ENV="$DEPLOY_ROOT/backend/guilan-ace-backend/.env"
|
||||||
|
FRONTEND_ENV="$DEPLOY_ROOT/frontend/guilan-ace-frontend/.env"
|
||||||
|
DEPLOY_ENV="$DEPLOY_ROOT/.env"
|
||||||
|
|
||||||
|
require_file "$DEPLOY_ROOT/docker-compose.yml"
|
||||||
|
require_file "$DEPLOY_ENV"
|
||||||
|
require_file "$BACKEND_ENV"
|
||||||
|
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
TIMESTAMP="$(date -u +'%Y%m%d-%H%M%S')"
|
||||||
|
ARCHIVE_NAME="guilan-ace-backup-$TIMESTAMP.tar.gz"
|
||||||
|
ARCHIVE_PATH="$OUTPUT_DIR/$ARCHIVE_NAME"
|
||||||
|
|
||||||
|
mkdir -p "$WORK_DIR/env"
|
||||||
|
|
||||||
|
log "Checking Docker Compose configuration"
|
||||||
|
compose config -q
|
||||||
|
|
||||||
|
log "Starting database service if needed"
|
||||||
|
compose up -d db
|
||||||
|
wait_for_db
|
||||||
|
|
||||||
|
log "Dumping database from db service"
|
||||||
|
compose exec -T db sh -c '
|
||||||
|
set -eu
|
||||||
|
: "${DB_USER:?DB_USER is missing}"
|
||||||
|
: "${DB_NAME:?DB_NAME is missing}"
|
||||||
|
pg_dump \
|
||||||
|
--username="$DB_USER" \
|
||||||
|
--dbname="$DB_NAME" \
|
||||||
|
--format=plain \
|
||||||
|
--clean \
|
||||||
|
--if-exists \
|
||||||
|
--no-owner \
|
||||||
|
--no-privileges
|
||||||
|
' > "$WORK_DIR/database.sql"
|
||||||
|
|
||||||
|
log "Archiving media files from Django media volume"
|
||||||
|
if compose ps --status running --services | grep -qx 'web'; then
|
||||||
|
compose exec -T web sh -c 'mkdir -p /app/media && tar -C /app/media -czf - .' > "$WORK_DIR/media.tar.gz"
|
||||||
|
else
|
||||||
|
log "Web service is not running; using a temporary container for media volume"
|
||||||
|
compose run --rm --no-deps --entrypoint sh web -c 'mkdir -p /app/media && tar -C /app/media -czf - .' > "$WORK_DIR/media.tar.gz"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Copying environment files"
|
||||||
|
copy_env_if_exists "$DEPLOY_ENV" "$WORK_DIR/env/deployment.env"
|
||||||
|
copy_env_if_exists "$BACKEND_ENV" "$WORK_DIR/env/backend.env"
|
||||||
|
copy_env_if_exists "$FRONTEND_ENV" "$WORK_DIR/env/frontend.env"
|
||||||
|
|
||||||
|
cat > "$WORK_DIR/manifest.txt" <<EOF
|
||||||
|
name=$ARCHIVE_NAME
|
||||||
|
created_at_utc=$TIMESTAMP
|
||||||
|
deploy_root=$DEPLOY_ROOT
|
||||||
|
includes=database.sql,media.tar.gz,env/deployment.env,env/backend.env,env/frontend.env
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "Creating archive $ARCHIVE_PATH"
|
||||||
|
tar -C "$WORK_DIR" -czf "$ARCHIVE_PATH" .
|
||||||
|
|
||||||
|
log "Backup completed: $ARCHIVE_PATH"
|
||||||
153
scripts/restore-from-s3.sh
Normal file
153
scripts/restore-from-s3.sh
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
restore-from-s3.sh latest
|
||||||
|
restore-from-s3.sh <s3-object-key>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
restore-from-s3.sh latest
|
||||||
|
restore-from-s3.sh guilan-ace-backup-YYYYMMDD-HHMMSS.tar.gz.enc
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
DEPLOY_ROOT Deployment directory. Defaults to ~/guilan-ace-deployment.
|
||||||
|
S3_BACKUP_BUCKET Rclone bucket/root name, e.g. c284984.
|
||||||
|
S3_BACKUP_PREFIX Backup folder path. Defaults to backup/guilan-ace.
|
||||||
|
S3_BACKUP_ENDPOINT_URL S3-compatible endpoint URL.
|
||||||
|
S3_BACKUP_ACCESS_KEY_ID S3 access key.
|
||||||
|
S3_BACKUP_SECRET_ACCESS_KEY S3 secret key.
|
||||||
|
S3_BACKUP_REGION S3 region. Defaults to us-east-1.
|
||||||
|
BACKUP_ENCRYPTION_PASSPHRASE Passphrase used to decrypt backup archives.
|
||||||
|
|
||||||
|
Existing RESTORE_SKIP_DB, RESTORE_SKIP_MEDIA, and RESTORE_SKIP_ENV flags are
|
||||||
|
passed through to restore.sh.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[restore-rclone] %s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf '[restore-rclone] %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_var() {
|
||||||
|
local name="$1"
|
||||||
|
[[ -n "${!name:-}" ]] || fail "$name is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env() {
|
||||||
|
local env_path="$1"
|
||||||
|
[[ -f "$env_path" ]] || fail "Deployment env file not found: $env_path"
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$env_path"
|
||||||
|
set +a
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_path() {
|
||||||
|
local value="${1:-backup/guilan-ace}"
|
||||||
|
value="${value#/}"
|
||||||
|
value="${value%/}"
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
rclone_remote_path() {
|
||||||
|
printf 'parspack:%s/%s' "$S3_BACKUP_BUCKET" "$S3_BACKUP_PREFIX"
|
||||||
|
}
|
||||||
|
|
||||||
|
latest_object_name() {
|
||||||
|
rclone lsf "$REMOTE_PATH" \
|
||||||
|
--config "$RCLONE_CONFIG" \
|
||||||
|
--s3-no-check-bucket \
|
||||||
|
--files-only \
|
||||||
|
| grep '\.tar\.gz\.enc$' \
|
||||||
|
| sort \
|
||||||
|
| tail -n 1 \
|
||||||
|
|| true
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
REQUESTED_KEY="${1:-}"
|
||||||
|
[[ -n "$REQUESTED_KEY" ]] || {
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
command -v rclone >/dev/null 2>&1 || fail "rclone is required"
|
||||||
|
command -v openssl >/dev/null 2>&1 || fail "openssl is required"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DEPLOY_ROOT="${DEPLOY_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
||||||
|
DEPLOY_ENV="$DEPLOY_ROOT/.env"
|
||||||
|
RESTORE_SCRIPT="$SCRIPT_DIR/restore.sh"
|
||||||
|
|
||||||
|
[[ -x "$RESTORE_SCRIPT" ]] || fail "Restore script is not executable: $RESTORE_SCRIPT"
|
||||||
|
load_env "$DEPLOY_ENV"
|
||||||
|
|
||||||
|
S3_BACKUP_PREFIX="$(normalize_path "${S3_BACKUP_PREFIX:-backup/guilan-ace}")"
|
||||||
|
|
||||||
|
require_var S3_BACKUP_BUCKET
|
||||||
|
require_var S3_BACKUP_ENDPOINT_URL
|
||||||
|
require_var S3_BACKUP_ACCESS_KEY_ID
|
||||||
|
require_var S3_BACKUP_SECRET_ACCESS_KEY
|
||||||
|
require_var BACKUP_ENCRYPTION_PASSPHRASE
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
RCLONE_CONFIG="$WORK_DIR/rclone.conf"
|
||||||
|
|
||||||
|
cat > "$RCLONE_CONFIG" <<EOF
|
||||||
|
[parspack]
|
||||||
|
type = s3
|
||||||
|
provider = Other
|
||||||
|
access_key_id = $S3_BACKUP_ACCESS_KEY_ID
|
||||||
|
secret_access_key = $S3_BACKUP_SECRET_ACCESS_KEY
|
||||||
|
endpoint = $S3_BACKUP_ENDPOINT_URL
|
||||||
|
region = ${S3_BACKUP_REGION:-us-east-1}
|
||||||
|
acl = private
|
||||||
|
force_path_style = true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
REMOTE_PATH="$(rclone_remote_path)"
|
||||||
|
|
||||||
|
if [[ "$REQUESTED_KEY" == "latest" ]]; then
|
||||||
|
log "Resolving latest encrypted backup object"
|
||||||
|
OBJECT_NAME="$(latest_object_name)"
|
||||||
|
[[ -n "$OBJECT_NAME" ]] || fail "No encrypted backups found in $REMOTE_PATH"
|
||||||
|
else
|
||||||
|
OBJECT_KEY="${REQUESTED_KEY#parspack:$S3_BACKUP_BUCKET/}"
|
||||||
|
OBJECT_KEY="${OBJECT_KEY#s3://$S3_BACKUP_BUCKET/}"
|
||||||
|
OBJECT_KEY="${OBJECT_KEY#$S3_BACKUP_PREFIX/}"
|
||||||
|
OBJECT_NAME="${OBJECT_KEY##*/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ENCRYPTED_PATH="$WORK_DIR/$OBJECT_NAME"
|
||||||
|
DECRYPTED_PATH="$WORK_DIR/${ENCRYPTED_PATH##*/}"
|
||||||
|
DECRYPTED_PATH="${DECRYPTED_PATH%.enc}"
|
||||||
|
|
||||||
|
log "Downloading encrypted backup from $REMOTE_PATH/$OBJECT_NAME"
|
||||||
|
rclone copyto "$REMOTE_PATH/$OBJECT_NAME" "$ENCRYPTED_PATH" \
|
||||||
|
--config "$RCLONE_CONFIG" \
|
||||||
|
--s3-no-check-bucket \
|
||||||
|
--progress
|
||||||
|
|
||||||
|
log "Decrypting backup archive"
|
||||||
|
openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \
|
||||||
|
-in "$ENCRYPTED_PATH" \
|
||||||
|
-out "$DECRYPTED_PATH" \
|
||||||
|
-pass env:BACKUP_ENCRYPTION_PASSPHRASE
|
||||||
|
|
||||||
|
log "Restoring decrypted backup archive"
|
||||||
|
DEPLOY_ROOT="$DEPLOY_ROOT" "$RESTORE_SCRIPT" "$DECRYPTED_PATH"
|
||||||
|
|
||||||
|
log "S3 restore completed from: $REMOTE_PATH/$OBJECT_NAME"
|
||||||
135
scripts/restore.sh
Normal file
135
scripts/restore.sh
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
restore.sh <backup-archive.tar.gz>
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
DEPLOY_ROOT Deployment directory. Defaults to ~/guilan-ace-deployment.
|
||||||
|
RESTORE_SKIP_ENV Set to 1 to keep current .env files.
|
||||||
|
RESTORE_SKIP_MEDIA Set to 1 to keep current media files.
|
||||||
|
RESTORE_SKIP_DB Set to 1 to keep current database.
|
||||||
|
|
||||||
|
This script restores:
|
||||||
|
- deployment, backend, and frontend .env files
|
||||||
|
- media files into the Django media volume
|
||||||
|
- PostgreSQL database from database.sql
|
||||||
|
|
||||||
|
Database restore is destructive unless RESTORE_SKIP_DB=1 is set.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[restore] %s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf '[restore] %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_file() {
|
||||||
|
local path="$1"
|
||||||
|
[[ -f "$path" ]] || fail "Required file not found: $path"
|
||||||
|
}
|
||||||
|
|
||||||
|
compose() {
|
||||||
|
docker compose --env-file "$DEPLOY_ENV" -f "$DEPLOY_ROOT/docker-compose.yml" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_db() {
|
||||||
|
compose exec -T db sh -c '
|
||||||
|
set -eu
|
||||||
|
: "${DB_USER:?DB_USER is missing}"
|
||||||
|
: "${DB_NAME:?DB_NAME is missing}"
|
||||||
|
until pg_isready --username="$DB_USER" --dbname="$DB_NAME" --host=127.0.0.1; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
' >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_env_if_present() {
|
||||||
|
local source_path="$1"
|
||||||
|
local target_path="$2"
|
||||||
|
|
||||||
|
if [[ -f "$source_path" ]]; then
|
||||||
|
mkdir -p "$(dirname "$target_path")"
|
||||||
|
cp "$source_path" "$target_path"
|
||||||
|
chmod 600 "$target_path" || true
|
||||||
|
log "Restored $target_path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
ARCHIVE_PATH="${1:-}"
|
||||||
|
[[ -n "$ARCHIVE_PATH" ]] || {
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/guilan-ace-deployment}"
|
||||||
|
BACKEND_ENV="$DEPLOY_ROOT/backend/guilan-ace-backend/.env"
|
||||||
|
FRONTEND_ENV="$DEPLOY_ROOT/frontend/guilan-ace-frontend/.env"
|
||||||
|
DEPLOY_ENV="$DEPLOY_ROOT/.env"
|
||||||
|
|
||||||
|
require_file "$ARCHIVE_PATH"
|
||||||
|
require_file "$DEPLOY_ROOT/docker-compose.yml"
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
log "Extracting backup archive"
|
||||||
|
tar -C "$WORK_DIR" -xzf "$ARCHIVE_PATH"
|
||||||
|
|
||||||
|
if [[ "${RESTORE_SKIP_ENV:-0}" != "1" ]]; then
|
||||||
|
log "Restoring environment files"
|
||||||
|
restore_env_if_present "$WORK_DIR/env/deployment.env" "$DEPLOY_ENV"
|
||||||
|
restore_env_if_present "$WORK_DIR/env/backend.env" "$BACKEND_ENV"
|
||||||
|
restore_env_if_present "$WORK_DIR/env/frontend.env" "$FRONTEND_ENV"
|
||||||
|
else
|
||||||
|
log "Skipping environment restore"
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_file "$BACKEND_ENV"
|
||||||
|
require_file "$DEPLOY_ENV"
|
||||||
|
|
||||||
|
log "Checking Docker Compose configuration"
|
||||||
|
compose config -q
|
||||||
|
|
||||||
|
if [[ "${RESTORE_SKIP_MEDIA:-0}" != "1" ]]; then
|
||||||
|
require_file "$WORK_DIR/media.tar.gz"
|
||||||
|
log "Restoring media files into Django media volume"
|
||||||
|
compose run --rm --no-deps --entrypoint sh web -c 'rm -rf /app/media/* /app/media/.[!.]* /app/media/..?* 2>/dev/null || true'
|
||||||
|
compose run --rm --no-deps -T --entrypoint sh web -c 'mkdir -p /app/media && tar -C /app/media -xzf -' < "$WORK_DIR/media.tar.gz"
|
||||||
|
else
|
||||||
|
log "Skipping media restore"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${RESTORE_SKIP_DB:-0}" != "1" ]]; then
|
||||||
|
require_file "$WORK_DIR/database.sql"
|
||||||
|
log "Starting database service"
|
||||||
|
compose up -d db
|
||||||
|
wait_for_db
|
||||||
|
|
||||||
|
log "Recreating and restoring database"
|
||||||
|
compose exec -T db sh -c '
|
||||||
|
set -eu
|
||||||
|
: "${DB_USER:?DB_USER is missing}"
|
||||||
|
: "${DB_NAME:?DB_NAME is missing}"
|
||||||
|
psql --username="$DB_USER" --dbname=postgres --command="SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '\''$DB_NAME'\'' AND pid <> pg_backend_pid();" >/dev/null
|
||||||
|
dropdb --username="$DB_USER" --if-exists "$DB_NAME"
|
||||||
|
createdb --username="$DB_USER" "$DB_NAME"
|
||||||
|
psql --username="$DB_USER" --dbname="$DB_NAME"
|
||||||
|
' < "$WORK_DIR/database.sql"
|
||||||
|
else
|
||||||
|
log "Skipping database restore"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Restore completed"
|
||||||
|
log "Run: docker compose up -d --build"
|
||||||
Reference in New Issue
Block a user