Skip to content

EC2 Production Deployment Guide

Roni bhakta edited this page Mar 4, 2026 · 1 revision

This guide covers hardening a Lenny installation on a cloud VM (AWS EC2, DigitalOcean Droplet, etc.) for production use. The default install.sh one-liner is fine for local testing, but production deployments need OS-level security, proper user isolation, and a backup strategy.

For domain and SSL setup, see CUSTOM_DOMAIN_SETUP.


1. Prerequisites

Instance requirements:

  • Ubuntu 22.04 LTS (recommended)
  • Minimum: 2 vCPUs, 4 GB RAM, 20 GB disk
  • Recommended: 4 vCPUs, 8 GB RAM, 50 GB+ disk (more if storing large collections)

Security group / firewall:

Port Protocol Purpose
22 TCP SSH
8080 TCP Lenny (direct, no SSL) — temporary or internal use
80 TCP HTTP → HTTPS redirect (once Nginx is set up)
443 TCP HTTPS (once Nginx + SSL are set up)

By default, Lenny listens on port 8080 using the EC2/Droplet's public IP. This is fine for initial setup and testing. For production, set up Nginx as a reverse proxy (see CUSTOM_DOMAIN_SETUP) and close 8080 once traffic flows through 443. Until then, opening 8080 in your security group is required to reach the app.

Assign a static IP before proceeding. On AWS, allocate an Elastic IP and associate it with your instance. On DigitalOcean, use a Reserved IP. This ensures your domain DNS record and SSH config don't break on reboot.


2. Create a Limited OS User

Never run Lenny as root. Create a dedicated unprivileged user instead:

sudo adduser lenny
sudo usermod -aG docker lenny

Switch to that user for all remaining steps:

su - lenny

All Lenny files and Docker operations will run under the lenny user. If the application is ever compromised, the attacker is confined to this user's permissions — no root access, no access to other users' files.


3. SSH Hardening

Password-based SSH is a common attack vector. Disable it and use key-based authentication only.

First, make sure your SSH key is already working:

# From your local machine — confirm you can log in with your key before proceeding
ssh lenny@your-server-ip

Then disable password authentication:

sudo nano /etc/ssh/sshd_config

Find and set:

PasswordAuthentication no

Restart SSH to apply:

sudo systemctl restart ssh

Do not close your current SSH session until you've verified you can open a new session successfully. If you lock yourself out, use your cloud provider's console to recover.


4. Firewall Setup

Enable ufw (Uncomplicated Firewall). The rules depend on your setup stage:

During initial setup / before Nginx:

sudo ufw allow 22
sudo ufw allow 8080
sudo ufw enable

This lets you reach Lenny at http://your-ip:8080 while you're getting things running.

After Nginx + SSL are configured (see CUSTOM_DOMAIN_SETUP):

sudo ufw allow 80
sudo ufw allow 443
sudo ufw delete allow 8080

Once traffic flows through 443, close 8080 — it exposes the app without TLS and bypasses any Nginx-level protections.

Verify the final rules:

sudo ufw status

Expected output once hardened:

Status: active

To                         Action      From
--                         ------      ----
22                         ALLOW       Anywhere
80                         ALLOW       Anywhere
443                        ALLOW       Anywhere

5. Install Lenny Under the Limited User

You have two options. Both work — choose based on your preference.

Option A: Manual install

Gives you full control over file placement and ownership. As the lenny user:

git clone https://github.com/ArchiveLabs/lenny.git
cd lenny
make configure

Edit .env and reader.env with your production values (database credentials, S3 keys, domain name, etc.), then build and start:

make build
make start

Option B: Use the install script

If you prefer the one-liner, you can still use it after creating the OS user manually (section 2). Switch back to a sudo-capable user and run:

curl -s https://raw.githubusercontent.com/ArchiveLabs/lenny/main/install.sh | sudo bash

The script handles Docker setup and Lenny configuration automatically. The trade-off is that it runs as root and installs files in its own default locations — you won't have the same control over file ownership as Option A.

After either option, verify it's running:

docker ps

6. Secure the .env Files

Your .env and reader.env files contain database passwords, S3 credentials, and the application seed. Restrict them to owner-read only:

chmod 600 .env
chmod 600 reader.env

This prevents other OS users from reading them. Because Lenny runs under its own user, the lenny user can still read them, but no other unprivileged user can.


7. Automatic Security Updates

Keep the OS patched without manual effort:

sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades

When prompted, select Yes to enable automatic updates. This applies security patches to OS packages automatically. It does not affect Lenny itself — Lenny updates are managed separately (see section 10).


8. Backups (Optional)

Backups are optional but strongly recommended if you're lending real books to real patrons. Set up what makes sense for your situation.

Database

Dump the PostgreSQL database to a file:

docker exec lenny_db pg_dump -U librarian lenny > backup_$(date +%F).sql

To automate with a daily cron job, as the lenny user:

mkdir -p /home/lenny/backups
crontab -e

Add:

# set timings as per your needs to run cron job's
0 2 * * * docker exec lenny_db pg_dump -U librarian lenny > /home/lenny/backups/db_$(date +\%F).sql

MinIO / S3 Storage

If using MinIO (the default), back up the s3_data Docker volume. The simplest approach is to use mc mirror to sync to a remote bucket:

# Install mc (MinIO client) if not already present
docker exec lenny_minio mc mirror /data s3/your-remote-bucket

Or copy the volume contents directly to external storage weekly via cron.

If you do set up backups, store them off-server. A backup on the same machine as the application will be lost if the instance is terminated or the disk fails. Use S3, an external drive, or another cloud provider.


9. Branch Strategy

Don't run production off main directly. The main branch receives active development commits that may be unstable.

Create a dedicated production branch:

git checkout -b production
git push origin production

Workflow for updates:

  1. Pull and test changes on a staging machine or branch first
  2. Once verified stable, merge into production
  3. On the production server: git pull origin production && make build

To roll back to a previous version:

git log --oneline          # find the last stable commit hash
git checkout <commit-hash>
make build

Important: Rolling back the code does not roll back the database or stored files. If a newer version ran database migrations or wrote data in a new format, reverting the code may cause errors or data inconsistencies. Before rolling back:

  1. Take a DB snapshot: docker exec lenny_db pg_dump -U librarian lenny > pre_rollback_$(date +%F).sql
  2. Check whether the version you're rolling back from introduced any migrations
  3. If it did, you may need to restore from a pre-upgrade backup rather than just checking out old code

For safe rollbacks, having a DB backup from before the upgrade (see section 8) is the most reliable recovery path.


10. Future: Docker Image Releases

Future Lenny releases will publish versioned Docker images. When available, updates will be as simple as:

docker pull archivelabs/lenny:1.x.x
make up

This eliminates the need to git pull and rebuild from source, making updates faster and rollbacks more reliable. Until then, the git-based workflow in section 9 applies.

Clone this wiki locally