Learn how to self-host n8n on a VPS using Docker Compose, Caddy HTTPS, persistent storage, and secure encryption keys. This step-by-step guide covers installation, configuration, backups, updates, webhook testing, and troubleshooting.

CEO of Sharcon LLC
15+ years in marketing and development. Leading a team of 30+ professionals at Sharcon.
To self-host n8n with Docker, you need a Linux VPS, Docker Engine, Docker Compose, a persistent data volume, and a reverse proxy that provides HTTPS. The recommended setup runs n8n inside a Docker container, stores its data outside the container, and exposes the application through a domain secured with SSL/TLS.
This guide walks through a complete n8n Docker Compose deployment on Ubuntu, including HTTPS with Caddy, environment variables, encryption keys, persistent storage, backups, updates, and troubleshooting. By the end, you'll have an n8n instance accessible through your own domain without relying on n8n Cloud.
n8n is a workflow automation platform that connects APIs, databases, CRMs, webhooks, and AI services. Running it yourself gives you control over the infrastructure, data storage, deployment schedule, and integrations.
For this tutorial, we'll use the following stack:
| Component | Configuration | Purpose |
|---|---|---|
| VPS | Ubuntu 24.04 LTS | Hosts the application |
| Docker Engine | Current stable release | Runs containers |
| Docker Compose | Docker Compose v2 | Manages services |
| n8n | Official Docker image | Executes automation workflows |
| Database | SQLite | Stores workflows, credentials, and execution metadata |
| Reverse proxy | Caddy | Handles HTTPS and incoming requests |
| Domain | automation.example.com | Provides a stable public URL |
| Persistent storage | Docker named volume | Preserves n8n data across container replacements |
For a small deployment, a reasonable starting configuration is:
These are practical starting specifications, not official minimum requirements or performance guarantees. Resource usage depends on workflow concurrency, execution history, file processing, and the services your workflows call.
A 2 GB VPS may work for light experimentation. For production workflows involving AI APIs, CRM synchronization, scheduled jobs, or multiple simultaneous executions, 4 GB provides more breathing room.
SQLite is sufficient for getting started and for many modest workloads. PostgreSQL is worth considering when operational requirements become more demanding, particularly if you expect to scale beyond a single n8n process.
Before starting: Create a DNS A record pointing automation.example.com to your VPS public IP address. Replace this example hostname with your actual domain throughout the tutorial.
Also confirm that your VPS provider allows inbound TCP traffic on ports 80 and 443.
Connect using SSH:
ssh ubuntu@YOUR_SERVER_IP
Replace ubuntu with your VPS username.
Update installed packages:
sudo apt update && sudo apt upgrade -y
Install basic utilities:
sudo apt install -y ca-certificates curl gnupg openssl
Check your operating system:
cat /etc/os-release
The commands below assume Ubuntu 24.04 LTS. Other Linux distributions may require different package installation commands.
For a production server, use SSH key authentication, disable unnecessary services, and keep the operating system updated.
Install Docker from Docker's official Ubuntu package repository rather than relying on an outdated package supplied by the operating system.
First, remove potentially conflicting packages:
sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc
It's normal if some packages are not installed.
Create the Docker repository key directory:
sudo install -m 0755 -d /etc/apt/keyrings
Download Docker's GPG key:
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
Add the official repository:
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
Install Docker and the Compose plugin:
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Verify the installation:
sudo docker --version
sudo docker compose version
Test Docker:
sudo docker run --rm hello-world
You should see a message confirming that Docker can download and run containers.
Security note: This tutorial uses sudo docker because membership in the Docker group effectively grants root-level control over the host. Avoid adding users to that group unless you understand the implications.
The deployment will expose only SSH, HTTP, and HTTPS publicly.
If you use UFW, allow SSH before enabling the firewall:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Verify the rules:
sudo ufw status
Expected allowed ports:
| Port | Protocol | Purpose |
|---|---|---|
| 22 | TCP | SSH administration |
| 80 | TCP | HTTP and certificate validation |
| 443 | TCP | HTTPS access |
You do not need to open port 5678 publicly.
n8n listens on port 5678 inside its Docker network, while Caddy receives external requests on ports 80 and 443.
This separation reduces unnecessary exposure of the application.
Docker firewall caveat: Docker-published ports can bypass some UFW filtering behavior. The Compose configuration below publishes only Caddy's ports 80 and 443. The n8n service uses expose, not ports, so its application port is not published on the host.
If your VPS provider offers a network firewall, apply equivalent inbound restrictions there as well.
Create a dedicated directory:
sudo mkdir -p /opt/n8n
sudo chown "$USER":"$USER" /opt/n8n
cd /opt/n8n
We'll keep three configuration files here:
/opt/n8n/
├── compose.yaml
├── .env
└── Caddyfile
The .env file stores configuration values and secrets. The Compose file defines the containers. The Caddyfile controls HTTPS and reverse proxy behavior.
n8n encrypts saved credentials before storing them in its database.
Generate a random encryption key:
openssl rand -hex 32
This produces a 64-character hexadecimal string representing 32 random bytes.
Example format:
8f2c...your-generated-64-character-key...a91d
Use the actual complete output from your command, not the abbreviated example.
Keep this key permanently. Losing it can make previously saved credentials unreadable, even if you still have the database.
Run:
nano .env
Add:
N8N_HOST=automation.example.com
N8N_PROTOCOL=https
WEBHOOK_URL=https://automation.example.com/
N8N_EDITOR_BASE_URL=https://automation.example.com/
N8N_ENCRYPTION_KEY=REPLACE_WITH_YOUR_GENERATED_KEY
GENERIC_TIMEZONE=America/New_York
TZ=America/New_York
Replace the hostname and encryption key.
The timezone values above are examples. Use the appropriate IANA timezone for your deployment, such as Europe/London or America/Chicago.
Protect the file:
chmod 600 .env
Keep .env out of public repositories and shared backups that are not encrypted.
| Variable | Purpose |
|---|---|
N8N_HOST | Public hostname for n8n |
N8N_PROTOCOL | Indicates HTTPS is used externally |
WEBHOOK_URL | Public base URL for webhook endpoints |
N8N_EDITOR_BASE_URL | Public editor URL |
N8N_ENCRYPTION_KEY | Encrypts stored credentials |
GENERIC_TIMEZONE | Default timezone for workflow scheduling |
TZ | Sets the container's system timezone |
Two additional settings will be defined directly in Docker Compose:
N8N_PORT=5678 keeps n8n listening on its internal application port.N8N_PROXY_HOPS=1 tells n8n to trust forwarded proxy information from one reverse proxy.Setting N8N_PROTOCOL=https does not make the n8n container serve HTTPS directly. Caddy terminates TLS and forwards requests to n8n over the private Docker network.
Create the Compose configuration:
nano compose.yaml
Paste the following:
services:
n8n:
image: docker.n8n.io/n8nio/n8n:stable
restart: unless-stopped
environment:
- N8N_HOST=${N8N_HOST}
- N8N_PORT=5678
- N8N_PROTOCOL=${N8N_PROTOCOL}
- WEBHOOK_URL=${WEBHOOK_URL}
- N8N_EDITOR_BASE_URL=${N8N_EDITOR_BASE_URL}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_PROXY_HOPS=1
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- TZ=${TZ}
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
volumes:
- n8n_data:/home/node/.n8n
expose:
- "5678"
networks:
- automation
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- n8n
networks:
- automation
volumes:
n8n_data:
caddy_data:
caddy_config:
networks:
automation:
driver: bridge
Persistent n8n storage:
volumes:
- n8n_data:/home/node/.n8n
This stores n8n's application data in a Docker-managed volume instead of the container's writable filesystem.
For the default SQLite setup, this location contains the database and other important application files.
Removing and recreating the container does not remove the named volume unless you explicitly delete it.
No public n8n port:
expose:
- "5678"
The expose directive documents the internal port without publishing it to the VPS network interfaces.
Caddy reaches n8n through Docker's internal DNS using the service name n8n.
Automatic container restart:
restart: unless-stopped
Docker restarts the service after failures or host reboots, unless it was explicitly stopped.
Image versioning:
The stable tag is convenient for an initial installation, but it is mutable. For production deployments, replace it with a tested, explicit n8n version after verifying compatibility with your workflows and integrations.
Avoid automatically upgrading n8n without a backup and rollback plan. Database migrations can make rolling back more complicated than simply restoring an older container image.
n8n integrations frequently depend on webhooks, OAuth callbacks, and external API requests.
A stable HTTPS URL is therefore a practical requirement for most production deployments.
Caddy provides automatic TLS certificate management when your domain resolves correctly and the required ports are reachable.
Create the configuration:
nano Caddyfile
Add:
automation.example.com {
encode zstd gzip
reverse_proxy n8n:5678
}
Replace automation.example.com with your real hostname.
Caddy will:
Caddy's reverse proxy handles WebSocket upgrades, which n8n can use for editor functionality.
From the VPS, run:
getent ahostsv4 automation.example.com
The returned address should include your VPS public IPv4 address.
If the hostname has an AAAA record, ensure IPv6 also points to a reachable server configured to accept the relevant traffic. Otherwise, remove the incorrect AAAA record.
If you use a DNS proxy or CDN, certificate issuance and request routing may require additional configuration.
For the simplest initial deployment, point the hostname directly to the VPS.
From /opt/n8n, validate the Compose configuration:
sudo docker compose config --quiet
If the command exits successfully, start the containers:
sudo docker compose up -d
Docker downloads the required images and starts n8n and Caddy.
Check their status:
sudo docker compose ps
You should see both services running.
Inspect n8n startup logs:
sudo docker compose logs --tail=100 n8n
Inspect Caddy logs:
sudo docker compose logs --tail=100 caddy
Once DNS and HTTPS are working, open:
https://automation.example.com
You should reach the n8n setup screen.
Create the initial owner account and complete any setup prompts presented by your installed n8n version.
Use a strong, unique password and enable additional authentication protections supported by your deployment.
Your self-hosted n8n instance is now running.
A successful login confirms that the editor works, but it does not prove that workflows and incoming webhooks are configured correctly.
Test both before using the instance for business-critical automations.
Inside n8n:
GET.docker-test.The production webhook URL should resemble:
https://automation.example.com/webhook/docker-test
Test it:
curl -i https://automation.example.com/webhook/docker-test
A successful request should reach your workflow and produce a response.
The exact response depends on the webhook node's response settings.
Production versus test webhooks: n8n distinguishes between test webhook URLs and production webhook URLs. Test URLs are generally available while the editor is listening for a test event. Production URLs require the workflow to be active or published.
If a test URL works but the production URL does not, verify the workflow's activation state before changing your reverse proxy.
Create a simple workflow and save it.
Then restart the n8n container:
sudo docker compose restart n8n
Refresh the editor and confirm that the workflow still exists.
You can also recreate the container:
sudo docker compose up -d --force-recreate n8n
The saved workflow should remain available because the database lives in the persistent Docker volume.
This verifies basic persistence, but it is not a substitute for testing backups.
A self-hosted n8n installation contains more than workflow definitions.
Important data can include:
For the SQLite configuration in this guide, the persistent n8n volume is the primary application data source.
Stop n8n temporarily to avoid copying SQLite database files while they are being modified:
cd /opt/n8n
sudo docker compose stop n8n
Create a backup directory:
sudo mkdir -p /opt/n8n-backups
Back up the named volume:
sudo docker run --rm \
-v n8n_n8n_data:/source:ro \
-v /opt/n8n-backups:/backup \
alpine \
tar czf /backup/n8n-data-$(date +%F-%H%M%S).tar.gz -C /source .
This assumes your Compose project is named n8n, which is the default when running from /opt/n8n without an overridden project name.
Confirm the actual volume name if necessary:
sudo docker volume ls
Restart n8n:
sudo docker compose start n8n
Back up the configuration files separately:
sudo tar czf /opt/n8n-backups/n8n-config-$(date +%F-%H%M%S).tar.gz \
-C /opt/n8n compose.yaml .env Caddyfile
Because this archive contains your encryption key, treat it as sensitive secret material.
Encrypt backups before storing them outside the VPS, restrict access, and use a backup destination that is separate from the production server.
Critical detail: If you restore the database without the original N8N_ENCRYPTION_KEY, saved credentials may no longer be decryptable.
Before relying on backups, verify that you can:
A backup that has never been restored is an unverified recovery plan.
For organizations running customer-facing business automation workflows, recovery planning should be part of the deployment rather than an afterthought.
Self-hosting means you are responsible for installing security patches, reviewing version changes, and maintaining compatibility.
Before upgrading:
To download the image configured in your Compose file:
cd /opt/n8n
sudo docker compose pull n8n
Recreate n8n using that image:
sudo docker compose up -d --no-deps n8n
Check startup logs:
sudo docker compose logs --tail=100 n8n
Verify that:
When using an explicit image version, update the version in compose.yaml before pulling.
Do not assume downgrading the Docker image will reverse database migrations. If an upgrade changes the database schema, recovery may require restoring the database backup created before the upgrade.
Caddy should also receive periodic security updates, but update and verify it separately when practical.
The encryption key is one of the most consequential settings in a self-hosted deployment.
n8n uses it to protect credentials such as API tokens, OAuth credentials, and database passwords stored by the application.
The setting is:
N8N_ENCRYPTION_KEY=YOUR_PERSISTENT_SECRET
n8n can generate an encryption key and store it in its configuration directory.
That behavior can work when the configuration directory is persistent, but explicitly setting N8N_ENCRYPTION_KEY makes the secret easier to manage through a controlled deployment process.
It also reduces the risk of accidentally relying on a key stored only inside an ephemeral container.
Do not casually replace the encryption key on an existing installation.
Credentials encrypted with the previous key generally cannot be decrypted using a different one.
Changing the environment variable alone does not re-encrypt existing credentials.
If key rotation is required, follow a migration procedure compatible with your installed n8n version. Preserve the original key and a verified backup until the migration is complete.
Avoid placing secret values directly inside compose.yaml, especially if that file is tracked in Git.
The .env approach shown here keeps the value separate from the Compose definition, although it is not a full secret-management solution.
Docker Compose substitutes environment variables into the container configuration. Users with sufficient Docker or host privileges can potentially inspect those values.
For deployments with stronger compliance or security requirements, use a suitable secrets-management system and document how the application receives its key.
Most failed VPS deployments come down to networking, file permissions, missing environment variables, or persistence issues.
A 502 response from Caddy usually indicates that the reverse proxy cannot successfully connect to n8n.
Check container status:
sudo docker compose ps
Then inspect logs:
sudo docker compose logs --tail=100 n8n
sudo docker compose logs --tail=100 caddy
Verify that the Caddyfile contains:
reverse_proxy n8n:5678
Do not use localhost:5678 in Caddy's container configuration. Inside the Caddy container, localhost refers to Caddy itself, not the n8n container.
Also confirm that both containers belong to the same Docker network.
Check that your domain points to the correct VPS:
getent ahostsv4 automation.example.com
Confirm that ports 80 and 443 are reachable from the internet and that another web server is not occupying them.
Check Caddy logs:
sudo docker compose logs caddy
Common causes include incorrect DNS records, blocked ports, and conflicting reverse proxies.
Certificate issuance can also fail because of certificate authority validation errors or rate limits. The Caddy logs should provide more specific information.
Check these values in .env:
N8N_HOST=automation.example.com
N8N_PROTOCOL=https
WEBHOOK_URL=https://automation.example.com/
N8N_EDITOR_BASE_URL=https://automation.example.com/
Confirm that Compose also sets:
- N8N_PROXY_HOPS=1
After changing .env, recreate n8n:
sudo docker compose up -d --no-deps --force-recreate n8n
A regular container restart does not apply changed Compose environment variables to an existing container.
The official n8n container runs the application using a non-root user.
Problems can occur when a host directory mounted into /home/node/.n8n has incompatible ownership.
The named-volume configuration in this tutorial avoids many common bind-mount permission problems.
If you switch to a bind mount, ensure that the directory is writable by the container's application user. Do not make the directory world-writable as a shortcut.
Inspect the exact error before changing ownership or permissions.
This commonly indicates that the application is using a different encryption key from the one used to encrypt existing credentials.
Check whether the original .env was restored and whether the key was changed during deployment.
Do not generate a replacement key in an attempt to repair existing encrypted credentials.
Restore the correct key from a secure backup.
Check whether /home/node/.n8n is attached to persistent storage.
Inspect the mounted volumes:
sudo docker inspect $(sudo docker compose ps -q n8n) --format '{{json .Mounts}}'
The output should show a mount targeting /home/node/.n8n.
Also check whether you accidentally started the application under a different Compose project name, which can create a separate named volume.
Avoid running:
sudo docker compose down -v
The -v option removes Compose-managed named volumes, including the volume containing your n8n data.
A configuration such as:
ports:
- "5678:5678"
publishes n8n on the VPS network interfaces by default.
For this architecture, that is unnecessary. Let Caddy handle public requests over HTTPS while n8n remains accessible only inside the Docker network.
Containers are replaceable. Application data must survive container replacement.
Always mount persistent storage at /home/node/.n8n and verify that workflows survive recreation.
Backing up the database without preserving the matching encryption key can leave credentials unusable after restoration.
Store the key securely and include it in your recovery documentation.
TLS protects traffic between clients and the server, but it does not protect against every application vulnerability, compromised credential, or malicious workflow.
Keep n8n updated, restrict administrative access, protect server credentials, and review who can create or modify workflows.
Workflow behavior can change between n8n versions, particularly when nodes, authentication mechanisms, or integrations change.
Pin tested versions for production systems and review release notes before upgrading.
A backup stored only on the production disk may be lost during the same server failure or compromise.
Keep encrypted copies in independent storage and periodically test restoration.
Execution records can increase database size over time.
Review your n8n version's execution-data retention and pruning settings. Configure retention based on debugging needs, privacy requirements, and available storage.
Monitor disk usage:
df -h
sudo docker system df
Do not indiscriminately delete Docker volumes to recover space.
A single VPS remains a single point of failure.
Automatic container restarts help recover from individual process failures, but they do not protect against VPS outages, disk corruption, or provider-level incidents.
For critical automations, define acceptable downtime, backup frequency, and recovery procedures before going live.
The deployment above uses SQLite because it keeps the initial setup manageable.
For a single n8n instance handling a modest workload, SQLite can be a reasonable choice.
However, PostgreSQL provides a more flexible foundation for deployments with growing operational requirements.
| Consideration | SQLite | PostgreSQL |
|---|---|---|
| Initial setup | Simpler | Requires a database service |
| Separate database server | Not required | Supported |
| Database backup options | File-based or SQLite-aware backup | PostgreSQL backup and recovery tools |
| Operational complexity | Lower | Higher |
| Scaling options | More limited | Better suited to advanced architectures |
| Best fit | Small single-instance deployments | Larger or more demanding deployments |
For queue-mode architectures and distributed execution, consult the current n8n documentation for supported database and infrastructure requirements. Do not assume a SQLite deployment can be expanded simply by adding more worker containers.
If your automations synchronize customer records across HubSpot, Zoho, or other platforms, database selection should reflect execution volume, reliability requirements, and how much downtime the business can tolerate.
The same applies to CRM integrations that depend on incoming webhooks and consistent credential availability.
Before connecting real customer data or business-critical APIs, verify the following:
| Check | Expected result |
|---|---|
| DNS | Domain resolves to the intended VPS |
| HTTPS | Valid certificate and successful HTTPS connection |
| Network exposure | Only required ports are publicly reachable |
| n8n access | Owner account created and protected |
| Persistent storage | Workflows survive container recreation |
| Encryption key | Explicitly configured and securely backed up |
| Webhooks | Production webhook endpoint responds correctly |
| Timezone | Matches workflow scheduling requirements |
| Backups | Database, encryption key, and config are recoverable |
| Restore test | Successful restoration in a separate environment |
| Updates | Versioning and maintenance process documented |
| Monitoring | Container failures and disk usage can be detected |
| Access control | SSH and administrative access are restricted |
| Recovery | Server replacement procedure is documented |
For a small deployment, establish a recurring maintenance process rather than waiting for something to fail.
Every week: Check disk usage, container status, failed workflows, and whether backups completed successfully.
Every month: Review n8n and Caddy security updates, inspect execution-data growth, and confirm that unnecessary integrations and credentials have been removed.
Periodically: Restore a backup in a separate environment and verify that workflows and credentials work as expected.
The appropriate maintenance frequency depends on business impact. A workflow that sends internal notifications has different recovery requirements from one that handles customer appointments or payment-related events.
Self-hosting is a good fit when you want control over infrastructure, need custom networking, or expect to connect n8n to private systems and internal APIs.
It is also useful when workflows require specialized dependencies or deployment configurations that are easier to manage on your own server.
However, self-hosting does not eliminate operating costs. You still pay for VPS resources, backups, monitoring, administration, and any third-party APIs used by your workflows.
AI model usage, SMS messages, telephony, email delivery, and external SaaS subscriptions are generally separate from the cost of running n8n itself.
For example, an automation that receives a lead through a webhook, validates the data, creates a CRM contact, and sends a notification may involve charges from the CRM, messaging provider, or other connected services.
Self-hosting n8n controls where the orchestration runs. It does not make external services free.
Also review n8n's current licensing terms before offering hosted automation services to third parties or embedding n8n into a commercial product. Self-hosting does not automatically grant unrestricted rights for every commercial use case.
The simplest persistent setup uses the official n8n Docker image, a Docker named volume mounted at /home/node/.n8n, and a Compose file that defines the container's environment variables. For a publicly accessible installation, add a reverse proxy such as Caddy to provide HTTPS and route requests to n8n's internal port 5678. The Compose configuration in this guide includes all three components.
Yes, a 2 GB VPS can be sufficient for experimentation and light workflows, depending on their memory requirements and concurrency. For a production deployment, 4 GB RAM is a more comfortable starting point, especially when executing multiple workflows or processing larger payloads. Monitor actual memory consumption before deciding whether the server needs more resources.
Generate a random key with openssl rand -hex 32, save it as N8N_ENCRYPTION_KEY in your .env file, and reference that variable in Docker Compose. Keep the value unchanged for the lifetime of the associated encrypted credentials unless you perform a supported key migration. Back up the encryption key separately and securely alongside your database recovery materials.
Store n8n data in a persistent volume, create a verified backup, and update the configured Docker image using docker compose pull n8n followed by docker compose up -d --no-deps n8n. Recreating the container preserves the named volume. However, updates may perform database migrations, so review release notes and retain a compatible pre-upgrade backup.
No. n8n supports SQLite, which is the default database used by the configuration in this tutorial. PostgreSQL is an option for deployments that need more advanced database operations or scaling architectures. Choose the database based on workload, recovery requirements, and the deployment model rather than assuming every VPS installation needs PostgreSQL immediately.
A working n8n Docker deployment provides the infrastructure, but dependable automation also requires workflow error handling, API authentication, webhook validation, and monitoring.
Once your instance is running, start with one workflow, verify its failure behavior, and document how to recover it before adding more integrations.
If you need help connecting your self-hosted n8n instance to CRMs, APIs, or AI services, explore Sharcon's AI automation services.

CEO of Sharcon LLC
15+ years in marketing and development. Leading a team of 30+ professionals at Sharcon.