Skip to content

feat: add digital ocean snapshot builder#2345

Open
im-adithya wants to merge 1 commit into
masterfrom
feat/digitalocean
Open

feat: add digital ocean snapshot builder#2345
im-adithya wants to merge 1 commit into
masterfrom
feat/digitalocean

Conversation

@im-adithya
Copy link
Copy Markdown
Member

@im-adithya im-adithya commented May 14, 2026

Adds a snapshot builder which we can use to publish on DigitalOcean marketplace

Next steps:

  • Make a marketplace publish script once we get an APP_ID
  • CI to automate this in release workflow

But this PR is fully functional and can also be merged as-is

Summary by CodeRabbit

  • New Features

    • Added DigitalOcean deployment automation with image building and droplet configuration support.
  • Documentation

    • Comprehensive guide for deploying Alby Hub to DigitalOcean including setup prerequisites and step-by-step instructions.

Review Change Stack

@im-adithya im-adithya requested review from reneaaron and rolznz May 14, 2026 09:43
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

📝 Walkthrough

Walkthrough

This pull request adds a complete DigitalOcean deployment infrastructure for Alby Hub, enabling automated snapshot building via Packer and one-click droplet creation. The implementation includes build orchestration, system provisioning scripts, application configuration, runtime initialization, and security hardening.

Changes

DigitalOcean Droplet Deployment Infrastructure

Layer / File(s) Summary
Build orchestration and Packer template
deploy/digitalocean/README.md, deploy/digitalocean/build.sh, deploy/digitalocean/template.json
Documentation, build script, and Packer template define the image build process with environment variable validation, version derivation, provisioning orchestration, file uploads, and sensitive API token configuration.
Application service configuration and provisioning
deploy/digitalocean/files/opt/albyhub/docker-compose.yml, deploy/digitalocean/scripts/015-albyhub.sh
Docker Compose service definition with version placeholder and provisioning script that substitutes version tokens and pre-pulls the versioned Alby Hub image during image build.
Runtime initialization and user messaging
deploy/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot, deploy/digitalocean/files/etc/update-motd.d/99-one-click
Boot-time initialization starts Docker Compose, and MOTD script displays droplet IP, access instructions, and operational commands to users upon first SSH login.
System provisioning and security hardening
deploy/digitalocean/scripts/010-docker.sh, deploy/digitalocean/scripts/012-grub-opts.sh, deploy/digitalocean/scripts/014-ufw-albyhub.sh, deploy/digitalocean/scripts/020-application-tag.sh, deploy/digitalocean/scripts/900-cleanup.sh
Docker installation and repository setup, kernel parameters for container memory control, UFW firewall rules for SSH/HTTP/HTTPS, build metadata tagging, and comprehensive cleanup with secure disk zeroing and SSH key revocation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • rolznz

Poem

🐰 A droplet springs to life with care,
With Docker running in the air,
Alby Hub ready, bright and free,
Packer builds it seamlessly!
One-click deployment, secure and neat,
Infrastructure complete. 🚀

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add digital ocean snapshot builder' accurately describes the main change - adding DigitalOcean snapshot builder infrastructure with multiple deployment scripts and configuration files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/digitalocean

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

"type": "digitalocean",
"api_token": "{{user `do_api_token`}}",
"image": "ubuntu-24-04-x64",
"region": "fra1",
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can also specify multiple regions here using "snapshot_regions", wanted to know what's the preference here before proceeding

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't the user chose where to run?

Copy link
Copy Markdown
Member Author

@im-adithya im-adithya May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm not sure about this exactly but they do provide this option of choosing for the published images, maybe it's exclusive to published snapshots

