Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Mysql support #26

Merged
merged 11 commits into from
Apr 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions .env.dist
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
S3_ENDPOINT=
S3_BUCKET=postgres-backups
S3_ACCESS_KEY=
S3_SECRET_KEY=

POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres

EVERY=24h
URLS="postgres://user:password@host:port/dbname,mysql://user:password@host:port/dbname"
S3_ENDPOINT="your_s3_endpoint"
S3_BUCKET="your_s3_bucket"
S3_ACCESS_KEY="your_s3_access_key"
S3_SECRET_KEY="your_s3_secret_key"
INTERVAL="24h"
2 changes: 1 addition & 1 deletion .github/workflows/ghcr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:

env:
REGISTRY: ghcr.io
FQDN: "ghcr.io/thedevminertv/postgres-s3-backup"
FQDN: "ghcr.io/thedevminertv/database-s3-backup"

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:

env:
REGISTRY: ghcr.io
FQDN: "ghcr.io/thedevminertv/postgres-s3-backup"
FQDN: "ghcr.io/thedevminertv/database-s3-backup"

jobs:
build:
Expand Down
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
FROM alpine:latest
WORKDIR /root/

RUN apk --no-cache add ca-certificates postgresql-client
RUN apk --no-cache add ca-certificates postgresql-client mysql-client

COPY --from=builder /app/main /bin/postgres-s3-backup
COPY --from=builder /app/main /bin/database-s3-backup

CMD ["/bin/postgres-s3-backup"]
CMD ["/bin/database-s3-backup"]
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# PostgreSQL Backup to S3 with Docker
# Database Backup to S3 with Docker

This application automates the process of backing up PostgreSQL databases and uploading them to an S3-compatible storage service, utilizing Docker for easy deployment and scheduling.
This application automates the process of backing up PostgreSQL and MySQL databases and uploading them to an S3-compatible storage service, utilizing Docker for easy deployment and scheduling.

## Features

- Easy deployment with Docker and Docker Compose.
- Support for multiple PostgreSQL databases.
- Support for multiple PostgreSQL and MySQL databases.
- Customizable backup intervals.
- Direct upload of backups to an S3-compatible storage bucket.
- Environment variable and command-line configuration for flexibility.
Expand All @@ -14,7 +14,7 @@ This application automates the process of backing up PostgreSQL databases and up
## Prerequisites

- Docker and Docker Compose installed on your system.
- Access to a PostgreSQL database.
- Access to PostgreSQL and/or MySQL databases.
- Access to an S3-compatible storage service.

## Configuration
Expand All @@ -25,7 +25,7 @@ Before running the application, you need to configure it either by setting envir

Create a `.env` file in the project directory with the following variables:

- `URLS`: Comma-separated list of PostgreSQL database URLs to backup. Format: `postgres://<user>:<password>@<host>[:<port>]/<dbname>`
- `URLS`: Comma-separated list of database URLs to backup. Format for PostgreSQL: `postgres://<user>:<password>@<host>[:<port>]/<dbname>` and for MySQL: `mysql://<user>:<password>@<host>[:<port>]/<dbname>`
- `S3_ENDPOINT`: The endpoint URL of your S3-compatible storage service.
- `S3_BUCKET`: The name of the bucket where backups will be stored.
- `S3_ACCESS_KEY`: Your S3 access key.
Expand All @@ -41,7 +41,7 @@ services:
app:
build: .
environment:
URLS: "postgres://user:password@host:port/dbname"
URLS: "postgres://user:password@host:port/dbname,mysql://user:password@host:port/dbname"
S3_ENDPOINT: "your_s3_endpoint"
S3_BUCKET: "your_s3_bucket"
S3_ACCESS_KEY: "your_s3_access_key"
Expand All @@ -51,7 +51,7 @@ services:

## Running the Application with Docker

There is an image available on `ghcr.io/thedevminertv/postgres_s3_backup` that you can use.
There is an image available on `ghcr.io/thedevminertv/database-s3-backup` that you can use.

Alternatively, you can build the image yourself:

