-
Notifications
You must be signed in to change notification settings - Fork 17
EC2 Production Deployment Guide
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.
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.
Never run Lenny as root. Create a dedicated unprivileged user instead:
sudo adduser lenny
sudo usermod -aG docker lennySwitch to that user for all remaining steps:
su - lennyAll 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.
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-ipThen disable password authentication:
sudo nano /etc/ssh/sshd_configFind and set:
PasswordAuthentication no
Restart SSH to apply:
sudo systemctl restart sshDo 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.
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 enableThis 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 8080Once traffic flows through 443, close 8080 — it exposes the app without TLS and bypasses any Nginx-level protections.
Verify the final rules:
sudo ufw statusExpected output once hardened:
Status: active
To Action From
-- ------ ----
22 ALLOW Anywhere
80 ALLOW Anywhere
443 ALLOW Anywhere
You have two options. Both work — choose based on your preference.
Gives you full control over file placement and ownership. As the lenny user:
git clone https://github.com/ArchiveLabs/lenny.git
cd lenny
make configureEdit .env and reader.env with your production values (database credentials, S3 keys, domain name, etc.), then build and start:
make build
make startIf 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 bashThe 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 psYour .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.envThis 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.
Keep the OS patched without manual effort:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgradesWhen 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).
Backups are optional but strongly recommended if you're lending real books to real patrons. Set up what makes sense for your situation.
Dump the PostgreSQL database to a file:
docker exec lenny_db pg_dump -U librarian lenny > backup_$(date +%F).sqlTo automate with a daily cron job, as the lenny user:
mkdir -p /home/lenny/backups
crontab -eAdd:
# 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).sqlIf 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-bucketOr 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.
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 productionWorkflow for updates:
- Pull and test changes on a staging machine or branch first
- Once verified stable, merge into
production - 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 buildImportant: 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:
- Take a DB snapshot:
docker exec lenny_db pg_dump -U librarian lenny > pre_rollback_$(date +%F).sql- Check whether the version you're rolling back from introduced any migrations
- 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.
Future Lenny releases will publish versioned Docker images. When available, updates will be as simple as:
docker pull archivelabs/lenny:1.x.x
make upThis 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.