(but I just remembered I didn't try building without a region, will see what happens then and update EDIT: region is required)

@im-adithya im-adithya requested review from bumi and frnandu May 14, 2026 09:44
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
deploy/digitalocean/scripts/012-grub-opts.sh (1)

3-4: ⚡ Quick win

Make the GRUB update idempotent.

Line 3 can append duplicate cgroup_enable=memory swapaccount=1 values if rerun. Add a guard so the flags are only inserted once.

Suggested patch
-sed -e 's|GRUB_CMDLINE_LINUX="|GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1|g' \
-    -i /etc/default/grub
+if ! grep -q 'cgroup_enable=memory swapaccount=1' /etc/default/grub; then
+  sed -e 's|GRUB_CMDLINE_LINUX="|GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1 |g' \
+      -i /etc/default/grub
+fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/digitalocean/scripts/012-grub-opts.sh` around lines 3 - 4, The current
sed invocation can duplicate the flags because it blindly replaces the
GRUB_CMDLINE_LINUX value; change the script to first check /etc/default/grub for
the presence of cgroup_enable=memory and swapaccount=1 in the GRUB_CMDLINE_LINUX
line and only perform the modification if those flags are missing. Concretely,
locate the GRUB_CMDLINE_LINUX line in /etc/default/grub (reference symbol:
GRUB_CMDLINE_LINUX) and if the flags are absent, update that line to include
them (and then run update-grub); otherwise leave the file unchanged to make the
operation idempotent.
deploy/digitalocean/template.json (1)

5-5: ⚡ Quick win

Use linux-image-extra-virtual-hwe-24.04 for optimal Noble compatibility.

Line 5 includes linux-image-extra-virtual, which exists on Ubuntu 24.04 but is not the recommended variant. Replace with linux-image-extra-virtual-hwe-24.04 to ensure supported Docker host/kernel compatibility with the HWE (Hardware Enablement) kernel stream tracking on Noble.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/digitalocean/template.json` at line 5, The apt_packages entry
currently lists "linux-image-extra-virtual"; update the apt_packages string to
replace linux-image-extra-virtual with linux-image-extra-virtual-hwe-24.04 so
the template uses the HWE kernel variant for Noble compatibility (modify the
"apt_packages" value in the JSON to include linux-image-extra-virtual-hwe-24.04
instead of linux-image-extra-virtual).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deploy/digitalocean/files/etc/update-motd.d/99-one-click`:
- Line 3: Replace the unreliable hostname-based IP lookup used to set the myip
variable with a call to the DigitalOcean metadata service; specifically, set
myip by querying the metadata endpoint for the droplet's public IPv4 (e.g.,
metadata/v1/interfaces/public/0/ipv4/address) with a short curl timeout and a
sensible fallback (keep the existing hostname-based command as fallback) so the
onboarding URL always uses the droplet's public IP; update the assignment to
myip in the 99-one-click script accordingly.

In `@deploy/digitalocean/scripts/014-ufw-albyhub.sh`:
- Around line 3-4: The ufw forward policy is set to a global ACCEPT which is too
permissive for a hardened image; change the value of DEFAULT_FORWARD_POLICY in
/etc/default/ufw from "ACCEPT" to "DROP" (i.e., ensure the sed replacement sets
DEFAULT_FORWARD_POLICY="DROP") and instead add explicit ufw route/forward rules
only for the specific services or subnets that require forwarding; locate the
occurrence of DEFAULT_FORWARD_POLICY in the sed command and update it, and
document or add corresponding ufw allow/route rules for any required forwarded
traffic.

In `@deploy/digitalocean/scripts/900-cleanup.sh`:
- Line 45: The purge command in 900-cleanup.sh uses sudo and an unquoted glob
which can lead to unintended shell expansion; change the invocation to run as
root (remove sudo) and quote the package pattern (e.g., "droplet-agent*") so the
apt-get --yes purge call in the script invokes apt-get as root and treats the
pattern literally.

---

Nitpick comments:
In `@deploy/digitalocean/scripts/012-grub-opts.sh`:
- Around line 3-4: The current sed invocation can duplicate the flags because it
blindly replaces the GRUB_CMDLINE_LINUX value; change the script to first check
/etc/default/grub for the presence of cgroup_enable=memory and swapaccount=1 in
the GRUB_CMDLINE_LINUX line and only perform the modification if those flags are
missing. Concretely, locate the GRUB_CMDLINE_LINUX line in /etc/default/grub
(reference symbol: GRUB_CMDLINE_LINUX) and if the flags are absent, update that
line to include them (and then run update-grub); otherwise leave the file
unchanged to make the operation idempotent.

In `@deploy/digitalocean/template.json`:
- Line 5: The apt_packages entry currently lists "linux-image-extra-virtual";
update the apt_packages string to replace linux-image-extra-virtual with
linux-image-extra-virtual-hwe-24.04 so the template uses the HWE kernel variant
for Noble compatibility (modify the "apt_packages" value in the JSON to include
linux-image-extra-virtual-hwe-24.04 instead of linux-image-extra-virtual).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3841a427-3041-43f6-bab3-b2a0bbf94aa5

📥 Commits

Reviewing files that changed from the base of the PR and between 1629653 and 2a3e32d.

📒 Files selected for processing (13)
  • deploy/digitalocean/README.md
  • deploy/digitalocean/build.sh
  • deploy/digitalocean/files/etc/update-motd.d/99-one-click
  • deploy/digitalocean/files/opt/albyhub/docker-compose.yml
  • deploy/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot
  • deploy/digitalocean/files/var/lib/digitalocean/application.info
  • deploy/digitalocean/scripts/010-docker.sh
  • deploy/digitalocean/scripts/012-grub-opts.sh
  • deploy/digitalocean/scripts/014-ufw-albyhub.sh
  • deploy/digitalocean/scripts/015-albyhub.sh
  • deploy/digitalocean/scripts/020-application-tag.sh
  • deploy/digitalocean/scripts/900-cleanup.sh
  • deploy/digitalocean/template.json

@@ -0,0 +1,32 @@
#!/bin/sh

myip=$(hostname -I | awk '{print$1}')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n deploy/digitalocean/files/etc/update-motd.d/99-one-click

Repository: getAlby/hub

Length of output: 1397


🏁 Script executed:

# Check if this file is actually used in the codebase and see its full context
head -20 deploy/digitalocean/files/etc/update-motd.d/99-one-click

Repository: getAlby/hub

Length of output: 722


🏁 Script executed:

# Search for similar patterns in other parts of the codebase that might handle IP detection
rg "hostname -I" --type sh

Repository: getAlby/hub

Length of output: 37


🌐 Web query:

DigitalOcean metadata service public IP address 169.254.169.254

💡 Result:

DigitalOcean droplet metadata service is accessible from a Droplet via the special static link-local IP address 169.254.169.254. [1][2] The metadata API is queried with HTTP GET requests to endpoints under /metadata/v1/. For example, to retrieve the Droplet’s public IPv4 address: curl -s http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address [1][2] Note: for “Private” Droplets, the interfaces/public/ path is absent; you should use interfaces/private/0/ipv4/address instead. [1][2]

Citations:


🌐 Web query:

DigitalOcean hostname -I private IP address first issue

💡 Result:

DigitalOcean/VPC Droplets can have both a public IP (usually on eth0) and a private IP (on eth1). On such systems, the command hostname -I prints ALL configured IP addresses “enumerates all configured addresses on all network interfaces” and “Do not make any assumptions about the order of the output,” so cutting on the first address (hostname -I | cut -d' ' -f1) is inherently unreliable; it may output the private IP first or the public IP first depending on interface/address enumeration order.[1] Why you saw “private IP first” on DigitalOcean: 1) Your Droplet likely has both interfaces up: eth0 (public) and eth1 (private). DigitalOcean documents that eth1 is the private/VPC network interface and its address is the Droplet’s private IP.[2][3] 2) hostname -I does not guarantee output order across interfaces/addresses, so “first field = external/public” is a legacy assumption that can break after changes (kernel/libc/tooling/network config, etc.). The hostname(1) man-page guidance is explicitly: do not assume order of hostname -I output.[1] What to do instead (reliable ways): - Grab the IP from the interface you actually want. - Public (eth0): ip -4 addr show dev eth0 |... - Private (eth1): ip -4 addr show dev eth1 |... DigitalOcean’s own guidance and troubleshooting consistently references eth0/public vs eth1/private and that eth1 carries the private IP.[2][3] - If you just need the “private IP that DigitalOcean uses internally,” use the metadata service for the private interface rather than parsing hostname -I. DigitalOcean describes retrieving the anchor/private addressing via the metadata service; for private networking, the “private” interface is used (interfaces/private/0/ipv4/address). [4][5] If you tell me your OS (Ubuntu version) and whether this is a “traditional” Droplet (eth0+eth1) or a “Private Droplet” (single private interface), I can give you the exact one-liner command to extract the correct IP for your case.

Citations:


Use the DigitalOcean metadata service to reliably detect the public IP.

hostname -I | awk '{print$1}' can return a private IP address first on DigitalOcean droplets with both public (eth0) and private (eth1) interfaces. Since hostname -I output order is not guaranteed by the system, the generated onboarding URL becomes inaccessible from outside the droplet.

Suggested fix
-myip=$(hostname -I | awk '{print$1}')
+myip=$(curl -fsS http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address || true)
+[ -n "$myip" ] || myip=$(hostname -I | awk '{print $1}')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
myip=$(hostname -I | awk '{print$1}')
myip=$(curl -fsS http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address || true)
[ -n "$myip" ] || myip=$(hostname -I | awk '{print $1}')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/digitalocean/files/etc/update-motd.d/99-one-click` at line 3, Replace
the unreliable hostname-based IP lookup used to set the myip variable with a
call to the DigitalOcean metadata service; specifically, set myip by querying
the metadata endpoint for the droplet's public IPv4 (e.g.,
metadata/v1/interfaces/public/0/ipv4/address) with a short curl timeout and a
sensible fallback (keep the existing hostname-based command as fallback) so the
onboarding URL always uses the droplet's public IP; update the assignment to
myip in the 99-one-click script accordingly.

