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

I like to create /etc/nginx/sites-available/subdomain.mydomain.tld.conf for each, then symbolic link to sites-enabled with. Which is I think the best 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:

  • The root location isn’t used. I believe that just shows the nginx landing page. Everything the app and web interface uses is at /joplin/ location. You can change this in the compose file, but in nginx the root block still is unused. Just use the proxy pass block with the adjusted URL if desired.
  • replace mysubdomain.fitib.us with your own, as long as it matches the compose file
  • The proxy_pass will change if you aren’t running your docker Joplin Server instance on the same machine. Change the IP if you need to
  • The port 22300 is the default. Change that if needed, as long as it matches the compose file external port. It should remain 22300 for docker network internal unless you want to exceed the scope of this tutorial.
  • Access and error logs are not required, but nice to have.
  • I really don’t understand ipv6 yet and don’t need to. But that block always throws an error since it is a duplicate with other config files.
  • The last 4 lines of the first server block handle SSL. I’m using my existing wildcard cert. You can get a cert for a single subdomain with certbot easily, or use self-signed. Just update the cert and key file locations.

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.


Posted

in

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *