Getting Joplin Server to work behind NGINX

I spent multiple days trying to get Joplin Server to work with my existing NGINX instance. As usual, most tutorials don’t quite match the setup that I have. And I’m here to share the love.

The setup:

A Debian virtual machine running NGINX (as a service) all port 80 and 443 traffic in my house is forwarded to this IP address

The same virtual machine running Joplin Server, but in a docker container with a compose file

I want https to connect to the server externally, and http traffic is fine internally. Especially because it’s all being routed internally between the host (VM) and docker network stack. So it’s not even physically leaving my server rack unencrypted on a cable.

I will be using an existing wildcard certificate for the SSL handshake.

The problem:

I ran into a few of them trying different things.

Error 502 bad gateway

Error 404 not found. Generally trying to switch between locations of / and /joplin/. Loading sub.mydomain.tld or sub.mydomain.tld/joplin in a browser wasn’t resolving, or was showing the nginx homepage only. Adding /joplin/ didn’t work either.

Trying to access http://192.168.xx.xx at some points didn’t work either. With http or https, with /joplin/ or at the root location, with :443 :22300 and :80.

The Solution:

Joplin seems to be pretty particular about both the server side and client side. I guess this is probably a feature for security, but logs are not very detailed. So things need to match on both, or connections will fail with almost no useful information for debugging. Even wireshark didn’t yield much. I mostly just saw TLS switching between 1.2 and 1.3, a handshake, then doing it all over again every 15 seconds or so.

My docker-compose.yaml is the default provided by the developer. Usernames and passwords have been changed, as well as the APP_BASE_URL. Literally nothing else is changed.

# This is a sample docker-compose file that can be used to run Joplin Server
# along with a PostgreSQL server.
#
# Update the following fields in the stanza below:
#
# POSTGRES_USER
# POSTGRES_PASSWORD
# APP_BASE_URL
#
# APP_BASE_URL: This is the base public URL where the service will be running.
#       - If Joplin Server needs to be accessible over the internet, configure APP_BASE_URL as follows: https://example.com/joplin.
#       - If Joplin Server does not need to be accessible over the internet, set the the APP_BASE_URL to your server's hostname.
#     For Example: http://[hostname]:22300. The base URL can include the port.
# APP_PORT: The local port on which the Docker container will listen.
#       - This would typically be mapped to port to 443 (TLS) with a reverse proxy.
#       - If Joplin Server does not need to be accessible over the internet, the port can be mapped to 22300.
version: '3'
services:
    db:
        image: postgres:16
        volumes:
            - ./data/postgres:/var/lib/postgresql/data
        ports:
            - "5432:5432"
        restart: unless-stopped
        environment:
            - POSTGRES_PASSWORD=CHANGE-ME
            - POSTGRES_USER=CHANGE-ME-TOO
            - POSTGRES_DB=joplindb
    app:
        image: joplin/server:latest
        depends_on:
            - db
        ports:
            - "22300:22300"
        restart: unless-stopped
        environment:
            - APP_PORT=22300
            - APP_BASE_URL=https://(my-subdomain).fitib.us/joplin
            - DB_CLIENT=pg
            - POSTGRES_PASSWORD=CHANGE-ME
            - POSTGRES_DATABASE=joplindb
            - POSTGRES_USER=CHANGE-ME-TOO
            - POSTGRES_PORT=5432
            - POSTGRES_HOST=db

Create /etc/nginx/sites-available/subdomain.mydomain.tld.conf for each, then symbolic link to sites-enabled with. Which is I think the best and only practice, according to the grey beards.

/etc/nginx/sites-available/$ ln -s ./my-subdomain.fitib.us.conf ../sites-enabled/my-subdomain.fitib.us.conf

My nginx .conf file below

#Joplin
server{
#  root /var/www/html;
  server_name mysubdomain.fitib.us;

  proxy_set_header X-Forwarded-Host $host;
  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 X-Real-IP $remote_addr;
  proxy_pass_header Content-Type;

  location /joplin/ {
      proxy_redirect off;
      rewrite ^/joplin/(.*)$ /$1 break;
      proxy_pass http://127.0.0.1:22300;
  }

    error_log /var/log/nginx/mysubdomain.fitib.us.access.log;
    access_log /var/log/nginx/mysubdomain.fitib.us.error.log;

#    listen [::]:443 ssl ipv6only=on;
    listen 443 ssl http2;

    ssl_certificate /etc/letsencrypt/live/fitib.us/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/fitib.us/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
#https redirect
server{
  listen 80;
  listen [::]:80;
  server_name mysubdomain.fitib.us;
  return 301 https://mysubdomain.fitib.us$request_uri;
}  

Notes about the config file above, from the top:

That’s it, it works now. It just works consistently (and joplin-server is much faster than other solutions like one drive). I spent maybe an hour before getting frustrated and reading more forum posts with solutions, then came back 3-4 times over the course of the week. I was about to just make it accessible locally and VPN into the house to be able to sync.

Final thoughts:

The sync settings should match the URL that was in both files above. As seen in the linux application screenshot below.

The default username = admin@localhost, password = admin. You can get there through a browser and should change the password immediately.

You should also make yourself a username and password separate from the admin. I don’t have e-mail setup, so when an e-mail is sent to you to confirm yourself, you can’t get to it. It never went out. You get around this by logging into the admin dashboard on the browser and viewing past outbound e-mails. You can copy/paste the confirmation link that was generated that way.

There seems to also be a feature to encrypt even further, which I will be using in the future. Hopefully this encrypts the files on the disk, but I’ll have to do more reading about that.