diff --git a/README.md b/README.md index 3917e60ac..75952735f 100644 --- a/README.md +++ b/README.md @@ -98,15 +98,15 @@ Distributed under the AGPLv3 License. Read more [here](https://www.getlago.com/b | **Lago Ruby Client** | [![Lago Ruby Client Release](https://img.shields.io/github/v/release/getlago/lago-ruby-client)](https://github.com/getlago/lago-ruby-client/releases) | -## 💻 Deploy locally +## 💻 Self-host deployment ### Requirements 1. Install Docker on your machine; 2. Make sure Docker Compose is installed and available (it should be the case if you have chosen to install Docker via Docker Desktop); and 3. Make sure Git is installed on your machine. -### Run the app -To start using Lago, run the following commands in a shell: +### Run the app locally +To start using Lago locally, run the following commands in a shell: #### On a fresh install @@ -140,6 +140,15 @@ LAGO_API_URL="http://192.168.122.71:3000" LAGO_FRONT_URL="http://192.168.122.71" ``` +### Run Lago on a VPS or behind a reverse proxy + +For non-localhost setups (domain name, TLS, and reverse proxy), use the deployment templates in [`deploy/README.md`](./deploy/README.md): + +- `Local` for basic single-host usage +- `Light` for small production workloads with Traefik + Let's Encrypt +- `Production` for higher-throughput setups with additional services +- `deploy.sh` for an interactive guided installation + ### Find your API key Your API Key can be found directly in the UI: diff --git a/deploy/README.md b/deploy/README.md index 59d0a9765..9c0358426 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,175 +1,171 @@ # Lago Deploy -This repository contains the necessary files to deploy the Lago project. +This directory contains deployment templates for self-hosting Lago with Docker Compose, including VPS and reverse-proxy friendly setups. -## Docker Compose Local +## Deployment modes -To deploy the project locally, you need to have Docker and Docker Compose installed on your machine. -This configuration can be used for small production usages but it's not recommended for large scale deployments. +| Mode | Best for | SSL / Reverse proxy | Files | +| --- | --- | --- | --- | +| Quickstart | Fast evaluation on one host | No | `docker run` | +| Local | Local testing and staging | No | `docker-compose.local.yml` | +| Light | Small production workloads | Yes (Traefik + Let's Encrypt) | `docker-compose.light.yml` + `.env.light.example` | +| Production | Higher throughput production | Yes (Traefik + Let's Encrypt) | `docker-compose.production.yml` + `.env.production.example` | -### Get Started +## Prerequisites -1. Get the docker compose file +1. Docker engine installed +2. Docker Compose (`docker compose` plugin or `docker-compose`) +3. For `Light` and `Production`: a public domain with valid DNS A/AAAA records +4. For `Light` and `Production`: port `443` reachable from the internet (`8080` is used for Traefik dashboard/health checks) -```bash -curl -o docker-compose.yml https://raw.githubusercontent.com/getlago/lago/main/deploy/docker-compose.local.yml -``` +## Option A: Interactive deploy script -2. Run the following command to start the project: +Use the guided deploy script when you want the quickest path on a VPS: ```bash -docker compose up --profile all - -# If you want to run it in the background -docker compose up -d --profile all +curl -fsSL -o deploy.sh https://raw.githubusercontent.com/getlago/lago/main/deploy/deploy.sh +bash deploy.sh ``` -## Docker Compose Light +The script lets you choose the deployment mode, downloads the right files, asks for required environment variables, and starts the stack. -This configuration provide Traefik as a reverse proxy to ease your deployment. -It supports SSL with Let's Encrypt. :warning: You need a valid domain (with at least one A or AAA record)! +## Option B: Manual Docker Compose deployment -1. Get the docker compose file +### Local mode ```bash -curl -o docker-compose.yml https://raw.githubusercontent.com/getlago/lago/main/deploy/docker-compose.light.yml -curl -o .env https://raw.githubusercontent.com/getlago/lago/main/deploy/.env.light.example +curl -fsSL -o docker-compose.local.yml https://raw.githubusercontent.com/getlago/lago/main/deploy/docker-compose.local.yml +docker compose -f docker-compose.local.yml up -d --profile all ``` -2. Replace the .env values with yours +### Light mode (VPS + reverse proxy + TLS) ```bash -LAGO_DOMAIN=domain.tld -LAGO_ACME_EMAIL=email@domain.tld +curl -fsSL -o docker-compose.light.yml https://raw.githubusercontent.com/getlago/lago/main/deploy/docker-compose.light.yml +curl -fsSL -o .env https://raw.githubusercontent.com/getlago/lago/main/deploy/.env.light.example ``` -3. Run the following command to start the project +Set `.env`: ```bash -docker compose up --profile all - -# If you want to run it in the background -docker compose up -d --profile all +LAGO_DOMAIN=billing.example.com +LAGO_ACME_EMAIL=infra@example.com ``` -## Docker Compose Production +Run: + +```bash +docker compose -f docker-compose.light.yml up -d --profile all +``` -This configuration provide Traefik as a reverse proxy to ease your deployment. -It supports SSL wth Let's Encrypt. :warning: You need a valid domain (with at least one A or AAA record)! -It also adds multiple services that will help your to handle more load. -Portainer is also packed to help you scale services and manage your Lago stack. +### Production mode ```bash -curl -o docker-compose.yml https://raw.githubusercontent.com/getlago/lago/main/deploy/docker-compose.production.yml -curl -o .env https://raw.githubusercontent.com/getlago/lago/main/deploy/.env.production.example +curl -fsSL -o docker-compose.production.yml https://raw.githubusercontent.com/getlago/lago/main/deploy/docker-compose.production.yml +curl -fsSL -o .env https://raw.githubusercontent.com/getlago/lago/main/deploy/.env.production.example ``` -2. Replace the .env values with yours +Set `.env`: ```bash -LAGO_DOMAIN=domain.tld -LAGO_ACME_EMAIL=email@domain.tld +LAGO_DOMAIN=billing.example.com +LAGO_ACME_EMAIL=infra@example.com PORTAINER_USER=lago -PORTAINER_PASSWORD=changeme +PORTAINER_PASSWORD=change-me ``` -3. Run the following command to start the project +Run: ```bash -docker compose up --profile all - -# If you want to run it in the background -docker compose up -d --profile all +docker compose -f docker-compose.production.yml up -d --profile all ``` +## VPS and reverse-proxy checklist + +1. Point DNS to your VPS (`A`/`AAAA` record for `LAGO_DOMAIN`) +2. Open inbound port `443` (and `8080` only if you expose Traefik dashboard/health checks) +3. Use `Light` or `Production` mode (both ship with Traefik) +4. Set `LAGO_DOMAIN` and `LAGO_ACME_EMAIL` in `.env` +5. Start with `--profile all` (or selective profiles below) +6. Verify `https://` and `https:///api` ## Configuration ### Profiles -The docker compose file contains multiple profiles to enable or disable some services. -Here are the available profiles: -- `all`: Enable all services -- `all-no-pg`: Disable the PostgreSQL service -- `all-no-redis`: Disable the Redis service -- `all-no-keys`: Disable the RSA keys generation service +The compose files support these profiles: -This allow you to start only the service you want to use, please see the following sections for more information. - -```bash -# Start all services -docker compose up --profile all +- `all`: enable all services +- `all-no-pg`: disable PostgreSQL (use external PostgreSQL) +- `all-no-redis`: disable Redis (use external Redis) +- `all-no-db`: disable PostgreSQL and Redis +- `all-no-keys`: disable RSA key generation -# Start without PostgreSQL -docker compose up --profile all-no-pg +Examples: -# Start without Redis -docker compose up --profile all-no-redis +```bash +# Without PostgreSQL +docker compose -f docker-compose.light.yml up -d --profile all-no-pg -# Start without PostgreSQL and Redis -docker compose up --profile all-no-db +# Without Redis +docker compose -f docker-compose.light.yml up -d --profile all-no-redis -# Start without RSA keys generation -docker compose up --profile all-no-keys +# Without PostgreSQL and Redis +docker compose -f docker-compose.light.yml up -d --profile all-no-db -# Start without PostgreSQL, Redis and RSA keys generation -docker compose up +# Without generated RSA key +docker compose -f docker-compose.light.yml up -d --profile all-no-keys ``` -### PostgreSQL - -It is possible to disable the usage of the PostgreSQL database to use an external database instance. +### External PostgreSQL -1. Set those environment variables: +Set: +- `POSTGRES_HOST` +- `POSTGRES_PORT` - `POSTGRES_USER` - `POSTGRES_PASSWORD` - `POSTGRES_DB` -- `POSTGRES_HOST` -- `POSTGRES_PORT` -- `POSTGRES_SCHEMA` optional - -2. Run the following command to start the project without PostgreSQL: - -```bash -docker compose up --profile all-no-pg -``` +- `POSTGRES_SCHEMA` (optional) -### Redis +Then run with `--profile all-no-pg`. -It is possible to disable the usage of the Redis database to use an external Redis instance. +### External Redis -1. Set those environment variables: +Set: - `REDIS_HOST` - `REDIS_PORT` -- `REDIS_PASSWORD` optional +- `REDIS_PASSWORD` (optional) -2. Run the following command to start the project without Redis: +Then run with `--profile all-no-redis`. -```bash -docker compose up --profile all-no-redis -``` +### RSA key management + +By default, compose generates an RSA key pair used for JWT signing. To provide your own key: -### RSA Keys +1. Remove the `lago_rsa_data` volume +2. Generate a key with `openssl genrsa 2048 | openssl base64 -A` +3. Set `LAGO_RSA_PRIVATE_KEY` +4. Start with `--profile all-no-keys` -Those docker compose file generates an RSA Keys pair for the JWT tokens generation. -You can find the keys in the `lago_rsa_data` volume or in the `/app/config/keys` directory in the backends containers. -If you do not want to use those keys: -- Remove the `lago_rsa_data` volume -- Generate your own key using `openssl genrsa 2048 | openssl base64 -A` -- Export this generated key to the `LAGO_RSA_PRIVATE_KEY` env var. -- Run the following command to start the project without the RSA keys generation: +All backend services must share the same private key. + +### Apply `.env` changes safely + +When changing public URL variables (`LAGO_DOMAIN`, `LAGO_API_URL`, `LAGO_FRONT_URL`, `API_URL`), recreate the impacted services so runtime config is regenerated: ```bash -docker compose up --profile all-no-keys +docker compose -f .yml down +docker compose -f .yml up -d --profile all ``` -*All BE Services use the same RSA key, they will exit immediately if no key is provided.* +Use the same compose file you started with (for example `docker-compose.light.yml` or `docker-compose.production.yml`). ## Monitoring -For production deployments, we recommend setting up monitoring for Sidekiq workers. See the [Monitoring documentation](../docs/monitoring.md) for: -- Prometheus metrics endpoints and available metrics +For production deployments, set up Sidekiq monitoring. See [Monitoring documentation](../docs/monitoring.md) for: + +- Prometheus metrics and available metrics - Recommended alerting rules - Grafana dashboard recommendations diff --git a/deploy/deploy.sh b/deploy/deploy.sh index d13e3a6ab..efb0a6e06 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -8,6 +8,7 @@ NORMAL=$(tput sgr0) BOLD=$(tput bold) ENV_FILE=".env" +COMPOSE_FILE="" check_command() { if ! command -v "$1" &> /dev/null; then @@ -19,6 +20,82 @@ check_command() { fi } +check_docker_compose() { + if docker compose version &> /dev/null; then + echo "${GREEN}✅ docker compose is installed.${NORMAL}" + return 0 + fi + + if command -v docker-compose &> /dev/null; then + echo "${GREEN}✅ docker-compose is installed.${NORMAL}" + return 0 + fi + + echo "${RED}❌ Error:${NORMAL} ${BOLD}Docker Compose${NORMAL} is not installed." + return 1 +} + +run_compose() { + if docker compose version &> /dev/null; then + docker compose -f "$COMPOSE_FILE" "$@" + else + docker-compose -f "$COMPOSE_FILE" "$@" + fi +} + +download_file() { + local url="$1" + local output="$2" + local label="$3" + + if curl -fsSL -o "$output" "$url"; then + echo "${GREEN}✅ Downloaded ${label}.${NORMAL}" + else + echo "${RED}❌ Failed to download ${label} from ${url}.${NORMAL}" + exit 1 + fi +} + +check_domain_dns() { + local domain="$1" + local has_a=false + local has_aaaa=false + + # Remove protocol if present + domain=$(echo "$domain" | sed -E 's|^https?://||') + + echo "${CYAN}${BOLD}🔍 Checking DNS records (A/AAAA) for ${domain}...${NORMAL}" + + if command -v dig &> /dev/null; then + dig +short A "$domain" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' && has_a=true + dig +short AAAA "$domain" | grep -Eq ':' && has_aaaa=true + elif command -v nslookup &> /dev/null; then + nslookup -type=A "$domain" 2>/dev/null | grep -Eq 'Address: [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' && has_a=true + nslookup -type=AAAA "$domain" 2>/dev/null | grep -Eq 'Address: .*:' && has_aaaa=true + else + echo "${YELLOW}⚠️ Cannot check domain DNS record - neither dig nor nslookup available${NORMAL}" + return 2 + fi + + if $has_a || $has_aaaa; then + local record_types="" + $has_a && record_types="A" + if $has_aaaa; then + if [ -n "$record_types" ]; then + record_types="${record_types}/AAAA" + else + record_types="AAAA" + fi + fi + + echo "${GREEN}✅ Valid DNS record (${record_types}) found for ${BOLD}${domain}${NORMAL}" + return 0 + fi + + echo "${RED}❌ No valid A or AAAA record found for ${BOLD}${domain}${NORMAL}" + return 1 +} + ask_yes_no() { while true; do read -p "${YELLOW}👉 $1 [y/N]: ${NORMAL}" yn /dev/null || docker-compose -p "$project" ps -q &>/dev/null) + if docker compose version &> /dev/null; then + running_services=$(docker compose -p "$project" ps -q 2>/dev/null) + else + running_services=$(docker-compose -p "$project" ps -q 2>/dev/null) + fi + if [ -n "$running_services" ]; then echo "${YELLOW}⚠️ Detected running Docker Compose project: ${BOLD}$project${NORMAL}" if ask_yes_no "Do you want to stop ${BOLD}${project}${NORMAL}?"; then - docker compose -p "$project" down &>/dev/null || docker-compose -p "$project" down &>/dev/null + if docker compose version &> /dev/null; then + docker compose -p "$project" down &>/dev/null + else + docker-compose -p "$project" down &>/dev/null + fi echo "${GREEN}✅ ${project} stopped.${NORMAL}" if ask_yes_no "Do you want to clean volumes and all data from ${BOLD}${project}${NORMAL}?"; then @@ -166,35 +254,23 @@ profile="all" case "$selected_key" in "Local") echo "${CYAN}${BOLD}🚀 Downloading Local deployment files...${NORMAL}" - curl -s -o docker-compose.yml https://deploy.getlago.com/docker-compose.local.yml - if [ $? -eq 0 ]; then - echo "${GREEN}✅ Successfully downloaded Local deployment files${NORMAL}" - else - echo "${RED}❌ Failed to download Local deployment files${NORMAL}" - exit 1 - fi + COMPOSE_FILE="docker-compose.local.yml" + download_file "https://deploy.getlago.com/docker-compose.local.yml" "$COMPOSE_FILE" "Local deployment compose file" + echo "${GREEN}✅ Successfully downloaded Local deployment files${NORMAL}" ;; "Light") echo "${CYAN}${BOLD}🚀 Downloading Light deployment files...${NORMAL}" - curl -s -o docker-compose.yml https://deploy.getlago.com/docker-compose.light.yml - curl -s -o .env https://deploy.getlago.com/.env.light.example - if [ $? -eq 0 ]; then - echo "${GREEN}✅ Successfully downloaded Light deployment files${NORMAL}" - else - echo "${RED}❌ Failed to download Light deployment files${NORMAL}" - exit 1 - fi + COMPOSE_FILE="docker-compose.light.yml" + download_file "https://deploy.getlago.com/docker-compose.light.yml" "$COMPOSE_FILE" "Light deployment compose file" + download_file "https://deploy.getlago.com/.env.light.example" ".env" "Light deployment environment file" + echo "${GREEN}✅ Successfully downloaded Light deployment files${NORMAL}" ;; "Production") echo "${CYAN}${BOLD}🚀 Downloading Production deployment files...${NORMAL}" - curl -s -o docker-compose.yml https://deploy.getlago.com/docker-compose.production.yml - curl -s -o .env https://deploy.getlago.com/.env.production.example - if [ $? -eq 0 ]; then - echo "${GREEN}✅ Successfully downloaded Production deployment files${NORMAL}" - else - echo "${RED}❌ Failed to download Production deployment files${NORMAL}" - exit 1 - fi + COMPOSE_FILE="docker-compose.production.yml" + download_file "https://deploy.getlago.com/docker-compose.production.yml" "$COMPOSE_FILE" "Production deployment compose file" + download_file "https://deploy.getlago.com/.env.production.example" ".env" "Production deployment environment file" + echo "${GREEN}✅ Successfully downloaded Production deployment files${NORMAL}" ;; esac @@ -202,16 +278,23 @@ echo "" # Check Env Vars depending on the deployment if [[ "$selected_key" == "Light" || "$selected_key" == "Production" ]]; then - mandatory_vars=("LAGO_DOMAIN" "LAGO_ACME_EMAIL" "PORTAINER_USER" "PORTAINER_PASSWORD") + mandatory_vars=("LAGO_DOMAIN" "LAGO_ACME_EMAIL") + if [[ "$selected_key" == "Production" ]]; then + mandatory_vars+=("PORTAINER_USER" "PORTAINER_PASSWORD") + fi external_pg=false external_redis=false - if [[ -n "$LAGO_DOMAIN" ]]; then - check_domain_dns "$LAGO_DOMAIN" - if [[ $? -eq 1 ]] && ! ask_yes_no "No valid DNS record found. Continue anyway?"; then - echo "${YELLOW}⚠️ Deployment aborted.${NORMAL}" - exit 1 - fi + echo "${CYAN}${BOLD}🔧 Checking mandatory environment variables...${NORMAL}" + + # Load Existing .env values + if [ -f "$ENV_FILE" ]; then + # shellcheck disable=SC2046 + export $(grep -v '^#' "$ENV_FILE" | xargs) + echo "${GREEN}✅ Loaded existing .env file.${NORMAL}" + else + touch "$ENV_FILE" + echo "${YELLOW}⚠️ No .env file found. Created a new one.${NORMAL}" fi if ask_yes_no "Do you want to use an external PostgreSQL instance?"; then @@ -231,7 +314,7 @@ if [[ "$selected_key" == "Light" || "$selected_key" == "Production" ]]; then mandatory_vars+=("REDIS_PASSWORD") fi fi - + if $external_pg && $external_redis; then profile="all-no-db" elif $external_pg; then @@ -242,18 +325,6 @@ if [[ "$selected_key" == "Light" || "$selected_key" == "Production" ]]; then echo "" - echo "${CYAN}${BOLD}🔧 Checking mandatory environment variables...${NORMAL}" - - # Load Existing .env values - if [ -f "$ENV_FILE" ]; then - # shellcheck disable=SC2046 - export $(grep -v '^#' "$ENV_FILE" | xargs) - echo "${GREEN}✅ Loaded existing .env file.${NORMAL}" - else - touch "$ENV_FILE" - echo "${YELLOW}⚠️ No .env file found. Created a new one.${NORMAL}" - fi - { echo "# Updated by Lago Deploy" for var in "${mandatory_vars[@]}"; do @@ -269,38 +340,13 @@ if [[ "$selected_key" == "Light" || "$selected_key" == "Production" ]]; then echo "${GREEN}${BOLD}✅ .env file updated successfully.${NORMAL}" echo "" -fi -# Check if domain has A record -check_domain_dns() { - local domain="$1" - - # Remove protocol if present - domain=$(echo "$domain" | sed -E 's|^https?://||') - - echo "${CYAN}${BOLD}🔍 Checking DNS A record for ${domain}...${NORMAL}" - - if command -v dig &> /dev/null; then - if dig +short A "$domain" | grep -q '^[0-9]'; then - echo "${GREEN}✅ Valid A record found for ${BOLD}${domain}${NORMAL}" - return 0 - else - echo "${RED}❌ No valid A record found for ${BOLD}${domain}${NORMAL}" - return 1 - fi - elif command -v nslookup &> /dev/null; then - if nslookup "$domain" | grep -q 'Address: [0-9]'; then - echo "${GREEN}✅ Valid A record found for ${BOLD}${domain}${NORMAL}" - return 0 - else - echo "${RED}❌ No valid A record found for ${BOLD}${domain}${NORMAL}" - return 1 - fi - else - echo "${YELLOW}⚠️ Cannot check domain DNS record - neither dig nor nslookup available${NORMAL}" - return 2 + check_domain_dns "$LAGO_DOMAIN" + if [[ $? -eq 1 ]] && ! ask_yes_no "No valid DNS record found. Continue anyway?"; then + echo "${YELLOW}⚠️ Deployment aborted.${NORMAL}" + exit 1 fi -} +fi # Execute selected deployment case "$selected_key" in @@ -310,19 +356,15 @@ case "$selected_key" in ;; Local) echo "${CYAN}🚧 Running Local Docker Compose deployment...${NORMAL}" - docker compose -f docker-compose.local.yml up -d || docker-compose -f docker-compose.local.yml up -d &>/dev/null + run_compose up -d --profile "$profile" ;; Light) echo "${CYAN}🚧 Running Light Docker Compose deployment...${NORMAL}" - - docker compose -f docker-compose.light.yml --profile "$profile" up -d &>/dev/null || \ - docker-compose -f docker-compose.light.yml --profile "$profile" up -d &>/dev/null + run_compose up -d --profile "$profile" ;; Production) echo "${CYAN}🚧 Running Production Docker Compose deployment...${NORMAL}" - - docker compose -f docker-compose.production.yml --profile "$profile" up -d &>/dev/null || \ - docker-compose -f docker-compose.production.yml --profile "$profile" up -d &>/dev/null + run_compose up -d --profile "$profile" ;; esac