Hosting an app in a Docker container. What the heck is a reverse proxy?
One of my goals recently has been to try and automate some of my PC routines. I didn't think it was possible at all, but then Lena Shakurova wrote/spoke about n8n.io, so I decided to try it.
Now, n8n.io has no free plan as of June 2025, just a short trial period of a paid plan. But that's if you use their cloud to store your project.
But you can run a self-hosted version for free. (Well, you still need some investment, but we'll get to that later.)
What n8n already has in their docs when it comes to deployment
- There are some instructions on server setup listed in the official docs.
- There is a repo where you can download a self-hosted AI starter kit.
I found no simple instructions for setting up n8n without docker compose, that I'm not yet fully familiar with, and on just some random web server, not Digital Ocean or AWS.
So I thought that I would figure it out, learning basics of Docker along the way and refreshing my memory of what a reverse proxy actually is because I'm going to need it.
Important concepts
Ok, here's what I needed to learn/know/understand in order to run n8n on my own server:
Docker
Nana Janashia made an incredibly helpful intro to Docker (just take a moment to look at the view count, it's more than many music and standup videos!). There was all I needed to know: basically, a Docker container is a thing that uses your operating system (Linux for vast majority of servers) and puts an application layer on top of it. You have to get a Docker image, which contains the setup of this application layer, and create a container where it will be installed and run.
Nginx as a reverse proxy
I had already known something about nginx. Just in case: it's a web server, i.e. a program that runs on the remote server and answers HTTP(S) requests. I know that there are configuration files you have to create in the right places. But what was also important to refresh in my memory was that nginx acts as a reverse proxy, and this is relevant for serving n8n (or any other app?).
A "normal" proxy is what you could use on your own computer to access some web resources. It sits between your computer and the web server you're trying to access.
A reverse proxy does the thing in the opposite direction: it receives a request on some port that is exposed to the internet, and then forwards it to (in my case) an app that is listening to some other port. That other port is not exposed to the internet. This way nginx handles all incoming requests, improving security.
Setting up
Install and start Docker and NGINX
sudo apt update
sudo apt install docker.io
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER
sudo apt install nginx
sudo systemctl start nginx
sudo systemctl enable nginx
Make sure you keep nginx and Docker up to date in your system.
If you're running an older version of Ubuntu, you might need to install nginx manually from official nginx repo. For example, I was running 22.04 LTS and had nginx 1.18.0, which triggered pesky errors. I spent quite a lot of time debugging them before realizing that Ubuntu 22.04 thought that nginx 1.18.0 was perfectly okay. And it wasn't. Upgrading nginx fixed some of the issues right away.
Set up a certificate for your domain
This is the investment bit :) n8n will rightfully complain without https, so we need an SSL certificate.
(Even if you ignore n8n's complaints, you won't be able to connect e.g. your Google drive for n8n to access your files unless HTTPS connection can be established.)
The investment is not in a certificate as it can be generated for free, but in a domain name you will generate the certificate for.
So, once the domain name is set up to forward to your server, let's generate a Let's Encrypt certificate.
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com -d www.your-domain.com
Certbot will update your NGINX config to use HTTPS and redirect HTTP to HTTPS.
Create a Docker Volume for n8n Data
sudo docker volume create n8n_data
n8n will use it as persistent storage so that your data remains intact even if you delete the container
Change owner for the volume (ensures that all files and folders in your n8n Docker volume are owned by the same user that n8n runs as inside the container):
sudo chown -R 1000:1000 /var/lib/docker/volumes/n8n_data/_data
Change permissions for the config file (not strictly necessary for the version of n8n I'm using here but will trigger a warning in logs):
sudo chmod 600 /var/lib/docker/volumes/n8n_data/_data/config
Create .env file
Since we'll be using n8n to connect to some external services, it will need some environment variables. The easiest way to define them is in a file called .env that you can put in basically any directory.
It will contain following variables:
N8N_EXTERNAL_URL=https://your.domain.com/
WEBHOOK_URL=https://your.domain.com/
N8N_PROTOCOL=https
N8N_RUNNERS_ENABLED=true
N8N_TRUST_PROXY=true
N8N_PROXY_HOPS=1
The last two lines are determined by the fact that we are using a reverse proxy. We have to tell n8n to trust the proxy and also specify that we only have one reverse proxy. Otherwise you might encounter an error.
N8N_RUNNERS_ENABLED=true is not strictly necessary for the version of n8n I'm using here, but it ensures forwards compatibility.
Start the n8n Container
As Nana Janashia explains in her Docker crash course, you can either first pull the image from a remote registry and then run it, but you can just say docker run and, if there is no local image, it will pull it for you. Also, you don't have to specify where to pull it from, because n8n.io has images uploaded to the official registry, Docker Hub.
sudo docker run -d --restart unless-stopped --name n8n --env-file ~/.env -p 5678:5678 -v n8n_data:/home/node/.n8n docker.n8n.io/n8nio/n8n:1.98.0
Let's break down the parameters of docker run:
-d: Run container in detached mode (in the background)--restart unless-stopped: Automatically restart the container unless explicitly stopped--name n8n: Name the container "n8n" for easier reference. It's much easier to use the name, not the ID.--env-file ~/.env: Use the specified.envfile (this example assumes it was created in the home directory)-p 5678:5678: Map port 5678 on the host to port 5678 in the container. As Nana says, it's good practice to preserve the port number that the app is using by default. Note that when I say "map port 5678" on the host, I meanlocalhoston my webserver. It will be accessed internally by nginx, not from the outside using browsers!-v n8n_data:/home/node/.n8n: Attach the volume for data persistence that we created earlier.docker.n8n.io/n8nio/n8n:1.98.0: The Docker image to use (official n8n image from Docker Hub). I'm using the tag1.98.0here, you can use a different one. Using tagslatestorstableis fine, but could lead to unexpected changes in behavior.
Configure NGINX as a Reverse Proxy
Edit or create the NGINX configuration file (you can put anything instead of n8n, of course):
sudo nano /etc/nginx/sites-available/n8n
Directives that must be in the configuration file for nginx to forward requests to an app in a container are:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name your-domain.com www.your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://localhost:5678;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Origin https://your-domain.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
add_header Access-Control-Allow-Origin "https://your.domain.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Origin, Accept" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Max-Age 86400 always; }
}
Some directives must be there for any app, some are required by n8n specifically and/or fix connectivity issues (see this discussion, this discussion).
Note that the location / block is where we set up this reverse proxying.
proxy_pass tells nginx to forward incoming requests that came to port 443
(standard port for https) to port 5678 on localhost.
Which is the port that n8n is listening to.
Enable the NGINX Configuration
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
Now this part used to confuse me a bit, but after a while it makes sense.
You create a site configuration in sites-available and it just sits there.
But when you decide to actually use this site, you enable it by creating a symlink (like a shortcut, so you don't the file itself) in sites-enabled.
nginx only tries to serve sites that are enabled, not all sites that are simply available.
Check NGINX Configuration
This will return errors if something is wrong with the config file:
sudo nginx -t
Open Port 443 in the Firewall
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
Remember that we do not open port 5678 because it's for internal use.
Restart NGINX
sudo systemctl restart nginx`
Test Your Setup
Access n8n via your domain. You should see the n8n login or setup page.
Diagnostics: in case something doesn't work
Check which processes are listening on ports 443 and 5678
sudo ss -tulnp | grep 443
sudo ss -tulnp | grep 5678
It should say "nginx" for port 443 and "docker-proxy" (that's n8n) for port 5678.
Check NGINX configuration for errors
sudo nginx -t
Tests NGINX configuration syntax.
Check NGINX service status
sudo systemctl status nginx
Shows if NGINX is running and recent logs.
Check NGINX error messages in real time
sudo tail -f /var/log/nginx/error.log
Check running Docker containers
sudo docker ps
Check Docker container logs
sudo docker logs n8n
Check firewall rules
sudo ufw status
Port 443 should be open.
Check Browser Console and Network Tab
Open your browser’s developer tools and look for:
- JavaScript errors (often CORS or connection-related).
- Failed network requests (status codes like 502, 504, or CORS errors).