Skip to content

Instantly share code, notes, and snippets.

@zavan
Last active July 6, 2024 11:55
Show Gist options
  • Save zavan/ab00ddb0d1c628430b43c46a25eba495 to your computer and use it in GitHub Desktop.
Save zavan/ab00ddb0d1c628430b43c46a25eba495 to your computer and use it in GitHub Desktop.
Poor man's Rails backup
#!/usr/bin/env ruby
require "bundler/inline"
require "logger"
gemfile do
source "https://rubygems.org"
gem "bcrypt_pbkdf"
gem "ed25519"
gem "net-scp"
gem "net-ssh"
end
host = ARGV[0]
user = ARGV[1]
key = ARGV[2]
db_uri = ARGV[3]
app_dir = ARGV[4]
output_dir = ARGV[5]
backup_pass = ARGV[6]
retention = ARGV[7].to_i
class Backup
attr_reader :host, :user, :key, :db_uri, :app_dir, :output_dir, :backup_pass, :retention, :logger
def initialize(host:, user:, key:, db_uri:, app_dir:, output_dir:, backup_pass:, retention: 7)
@host = host
@user = user
@key = key
@db_uri = db_uri
@app_dir = app_dir
@output_dir = output_dir
@retention = retention
@backup_pass = backup_pass
@logger = Logger.new(STDOUT)
@logger.level = Logger::INFO
@work_dir = "/tmp"
@tarball_name = "rails_backup-#{Time.now.strftime("%Y%m%d%H%M%S")}.tar.gz"
@tarball_path = File.join(@work_dir, @tarball_name)
@db_dump_name = "rails_db_dump.sql"
@db_dump_path = File.join(@work_dir, @db_dump_name)
end
def self.decrypt(file, pass)
enc_tarball_path = file
tarball_path = enc_tarball_path.sub(".gpg.tar.gz", ".tar.gz")
command = "gpg --batch --yes --output #{tarball_path} --decrypt --passphrase #{pass} #{enc_tarball_path}"
system(command)
tarball_path
end
def run
logger.info "Starting backup..."
pg_dump
tar
encrypt
download
cleanup_server
cleanup_retention
logger.info "Backup finished successfully"
ensure
ssh.close
end
def pg_dump
channel_exec("pg_dump", "pg_dump #{db_uri} > #{@db_dump_path}")
end
def tar
channel_exec("tar", "tar -czf #{@tarball_path} -C #{app_dir} storage -C /tmp #{@db_dump_name}")
end
def encrypt
logger.info "Encrypting tarball..."
@enc_tarball_name = @tarball_name.sub(".tar.gz", ".gpg.tar.gz")
@enc_tarball_path = File.join(@work_dir, @enc_tarball_name)
command = "gpg --batch --yes --output #{@enc_tarball_path} --symmetric --cipher-algo AES256 --passphrase #{@backup_pass} #{@tarball_path}"
channel_exec("encrypt", command)
end
def download
output_path = File.join(output_dir, @enc_tarball_name)
logger.info "Downloading tarball to #{output_path}..."
ssh.scp.download!(@enc_tarball_path, output_path) do |ch, name, dowloaded, total|
# print "\rDownloading #{name}: #{dowloaded}/#{total}"
end
puts
end
def cleanup_server
logger.info "Cleaning up server..."
ssh.exec!("rm #{@db_dump_path} #{@tarball_path} #{@enc_tarball_path}")
end
def cleanup_retention
logger.info "Retaining #{retention} backups..."
backups = Dir.glob(File.join(output_dir, "rails_backup-*.tar.gz")).sort
to_delete = backups[0...-retention]
logger.info "Deleting #{to_delete.size} old backups..."
to_delete.each do |old|
logger.info "Deleting #{old}..."
File.delete(old)
end
end
private
def ssh
@ssh ||= Net::SSH.start(
host,
user,
keys: [key],
keys_only: true,
verbose: :warn
)
end
def channel_exec(name, command)
channel = ssh.open_channel do |ch|
logger.info "Running #{name}..."
ch.exec(command) do |ch, success|
raise "Could not start #{name}" unless success
ch.on_data { |c, data| logger.info data }
ch.on_extended_data { |c, type, data| logger.warn data }
ch.on_request("exit-status") do |c, data|
status = data.read_long
if status.zero?
logger.info "#{name} finished successfully"
else
raise "#{name} failed with status #{status}"
end
end
end
end
channel.wait
end
end
Backup.new(
host:,
user:,
key:,
db_uri:,
app_dir:,
output_dir:,
retention:,
backup_pass:
).run
# Backup.decrypt("/path/to/rails_backup-20240706124029.gpg.tar.gz", "password")
@zavan
Copy link
Author

zavan commented Jul 6, 2024

You don't pay for RDS. You don't pay for S3. You don't pay for AWS Backup/snapshots...

Run this with a cron on your machine (we both know you never turn it off) to download an encrypted backup of your PostgreSQL database and ActiveStorage file system uploads.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment