Skip to content

Commit d5d669a

Browse files
authored
Merge branch 'main' into dependabot/github_actions/actions/checkout-6
2 parents 80383ac + c2d4f8f commit d5d669a

File tree

30 files changed

+539
-57
lines changed

30 files changed

+539
-57
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
name: Build & Publish Database Backup
3+
4+
on:
5+
pull_request:
6+
paths: &watch_paths
7+
- '.github/actions/**'
8+
- '.github/workflows/build-and-publish-database-backup.yml'
9+
- '.github/workflows/internal-build-test-and-publish.yml'
10+
- 'common/**'
11+
- 'database-backup/**'
12+
push:
13+
branches:
14+
- main
15+
paths: *watch_paths
16+
schedule:
17+
- cron: "0 0 1,15 * *" # Every 2 weeks
18+
workflow_dispatch:
19+
20+
jobs:
21+
build-test-and-publish:
22+
uses: ./.github/workflows/internal-build-test-and-publish.yml
23+
strategy:
24+
matrix:
25+
version:
26+
- alpine: '3.23'
27+
name: lts
28+
scw: '2.52'
29+
with:
30+
image: database-backup
31+
version: ${{ matrix.version.name }}
32+
run-tests: false
33+
build-args: |
34+
ALPINE_VERSION=${{ matrix.version.alpine }}
35+
SCW_VERSION=${{ matrix.version.scw }}

common/config/s6-overlay/startup-scripts/data/run.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#!/usr/bin/env sh
22

3-
set -euo pipefail
3+
set -eu
44

55
# Set defaults
6-
SECRET_DIR=/secrets
6+
readonly SECRET_DIR=/secrets
77

88
# Correct permissions so we can run as `nobody`
99
EXTRA_DIRS=
@@ -16,7 +16,7 @@ chown nobody:nogroup \
1616
$EXTRA_DIRS
1717

1818
# Ensure our working path is correct
19-
OLD_PWD="$PWD"
19+
readonly OLD_PWD="$PWD"
2020
if [ -d /app/www ]; then
2121
cd /app/www
2222
fi

common/scripts/docker-entrypoint.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#!/usr/bin/env sh
22

3-
set -euo pipefail
3+
set -eu
44

55
# Set defaults
6-
SECRET_DIR=/secrets
6+
readonly SECRET_DIR=/secrets
77

88
# Helper to run a command, invoking startup scripts, dropping down to `nobody`
99
# user, and loading secrets into ENV.
@@ -17,7 +17,7 @@ safe_exec() {
1717
}
1818

1919
# Check if command matches, otherwise fallback to executing it
20-
COMMAND_SCRIPT="/scripts/commands/${1}.sh"
20+
readonly COMMAND_SCRIPT="/scripts/commands/${1}.sh"
2121
if [ -x "$COMMAND_SCRIPT" ]; then
2222
shift 1
2323
. "$COMMAND_SCRIPT"

common/scripts/startup/50-env-configure-nginx-api-paths.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
#!/usr/bin/env sh
22

3-
set -euo pipefail
3+
set -eu
44

55
# Configure nginx CORS rules based on ENV vars
66
#
77
# Inputs:
88
# - NGINX_API_PATHS: defaults to '' (empty list)
99

