Initial Commit
This commit is contained in:
parent
baa561c56e
commit
8430ff9304
19 changed files with 562 additions and 1 deletions
5
.dockerignore
Normal file
5
.dockerignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
LICENSE
|
||||
README.md
|
||||
docker-compose.yml
|
||||
./examples/*
|
||||
.tool-versions
|
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
@ -0,0 +1 @@
|
|||
ruby 3.3.5
|
35
Dockerfile
Normal file
35
Dockerfile
Normal file
|
@ -0,0 +1,35 @@
|
|||
FROM ruby:3.3-alpine
|
||||
|
||||
RUN mkdir /backups
|
||||
|
||||
# Install dependencies
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
libffi-dev \
|
||||
linux-headers \
|
||||
postgresql-dev \
|
||||
tzdata \
|
||||
git \
|
||||
curl
|
||||
|
||||
RUN gem install bundler -v 2.6.7
|
||||
|
||||
COPY entrypoint.sh /
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
# Copy the Gemfile and Gemfile.lock
|
||||
COPY ./app/Gemfile ./app/Gemfile.lock /app/
|
||||
# Install the gems
|
||||
RUN bundle config set --local deployment 'true' && \
|
||||
bundle config set --local path 'vendor/bundle' && \
|
||||
bundle config set --local without 'development test' && \
|
||||
bundle install
|
||||
|
||||
|
||||
COPY ./app/ /app/
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["bundle", "exec", "ruby", "./app.rb"]
|
63
README.md
63
README.md
|
@ -1,3 +1,64 @@
|
|||
# baktainer
|
||||
Easily backup databases running in docker containers.
|
||||
## Features
|
||||
- Backup MySQL, PostgreSQL, MongoDB, and SQLite databases
|
||||
- Run on a schedule using cron expressions
|
||||
- Backup databases running in docker containers
|
||||
- Define which databases to backup using docker labels
|
||||
## Installation
|
||||
```yaml
|
||||
services:
|
||||
baktainer:
|
||||
image: jamez01/baktainer:latest
|
||||
container_name: baktainer
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./backups:/backups
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- BT_CRON="0 0 * * *" # Backup every day at midnight
|
||||
- "BT_DOCKER_URL=unix:///var/run/docker.sock"
|
||||
- BT_THREADS=4
|
||||
- BT_BACKUP_DIR=/backups
|
||||
- BT_LOG_LEVEL=info
|
||||
# Enable if using SSL over tcp
|
||||
#- BT_SSL = true
|
||||
#- BT_CA
|
||||
#- BT_CERT
|
||||
#- BT_KEY
|
||||
```
|
||||
|
||||
Easily backup your docker databases
|
||||
## Environment Variables
|
||||
| Variable | Description | Default |
|
||||
| -------- | ----------- | ------- |
|
||||
| BT_CRON | Cron expression for scheduling backups | 0 0 * * * |
|
||||
| BT_THREADS | Number of threads to use for backups | 4 |
|
||||
| BT_BACKUP_DIR | Directory to store backups | /backups |
|
||||
| BT_LOG_LEVEL | Log level (debug, info, warn, error) | info |
|
||||
| BT_SSL | Enable SSL for docker connection | false |
|
||||
| BT_CA | Path to CA certificate | none |
|
||||
| BT_CERT | Path to client certificate | none |
|
||||
| BT_KEY | Path to client key | none |
|
||||
| BT_DOCKER_URL | Docker URL | unix:///var/run/docker.sock |
|
||||
|
||||
## Usage
|
||||
Add labels to your docker containers to specify which databases to backup.
|
||||
```yaml
|
||||
services:
|
||||
db:
|
||||
image: postgres:17
|
||||
container_name: my-db
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- db:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_DB: "${DB_BASE:-database}"
|
||||
POSTGRES_USER: "${DB_USER:-user}"
|
||||
POSTGRES_PASSWORD: "${DB_PASSWORD:-StrongPassword}"
|
||||
labels:
|
||||
- baktainer.backup: "true"
|
||||
- baktainer.db.name: "my-db"
|
||||
- baktainer.db.password: "StrongPassword"
|
||||
- baktainer.db.engine: "postgres"
|
||||
- baktainer.name: "MyApp"
|
||||
```
|
||||
|
|
7
app/Gemfile
Normal file
7
app/Gemfile
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
gem 'base64', '~> 0.2.0'
|
||||
gem 'concurrent-ruby', '~> 1.3.5'
|
||||
gem 'docker-api', '~> 2.4.0'
|
||||
gem 'cron_calc', '~> 1.0.0'
|
26
app/Gemfile.lock
Normal file
26
app/Gemfile.lock
Normal file
|
@ -0,0 +1,26 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
base64 (0.2.0)
|
||||
concurrent-ruby (1.3.5)
|
||||
cron_calc (1.0.0)
|
||||
docker-api (2.4.0)
|
||||
excon (>= 0.64.0)
|
||||
multi_json
|
||||
excon (1.2.5)
|
||||
logger
|
||||
logger (1.7.0)
|
||||
multi_json (1.15.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
base64 (~> 0.2.0)
|
||||
concurrent-ruby (~> 1.3.5)
|
||||
cron_calc (~> 1.0.0)
|
||||
docker-api (~> 2.4.0)
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.2
|
33
app/app.rb
Normal file
33
app/app.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rubygems'
|
||||
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/lib"
|
||||
require 'bundler/setup'
|
||||
require 'baktainer'
|
||||
require 'baktainer/logger'
|
||||
require 'baktainer/container'
|
||||
require 'baktainer/backup_command'
|
||||
|
||||
require 'optparse'
|
||||
|
||||
options = {}
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = 'Usage: baktainer.rb [options]'
|
||||
|
||||
opts.on('-N', '--now', 'Run immediately and exit.') do
|
||||
options[:now] = true
|
||||
end
|
||||
end.parse!
|
||||
|
||||
LOGGER.info('Starting')
|
||||
baktainer = Baktainer::Runner.new(
|
||||
url: ENV['BT_DOCKER_URL'] || 'unix:///var/run/docker.sock',
|
||||
ssl: ENV['BT_SSL'] || false,
|
||||
ssl_options: {
|
||||
ca_file: ENV['BT_CA'],
|
||||
client_cert: ENV['BT_CERT'],
|
||||
client_key: ENV['BT_KEY']
|
||||
}
|
||||
)
|
||||
|
||||
baktainer.run
|
107
app/lib/baktainer.rb
Normal file
107
app/lib/baktainer.rb
Normal file
|
@ -0,0 +1,107 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Baktainer is a class responsible for managing database backups using Docker containers.
|
||||
#
|
||||
# It supports the following database engines: PostgreSQL, MySQL, MariaDB, and Sqlite3.
|
||||
#
|
||||
# @example Initialize a Baktainer instance
|
||||
# baktainer = Baktainer.new(url: 'unix:///var/run/docker.sock', ssl: true, ssl_options: {})
|
||||
#
|
||||
# @example Run the backup process
|
||||
# baktainer.run
|
||||
#
|
||||
# @!attribute [r] SUPPORTED_ENGINES
|
||||
# @return [Array<String>] The list of supported database engines.
|
||||
#
|
||||
# @param url [String] The Docker API URL. Defaults to 'unix:///var/run/docker.sock'.
|
||||
# @param ssl [Boolean] Whether to use SSL for Docker API communication. Defaults to false.
|
||||
#
|
||||
# @method perform_backup
|
||||
# Starts the backup process by searching for Docker containers and performing backups.
|
||||
# Logs the process at various stages.
|
||||
#
|
||||
# @method run
|
||||
# Schedules and runs the backup process at a specified time.
|
||||
# If the time is invalid or not provided, defaults to 05:00.
|
||||
#
|
||||
# @private
|
||||
# @method setup_ssl
|
||||
# Configures SSL settings for Docker API communication if SSL is enabled.
|
||||
# Uses environment variables `BT_CA`, `BT_CERT`, and `BT_KEY` for SSL certificates and keys.
|
||||
module Baktainer
|
||||
end
|
||||
|
||||
require 'docker-api'
|
||||
require 'cron_calc'
|
||||
require 'concurrent/executor/fixed_thread_pool'
|
||||
require 'baktainer/logger'
|
||||
require 'baktainer/container'
|
||||
require 'baktainer/backup_command'
|
||||
|
||||
STDOUT.sync = true
|
||||
|
||||
|
||||
class Baktainer::Runner
|
||||
SUPPORTED_ENGINES = %w[postgres postgres-all sqlite mongodb mysql mariadb].freeze
|
||||
def initialize(url: 'unix:///var/run/docker.sock', ssl: false, ssl_options: {}, threads: 5)
|
||||
@pool = Concurrent::FixedThreadPool.new(threads)
|
||||
@url = url
|
||||
@ssl = ssl
|
||||
@ssl_options = ssl_options
|
||||
Docker.url = @url
|
||||
setup_ssl
|
||||
LOGGER.level = ENV['LOG_LEVEL'] || :debug
|
||||
end
|
||||
|
||||
def perform_backup
|
||||
LOGGER.info('Starting backup process.')
|
||||
LOGGER.debug('Docker Searching for containers.')
|
||||
Containers.find_all.each do |container|
|
||||
# @pool.post do
|
||||
begin
|
||||
LOGGER.info("Backing up container #{container.name} with engine #{container.engine}.")
|
||||
container.backup
|
||||
LOGGER.info("Backup completed for container #{container.name}.")
|
||||
rescue StandardError => e
|
||||
LOGGER.error("Error backing up container #{container.name}: #{e.message}")
|
||||
LOGGER.debug(e.backtrace.join("\n"))
|
||||
end
|
||||
# end
|
||||
end
|
||||
end
|
||||
|
||||
def run
|
||||
run_at = ENV['BT_CRON'] || '0 0 * * *'
|
||||
begin
|
||||
@cron = CronCalc.new(run_at)
|
||||
rescue
|
||||
LOGGER.error("Invalid cron format for BT_CRON: #{run_at}.")
|
||||
end
|
||||
|
||||
loop do
|
||||
now = Time.now
|
||||
next_run = @cron.next.first
|
||||
# sleep_duration = next_run - now
|
||||
sleep_duration = 5
|
||||
LOGGER.info("Sleeping for #{sleep_duration} seconds until #{next_run}.")
|
||||
sleep(sleep_duration)
|
||||
perform_backup
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def setup_ssl
|
||||
return unless @ssl
|
||||
|
||||
@cert_store = OpenSSL::X509::Store.new
|
||||
@cerificate = OpenSSL::X509::Certificate.new(ENV['BT_CA'])
|
||||
@cert_store.add_cert(@cerificate)
|
||||
Docker.options = {
|
||||
client_cert_data: ENV['BT_CERT'],
|
||||
client_key_data: ENV['BT_KEY'],
|
||||
ssl_cert_store: @cert_store,
|
||||
scheme: 'https'
|
||||
}
|
||||
end
|
||||
end
|
19
app/lib/baktainer/backup_command.rb
Normal file
19
app/lib/baktainer/backup_command.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'baktainer/mysql'
|
||||
require 'baktainer/postgres'
|
||||
require 'baktainer/mariadb'
|
||||
require 'baktainer/sqlite'
|
||||
|
||||
# This class is responsible for generating the backup command for the database engine
|
||||
# It uses the environment variables to set the necessary parameters for the backup command
|
||||
# The class methods return a hash with the environment variables and the command to run
|
||||
# The class methods are used in the Baktainer::Container class to generate the backup command
|
||||
class Baktainer::BackupCommand
|
||||
def custom(command: nil)
|
||||
{
|
||||
env: [],
|
||||
cmd: command.split(/\s+/)
|
||||
}
|
||||
end
|
||||
end
|
111
app/lib/baktainer/container.rb
Normal file
111
app/lib/baktainer/container.rb
Normal file
|
@ -0,0 +1,111 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# The `Container` class represents a container abstraction within the Baktainer application.
|
||||
# It is responsible for encapsulating the logic and behavior related to managing containers.
|
||||
# This class serves as a core component of the application, providing methods and attributes
|
||||
# to interact with and manipulate container instances.
|
||||
|
||||
require 'fileutils'
|
||||
require 'date'
|
||||
|
||||
class Baktainer::Container
|
||||
def initialize(container)
|
||||
@container = container
|
||||
@backup_command = Baktainer::BackupCommand.new
|
||||
end
|
||||
|
||||
def id
|
||||
@container.id
|
||||
end
|
||||
|
||||
def labels
|
||||
@container.info['Labels']
|
||||
end
|
||||
|
||||
def name
|
||||
labels["baktainer.name"] || @container.info['Names'].first
|
||||
end
|
||||
|
||||
def state
|
||||
@container.info['State']
|
||||
end
|
||||
|
||||
def running?
|
||||
state == 'running'
|
||||
end
|
||||
|
||||
def engine
|
||||
labels['baktainer.db.engine']&.downcase
|
||||
end
|
||||
|
||||
def login
|
||||
labels['baktainer.db.user'] || nil
|
||||
end
|
||||
|
||||
def password
|
||||
labels['baktainer.db.password'] || nil
|
||||
end
|
||||
|
||||
def database
|
||||
labels['baktainer.db.name'] || nil
|
||||
end
|
||||
|
||||
def validdate
|
||||
return raise 'Unable to parse container' if @container.nil?
|
||||
return raise 'Container not running' if state.nil? || state != 'running'
|
||||
return raise 'Use docker labels to define db settings' if labels.nil? || labels.empty?
|
||||
if labels['baktainer.backup']&.downcase != 'true'
|
||||
return raise 'Backup not enabled for this container. Set docker label baktainer.backup=true'
|
||||
end
|
||||
LOGGER.debug("Container labels['baktainer.db.engine']: #{labels['baktainer.db.engine']}")
|
||||
if engine.nil? || !@backup_command.respond_to?(engine.to_sym)
|
||||
return raise 'DB Engine not defined. Set docker label baktainer.engine.'
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def backup
|
||||
LOGGER.debug("Starting backup for container #{name} with engine #{engine}.")
|
||||
return unless validdate
|
||||
LOGGER.debug("Container #{name} is valid for backup.")
|
||||
backup_dir = "/backups/#{Date.today}"
|
||||
FileUtils.mkdir_p("/backups/#{Date.today}") unless Dir.exist?(backup_dir)
|
||||
sql_dump = File.open("/backups/#{Date.today}/#{name}-#{Time.now.to_i}.sql", 'w')
|
||||
command = backup_command
|
||||
LOGGER.debug("Backup command environment variables: #{command[:env].inspect}")
|
||||
@container.exec(command[:cmd], env: command[:env]) do |stream, chunk|
|
||||
sql_dump.write(chunk) if stream == :stdout
|
||||
LOGGER.warn("#{name} stderr: #{chunk}") if stream == :stderr
|
||||
end
|
||||
sql_dump.close
|
||||
LOGGER.debug("Backup completed for container #{name}.")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def backup_command
|
||||
if @backup_command.respond_to?(engine.to_sym)
|
||||
return @backup_command.send(engine.to_sym, login: login, password: password, database: database)
|
||||
elsif engine == 'custom'
|
||||
return @backup_command.custom(command: labels['baktainer.command']) || raise('Custom command not defined. Set docker label bt_command.')
|
||||
else
|
||||
raise "Unsupported engine: #{engine}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# :NODOC:
|
||||
class Containers
|
||||
def self.find_all
|
||||
LOGGER.debug('Searching for containers with backup labels.')
|
||||
containers = Docker::Container.all.select do |container|
|
||||
container.info['Labels']['baktainer.backup'] == 'true'
|
||||
end
|
||||
LOGGER.debug("Found #{containers.size} containers with backup labels.")
|
||||
LOGGER.debug(containers.first.class)
|
||||
containers.map do |container|
|
||||
Baktainer::Container.new(container)
|
||||
end
|
||||
end
|
||||
end
|
22
app/lib/baktainer/logger.rb
Normal file
22
app/lib/baktainer/logger.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'logger'
|
||||
require 'json'
|
||||
|
||||
# Log messages in JSON format
|
||||
class JsonLogger < Logger
|
||||
def format_message(severity, timestamp, progname, msg)
|
||||
{
|
||||
severity: severity,
|
||||
timestamp: timestamp,
|
||||
progname: progname || 'backtainer',
|
||||
message: msg
|
||||
}.to_json + "\n"
|
||||
end
|
||||
|
||||
def initialize
|
||||
super(STDOUT)
|
||||
end
|
||||
end
|
||||
|
||||
LOGGER = JsonLogger.new
|
11
app/lib/baktainer/mariadb.rb
Normal file
11
app/lib/baktainer/mariadb.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# mariadb backup command generator
|
||||
class Baktainer::BackupCommand
|
||||
def mariadb(login:, password:, database:)
|
||||
{
|
||||
env: [],
|
||||
cmd: ['mariadb-dump', "-u#{login}", "-p#{password}", '--databases', database]
|
||||
}
|
||||
end
|
||||
end
|
11
app/lib/baktainer/mysql.rb
Normal file
11
app/lib/baktainer/mysql.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# MySQL backup command generator
|
||||
class Baktainer::BackupCommand
|
||||
def mysql(login:, password:, database:)
|
||||
{
|
||||
env: [],
|
||||
cmd: ['mysqldump', '-u', login, "-p#{password}", database]
|
||||
}
|
||||
end
|
||||
end
|
28
app/lib/baktainer/postgres.rb
Normal file
28
app/lib/baktainer/postgres.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Postgres backup command generator
|
||||
class Baktainer::BackupCommand
|
||||
def postgres(login: 'postgres', password: nil, database: nil, all: false)
|
||||
{
|
||||
env: [
|
||||
"PGPASSWORD=#{password}",
|
||||
"PGUSER=#{login}",
|
||||
"PGDATABASE=#{database}",
|
||||
'PGAPPNAME=Baktainer'
|
||||
],
|
||||
cmd: [all ? 'pg_dumpall' : 'pg_dump']
|
||||
}
|
||||
end
|
||||
|
||||
def postgres_all(login: 'postgres', password: nil, database: nil)
|
||||
posgres(login: login, password: password, database: database, all: true)
|
||||
end
|
||||
|
||||
def postgresql(*args)
|
||||
postgres(*args)
|
||||
end
|
||||
|
||||
def postgresql_all(*args)
|
||||
postgres_all(*args)
|
||||
end
|
||||
end
|
13
app/lib/baktainer/sqlite.rb
Normal file
13
app/lib/baktainer/sqlite.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# sqlite backup command generator
|
||||
class Baktainer::BackupCommand
|
||||
class << self
|
||||
def sqlite(database:, _login: nil, _password: nil)
|
||||
{
|
||||
env: [],
|
||||
cmd: ['sqlite3', database, '.dump']
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
23
backups/2025-04-14/MyApp-1744637779.sql
Normal file
23
backups/2025-04-14/MyApp-1744637779.sql
Normal file
|
@ -0,0 +1,23 @@
|
|||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
-- Dumped from database version 17.4
|
||||
-- Dumped by pg_dump version 17.4
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET transaction_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
23
backups/2025-04-14/MyApp-1744637906.sql
Normal file
23
backups/2025-04-14/MyApp-1744637906.sql
Normal file
|
@ -0,0 +1,23 @@
|
|||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
-- Dumped from database version 17.4
|
||||
-- Dumped by pg_dump version 17.4
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET transaction_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
|
@ -0,0 +1,23 @@
|
|||
services:
|
||||
baktainer:
|
||||
build: .
|
||||
image: jamez01/baktainer:latest
|
||||
container_name: baktainer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./backups:/backups
|
||||
- ./config:/config
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- "BT_DOCKER_URL=unix:///var/run/docker.sock"
|
||||
- BT_CRON=* * * * * # Backup every day at midnight
|
||||
- BT_THREADS=4 # Number of threads to use for backups
|
||||
- BT_BACKUP_DIR=/backups
|
||||
- BT_LOG_LEVEL=info
|
||||
# Enable if using SSL over tcp
|
||||
#- BT_SSL = true
|
||||
#- BT_CA
|
||||
#- BT_CERT
|
||||
#- BT_KEY
|
2
entrypoint.sh
Normal file
2
entrypoint.sh
Normal file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
exec $@
|
Loading…
Add table
Reference in a new issue