Expand All @@ -67,7 +67,7 @@ Alternatively, you can build the image yourself:
docker compose up -d
```

This will start the application in the background. It will automatically perform backups based on the configured interval and upload them to the specified S3 bucket.
This will start the application in the background. It will automatically perform backups for both PostgreSQL and MySQL databases based on the configured interval and upload them to the specified S3 bucket.

## Monitoring and Logs

Expand Down
118 changes: 118 additions & 0 deletions dump.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package main

import (
"bufio"
"errors"
"fmt"
"os/exec"
"strconv"
"time"
)

type connectionOptions struct {
Host string
DbType string
Port int
Database string
Username string
Password string
}

var (
PGDumpCmd = "pg_dump"
pgDumpStdOpts = []string{"--no-owner", "--no-acl", "--clean", "--blobs", "-v"}
pgDumpDefaultFormat = "c"
ErrPgDumpNotFound = errors.New("pg_dump not found")

MysqlDumpCmd = "mysqldump"
mysqlDumpStdOpts = []string{"--compact", "--skip-add-drop-table", "--skip-add-locks", "--skip-disable-keys", "--skip-set-charset", "-v"}
ErrMySqlDumpNotFound = errors.New("mysqldump not found")

ErrUnsupportedType = errors.New("unsupported database type")
)

func RunDump(connectionOpts *connectionOptions, outFile string) error {
cmd, err := buildDumpCommand(connectionOpts, outFile)
if err != nil {
return err
}

return executeCommand(cmd)
}

func buildDumpCommand(opts *connectionOptions, outFile string) (*exec.Cmd, error) {
switch opts.DbType {
case "postgres":
if !commandExist(PGDumpCmd) {
return nil, ErrPgDumpNotFound
}
options := append(
pgDumpStdOpts,
fmt.Sprintf("-f%s", outFile),
fmt.Sprintf("--dbname=%s", opts.Database),
fmt.Sprintf("--host=%s", opts.Host),
fmt.Sprintf("--port=%d", opts.Port),
fmt.Sprintf("--username=%s", opts.Username),
fmt.Sprintf("--format=%s", pgDumpDefaultFormat),
)
return exec.Command(PGDumpCmd, options...), nil

case "mysql":
mysqldumpCmd := "mysqldump"
if !commandExist(mysqldumpCmd) {
return nil, ErrMySqlDumpNotFound
}
options := append(
mysqlDumpStdOpts,
"-h", opts.Host,
"-P", strconv.Itoa(opts.Port),
"-u", opts.Username,
fmt.Sprintf("--password=%s", opts.Password),
"--databases", opts.Database,
"-r", outFile,
)

return exec.Command(mysqldumpCmd, options...), nil

default:
return nil, ErrUnsupportedType
}
}

func executeCommand(cmd *exec.Cmd) error {
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}

if err := cmd.Start(); err != nil {
return err
}

go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}()

if err := cmd.Wait(); err != nil {
return err
}
return nil
}

func commandExist(command string) bool {
_, err := exec.LookPath(command)
return err == nil
}

func newFileName(db string, dbType string) string {
switch dbType {
case "postgres":
return fmt.Sprintf(`%v_%v.pgdump`, db, time.Now().Unix())
case "mysql":
return fmt.Sprintf(`%v_%v.sql`, db, time.Now().Unix())
}
return fmt.Sprintf(`%v_%v`, db, time.Now().Unix())
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/testify v1.7.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect
Expand Down
41 changes: 0 additions & 41 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,19 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.63 h1:GbZ2oCvaUdgT5640WJOpyDhhDxvknAJU2/T3yurwcbQ=
github.com/minio/minio-go/v7 v7.0.63/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
github.com/minio/minio-go/v7 v7.0.64 h1:Zdza8HwOzkld0ZG/og50w56fKi6AAyfqfifmasD9n2Q=
github.com/minio/minio-go/v7 v7.0.64/go.mod h1:R4WVUR6ZTedlCcGwZRauLMIKjgyaWxhs4Mqi/OMPmEc=
github.com/minio/minio-go/v7 v7.0.65 h1:sOlB8T3nQK+TApTpuN3k4WD5KasvZIE3vVFzyyCa0go=
github.com/minio/minio-go/v7 v7.0.65/go.mod h1:R4WVUR6ZTedlCcGwZRauLMIKjgyaWxhs4Mqi/OMPmEc=
github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw=
github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs=
github.com/minio/minio-go/v7 v7.0.67 h1:BeBvZWAS+kRJm1vGTMJYVjKUNoo0FoEt/wUWdUtfmh8=
github.com/minio/minio-go/v7 v7.0.67/go.mod h1:+UXocnUeZ3wHvVh5s95gcrA4YjMIbccT6ubB+1m054A=
github.com/minio/minio-go/v7 v7.0.68 h1:hTqSIfLlpXaKuNy4baAp4Jjy2sqZEN9hRxD0M4aOfrQ=
github.com/minio/minio-go/v7 v7.0.68/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=
github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0=
github.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
Expand All @@ -52,36 +30,17 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
19 changes: 13 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ package main
import (
"context"
"flag"
"github.com/joho/godotenv"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"log"
"net/url"
"os"
"strconv"
"strings"
"time"

"github.com/joho/godotenv"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)

func main() {
Expand Down Expand Up @@ -42,7 +43,13 @@ func main() {
log.Fatalf("Failed to parse url %s: %s", rawUrl, err)
}

port := 5432
port := 0
TheDevMinerTV marked this conversation as resolved.
Show resolved Hide resolved
switch parsedUrl.Scheme {
case "postgres":
port = 5432
case "mysql":
port = 3306
}
rawPort := parsedUrl.Port()
if rawPort != "" {
port, err = strconv.Atoi(rawPort)
Expand All @@ -58,6 +65,7 @@ func main() {

urls[i] = connectionOptions{
Host: parsedUrl.Hostname(),
DbType: parsedUrl.Scheme,
Port: port,
Database: strings.TrimPrefix(parsedUrl.Path, "/"),
Username: parsedUrl.User.Username(),
Expand All @@ -79,8 +87,7 @@ func main() {
for {
for _, u := range urls {
log.Printf("Backing up %s", u.Database)

file := newFileName(u.Database)
file := newFileName(u.Database, u.DbType)

if err = RunDump(&u, file); err != nil {
log.Printf("WARNING: Failed to dump database: %s", err)
Expand Down
Loading
Loading