Recipebook Backup

While I was busy studying for my AWS certifications, my other half decided to finally tackle a project they’d been meaning to do for a while: an application to store all the recipes they’d collected from various sources over the years.
They built the front-end with the Tauri framework and, since the application would only ever be used at home, stored the data in a MongoDB database hosted on the Mac Mini running as our home server.
There was always the chance, however, that the server might fail – or that the data would need to be rolled back for some reason. Rather than lose a decade’s worth of collected recipes, I decided to implement a backup strategy that would automatically push daily backups of the database up to S3.
As it happened, this dovetailed nicely with my certification studies. It’s one thing to read about S3 lifecycle policies, quite another to actually configure one.
Creating the Backup Script
As it turns out, MongoDB ships with a mongodump command that can be executed against a running server to create a backup. All that was left was to wrap it in a script that executes the dump and uploads the archive to S3.
recipebook-backup.sh
Since the script would eventually run inside a Docker container on the same Docker network as MongoDB, the mongodump command is executed against the host.docker.internal hostname on port 27017:
daystokeep=90
currentdate=$(date +"%Y%m%d%H%M")
filename=RecipeBook_$currentdate.gz
mongodump --host="host.docker.internal:27017" --db=RecipeBook --archive=/data/$filename --gzip
aws s3 sync /data/ s3://maceage-recipebook-backups --exclude "*" --include "*.gz"
find /data/ -type f -iname '*' -mtime +$daystokeep -exec rm -v "{}" +The current date is used to generate the archive filename, which is then synced to S3. Finally, a separate command cleans up any local backups older than 90 days, so the container doesn’t quietly fill the disk over time.
crontab.txt
Running a cron job inside a Docker container turns out to be relatively straightforward - a crontab.txt entry runs the script at 1am every day. I also wanted to capture any errors from the shell script, so the output is appended to a log file:
0 1 * * * /recipebook-backup.sh >> /var/log/recipebook-backup.logentry.sh
The container also needs an entry point script to start the cron daemon in the foreground:
#!/bin/sh
# start cron
/usr/sbin/crond -f -l 8Dockerfile
Next, the Dockerfile for building the container uses the ridiculously small Alpine base image, adds the required files, and installs the necessary dependencies:
FROM alpine
ADD crontab.txt /
ADD recipebook-backup.sh /
COPY entry.sh /
RUN apk add --no-cache mongodb-tools && \
apk add --no-cache aws-cli && \
chmod +x /recipebook-backup.sh && \
chmod +x /entry.sh && \
/usr/bin/crontab /crontab.txt
CMD ["/entry.sh"]Building the container is then merely a case of running docker build . in the directory containing the Dockerfile.
docker-compose.yml
With the image built, the final step is a docker-compose.yml that passes the AWS credentials in as environment variables:
services:
recipebook-backup:
image: maceage/recipebook-backup
build: .
container_name: recipebook-backup
hostname: recipebook-backup
restart: unless-stopped
environment:
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
- AWS_DEFAULT_REGION
volumes:
- ${VOLUMES_DATA}:/data
stdin_open: true
tty: true
labels:
- "com.centurylinklabs.watchtower.enable=false"The container is then started with docker-compose up -d, and from that point onwards it performs a nightly backup of the MongoDB database to S3 without any further intervention.
S3 Lifecycle Policy
On the AWS side, the S3 bucket needs to be created and the credentials configured. These are then passed to the container via the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_DEFAULT_REGION environment variables.
Although the backup files themselves are tiny – a gzip-compressed dump of a recipe database doesn’t exactly strain the network - I wanted to ensure only the most recent backups were kept. A lifecycle policy on the S3 bucket deletes backups older than 365 days, which gives a comfortable window: even if a problem went unnoticed for months, there would still be a healthy history of restores to choose from.
Wrapping Up
Quite possibly the simplest Docker container I’ve ever built, and it’s been running without a single issue. There’s something satisfying about infrastructure you can safely forget about.
The only ongoing requirement is rotating the AWS keys from time to time – although I’m not sure who would be interested in our super-secret food recipes even if they did get hold of them 🤣