Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions deploy/digitalocean/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Deploy Alby Hub on DigitalOcean (Marketplace 1-Click App)

A submission-ready scaffold for publishing Alby Hub as a DigitalOcean Marketplace 1-Click Droplet App. Follows the layout documented at [digitalocean/marketplace-partners](https://github.com/digitalocean/marketplace-partners).

## What this scaffold produces

A Droplet image (Ubuntu 24.04) with:
- docker-ce pre-installed
- `ghcr.io/getalby/hub:latest` pre-pulled
- caddy 2 for TLS termination (Let's Encrypt)
- A `systemd` unit that runs Alby Hub at boot with a persistent `/opt/albyhub/data` volume
- A first-boot cloud-init script that prompts for `DOMAIN` via droplet user-data or auto-configures IP-only access
- A first-login MOTD with the Hub URL and onboarding steps

## Directory layout

```
deploy/digitalocean/
├── README.md # this file
├── marketplace-image.json # Packer template (input to `packer build`)
├── scripts/
│ ├── 01-install-albyhub.sh # installs docker, caddy, pulls image, drops systemd unit
│ ├── 90-cleanup.sh # DigitalOcean-required log/history cleanup
│ └── 99-img_check.sh # DigitalOcean-required image validation
└── files/
├── etc/
│ ├── systemd/system/albyhub.service # systemd unit for the container
│ ├── caddy/Caddyfile # caddy reverse-proxy config
│ └── update-motd.d/99-albyhub # first-login message
└── var/lib/cloud/scripts/per-instance/
└── 01-albyhub-firstboot.sh # first-boot config (runs once on each new droplet)
```

## Building the image

Prerequisites: `packer` ≥ 1.9, a DigitalOcean API token with `write` scope.

```bash
export DIGITALOCEAN_API_TOKEN=dop_v1_...
packer init deploy/digitalocean/marketplace-image.json
packer build deploy/digitalocean/marketplace-image.json
```

Packer creates a snapshot in your DO account. To test: launch a droplet from that snapshot, wait ~60 seconds, SSH in, read the MOTD, open the URL.

## Submitting to the Marketplace

1. Apply at [digitalocean.com/landing/partner-application](https://www.digitalocean.com/landing/partner-application) (Partner Pod program).
2. Fork [digitalocean/marketplace-partners](https://github.com/digitalocean/marketplace-partners) and open a PR that references the snapshot ID.
3. Provide marketing assets:
- Logo: [doc/logo.svg](../../doc/logo.svg)
- Category: `Blockchain` (they have a dedicated bitcoin subcategory)
- Short description: "Your own bitcoin lightning node — connectable, feature-rich, self-sovereign."
- Long description: adapt the top of the main [README.md](../../README.md).
- Recommended droplet size: **Basic Regular 2 GB RAM / 1 vCPU / 50 GB SSD** ($12/mo at the time of writing). Call out that 1 GB is not enough for the embedded LDK node.
4. Iterate on DO's validation feedback (their reviewers will run `99-img_check.sh` and manual checks).

## Droplet user-data options

Users can pre-configure the deploy by passing cloud-init user-data when creating the droplet:

```yaml
#cloud-config
write_files:
- path: /etc/albyhub/hub.env
content: |
DOMAIN=albyhub.example.com
BASE_URL=https://albyhub.example.com
```

If `DOMAIN` is set, Caddy requests a Let's Encrypt cert automatically on first boot. If not, the droplet serves Hub on `http://<public-ip>:8080` — the MOTD walks users through setting a domain afterwards.

## Affiliate attribution

Add `?refcode=albyhub` (or whatever Alby's DO affiliate code is at submission time) to every "Deploy on DigitalOcean" link in marketing materials. Per DO's affiliate terms: 10%/mo for 12 months on referred spend + $25 per paying referral after first $25 spend.

## Caveats to surface on the listing page

- **Backup `/opt/albyhub/data` regularly.** Holds the encrypted seed, channel state, and SQLite db. Loss = loss of funds.
- **Lightning p2p port 9735 is opened in the droplet firewall by the provisioner.** If the user enables DigitalOcean's Cloud Firewall on top, they must allow 9735 there too.
- **Self-custody tradeoff.** Same wording as the Hostinger listing — a VPS-hosted lightning node is less sovereign than a home node-distribution.

## Support surface

A new Alby Hub release that changes the container interface must trigger a new Packer build + snapshot + marketplace update. Add to the Hub release checklist. DO supports "update image in place" via a new snapshot push — existing users don't auto-upgrade, so release notes must tell them to `docker compose pull && docker compose up -d` (or use the MOTD update hint).
8 changes: 8 additions & 0 deletions deploy/digitalocean/files/etc/caddy/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Caddy terminates TLS on :443 and proxies to the Alby Hub container on 127.0.0.1:8080.
# The DOMAIN placeholder is rewritten by /var/lib/cloud/scripts/per-instance/01-albyhub-firstboot.sh
# based on droplet user-data. If no DOMAIN is set, the firstboot script replaces the whole
# file with a :80 site block so Hub is reachable over plain HTTP.

{$DOMAIN:localhost} {
reverse_proxy 127.0.0.1:8080
}
25 changes: 25 additions & 0 deletions deploy/digitalocean/files/etc/systemd/system/albyhub.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[Unit]

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.

can we reuse this systemd also?

so that we have only one systemd service file for all?

why is docker here used actually?

Description=Alby Hub (bitcoin lightning node)
After=docker.service network-online.target
Requires=docker.service
Wants=network-online.target

[Service]
Type=simple
EnvironmentFile=-/etc/albyhub/hub.env
Environment=ALBYHUB_IMAGE=ghcr.io/getalby/hub:latest
ExecStartPre=-/usr/bin/docker rm -f albyhub
ExecStart=/usr/bin/docker run --rm --name albyhub \
--env-file /etc/albyhub/hub.env \
-e WORK_DIR=/data/albyhub \
-e LDK_LISTENING_ADDRESSES=[::]:9735 \
-v /opt/albyhub/data:/data \
-p 127.0.0.1:8080:8080 \
-p 9735:9735 \
${ALBYHUB_IMAGE}
ExecStop=/usr/bin/docker stop albyhub
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
27 changes: 27 additions & 0 deletions deploy/digitalocean/files/etc/update-motd.d/99-albyhub
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/sh
# First-login motd for Alby Hub droplets.

PUBLIC_IP=$(curl -s --max-time 2 http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address || echo "<your-droplet-ip>")
DOMAIN=$(grep -s '^DOMAIN=' /etc/albyhub/hub.env | cut -d= -f2-)

cat <<EOF

✨ Alby Hub is installed on this droplet.

Status: systemctl status albyhub
Logs: journalctl -u albyhub -f
Data: /opt/albyhub/data (BACK THIS UP — holds your seed and channel state)
Config: /etc/albyhub/hub.env

EOF

if [ -n "$DOMAIN" ]; then
echo " Open Alby Hub: https://$DOMAIN"
else
echo " Open Alby Hub: http://$PUBLIC_IP:8080"

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.

isn't this also on port 80? as both are served by caddy?

echo ""
echo " To enable TLS, set DOMAIN in /etc/albyhub/hub.env, point your DNS at"
echo " $PUBLIC_IP, then run: systemctl restart caddy albyhub"
fi

echo ""
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash
# Runs once on the first boot of a droplet launched from the Alby Hub marketplace snapshot.
# Reads /etc/albyhub/hub.env (optionally pre-populated via cloud-init user-data) and
# rewrites the Caddy config to match: TLS + domain if DOMAIN is set, plain HTTP otherwise.
set -euo pipefail

ENV_FILE=/etc/albyhub/hub.env
CADDYFILE=/etc/caddy/Caddyfile

touch "$ENV_FILE"

DOMAIN=$(grep -s '^DOMAIN=' "$ENV_FILE" | cut -d= -f2- || true)

if [ -n "$DOMAIN" ]; then
cat > "$CADDYFILE" <<EOF
$DOMAIN {
reverse_proxy 127.0.0.1:8080
}
EOF
grep -q '^BASE_URL=' "$ENV_FILE" || echo "BASE_URL=https://$DOMAIN" >> "$ENV_FILE"
else
# No domain — expose Hub on :80 over plain HTTP for IP-based access.
cat > "$CADDYFILE" <<EOF
:80 {
reverse_proxy 127.0.0.1:8080
}
EOF
fi

systemctl restart caddy
systemctl restart albyhub
45 changes: 45 additions & 0 deletions deploy/digitalocean/marketplace-image.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"variables": {
"token": "{{env `DIGITALOCEAN_API_TOKEN`}}",
"image_name": "albyhub-snapshot-{{timestamp}}",
"albyhub_image": "ghcr.io/getalby/hub:latest"
},
"builders": [
{
"type": "digitalocean",
"api_token": "{{user `token`}}",
"image": "ubuntu-24-04-x64",
"region": "fra1",
"size": "s-1vcpu-2gb",

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.

does this get a volume?
last time we had the issue that volumes are not persistent by default.

"ssh_username": "root",
"snapshot_name": "{{user `image_name`}}",
"droplet_name": "packer-albyhub-{{timestamp}}"
}
],
"provisioners": [
{
"type": "file",
"source": "files/",
"destination": "/tmp/files"
},
{
"type": "shell",
"inline": [
"cp -r /tmp/files/* /",
"rm -rf /tmp/files"
]
},
{
"type": "shell",
"environment_vars": [
"ALBYHUB_IMAGE={{user `albyhub_image`}}",
"DEBIAN_FRONTEND=noninteractive"
],
"scripts": [
"scripts/01-install-albyhub.sh",
"scripts/90-cleanup.sh",
"scripts/99-img_check.sh"
]
}
]
}
36 changes: 36 additions & 0 deletions deploy/digitalocean/scripts/01-install-albyhub.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/bin/bash
# Provisioner: install docker + caddy, pre-pull Alby Hub image, register systemd unit.
# Runs during Packer build against a fresh Ubuntu 24.04 droplet.
set -euxo pipefail

: "${ALBYHUB_IMAGE:=ghcr.io/getalby/hub:latest}"

apt-get update
apt-get install -y ca-certificates curl gnupg ufw

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list

curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/gpg.key | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt | tee /etc/apt/sources.list.d/caddy-stable.list

apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin caddy

docker pull "$ALBYHUB_IMAGE"

mkdir -p /opt/albyhub/data /etc/albyhub
touch /etc/albyhub/hub.env

ufw --force enable
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 9735/tcp

systemctl enable albyhub.service
systemctl enable caddy
23 changes: 23 additions & 0 deletions deploy/digitalocean/scripts/90-cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash
# Standard DigitalOcean Marketplace cleanup: strip logs, shell history, cloud-init state,
# and package-manager caches before snapshot. Mirrors the upstream sample in
# https://github.com/digitalocean/marketplace-partners/blob/master/scripts/90-cleanup.sh
set -euxo pipefail

apt-get -y autoremove
apt-get -y autoclean

rm -rf /tmp/* /var/tmp/*
rm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-????????
: > /var/log/wtmp
: > /var/log/lastlog
: > /var/log/auth.log
: > /var/log/syslog

rm -rf /root/.ssh /root/.bash_history /root/.cache /root/.lesshst
rm -rf /home/*/.ssh /home/*/.bash_history /home/*/.cache /home/*/.lesshst

cloud-init clean --logs
rm -rf /var/lib/cloud/instances/*

truncate -s 0 /etc/machine-id
22 changes: 22 additions & 0 deletions deploy/digitalocean/scripts/99-img_check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash
# DigitalOcean Marketplace image validator (local dry-run). The official img_check.sh
# is shipped via https://github.com/digitalocean/marketplace-partners and must pass
# before submission. This local version covers the Alby Hub-specific invariants.
set -euo pipefail

fail() { echo "FAIL: $*" >&2; exit 1; }

command -v docker >/dev/null || fail "docker not installed"
docker image inspect ghcr.io/getalby/hub:latest >/dev/null || fail "alby hub image not pre-pulled"
command -v caddy >/dev/null || fail "caddy not installed"

[[ -f /etc/systemd/system/albyhub.service ]] || fail "albyhub.service missing"
systemctl is-enabled albyhub.service >/dev/null || fail "albyhub.service not enabled"
systemctl is-enabled caddy >/dev/null || fail "caddy not enabled"

[[ -f /etc/caddy/Caddyfile ]] || fail "Caddyfile missing"
[[ -f /var/lib/cloud/scripts/per-instance/01-albyhub-firstboot.sh ]] || fail "firstboot script missing"
[[ -f /etc/update-motd.d/99-albyhub ]] || fail "motd missing"
[[ -d /opt/albyhub/data ]] || fail "/opt/albyhub/data not created"

echo "OK: image looks valid for marketplace submission"
3 changes: 3 additions & 0 deletions deploy/hostinger/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{$DOMAIN} {
reverse_proxy albyhub:8080
}
60 changes: 60 additions & 0 deletions deploy/hostinger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Deploy Alby Hub on Hostinger VPS

A Hostinger Application Catalog template for Alby Hub. Hostinger wraps a `docker-compose.yml` with a prompt-driven env-var UI and ships it under their VPS Docker templates (see precedents: [openclaw](https://www.hostinger.com/vps/docker/openclaw), [hermes-agent](https://www.hostinger.com/vps/docker/hermes-agent)).

## Artifacts in this directory

- **[docker-compose.yml](./docker-compose.yml)** — two services: Alby Hub (port 8080, lightning p2p port 9735) and an optional Caddy reverse proxy (ports 80/443) activated with the `tls` compose profile. Volume `albyhub-data` persists the SQLite db, seed, channel state, and LDK data.
- **[Caddyfile](./Caddyfile)** — reverse proxy config that terminates TLS for `${DOMAIN}` and proxies to `albyhub:8080`. Caddy auto-provisions a Let's Encrypt cert on startup when a real DNS name is set.

## Deploying without submitting to Hostinger (works today)

Any Hostinger VPS with the Ubuntu 24.04 + Docker template already provisioned can run this:

```bash
curl -O https://raw.githubusercontent.com/getAlby/hub/master/deploy/hostinger/docker-compose.yml
curl -O https://raw.githubusercontent.com/getAlby/hub/master/deploy/hostinger/Caddyfile

# No TLS (IP-only access on port 8080):
docker compose up -d

# With TLS (requires DNS pointed at the VPS):
DOMAIN=albyhub.example.com BASE_URL=https://albyhub.example.com docker compose --profile tls up -d
```

## Submitting to the Hostinger Application Catalog

Hostinger does not publish a public submission process. Reach out to Hostinger BD / partnerships with this package:

| Asset | Source |
|---|---|
| Title | `Alby Hub` |
| Tagline | `Your own bitcoin lightning node` |
| Category | `Finance` / `Self-hosted` |
| Logo (SVG) | [doc/logo.svg](../../doc/logo.svg) |
| Long description | Adapt the top of the main [README.md](../../README.md) |
| `docker-compose.yml` | [./docker-compose.yml](./docker-compose.yml) |
| Reverse proxy config | [./Caddyfile](./Caddyfile) |
| User-facing env-var schema | See table below |
| Minimum VPS plan | KVM 2 (2 GB RAM, 1 shared vCPU, 50 GB NVMe) |
| Post-deploy URL | `http://{public_ip}:8080` (or `https://{domain}` when TLS profile active) |

### Env vars to surface in Hostinger's template UI

| Key | Label | Default | Required |
|---|---|---|---|
| `DOMAIN` | Your domain (optional, for TLS) | *empty* | No |
| `BASE_URL` | Public URL of this Hub (e.g. `https://albyhub.example.com`) | `http://localhost:8080` | No |
| `LDK_ESPLORA_SERVER` | Blockchain API endpoint | `https://electrs.getalbypro.com` | No |

Every other env var from [config/models.go](../../config/models.go) keeps its default; do not surface them in the Hostinger UI or users will be overwhelmed.

## Caveats to surface on the listing page

- **Backup the `albyhub-data` volume regularly.** It holds the encrypted seed, channel state, and SQLite db. Loss = loss of funds.
- **Lightning p2p on port 9735 needs the VPS firewall open.** Hostinger's default firewall blocks it; link users to Hostinger's firewall docs.
- **Self-custody tradeoff.** A VPS-hosted lightning node is less sovereign than a home node-distribution (umbrel, start9). Mention this honestly in the listing so users choose eyes-open.

## Support surface

A new Alby Hub release that changes the container interface must also be smoke-tested against this compose file. Add to the Hub release checklist.
Loading
Loading