Skip to content

BitcoinDistrict/n8n-deploy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

n8n on Existing Server — Ansible + SOPS + Compose + Actions

Goal: Create a repo that can deploy/update an n8n stack (with Caddy proxy and Watchtower) to an existing Ubuntu 24.04 server via GitHub Actions, using Ansible provisioning and SOPS (age)-encrypted secrets committed to the repo.

TL;DR Flow

  1. Commit this project skeleton.
  2. Generate an age keypair; commit the public key into .sops.yaml.
  3. Encrypt .env.sops with SOPS (no plaintext secrets in Git).
  4. Add GitHub Secrets: AGE_PRIVATE_KEY, SSH_PRIVATE_KEY, SSH_HOST, SSH_USER (and optional SSH_PORT).
  5. Push to main → CI runs Ansible → Docker Compose up → n8n available at your domain.

Requirements

  • You already have:

    • A reachable Ubuntu 24.04 server (public IP).
    • SSH access using a private key (add this as a GitHub Secret).
    • A DNS record pointing your chosen hostname (e.g., n8n.example.org) to the server (optional but recommended).
    • A deploy user with passwordless sudo access configured (see Server Setup below).
  • The repo will:

    • Optimize system performance for tiny Ubuntu VMs (ZRAM/swap, kernel tuning, systemd optimization),
    • Install Docker Engine + Compose plugin,
    • Place Caddyfile, docker-compose.yml, and .env (from SOPS),
    • Start caddy, n8n, watchtower.

Repository Layout

Create these files exactly:

.
├── ansible/
│   ├── inventory.ini                # generated by CI (templated from GH secrets)
│   └── playbook.yml
├── app/
│   ├── Caddyfile
│   └── workflows/                   # (optional) store exported n8n JSON here
├── ansible/roles/
│   ├── n8n/
│   │   ├── files/
│   │   │   └── docker-compose.yml
│   │   └── templates/
│   │       └── .env.sops            # SOPS-encrypted .env (commit encrypted only)
│   └── performance_optimization/    # System performance optimization for tiny VMs
│       ├── tasks/
│       ├── handlers/
│       ├── templates/
│       └── defaults/
├── .github/workflows/
│   └── deploy.yml
├── .sops.yaml
└── README.md                        # this file

File: ansible/playbook.yml

- hosts: n8n
  become: true
  gather_facts: true

  pre_tasks:
    - name: Ensure apt cache is fresh
      apt:
        update_cache: true
      changed_when: false

  tasks:
    - name: Install base packages
      apt:
        name:
          - curl
          - git
          - ufw
          - ca-certificates
          - gnupg
        state: present

    - name: Configure UFW (allow SSH, HTTP, HTTPS)
      ufw:
        state: enabled
        policy: deny
      notify: Reload UFW
    - ufw:
        rule: allow
        name: OpenSSH
    - ufw:
        rule: allow
        port: "80"
        proto: tcp
    - ufw:
        rule: allow
        port: "443"
        proto: tcp

    - name: Install Docker Engine + Compose plugin
      shell: |
        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
        . /etc/os-release
        echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $UBUNTU_CODENAME stable"           | tee /etc/apt/sources.list.d/docker.list > /dev/null
        apt-get update -y
        apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
      args: { executable: /bin/bash }

    - name: Create app directory
      file:
        path: /opt/n8n
        state: directory
        mode: "0755"

    - name: Create local_files directory (for n8n mounts)
      file:
        path: /opt/n8n/local_files
        state: directory
        mode: "0755"

    - name: Place docker-compose.yml
      copy:
        src: roles/n8n/files/docker-compose.yml
        dest: /opt/n8n/docker-compose.yml
        mode: "0644"

    - name: Place Caddyfile
      copy:
        src: ../app/Caddyfile
        dest: /opt/n8n/Caddyfile
        mode: "0644"

    - name: Decrypt .env.sops (SOPS)
      community.sops.sops_decrypt:
        path: roles/n8n/templates/.env.sops
      register: envfile

    - name: Write .env
      copy:
        content: "{{ envfile.content }}"
        dest: /opt/n8n/.env
        mode: "0600"

    - name: Start/Update stack
      community.docker.docker_compose_v2:
        project_src: /opt/n8n
        state: present

  handlers:
    - name: Reload UFW
      command: ufw reload