Comment on lines +3 to +4
sed -e 's|DEFAULT_FORWARD_POLICY=.*|DEFAULT_FORWARD_POLICY="ACCEPT"|g' \
-i /etc/default/ufw
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Forward policy is overly permissive for a hardened base image.

Line 3 sets routed/forwarded traffic to global ACCEPT, which broadens network exposure beyond required ingress rules. Prefer default deny for forwarding and explicit route allowances only where needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/digitalocean/scripts/014-ufw-albyhub.sh` around lines 3 - 4, The ufw
forward policy is set to a global ACCEPT which is too permissive for a hardened
image; change the value of DEFAULT_FORWARD_POLICY in /etc/default/ufw from
"ACCEPT" to "DROP" (i.e., ensure the sed replacement sets
DEFAULT_FORWARD_POLICY="DROP") and instead add explicit ufw route/forward rules
only for the specific services or subnets that require forwarding; locate the
occurrence of DEFAULT_FORWARD_POLICY in the sed command and update it, and
document or add corresponding ufw allow/route rules for any required forwarded
traffic.

done
sync; rm /zerofile; sync
cat /dev/null > /var/log/lastlog; cat /dev/null > /var/log/wtmp
sudo apt-get --yes purge droplet-agent*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid sudo and quote the purge pattern.

Line 45 should run directly as root and quote the package glob to avoid unintended shell expansion.

Suggested patch
-sudo apt-get --yes purge droplet-agent*
+apt-get --yes purge 'droplet-agent*'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sudo apt-get --yes purge droplet-agent*
apt-get --yes purge 'droplet-agent*'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/digitalocean/scripts/900-cleanup.sh` at line 45, The purge command in
900-cleanup.sh uses sudo and an unquoted glob which can lead to unintended shell
expansion; change the invocation to run as root (remove sudo) and quote the
package pattern (e.g., "droplet-agent*") so the apt-get --yes purge call in the
script invokes apt-get as root and treats the pattern literally.

@rolznz
Copy link
Copy Markdown
Member

rolznz commented May 14, 2026

fly and render files are in the root, I wonder if it's possible / makes sense to move them so all the third party deployment options are in this deploy folder

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants