Most n8n self-hosting tutorials stop at `docker compose up`. That gets you a running instance, but it leaves a real gap: where do your workflows live? How do you upgrade without losing them? How do you roll back a bad change? Treat your n8n server like any other application — workflows in Git, infrastructure in Docker Compose, deploys triggered from `main` — and self-hosting becomes a maintainable habit instead of a fragile pet project.

This guide walks through standing up n8n on a single VPS with Docker, putting it behind HTTPS, and wiring it to a [DeployHQ](https://www.deployhq.com) project so every change to your workflow repo updates the server. By the end you'll have a production-ready instance with backups, version control, and one-click rollback when something goes wrong.

## What you'll build

- **n8n** running in Docker on a single Ubuntu VPS
- **Postgres** alongside it as the data store (not SQLite — more on that below)
- **Nginx + Let's Encrypt** in front for HTTPS on your own domain
- **A Git repository** that holds the `docker-compose.yml`, environment template, and an exportable JSON copy of every workflow
- **DeployHQ** wiring the repo to the server, so a `git push` to `main` reconfigures the stack and re-imports workflows

By the end you'll be able to develop a workflow locally (or on a staging instance), export it, commit, push, and watch [DeployHQ](https://www.deployhq.com) deploy the change with rollback available if it misbehaves.

## Why self-host n8n at all?

n8n Cloud is good. So why bother with a VPS?

- **Cost predictability.** A $5-10/month VPS handles thousands of runs per day. Cloud pricing scales by execution.
- **Data stays on your infrastructure.** Webhooks, credentials, and execution logs never leave the box.
- **Custom nodes.** You can `npm install` community nodes the cloud version doesn't ship.
- **No execution limits.** Long-running flows that hit cloud step caps just run.

The trade-off is operational ownership — you patch the OS, renew the certificate, watch the disk. Most of that is one-time setup. The rest of this guide is the one-time setup, done properly.

## Prerequisites

- A VPS with Ubuntu 22.04 or 24.04 and SSH access — any of Hetzner, DigitalOcean, Vultr, Linode work fine
- A domain you control with an A record pointing at the VPS IP
- A [DeployHQ](https://www.deployhq.com) account (free trial — sign-up link at the end)
- A GitHub or GitLab repository
- Docker installed locally for testing

## Step 1: Provision the VPS

SSH in as root, install Docker, and set up a deploy user:

```
ssh root@your-vps-ip
curl -fsSL https://get.docker.com | sh

adduser deploy
usermod -aG docker deploy
mkdir -p /home/deploy/n8n
chown deploy:deploy /home/deploy/n8n
```

From your local machine, copy your SSH key to the deploy user:

```
ssh-copy-id deploy@your-vps-ip
```

Confirm passwordless login works:

```
ssh deploy@your-vps-ip "docker --version"
```

You should see the installed Docker version. That's the account [DeployHQ](https://www.deployhq.com) will use.

## Step 2: The n8n Docker Compose stack

In your local repo, create `docker-compose.yml`:

```
services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

  n8n:
    image: n8nio/n8n:1.74.0 # pin to a specific tag — don't use :latest in prod
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    ports:
      - "127.0.0.1:5678:5678" # localhost only — Nginx will proxy
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_DATABASE: ${POSTGRES_DB}
      DB_POSTGRESDB_USER: ${POSTGRES_USER}
      DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
      N8N_HOST: ${N8N_HOST}
      N8N_PROTOCOL: https
      N8N_PORT: 5678
      WEBHOOK_URL: https://${N8N_HOST}/
      GENERIC_TIMEZONE: ${TIMEZONE:-Europe/London}
      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
    volumes:
      - n8n_data:/home/node/.n8n
      - ./workflows:/workflows:ro

volumes:
  postgres_data:
  n8n_data:
```

Four things matter here:

1. **Postgres, not SQLite.** SQLite is fine for kicking the tires. For anything you care about, Postgres handles concurrent executions, backups, and growth without complaint.
2. **`N8N_ENCRYPTION_KEY` is sacred.** It encrypts the credentials n8n stores in the database. Change it and every credential breaks — you'll have to re-enter every API key, OAuth token, and SSH credential. Generate it once with `openssl rand -hex 32`, store it in [DeployHQ](https://www.deployhq.com) as a config file, and never lose it.
3. **`WEBHOOK_URL` must be your public HTTPS URL.** n8n bakes this into the URLs it gives webhook senders. If it's wrong, Stripe / GitHub / Slack will hit a dead address.
4. **Port 5678 is bound to `127.0.0.1`.** Don't expose n8n directly to the internet — put it behind Nginx with TLS. Anyone hitting port 5678 from outside gets nothing.

Create `.env.example` to commit to the repo (without secrets):

```
POSTGRES_USER=n8n
POSTGRES_PASSWORD=
POSTGRES_DB=n8n
N8N_HOST=workflows.yourdomain.com
N8N_ENCRYPTION_KEY=
TIMEZONE=Europe/London
```

The real `.env` lives in DeployHQ's server config — never in the repo.

## Step 3: Put Nginx in front with Let's Encrypt

On the VPS, install Nginx and Certbot:

```
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx
```

Add a server block at `/etc/nginx/sites-available/n8n`:

```
server {
    server_name workflows.yourdomain.com;

    client_max_body_size 50M;

    location / {
        proxy_pass http://127.0.0.1:5678;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 3600; # long-running executions
        proxy_send_timeout 3600;
    }

    listen 80;
}
```

Enable it and run Certbot:

```
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d workflows.yourdomain.com
```

The `proxy_read_timeout 3600` is important. n8n executions can run for minutes when a workflow waits on an external API — without that, Nginx kills the connection and the run appears to fail.

## Step 4: First run

Back on your local machine, commit the repo and push it to GitHub:

```
git init && git add . && git commit -m "Initial n8n stack"
git remote add origin git@github.com:you/n8n-stack.git
git push -u origin main
```

For the very first deploy, SSH into the VPS, copy the `.env` over (DeployHQ will manage it from here on), and bring the stack up manually:

```
ssh deploy@your-vps-ip
cd /home/deploy/n8n
# (DeployHQ will populate docker-compose.yml on first deploy — for now scp it manually)
docker compose up -d
```

Visit `https://workflows.yourdomain.com`, create the admin account, and you're live.

## Step 5: Wire up Git-based deploys with DeployHQ

This is the part most n8n tutorials skip. In DeployHQ:

1. **Create a new project** and point it at your repo. [DeployHQ](https://www.deployhq.com) supports [deploying directly from a GitHub repo to your server](https://www.deployhq.com/deploy-from-github) without you wiring webhooks by hand.
2. **Add the VPS as a server.** Username `deploy`, port 22, deployment path `/home/deploy/n8n`. Use the SSH key you authorised in Step 1.
3. **Add the `.env` as a config file.** In the server's config-files section, create `/home/deploy/n8n/.env` and paste the real values (Postgres password, `N8N_ENCRYPTION_KEY`, `N8N_HOST`). [DeployHQ](https://www.deployhq.com) writes this file before every deploy — your secrets stay out of Git.
4. **Configure the deploy SSH command:** `bash
cd /home/deploy/n8n && \
docker compose pull && \
docker compose up -d && \
docker compose exec -T n8n n8n import:workflow --input=/workflows/ --separate
`This pulls any new n8n image version, recreates containers with the new Compose config, and re-imports every workflow JSON from the mounted `/workflows` directory.
5. **Enable auto-deploy on push to `main`.** Every merge now triggers a deploy.

Push a no-op change to trigger the first run. Watch the [DeployHQ](https://www.deployhq.com) log — you'll see the repo transferred, the env file written, the SSH commands executed. The site shouldn't blink: `docker compose up -d` only recreates containers if something actually changed.

If a deploy ever breaks something, [DeployHQ's one-click rollback](https://www.deployhq.com/features/one-click-rollback) restores the previous repo state and re-runs the deploy command — a single button, no manual Compose juggling.

## Step 6: Workflows as code

This is the payoff for everything above. Instead of the workflow is whatever I last clicked in the UI, your repo becomes the source of truth.

The flow:

1. **Develop locally or on a staging instance.** A spare n8n container on your laptop works fine.
2. **Export the workflow.** In the n8n UI: open the workflow → menu → Download → save the JSON to `workflows/your-workflow-name.json` in your repo.
3. **Commit and push.** `bash
git add workflows/your-workflow-name.json
git commit -m "Add: Slack-to-Sheets sync workflow"
git push
`
4. **DeployHQ deploys.** The deploy command runs `n8n import:workflow --input=/workflows/ --separate`, which upserts the workflow into the database. Existing workflows with the same ID are updated; new ones are created.

A few practical notes:

- **One JSON file per workflow.** The `--separate` flag tells n8n's importer to treat each file independently. Easier diffs in PRs.
- **Credentials are not in the JSON.** n8n stores them encrypted in the database, referenced by ID. Set up credentials once in the UI; the workflow JSON just references them. This is why `N8N_ENCRYPTION_KEY` matters — drop it and the references become unusable.
- **The UI is now read-only in your head.** Anyone making changes in production goes back to staging, exports, commits. Drift kills you otherwise.

The same Compose pattern works for sidecar services — drop a Python agent or a Postgres backup container into the same `docker-compose.yml`, and [DeployHQ](https://www.deployhq.com) deploys all of it together.

## Step 7: Upgrade n8n safely

n8n releases often. The temptation is to use `n8nio/n8n:latest` and let it ride. Don't.

With a pinned tag, upgrading becomes a one-line PR:

```
- image: n8nio/n8n:1.74.0
+ image: n8nio/n8n:1.78.0
```

Push, [DeployHQ](https://www.deployhq.com) deploys, the new image is pulled, the container recreated. If anything misbehaves, hit rollback and the previous tag is back in seconds. You get a git history of every n8n version that has ever run in production — handy when a workflow stops working and you need to find what changed.

Two upgrade hygiene tips:

- **Read the changelog** before bumping a major. n8n occasionally deprecates nodes or changes execution semantics.
- **Snapshot Postgres before a major.** A `docker compose exec postgres pg_dump ...` to a file in a backup directory takes seconds and saves hours.

For deploys in general, [DeployHQ](https://www.deployhq.com) runs [zero downtime deployments](https://www.deployhq.com/features/zero-downtime-deployments) by default — when you eventually move to a multi-server setup, the rollout pattern stays the same.

## Step 8: Backups

Two things to back up:

**The Postgres database.** Everything important — workflows, credentials, execution history — lives here.

```
docker compose exec -T postgres \
  pg_dump -U n8n -d n8n --no-owner --clean \
  > backup-$(date +%F).sql
```

Drop a small script in your repo at `scripts/backup.sh` that runs this and uploads the file to S3 (or B2, or any object store). Cron it nightly on the VPS.

**The `n8n_data` volume.** Holds binary attachments and custom nodes. Tar it once a week to the same store:

```
docker run --rm -v n8n_n8n_data:/data -v $PWD:/backup alpine \
  tar czf /backup/n8n-data-$(date +%F).tar.gz -C /data .
```

The workflows themselves are already in Git, so you don't need to back those up — that's the whole point of the workflows-as-code pattern.

## Where to go from here

The pillar above gets you a single-node, production-ready n8n instance with version control and rollback. From here:

- **Drop a custom AI service next to n8n.** The same `docker-compose.yml` handles a sidecar Python or Node service — expose it at `http://agent:8000` and have any n8n workflow hit it as a webhook. Same VPS, same deploy pipeline, no extra infrastructure.
- **Compare with other self-hosted agent platforms.** If n8n's node-based model isn't the right fit, look at alternatives covered on the blog: [Paperclip as a self-hosted agent orchestrator](https://www.deployhq.com/blog/self-host-paperclip-vps-docker-deployhq), [OpenClaw as a self-hosted AI assistant with a Skills plugin system](https://www.deployhq.com/blog/deploy-configure-openclaw-vps), and [Hermes Agent for self-improving agents on a VPS](https://www.deployhq.com/blog/deploy-hermes-agent-vps).
- **Run agents inside CI/CD.** For a different shape of the same problem — agents that act on your repository — see [how AI agents fit into CI/CD pipelines from GitHub issue to production deploy](https://www.deployhq.com/blog/ai-agents-cicd-pipelines-github-issue-to-production-deploy).
- **Drive deploys from a terminal agent.** If you want Claude Code, Cursor, or Codex to trigger n8n redeploys for you, [the](https://www.deployhq.com/blog/deployhq-cli-deploy-from-terminal)[DeployHQ](https://www.deployhq.com) CLI exposes a deployment trigger your AI agent can call.

* * *

Ready to put n8n on infrastructure you control, with a Git-driven deploy pipeline behind it? [Start a free](https://www.deployhq.com/signup)[DeployHQ](https://www.deployhq.com) trial and wire your first push-to-deploy n8n stack in under twenty minutes.

Questions? Email us at **[support@deployhq.com](mailto:support@deployhq.com)** or find us on X at [@deployhq](https://x.com/deployhq).