File: ansible/roles/n8n/files/docker-compose.yml

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - n8n

  n8n:
    image: docker.n8n.io/n8nio/n8n:latest
    restart: unless-stopped
    env_file: .env
    environment:
      - N8N_PROTOCOL=https
      - N8N_SECURE_COOKIE=true
      - NODE_ENV=production
      - GENERIC_TIMEZONE=America/New_York
    volumes:
      - n8n_data:/home/node/.n8n
      - ./local_files:/files

  watchtower:
    image: containrrr/watchtower
    restart: unless-stopped
    command: --cleanup --interval 3600 caddy n8n
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

volumes:
  caddy_data:
  caddy_config:
  n8n_data:

File: app/Caddyfile

n8n.example.org {
  encode gzip
  reverse_proxy n8n:5678
}

File: .sops.yaml

creation_rules:
  - path_regex: ansible/roles/n8n/templates/\.env\.sops$
    key_groups:
      - age:
          - age1replace_this_with_your_public_key

File: ansible/roles/n8n/templates/.env.sops

Example plaintext (encrypt with SOPS before commit):

N8N_HOST=n8n.example.org
N8N_PORT=5678
N8N_ENCRYPTION_KEY=use-a-32-64-char-random-string
N8N_BASIC_AUTH_ACTIVE=true
N8N_BASIC_AUTH_USER=trey
N8N_BASIC_AUTH_PASSWORD=long-random-password

Encrypt:

sops --encrypt --in-place ansible/roles/n8n/templates/.env.sops

File: .github/workflows/deploy.yml

name: Deploy n8n to Existing Server

on:
  push:
    branches: [ "main" ]
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install Ansible + collections
        run: |
          python -m pip install --upgrade pip
          pip install ansible
          ansible-galaxy collection install community.docker community.sops

      - name: Install age and sops
        run: |
          sudo apt-get update
          sudo apt-get install -y age
          
          # Install sops from GitHub releases
          curl -LO https://github.com/getsops/sops/releases/download/v3.10.2/sops-v3.10.2.linux.amd64
          sudo mv sops-v3.10.2.linux.amd64 /usr/local/bin/sops
          sudo chmod +x /usr/local/bin/sops
          
          # Verify installation
          sops --version

      - name: Configure SOPS age key
        run: |
          mkdir -p ~/.config/sops/age
          echo "${{ secrets.AGE_PRIVATE_KEY }}" > ~/.config/sops/age/keys.txt
          chmod 600 ~/.config/sops/age/keys.txt

      - name: Start ssh-agent and add deploy key
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: |
            ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Add server to known_hosts
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan -p "${{ secrets.SSH_PORT || '22' }}" "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts

      - name: Write Ansible inventory from secrets
        run: |
          echo "[n8n]" > ansible/inventory.ini
          echo "server ansible_host=${{ secrets.SSH_HOST }} ansible_user=${{ secrets.SSH_USER }} ansible_port=${{ secrets.SSH_PORT || '22' }}" >> ansible/inventory.ini

      - name: Run Ansible playbook
        working-directory: ansible
        env:
          ANSIBLE_HOST_KEY_CHECKING: "False"
        run: |
          ansible-playbook -i inventory.ini playbook.yml -v

GitHub Secrets to Configure

  • SSH_HOST: server IP or DNS
  • SSH_USER: SSH user (e.g., root or deploy)
  • SSH_PORT: optional (default 22)
  • SSH_PRIVATE_KEY: private key contents
  • AGE_PRIVATE_KEY: age private key (for decrypting .env.sops)

Server Setup

Before running the deployment, ensure your Ubuntu 24.04 server is properly configured:

1. Create Deploy User

# Run these commands as root on your server
adduser --disabled-password --gecos "" deploy
usermod -aG sudo deploy

2. Configure Passwordless Sudo

# Allow deploy user to use sudo without password
echo "deploy ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/deploy
sudo chmod 440 /etc/sudoers.d/deploy

# Verify configuration
sudo visudo -c

3. Set Up SSH Key Access

# Create SSH directory for deploy user
mkdir -p /home/deploy/.ssh

# Add your public key (replace with your actual public key)
echo "your-ssh-public-key-here" > /home/deploy/.ssh/authorized_keys

# Set proper permissions
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

4. Configure SSH Security

Edit /etc/ssh/sshd_config:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AllowUsers deploy

Then restart SSH:

systemctl restart sshd

5. Test Connection

# Test from your local machine
ssh deploy@your-server-ip "sudo whoami"
# Should return "root" without password prompt

Setup Instructions

Quick Setup (Recommended)

Run the automated setup script:

# Install age from Ubuntu repositories
sudo apt-get update
sudo apt-get install age

# Install sops from GitHub releases (not available in Ubuntu repos)
curl -LO https://github.com/getsops/sops/releases/download/v3.10.2/sops-v3.10.2.linux.amd64
sudo mv sops-v3.10.2.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops

# Run the setup script
./setup.sh

The script will guide you through:

  1. Generating an age keypair
  2. Updating .sops.yaml with your public key
  3. Encrypting the environment file
  4. Showing you what GitHub secrets to configure

Manual Setup

1. Generate Age Keypair

# Install age if not already installed
sudo apt-get install age

# Generate keypair
age-keygen -o key.txt

# The public key will be displayed, copy it to .sops.yaml
# Keep the private key safe for GitHub secrets

2. Update Configuration Files

  1. Edit app/Caddyfile - replace n8n.example.org with your domain
  2. Edit ansible/roles/n8n/templates/.env.sops - update the environment variables
  3. Edit .sops.yaml - replace the public key with your generated age public key

3. Encrypt Secrets

# Encrypt the environment file
sops --encrypt --in-place ansible/roles/n8n/templates/.env.sops

4. Configure GitHub Secrets

In your GitHub repository settings, add these secrets:

  • SSH_HOST: Your server's IP address or hostname
  • SSH_USER: SSH username (e.g., root or deploy)
  • SSH_PORT: SSH port (optional, defaults to 22)
  • SSH_PRIVATE_KEY: Your SSH private key content
  • AGE_PRIVATE_KEY: Your age private key content (from key.txt)

5. Deploy

git add .
git commit -m "Initial n8n deployment setup"
git push origin main

The GitHub Action will automatically run and deploy n8n to your server.


First Deploy

git push origin main

Action runs, provisions server, deploys stack.
Visit https://n8n.example.org.


Post-Deployment

After successful deployment:

  1. Visit your n8n instance at your configured domain
  2. Complete the initial setup wizard
  3. Configure workflows as needed
  4. Export workflows to app/workflows/ directory for version control

Updating

To update the deployment:

  1. Make changes to configuration files
  2. If updating secrets, re-encrypt with SOPS
  3. Commit and push changes
  4. GitHub Actions will automatically redeploy

Performance Optimization

The playbook automatically includes performance optimizations for tiny Ubuntu VMs. This feature can be controlled with the enable_performance_optimization variable (default: true).

What Gets Optimized

  1. Swap Management

    • ZRAM for VMs < 2GB RAM (compressed, in-memory)
    • Swapfile for larger VMs (persistent, disk-based)
    • Conservative swappiness settings (reduces swap usage)
  2. Kernel/Network Tuning

    • BBR TCP congestion control (if available)
    • Optimized TCP buffer sizes and connection limits
    • Fair queueing (fq) traffic scheduler
  3. Systemd/Journal Optimization

    • Limited journal size (100MB max) to prevent disk issues
    • systemd-oomd for memory pressure handling
    • Faster boot/shutdown timeouts
  4. Filesystem/I/O

    • Increased file descriptor limits (65536)
    • Automatic I/O scheduler optimization
    • Optional noatime mounts for read-heavy servers

Configuration

To disable performance optimization, add to your playbook variables:

# In ansible/playbook.yml
vars:
  enable_performance_optimization: false

To run only performance optimization:

ansible-playbook -i inventory.ini playbook.yml --tags "performance"

Verification

After deployment, verify optimizations:

# Test performance optimizations only
ansible-playbook -i inventory.ini test-performance.yml

# Or check manually on the server
swapon --show                              # Check swap configuration
sysctl net.ipv4.tcp_congestion_control     # Check TCP optimization
ulimit -n                                  # Check file descriptor limits
journalctl --disk-usage                    # Check journal size

The optimization is Ubuntu-specific and includes safety checks to prevent running on incompatible systems.


Troubleshooting

Common Issues

  1. Domain not resolving: Ensure DNS is properly configured
  2. SSL certificate issues: Caddy handles automatic HTTPS, ensure domain points to server
  3. Permission errors: Check that the deploy user has proper sudo access
  4. Docker issues: Verify Ubuntu 24.04 compatibility

Logs

Check deployment logs in GitHub Actions or SSH to server:

# Check container logs
docker logs n8n-n8n-1
docker logs n8n-caddy-1

# Check Docker Compose status
cd /opt/n8n
docker compose ps

About

n8n automation deployment

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published