diff --git a/Dockerfile b/Dockerfile index 299908ebec..77bec3033b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,18 @@ -FROM n8nio/n8n:latest +# syntax=docker/dockerfile:1 +ARG N8N_VERSION=latest +FROM n8nio/n8n:${N8N_VERSION} + +# Run as root to allow runtime updates of the n8n CLI when requested. USER root -WORKDIR /home/node/packages/cli -ENTRYPOINT [] +# Replace the entrypoint with a Heroku-friendly bootstrap script that +# prepares the database configuration and optionally keeps n8n updated. +COPY entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh + +ENV N8N_RUNTIME_DIR=/tmp/n8n-runtime + +ENTRYPOINT ["/docker-entrypoint.sh"] -COPY ./entrypoint.sh / -RUN chmod +x /entrypoint.sh -CMD ["/entrypoint.sh"] \ No newline at end of file +CMD ["n8n", "start"] diff --git a/README.md b/README.md index a88c2faf4a..f62a3e83d1 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,29 @@ # n8n-heroku -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https://github.com/n8n-io/n8n-heroku/tree/main) +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https://github.com/kuromi04/n8n-heroku2025.git) +[![Actualizar n8n](https://img.shields.io/badge/Actualizar%20n8n-Deploy%20Update-79589f?logo=heroku&logoColor=white)](https://dashboard.heroku.com/new?template=https://github.com/kuromi04/n8n-heroku2025.git&env[N8N_FORCE_INSTALL]=true) ## n8n - Free and open fair-code licensed node based Workflow Automation Tool. This is a [Heroku](https://heroku.com/)-focused container implementation of [n8n](https://n8n.io/). -Use the **Deploy to Heroku** button above to launch n8n on Heroku. When deploying, make sure to check all configuration options and adjust them to your needs. It's especially important to set `N8N_ENCRYPTION_KEY` to a random secure value. +Use the **Deploy to Heroku** button above to launch n8n on Heroku. When deploying, make sure to check all configuration options +and adjust them to your needs. It's especially important to set `N8N_ENCRYPTION_KEY` to a random secure value. Refer to the [Heroku n8n tutorial](https://docs.n8n.io/hosting/server-setups/heroku/) for more information. If you have questions after trying the tutorials, check out the [forums](https://community.n8n.io/). + +## Automatic n8n version management + +This container keeps the n8n CLI up to date without requiring code changes: + +- Set the `N8N_VERSION` config var to pin a specific n8n release. The default value (`latest`) resolves to the newest stable version on each deploy or dyno restart. +- Automatic upgrades can be disabled by setting `N8N_AUTO_UPDATE=false` if you prefer to manage updates manually. +- To force an upgrade of the currently requested version (for example after a new `latest` is published), set `N8N_FORCE_INSTALL=true` or use the **Actualizar n8n** button above and select your existing application in the Heroku dialog. The flag will trigger a clean reinstall on the next deploy without touching other configuration and clears npm's cache to make sure the newest build is fetched. + +During startup the entrypoint script compares the installed version with the desired one and installs upgrades when required, ensuring that only n8n itself changes while the rest of the environment remains stable. + +Updated releases are placed in a writable runtime directory (default `/tmp/n8n-runtime`) and prepended to the `PATH`, so the Heroku slug remains untouched. The runtime installer automatically clears any previously staged binaries before installing the requested version. You can change the directory by defining the optional `N8N_RUNTIME_DIR` config var as long as the location is writable at boot time. + +Tanto la fase de *release* como los *dynos* web reutilizan el mismo entrypoint del contenedor, por lo que cada despliegue verifica si hay una versión más reciente de n8n, la instala antes de ejecutar las migraciones de base de datos y sólo entonces expone la aplicación. diff --git a/app.json b/app.json index 8218f22f19..69ca9fd054 100644 --- a/app.json +++ b/app.json @@ -1,43 +1,59 @@ { - "name": "n8n", - "description": "deploy n8n to heroku without any hassle", - "keywords": [ - "n8n", - "node", - "automation" - ], - "website": "https://n8n.io", - "repository": "https://github.com/n8n-io/n8n-heroku", - "logo": "https://raw.githubusercontent.com/n8n-io/n8n-heroku/main/n8n_logo.png", - "success_url": "/", - "stack": "container", - "env": { - "GENERIC_TIMEZONE": { - "description": "Time Zone to use with Heroku. You can find the name of your timezone for example here: https://momentjs.com/timezone/.", - "value": "Europe/Berlin" - }, - "N8N_ENCRYPTION_KEY": { - "description": "Set the n8n encryption key to a static value to avoid Heroku overriding it (causing authentication to fail).", - "value": "change-me-to-something-else" - }, - "WEBHOOK_URL": { - "description": "Replace with your Heroku application name. This will ensure the correct webhook URLs are being shown in n8n.", - "value": "https://.herokuapp.com" - }, - "DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED": { - "description": "SSL is required to connect to Postgres on Heroku", - "value": "false" - } + "name": "n8n", + "description": "Deploy n8n to Heroku without any hassle", + "keywords": [ + "n8n", + "node", + "automation" + ], + "website": "https://n8n.io", + "repository": "https://github.com/kuromi04/n8n-heroku2025.git", + "logo": "https://raw.githubusercontent.com/n8n-io/n8n-heroku/main/n8n_logo.png", + "success_url": "/", + "stack": "container", + "env": { + "GENERIC_TIMEZONE": { + "description": "Time Zone to use with Heroku. You can find the name of your timezone for example here: https://momentjs.com/timezone/.", + "value": "Europe/Berlin" + }, + "N8N_ENCRYPTION_KEY": { + "description": "Set the n8n encryption key to a static value to avoid Heroku overriding it (causing authentication to fail).", + "value": "change-me-to-something-else" + }, + "WEBHOOK_URL": { + "description": "Replace with your Heroku application name. This will ensure the correct webhook URLs are being shown in n8n.", + "value": "https://.herokuapp.com" + }, + "DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED": { + "description": "SSL is required to connect to Postgres on Heroku", + "value": "false" }, - "addons": [ - { - "plan": "heroku-postgresql", - "options": { - "version": "14" - } - }, - { - "plan": "papertrail:choklad" + "N8N_VERSION": { + "description": "Desired n8n release (for example 1.42.0). Leave as 'latest' to automatically install the newest stable version during deploy.", + "value": "latest" + }, + "N8N_AUTO_UPDATE": { + "description": "Set to 'false' to disable automatic updates when the running version differs from N8N_VERSION.", + "value": "true" + }, + "N8N_RUNTIME_DIR": { + "description": "Optional writable directory for storing on-boot n8n upgrades. Defaults to /tmp/n8n-runtime.", + "value": "/tmp/n8n-runtime" + }, + "N8N_FORCE_INSTALL": { + "description": "Temporarily set to 'true' to reinstall the requested n8n version from scratch on the next deploy without touching other settings. npm's cache will be cleared automatically to fetch the newest build.", + "value": "false" + } + }, + "addons": [ + { + "plan": "heroku-postgresql", + "options": { + "version": "15" } - ] - } + }, + { + "plan": "papertrail:choklad" + } + ] +} diff --git a/entrypoint.sh b/entrypoint.sh index dced804880..c05ace119f 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,26 +1,242 @@ -#!/bin/sh +#!/usr/bin/env bash +set -euo pipefail -# check if port variable is set or go with default -if [ -z ${PORT+x} ]; then echo "PORT variable not defined, leaving N8N to default port."; else export N8N_PORT="$PORT"; echo "N8N will start on '$PORT'"; fi +DEFAULT_CMD=("n8n" "start") +N8N_RUNTIME_DIR="${N8N_RUNTIME_DIR:-/tmp/n8n-runtime}" -# regex function -parse_url() { - eval $(echo "$1" | sed -e "s#^\(\(.*\)://\)\?\(\([^:@]*\)\(:\(.*\)\)\?@\)\?\([^/?]*\)\(/\(.*\)\)\?#${PREFIX:-URL_}SCHEME='\2' ${PREFIX:-URL_}USER='\4' ${PREFIX:-URL_}PASSWORD='\6' ${PREFIX:-URL_}HOSTPORT='\7' ${PREFIX:-URL_}DATABASE='\9'#") +log() { + local timestamp + timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" + printf '[%s] %s\n' "$timestamp" "$*" } -# prefix variables to avoid conflicts and run parse url function on arg url -PREFIX="N8N_DB_" parse_url "$DATABASE_URL" -echo "$N8N_DB_SCHEME://$N8N_DB_USER:$N8N_DB_PASSWORD@$N8N_DB_HOSTPORT/$N8N_DB_DATABASE" -# Separate host and port -N8N_DB_HOST="$(echo $N8N_DB_HOSTPORT | sed -e 's,:.*,,g')" -N8N_DB_PORT="$(echo $N8N_DB_HOSTPORT | sed -e 's,^.*:,:,g' -e 's,.*:\([0-9]*\).*,\1,g' -e 's,[^0-9],,g')" +normalize_bool() { + case "${1:-}" in + 1|true|TRUE|yes|YES|on|ON) + printf 'true' + ;; + *) + printf 'false' + ;; + esac +} + +configure_port() { + if [ -n "${PORT:-}" ]; then + export N8N_PORT="$PORT" + log "Using Heroku PORT=${PORT} for N8N_PORT." + else + log "PORT is not defined; using the default n8n port." + fi +} + +configure_database() { + if [ -z "${DATABASE_URL:-}" ]; then + log "DATABASE_URL not provided; skipping database configuration." + return + fi + + local db_values + IFS=$'\n' read -r -d '' -a db_values < <(node <<'NODE' && printf '\0' +const urlValue = process.env.DATABASE_URL; +try { + const parsed = new URL(urlValue); + if (!/^postgres/i.test(parsed.protocol)) { + console.log(''); + console.log(''); + console.log(''); + console.log(''); + console.log(''); + process.exit(0); + } + + const withoutLeadingSlash = parsed.pathname ? parsed.pathname.replace(/^\//, '') : ''; + console.log(parsed.hostname ?? ''); + console.log(parsed.port || '5432'); + console.log(parsed.username || ''); + console.log(parsed.password || ''); + console.log(withoutLeadingSlash); +} catch (error) { + console.error(error.message); + process.exit(1); +} +NODE + ) || { + log "Failed to parse DATABASE_URL; aborting."; + exit 1 + } + + # If the protocol was not Postgres we bail out silently. + if [ "${#db_values[@]}" -eq 0 ] || [ -z "${db_values[0]}${db_values[2]}${db_values[4]}" ]; then + log "DATABASE_URL is not a Postgres connection string; skipping database configuration." + return + fi + + export DB_TYPE=postgresdb + export DB_POSTGRESDB_HOST="${db_values[0]}" + export DB_POSTGRESDB_PORT="${db_values[1]}" + export DB_POSTGRESDB_USER="${db_values[2]}" + export DB_POSTGRESDB_PASSWORD="${db_values[3]}" + export DB_POSTGRESDB_DATABASE="${db_values[4]}" + + log "Configured Postgres database ${DB_POSTGRESDB_DATABASE} on ${DB_POSTGRESDB_HOST}:${DB_POSTGRESDB_PORT}." +} + +current_n8n_version() { + n8n --version 2>/dev/null | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true +} + +desired_n8n_version() { + local requested + requested="${N8N_VERSION:-latest}" + if [ "$requested" = "latest" ]; then + npm view n8n version 2>/dev/null || true + else + printf '%s\n' "$requested" + fi +} + +runtime_bin_dir() { + printf '%s/bin' "$N8N_RUNTIME_DIR" +} + +activate_runtime_dir() { + local bin_dir + bin_dir="$(runtime_bin_dir)" + if [ -d "$bin_dir" ] && [[ ":$PATH:" != *":$bin_dir:"* ]]; then + export PATH="$bin_dir:$PATH" + hash -r 2>/dev/null || true + fi +} + +install_n8n_runtime() { + local version + local force + local previous_version + version="$1" + force="${2:-false}" + previous_version="${3:-}" + + case "$force" in + true|TRUE) + force=true + ;; + *) + force=false + ;; + esac + + if [ "$force" = "true" ]; then + log "Clearing runtime directory ${N8N_RUNTIME_DIR} before reinstalling n8n ${version}." + rm -rf "$N8N_RUNTIME_DIR" + elif [ -n "$previous_version" ] && [ "$previous_version" != "$version" ]; then + log "Removing previously installed n8n ${previous_version} before upgrading to ${version}." + rm -rf "${N8N_RUNTIME_DIR}/lib/node_modules/n8n" "${N8N_RUNTIME_DIR}/bin/n8n" + fi -export DB_TYPE=postgresdb -export DB_POSTGRESDB_HOST=$N8N_DB_HOST -export DB_POSTGRESDB_PORT=$N8N_DB_PORT -export DB_POSTGRESDB_DATABASE=$N8N_DB_DATABASE -export DB_POSTGRESDB_USER=$N8N_DB_USER -export DB_POSTGRESDB_PASSWORD=$N8N_DB_PASSWORD + mkdir -p "$N8N_RUNTIME_DIR" + if ! command -v npm >/dev/null 2>&1; then + log "npm is not available; cannot install n8n ${version}." + return 1 + fi + + if [ "$force" = "true" ]; then + log "Clearing npm cache before the forced installation." + npm cache clean --force >/dev/null 2>&1 || true + fi + + log "Installing n8n ${version} into ${N8N_RUNTIME_DIR}." + local install_args + install_args=(--global --loglevel=error --no-fund --unsafe-perm --prefix "$N8N_RUNTIME_DIR") + if [ "$force" = "true" ]; then + install_args+=(--force) + fi + + if npm install "${install_args[@]}" "n8n@${version}"; then + activate_runtime_dir + local resolved + resolved="$(runtime_n8n_version)" + if [ "$resolved" != "$version" ]; then + log "Warning: expected n8n ${version} after installation but detected ${resolved:-unknown}." + else + log "Successfully installed n8n ${version}." + fi + return 0 + fi + + log "Failed to install n8n ${version}." + return 1 +} + +runtime_n8n_version() { + local runtime_bin + runtime_bin="$(runtime_bin_dir)/n8n" + if [ -x "$runtime_bin" ]; then + "$runtime_bin" --version 2>/dev/null | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true + fi +} + +maybe_update_n8n() { + local auto_update + auto_update="${N8N_AUTO_UPDATE:-true}" + local force_install + force_install="${N8N_FORCE_INSTALL:-false}" + + auto_update="$(normalize_bool "$auto_update")" + force_install="$(normalize_bool "$force_install")" + + mkdir -p "$N8N_RUNTIME_DIR" + activate_runtime_dir + + local target_version + target_version="$(desired_n8n_version)" + if [ -z "$target_version" ]; then + log "Unable to determine the desired n8n version; skipping update." + return + fi + + local installed_version + installed_version="$(runtime_n8n_version)" + if [ -z "$installed_version" ]; then + installed_version="$(current_n8n_version)" + fi + + if [ "$installed_version" = "$target_version" ] && [ "$force_install" != "true" ]; then + log "n8n ${installed_version} is already available." + return + fi + + if [ "$installed_version" = "$target_version" ] && [ "$force_install" = "true" ]; then + log "Reinstalling n8n ${target_version} because N8N_FORCE_INSTALL=true." + fi + + if [ "$auto_update" != "true" ] && [ "$force_install" != "true" ]; then + log "n8n ${installed_version:-unknown} does not match desired ${target_version}, but automatic updates are disabled." + return + fi + + if install_n8n_runtime "$target_version" "$force_install" "$installed_version"; then + if [ "$force_install" = "true" ]; then + log "Forced installation completed; unset N8N_FORCE_INSTALL to avoid reinstalling on every boot." + export N8N_FORCE_INSTALL=false + fi + return + fi + + log "Continuing with existing n8n ${installed_version:-unknown}." +} + +main() { + if [ "$#" -eq 0 ]; then + set -- "${DEFAULT_CMD[@]}" + fi + + configure_port + configure_database + maybe_update_n8n + + log "Starting n8n with command: $*" + exec "$@" +} -# kickstart nodemation -n8n \ No newline at end of file +main "$@" diff --git a/heroku.yml b/heroku.yml index 5f5fbe9c4a..b24816d76f 100644 --- a/heroku.yml +++ b/heroku.yml @@ -1,8 +1,13 @@ setup: - addons: - - plan: heroku-postgresql - as: DATABASE - + addons: + - plan: heroku-postgresql + as: DATABASE build: - docker: - web: Dockerfile \ No newline at end of file + docker: + web: Dockerfile +release: + image: web + command: + - n8n migrate:database +run: + web: n8n start