1010
# Set defaults & clean up (normalize, trim, …)
11-
NGINX_CONFIG_FILE=/etc/nginx/site-mods-enabled.d/generated-api-paths.conf
12-
NGINX_API_PATHS=$(echo "${NGINX_API_PATHS:-}" \
11+
readonly NGINX_CONFIG_FILE=/etc/nginx/site-mods-enabled.d/generated-api-paths.conf
12+
readonly NGINX_API_PATHS=$(echo "${NGINX_API_PATHS:-}" \
1313
| sed 's/,/ /g; s/^ *//; s/ *$//; s/ */ /g')
1414

1515
# Check nginx structure

common/scripts/startup/50-env-configure-nginx-cors.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env sh
22

3-
set -euo pipefail
3+
set -eu
44

55
# Configure nginx CORS rules based on ENV vars
66
#
@@ -9,10 +9,10 @@ set -euo pipefail
99
# - NGINX_CORS_RESOURCE_POLICY: defaults to 'same-origin'
1010

1111
# Set defaults & clean up (normalize, trim, …)
12-
NGINX_CONFIG_FILE=/etc/nginx/snippets/vars/cors-origin.conf
13-
NGINX_CORS_ORIGINS=$(echo "${NGINX_CORS_ORIGINS:-*}" \
12+
readonly NGINX_CONFIG_FILE=/etc/nginx/snippets/vars/cors-origin.conf
13+
readonly NGINX_CORS_ORIGINS=$(echo "${NGINX_CORS_ORIGINS:-*}" \
1414
| sed 's/,/ /g; s/^ *//; s/ *$//; s/ */ /g')
15-
NGINX_CORS_RESOURCE_POLICY="${NGINX_CORS_RESOURCE_POLICY:-same-origin}"
15+
readonly NGINX_CORS_RESOURCE_POLICY="${NGINX_CORS_RESOURCE_POLICY:-same-origin}"
1616

1717
# Check nginx structure
1818
if [ ! -f "${NGINX_CONFIG_FILE}" ]; then

common/scripts/startup/50-env-configure-nginx-csp.sh

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env sh
22

3-
set -euo pipefail
3+
set -eu
44

55
# Configure nginx security based on ENV vars, and if available the defaults
66
# located at `/etc/csp-generator/default`.
@@ -19,17 +19,17 @@ set -euo pipefail
1919
# - NGINX_FRAME_OPTIONS: defaults to 'deny', note that setting to `disable` removes the header completely.
2020

2121
# Set defaults
22-
NGINX_CONFIG_FILE='/etc/nginx/snippets/vars/csp-and-robots.conf'
23-
NGINX_CSP_ITEMS='child-src connect-src font-src form-action frame-ancestors frame-src img-src manifest-src media-src object-src require-trusted-types-for script-src style-src trusted-types worker-src'
24-
NGINX_CSP_MODE="${NGINX_CSP_MODE:-report-only}"
25-
NGINX_CSP_REPORT_URI="${NGINX_CSP_REPORT_URI:-}"
26-
NGINX_FRAME_OPTIONS="${NGINX_FRAME_OPTIONS:-deny}"
22+
readonly NGINX_CONFIG_FILE='/etc/nginx/snippets/vars/csp-and-robots.conf'
23+
readonly NGINX_CSP_ITEMS='child-src connect-src font-src form-action frame-ancestors frame-src img-src manifest-src media-src object-src require-trusted-types-for script-src style-src trusted-types worker-src'
24+
readonly NGINX_CSP_MODE="${NGINX_CSP_MODE:-report-only}"
25+
readonly NGINX_CSP_REPORT_URI="${NGINX_CSP_REPORT_URI:-}"
26+
readonly NGINX_FRAME_OPTIONS="${NGINX_FRAME_OPTIONS:-deny}"
2727

2828
# Validate input
2929
if [ "${NGINX_CSP_MODE}" = 'enforce' ]; then
30-
NGINX_CSP_VAR_NAME='content_security_policy'
30+
readonly NGINX_CSP_VAR_NAME='content_security_policy'
3131
elif [ "${NGINX_CSP_MODE}" = 'report-only' ]; then
32-
NGINX_CSP_VAR_NAME='content_security_policy_report_only'
32+
readonly NGINX_CSP_VAR_NAME='content_security_policy_report_only'
3333
else
3434
echo "Nginx: invalid CSP mode ${NGINX_CSP_MODE}"
3535
exit 1

common/scripts/startup/50-env-configure-nginx-robots.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env sh
22

3-
set -euo pipefail
3+
set -eu
44

55
# Configure nginx robots rules based on ENV vars
66
#
@@ -9,10 +9,10 @@ set -euo pipefail
99
# - NGINX_ROBOTS_TXT: defaults to 'Disallow: /', note that setting to `disable` removes the rule completely.
1010

1111
# Set defaults
12-
NGINX_CONFIG_FILE_MODS=/etc/nginx/site-mods-enabled.d/generated-robots.conf
13-
NGINX_CONFIG_FILE_VARS=/etc/nginx/snippets/vars/robots-tag.conf
14-
NGINX_ROBOTS_TAG="${NGINX_ROBOTS_TAG:-none}"
15-
NGINX_ROBOTS_TXT="${NGINX_ROBOTS_TXT:-Disallow: /}"
12+
readonly NGINX_CONFIG_FILE_MODS=/etc/nginx/site-mods-enabled.d/generated-robots.conf
13+
readonly NGINX_CONFIG_FILE_VARS=/etc/nginx/snippets/vars/robots-tag.conf
14+
readonly NGINX_ROBOTS_TAG="${NGINX_ROBOTS_TAG:-none}"
15+
readonly NGINX_ROBOTS_TXT="${NGINX_ROBOTS_TXT:-Disallow: /}"
1616

1717
# robots tag header
1818
if [ -f "${NGINX_CONFIG_FILE_VARS}" ]; then

database-backup/.dockerignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# General
2+
**/.DS_Store
3+
**/*.md
4+
**/LICENSE
5+
6+
# Git
7+
**/.git
8+
**/.github
9+
**/.gitattributes
10+
**/.gitignore
11+
12+
# Docker
13+
**/.dockerignore
14+
**/Dockerfile

database-backup/Dockerfile

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
ARG ALPINE_VERSION="3.23"
2+
ARG SCW_VERSION="2.49"
3+
4+
#
5+
# --- Stage 1: Tools ---
6+
#
7+
8+
FROM scaleway/cli:${SCW_VERSION} AS scaleway
9+
10+
#
11+
# --- Stage 2: Base ---
12+
#
13+
14+
FROM common:${ALPINE_VERSION} AS base
15+
16+
# Install packages
17+
# hadolint ignore=DL3018
18+
RUN apk add --no-cache \
19+
curl \
20+
openssh-client \
21+
rclone
22+
23+
# Install tools
24+
COPY --from=scaleway /scw /usr/local/bin/scw
25+
26+
# Copy configuration files
27+
# - init
28+
COPY scripts/ /scripts/
29+
30+
#
31+
# --- Variant: Default ---
32+
#
33+
34+
FROM base AS final
35+
36+
# Set default command
37+
CMD ["backup"]

database-backup/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Database Backup
2+
3+
🐳 A Docker image for automated database backups from Managed Databases to S3-compatible storage.
4+
5+
Available images:
6+
- `latest`: normal version.
7+
8+
## Commands
9+
10+
This image supports multiple commands:
11+
12+
- `backup`: creates backups of managed DBs (PostgreSQL, MySQL) and uploads them to any S3-compatible storage (Scaleway Object Storage, AWS S3, MinIO, etc.)
13+
- fallback: execute the provided command.
14+
15+
## Configuration
16+
17+
Configuration is done using environment variables, and may depend on the command.
18+
19+
### Command `backup`
20+
21+
You'll need to select a DB provider, and also:
22+
- Provider-specific configuration, see below for example for [Provider scw](#provider-scw).
23+
- Configure [general S3 remote](#general-s3-remote).
24+
25+
| Variable | Description | Required |
26+
|----------|-------------|----------|
27+
| `DB_PROVIDER` | Hosting provider (i.e. `scw`) | Yes |
28+
| `DB_INSTANCE_ID` | Database instance ID | Yes |
29+
| `DB_NAME` | Name of the database to backup | Yes |
30+
31+
### Provider `scw`
32+
33+
| Variable | Description | Required |
34+
|----------|-------------|----------|
35+
| `SCW_ACCESS_KEY` | Scaleway API access key | Yes |
36+
| `SCW_SECRET_KEY` | Scaleway API secret key | Yes |
37+
| `SCW_DEFAULT_ORGANIZATION_ID` | Scaleway organization ID | Yes |
38+
| `SCW_DEFAULT_PROJECT_ID` | Scaleway project ID | Yes |
39+
| `SCW_DEFAULT_REGION` | Scaleway region (e.g., `nl-ams`, `fr-par`) | Yes |
40+
41+
### General S3 Remote
42+
43+
Will be used by `rclone` to address a S3-bucket as a remote.
44+
45+
| Variable | Description | Required | Example |
46+
|----------|-------------|----------|---------|
47+
| `S3_PROVIDER` | S3 provider (e.g. `Scaleway`, see rclone docs) | Yes |
48+
| `S3_ACCESS_KEY_ID` | S3 access key | Yes | - |
49+
| `S3_SECRET_ACCESS_KEY` | S3 secret key | Yes | - |
50+
| `S3_REGION` | S3 region | Yes | `nl-ams` |
51+
| `S3_ENDPOINT` | S3 endpoint URL | Yes | `s3.nl-ams.scw.cloud` |
52+
| `S3_BUCKET` | S3 bucket name | Yes | `my-backups` |
53+
| `S3_PATH` | Path prefix in bucket | Yes | `database/production` |
54+
| `S3_STORAGE_CLASS` | S3 storage class | No | Scaleway: `STANDARD`, `ONEZONE_IA`, `GLACIER` |
55+
56+
### Webhook configuration
57+
58+
A heartbeat url can be triggered when the job has been succesfully completed.
59+
If the environment variable is not configured, this step will be skipped.
60+
61+
| Variable | Description | Required | Example |
62+
|----------|-------------|----------|---------|
63+
| `HEARTBEAT_URL` | Heartbeat webhook url | no | - |
64+
65+
## Backup File Format
66+
67+
Backups are stored with the following naming convention:
68+
69+
```
70+
{DB_NAME}_{TIMESTAMP}.custom
71+
```
72+
73+
Example: `production_20251226T01.custom`
74+
75+
The `.custom` format is PostgreSQL's custom format, which can be restored using `pg_restore`.
76+
77+
## Restoring Backups
78+
79+
### PostgreSQL
80+
81+
```bash
82+
# Restore to database
83+
pg_restore \
84+
-h ip \
85+
-p port \
86+
-U user \
87+
-d database \
88+
--no-owner \
89+
--no-privileges \
90+
--single-transaction \
91+
--exclude-schema=_timescaledb_internal \
92+
--exclude-schema=_timescaledb_cache \
93+
--exclude-schema=_timescaledb_catalog \
94+
--exclude-schema=_timescaledb_config \
95+
production_20251226T010203Z.custom
96+
```
97+
98+
### MySQL/MariaDB
99+
100+
For MySQL databases, the backup format may differ. Consult documentation for the exact format.
101+
102+
## Error Handling & Heartbeat
103+
104+
The script will exit with code 1 and provide error details if:
105+
106+
- Backup creation fails
107+
- S3 upload fails
108+
- Any critical step encounters an error
109+
110+
Backup cleanup run after a successful sync to avoid accumulating backups. If configured, a heartbeat webhook will be triggered.

0 commit comments

Comments
 (0)