Just today (12.05.2023) I have open-sourced optiboy. While functionality is limited, it provides a clear way forward to build my own email list. Several bloggers and marketers have iterated on why it is important. Even if blogging is just a thing you do from time to time, it can be a good way to get in touch — especially without being annoying.

Why should I host my own, then?

There are many good reasons to host your own. For one, especially if you(r business) is based in the European Union, transferring data to the US is a legal risk that might not be worth it. Besides that, owning an asset as crucial as your mailing list is an idea worth exploring.

Host responsibly!

With control also comes responsibility, so self-hosting also means that you effectively safeguard your infrastructure. When running Docker instances on your server, you should understand how docker compose works.

Hosting optiboy on your own server

As described in optiboy’s readme, you’ll need to have Docker installed on the server. The latest version already has docker compose included.

To start, clone the repository somewhere on your server

git clone git@github.com:timweiss/optiboy.git

Inside the folder of the cloned code, you’ll need to set up some environment variables first. There are some good services that provide an SMTP server for you to mass-send emails. I’m using Mailjet but you can use any other service, too.

APP_EMAIL_SENDER=<your email address>
APP_EMAIL_SMTP_HOST=<SMTP host>
APP_EMAIL_SMTP_USER=<SMTP username>
APP_EMAIL_SMTP_PASSWORD=<SMTP password>
APP_HOSTNAME=<your hostname>

The sender defines which email should be used to send the email. You very much also want to define APP_EMAIL_SENDER_NAME to something so it becomes recognizable to your subscribers.

APP_HOSTNAME is set to let the application know which hostname to use. It’s necessary for the conformation mail to know where it points to.

Once you’ve filled out the environment variables, you should set up your reverse proxy. I’m using nginx, as configuring it is farily simple.

Start by creating a new virtual host inside your sites-available repository.

server {
	# replace example.com with your domain name
	server_name example.com;

	location / {
		proxy_pass http://127.0.0.1:3000;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	}
}

I recommend securing your endpoint with certbot.

Now, you can finally start up optiboy:

docker compose up

Check if it’s online by going to the endpoint.

Submitting emails

If you want to integrate optiboy into your website, you can make a fetch request to your endpoint

await fetch('http://localhost:3000', {
  method: 'POST',
  body: JSON.stringify({ email }),
  headers: {
    'Content-Type': 'application/json',
  },
});

Additional considerations

Backing up your list

I’ve built optiboy to also notify me whenever somebody successfully signs up their email. This does already sort of provide a backup strategy in case the server I’m running it on just vanishes. It might be wise, however, to explore some secondary backup options — ideally running remotely.

I am a fan of just running a plain cronjob on my server to dump the database and uploading it to a remote server.

Rclone a pg_dump

One great tool to synchronize your local files is rclone. It is compatible with many storage services. I’m using Azure’s Storage Account as I’m already using Azure for many of my personal projects and I don’t have to juggle around many bills then — and I like the administration of it more than AWS.

So, as the only crucial part you need to care about is optiboy’s database, we can simply run a pg_dump on it and mirror it. This even gives us two backup options: one local on the server (in case the database just stops working) and one on a remote server (like Azure) to restore from in case everything goes south.

I’ll write a more in-depth guide on how I’m utilizing rclone at work for secure backups of our user’s file uploads, so this one will be just a brief script for you to use.

# get container id for running postgres, exchange optiboy-postgres if you've cloned it inside another folder
container=$(docker ps | grep 'optiboy-postgres' | awk '{ print $1 }')

dt=$(date '+%Y-%m-%d %H:%M:%S');
filename=$dt-optiboy-backup.sql
# any location you want to put it in, I'm using this one because it's simple to use
location=/home/tim/backup/optiboy/$filename

# runs the pg_dump inside the container and writes it to our host's location
docker exec "$container" pg_dump -U postgres postgres > "$location"

# copies the backup to my target
rclone sync --fast-list --checksum --update --use-server-modtime "/home/tim/backup/optiboy" "backups:/optiboy"

For rclone, you can go without the additional options, but generally --fast-list --checksum --update --use-server-modtime makes sure that my storage cost is not astronomically high, especially for many files. I have also created a container inside the storage account called optiboy in case I want to sync other backups in the future.

If you want to use Azure, please refer to rclone’s documentation for Azure Blob Storage for all options. My rclone config file looks like this:

[backups]
type = azureblob
account = <your azure storage account name>
key = <your access key>

Now, you can manually run this script whenever you feel like if you want to do a manual backup. I’ve decided to run daily backups in the night for now, so my cron config (access with crontab -e) looks like this:

0 1 * * * /home/tim/backup/optiboy-backup.sh

Customizing the email and confirmation page

While I am already happy with the result, you might want to have a custom confirmation page. I don’t yet have an ETA for when I’ll extend optiboy’s capabilities to allow for custom templates, but this is something I want to work on.