Skip to content

Commit a28e011

Browse files
feat: add multi-version Docker build, DB name parsing, tar compression, and flexible tagging
- Docker build system: - Add docker-compose.build.yml for local multi-version builds - Fix ARG placement in Dockerfiles (declare before FROM for proper expansion) - Switch Mongo Dockerfile to Alpine with apk mongodb-tools (simpler than manual download) - Upgrade all Dockerfiles to Go 1.25 - Update CI matrix with is_default flags for recommended versions - Image tagging strategy: - Version-specific: :pg-16, :mongo-8, :mysql-8, :mariadb-11, :redis-7 - Recommended (on release): :pg, :mongo, :mysql, :mariadb, :redis - Latest (on release): :pg-latest, :mongo-latest, etc. - CI creates unversioned + latest tags only for default versions on v* releases - DB name parsing from URI: - Config.DBNameOrDefault() now extracts database name from connection URIs - Parse path component and strip query params (e.g. mongodb+srv://.../@host/main?... -> main) - Falls back to 'unknown' if parsing fails - UUID template: - Use first 8 characters of UUIDv7 (no hyphens, e.g. 019c38fb) - Update README and godoc to reflect 8-char prefix - Tar mode compression: - Add gzip support: tar czf when BACKUP_COMPRESS=true - Automatic .tar.gz extension when compressed - Reduces backup size by ~75% (tested: 35KB -> 9KB) - Testing: - Comprehensive feature validation (stream, directory, tar, retention, hooks, dry-run) - All default images built and verified
1 parent d879a43 commit a28e011

12 files changed

Lines changed: 186 additions & 41 deletions

File tree

.github/workflows/build.yml

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,25 @@ jobs:
2626
build_args: DB_VERSION=15
2727
- engine: pg
2828
version: "16"
29+
is_default: true
2930
dockerfile: docker/Dockerfile.pg
3031
build_args: DB_VERSION=16
3132
- engine: pg
3233
version: "17"
3334
dockerfile: docker/Dockerfile.pg
3435
build_args: DB_VERSION=17
3536
# MongoDB
36-
- engine: mongo
37-
version: "6"
38-
dockerfile: docker/Dockerfile.mongo
39-
build_args: MONGO_TOOLS_VERSION=100.9.0
4037
- engine: mongo
4138
version: "7"
4239
dockerfile: docker/Dockerfile.mongo
43-
build_args: MONGO_TOOLS_VERSION=100.10.0
4440
- engine: mongo
4541
version: "8"
42+
is_default: true
4643
dockerfile: docker/Dockerfile.mongo
47-
build_args: MONGO_TOOLS_VERSION=100.10.0
4844
# MySQL
4945
- engine: mysql
5046
version: "8"
47+
is_default: true
5148
dockerfile: docker/Dockerfile.mysql
5249
build_args: DB_VERSION=8
5350
- engine: mysql
@@ -58,14 +55,20 @@ jobs:
5855
- engine: mariadb
5956
version: "10"
6057
dockerfile: docker/Dockerfile.mysql
61-
build_args: DB_VERSION=10
58+
build_args: |
59+
DB_VERSION=10
60+
ENGINE_NAME=mariadb
6261
- engine: mariadb
6362
version: "11"
63+
is_default: true
6464
dockerfile: docker/Dockerfile.mysql
65-
build_args: DB_VERSION=11
65+
build_args: |
66+
DB_VERSION=11
67+
ENGINE_NAME=mariadb
6668
# Redis
6769
- engine: redis
6870
version: "7"
71+
is_default: true
6972
dockerfile: docker/Dockerfile.redis
7073
build_args: DB_VERSION=7
7174
- engine: redis
@@ -80,7 +83,7 @@ jobs:
8083
- name: Set up Go
8184
uses: actions/setup-go@v5
8285
with:
83-
go-version: "1.22"
86+
go-version: "1.25"
8487

8588
- name: Build Go binary
8689
run: CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o dbstash ./cmd/dbstash
@@ -102,7 +105,8 @@ jobs:
102105
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
103106
tags: |
104107
type=raw,value=${{ matrix.engine }}-${{ matrix.version }}
105-
type=raw,value=${{ matrix.engine }}-latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
108+
type=raw,value=${{ matrix.engine }}-latest,enable=${{ startsWith(github.ref, 'refs/tags/v') && matrix.is_default == 'true' }}
109+
type=raw,value=${{ matrix.engine }},enable=${{ startsWith(github.ref, 'refs/tags/v') && matrix.is_default == 'true' }}
106110
107111
- name: Build and push
108112
uses: docker/build-push-action@v5

README.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,20 @@ docker run --rm \
1818

1919
## Available Images
2020

21-
| Database | Engine Key | Example Tags |
22-
|------------|------------|---------------------------|
23-
| PostgreSQL | `pg` | `pg-15`, `pg-16`, `pg-17` |
24-
| MongoDB | `mongo` | `mongo-6`, `mongo-7`, `mongo-8` |
25-
| MySQL | `mysql` | `mysql-8`, `mysql-9` |
26-
| MariaDB | `mariadb` | `mariadb-10`, `mariadb-11`|
27-
| Redis | `redis` | `redis-7`, `redis-8` |
21+
| Database | Engine Key | Recommended Tag | Version-Specific Tags | Default Version |
22+
|------------|------------|-----------------|------------------------|-----------------|
23+
| PostgreSQL | `pg` | `:pg`, `:pg-latest` | `:pg-15`, `:pg-16`, `:pg-17` | 16 |
24+
| MongoDB | `mongo` | `:mongo`, `:mongo-latest` | `:mongo-7`, `:mongo-8` | 8 |
25+
| MySQL | `mysql` | `:mysql`, `:mysql-latest` | `:mysql-8`, `:mysql-9` | 8 |
26+
| MariaDB | `mariadb` | `:mariadb`, `:mariadb-latest` | `:mariadb-10`, `:mariadb-11` | 11 |
27+
| Redis | `redis` | `:redis`, `:redis-latest` | `:redis-7`, `:redis-8` | 7 |
2828

29-
All images are available at `ghcr.io/viperadnan/dbstash:<engine>-<version>`.
29+
**Tag Strategy:**
30+
- **`:engine-version`** (e.g. `:pg-16`) — pinned to specific database version
31+
- **`:engine`** (e.g. `:pg`) — recommended stable version (updated on new releases)
32+
- **`:engine-latest`** (e.g. `:pg-latest`) — latest supported version (may change)
33+
34+
All images: `ghcr.io/viperadnan/dbstash:<tag>`
3035

3136
## How It Works
3237

@@ -124,7 +129,7 @@ The `BACKUP_NAME_TEMPLATE` value is expanded at backup time by replacing tokens
124129
| `{date}` | Current date as `YYYY-MM-DD` | `2026-02-07` |
125130
| `{time}` | Current time as `HHmmss` | `020000` |
126131
| `{ts}` | Unix timestamp in seconds | `1770508800` |
127-
| `{uuid}` | First 12 characters of a UUIDv7 (time-ordered, unique) | `01953528e5f6` |
132+
| `{uuid}` | First 8 characters of a UUIDv7 (time-ordered) | `019c38fb` |
128133

129134
**Default template:** `{db}-{date}-{time}` produces filenames like `myapp-2026-02-07-020000.sql`.
130135

docker-compose.build.yml

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
services:
2+
# PostgreSQL
3+
pg-15:
4+
build:
5+
context: .
6+
dockerfile: docker/Dockerfile.pg
7+
args:
8+
DB_VERSION: "15"
9+
image: ghcr.io/viperadnan/dbstash:pg-15
10+
11+
pg-16:
12+
build:
13+
context: .
14+
dockerfile: docker/Dockerfile.pg
15+
args:
16+
DB_VERSION: "16"
17+
image: ghcr.io/viperadnan/dbstash:pg-16
18+
19+
pg-17:
20+
build:
21+
context: .
22+
dockerfile: docker/Dockerfile.pg
23+
args:
24+
DB_VERSION: "17"
25+
image: ghcr.io/viperadnan/dbstash:pg-17
26+
27+
# MongoDB
28+
mongo-6:
29+
build:
30+
context: .
31+
dockerfile: docker/Dockerfile.mongo
32+
image: ghcr.io/viperadnan/dbstash:mongo-6
33+
34+
mongo-7:
35+
build:
36+
context: .
37+
dockerfile: docker/Dockerfile.mongo
38+
image: ghcr.io/viperadnan/dbstash:mongo-7
39+
40+
mongo-8:
41+
build:
42+
context: .
43+
dockerfile: docker/Dockerfile.mongo
44+
image: ghcr.io/viperadnan/dbstash:mongo-8
45+
46+
# MySQL
47+
mysql-8:
48+
build:
49+
context: .
50+
dockerfile: docker/Dockerfile.mysql
51+
args:
52+
DB_VERSION: "8"
53+
image: ghcr.io/viperadnan/dbstash:mysql-8
54+
55+
mysql-9:
56+
build:
57+
context: .
58+
dockerfile: docker/Dockerfile.mysql
59+
args:
60+
DB_VERSION: "9"
61+
image: ghcr.io/viperadnan/dbstash:mysql-9
62+
63+
# MariaDB (uses mysql Dockerfile with ENGINE_NAME override)
64+
mariadb-10:
65+
build:
66+
context: .
67+
dockerfile: docker/Dockerfile.mysql
68+
args:
69+
DB_VERSION: "10"
70+
ENGINE_NAME: mariadb
71+
image: ghcr.io/viperadnan/dbstash:mariadb-10
72+
73+
mariadb-11:
74+
build:
75+
context: .
76+
dockerfile: docker/Dockerfile.mysql
77+
args:
78+
DB_VERSION: "11"
79+
ENGINE_NAME: mariadb
80+
image: ghcr.io/viperadnan/dbstash:mariadb-11
81+
82+
# Redis
83+
redis-7:
84+
build:
85+
context: .
86+
dockerfile: docker/Dockerfile.redis
87+
args:
88+
DB_VERSION: "7"
89+
image: ghcr.io/viperadnan/dbstash:redis-7
90+
91+
redis-8:
92+
build:
93+
context: .
94+
dockerfile: docker/Dockerfile.redis
95+
args:
96+
DB_VERSION: "8"
97+
image: ghcr.io/viperadnan/dbstash:redis-8

docker/Dockerfile.mongo

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Stage 1: Build Go binary
2-
FROM golang:1.22-alpine AS builder
2+
FROM golang:1.25-alpine AS builder
33
WORKDIR /src
44
COPY go.mod go.sum ./
55
RUN go mod download
@@ -8,11 +8,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /dbstash ./cmd/dbstash
88

99
# Stage 2: Runtime image with mongodump + rclone
1010
FROM alpine:3.20
11-
ARG MONGO_TOOLS_VERSION=100.10.0
12-
RUN apk add --no-cache ca-certificates tzdata curl && \
13-
curl -fsSL "https://fastdl.mongodb.org/tools/db/mongodb-database-tools-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m)-${MONGO_TOOLS_VERSION}.tgz" \
14-
| tar -xz -C /usr/local/bin --strip-components=2 --wildcards '*/bin/mongodump' && \
15-
apk add --no-cache rclone
11+
RUN apk add --no-cache mongodb-tools rclone ca-certificates tzdata
1612
COPY --from=builder /dbstash /usr/local/bin/dbstash
1713
ENV ENGINE=mongo
1814
ENTRYPOINT ["dbstash"]

docker/Dockerfile.mysql

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Stage 1: Build Go binary
2-
FROM golang:1.22-alpine AS builder
2+
FROM golang:1.25-alpine AS builder
33
WORKDIR /src
44
COPY go.mod go.sum ./
55
RUN go mod download
@@ -11,5 +11,6 @@ ARG DB_VERSION=8
1111
FROM alpine:3.20
1212
RUN apk add --no-cache mysql-client rclone ca-certificates tzdata
1313
COPY --from=builder /dbstash /usr/local/bin/dbstash
14-
ENV ENGINE=mysql
14+
ARG ENGINE_NAME=mysql
15+
ENV ENGINE=${ENGINE_NAME}
1516
ENTRYPOINT ["dbstash"]

docker/Dockerfile.pg

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
# Build arguments
2+
ARG DB_VERSION=16
3+
14
# Stage 1: Build Go binary
2-
FROM golang:1.22-alpine AS builder
5+
FROM golang:1.25-alpine AS builder
36
WORKDIR /src
47
COPY go.mod go.sum ./
58
RUN go mod download
69
COPY . .
710
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /dbstash ./cmd/dbstash
811

912
# Stage 2: Runtime image with pg_dump + rclone
10-
ARG DB_VERSION=16
1113
FROM postgres:${DB_VERSION}-alpine
1214
RUN apk add --no-cache rclone ca-certificates tzdata
1315
COPY --from=builder /dbstash /usr/local/bin/dbstash

docker/Dockerfile.redis

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
# Build arguments
2+
ARG DB_VERSION=7
3+
14
# Stage 1: Build Go binary
2-
FROM golang:1.22-alpine AS builder
5+
FROM golang:1.25-alpine AS builder
36
WORKDIR /src
47
COPY go.mod go.sum ./
58
RUN go mod download
69
COPY . .
710
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /dbstash ./cmd/dbstash
811

912
# Stage 2: Runtime image with redis-cli + rclone
10-
ARG DB_VERSION=7
1113
FROM redis:${DB_VERSION}-alpine
1214
RUN apk add --no-cache rclone ca-certificates tzdata
1315
COPY --from=builder /dbstash /usr/local/bin/dbstash

internal/config/config.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,17 +171,46 @@ func Load() (*Config, error) {
171171
return cfg, nil
172172
}
173173

174-
// DBNameOrDefault returns the database name, or a fallback for display purposes.
174+
// DBNameOrDefault returns the database name. If DB_NAME is not set,
175+
// it attempts to extract the database name from DB_URI.
175176
func (c *Config) DBNameOrDefault() string {
176177
if c.DBName != "" {
177178
return c.DBName
178179
}
179180
if c.DBURI != "" {
180-
return "from-uri"
181+
return dbNameFromURI(c.DBURI)
181182
}
182183
return "unknown"
183184
}
184185

186+
// dbNameFromURI extracts the database name from a connection URI path.
187+
func dbNameFromURI(uri string) string {
188+
// Handle schemes like mongodb+srv:// , postgresql://, etc.
189+
idx := strings.Index(uri, "://")
190+
if idx < 0 {
191+
return "unknown"
192+
}
193+
rest := uri[idx+3:]
194+
// Skip user:pass@host portion
195+
if at := strings.Index(rest, "@"); at >= 0 {
196+
rest = rest[at+1:]
197+
}
198+
// Find path after host(:port)
199+
slash := strings.Index(rest, "/")
200+
if slash < 0 {
201+
return "unknown"
202+
}
203+
dbName := rest[slash+1:]
204+
// Strip query parameters
205+
if q := strings.Index(dbName, "?"); q >= 0 {
206+
dbName = dbName[:q]
207+
}
208+
if dbName == "" {
209+
return "unknown"
210+
}
211+
return dbName
212+
}
213+
185214
// resolveFileVar checks for a _FILE variant first. If the file exists, its
186215
// contents (trimmed) are returned. Otherwise falls back to the base env var.
187216
func resolveFileVar(baseVar, fileVar string) string {

internal/config/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ func TestDBNameOrDefault(t *testing.T) {
336336
expected string
337337
}{
338338
{"with name", Config{DBName: "mydb"}, "mydb"},
339-
{"with uri", Config{DBURI: "postgres://host/db"}, "from-uri"},
339+
{"with uri", Config{DBURI: "postgres://host/db"}, "db"},
340340
{"neither", Config{}, "unknown"},
341341
}
342342
for _, tt := range tests {

internal/pipeline/pipeline.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func New(mode string) (Pipeline, error) {
4444
// {date} — current date as YYYY-MM-DD (e.g. "2026-02-07")
4545
// {time} — current time as HHmmss (e.g. "020000")
4646
// {ts} — Unix timestamp in seconds (e.g. "1770508800")
47-
// {uuid} — first 12 characters of a UUIDv7 (e.g. "a1b2c3d4e5f6")
47+
// {uuid} — first 8 characters of a UUIDv7 (e.g. "019c38fb")
4848
//
4949
// The file extension is appended automatically based on the engine and
5050
// compression setting, unless overridden by BACKUP_EXTENSION. Timestamps
@@ -58,7 +58,7 @@ func resolveFilename(template string, cfg *config.Config, eng engine.Engine, ext
5858
}
5959

6060
dbName := cfg.DBNameOrDefault()
61-
shortUUID := uuid.Must(uuid.NewV7()).String()[:12]
61+
shortUUID := uuid.Must(uuid.NewV7()).String()[:8]
6262

6363
name := template
6464
name = strings.ReplaceAll(name, "{db}", dbName)
@@ -89,7 +89,7 @@ func resolveDirname(template string, cfg *config.Config, eng engine.Engine) stri
8989
}
9090

9191
dbName := cfg.DBNameOrDefault()
92-
shortUUID := uuid.Must(uuid.NewV7()).String()[:12]
92+
shortUUID := uuid.Must(uuid.NewV7()).String()[:8]
9393

9494
name := template
9595
name = strings.ReplaceAll(name, "{db}", dbName)

0 commit comments

Comments
 (0)