Skip to content

Reverse proxy

This page helps you put Goiabada on the internet behind Nginx, with certificates from Let’s Encrypt and no Cloudflare.

Nginx answers HTTPS on ports 80 and 443 and forwards each request to the Docker Compose file the setup wizard writes, which publishes the two servers on 127.0.0.1 alone:

Browser ── HTTPS ──▶ Nginx on the host ── HTTP ──▶ auth.example.com → 127.0.0.1:9090
admin.example.com → 127.0.0.1:9091

You need a server with Docker and Nginx, two hostnames under one domain, such as auth.example.com and admin.example.com, and the setup wizard. Both hostnames must share a registrable domain: see What every method needs.

  1. Point both hostnames at the server. At your DNS provider, add an A record for each, with the server’s IP address:

    auth A <your-server-ip>
    admin A <your-server-ip>

    If the server has an IPv6 address too, add an AAAA record for each with it.

    dig +short auth.example.com shows the address once the record has spread.

  2. Generate and start Goiabada. Run the setup wizard, choose Production with reverse proxy, enter https://auth.example.com and https://admin.example.com, then start the files it wrote, as Docker Compose describes:

    Terminal window
    docker compose up -d
    curl http://127.0.0.1:9090/health # healthy
  3. Get the certificates. Install certbot and give Nginx a temporary site that answers Let’s Encrypt’s challenge for both hostnames:

    Terminal window
    sudo apt-get update && sudo apt-get install certbot
    sudo mkdir -p /var/www/certbot

    Write /etc/nginx/sites-available/goiabada, with your own hostnames:

    server {
    listen 80;
    listen [::]:80;
    server_name auth.example.com admin.example.com;
    location /.well-known/acme-challenge/ {
    root /var/www/certbot;
    }
    }

    Enable it, and ask for one certificate per hostname:

    Terminal window
    sudo ln -s /etc/nginx/sites-available/goiabada /etc/nginx/sites-enabled/
    sudo nginx -t && sudo nginx -s reload
    sudo certbot certonly --webroot -w /var/www/certbot -d auth.example.com
    sudo certbot certonly --webroot -w /var/www/certbot -d admin.example.com
  4. Proxy both hostnames to Goiabada. Replace /etc/nginx/sites-available/goiabada with the full site:

    # Auth server
    server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name auth.example.com;
    ssl_certificate /etc/letsencrypt/live/auth.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    location / {
    proxy_pass http://127.0.0.1:9090;
    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;
    }
    location /.well-known/acme-challenge/ {
    root /var/www/certbot;
    }
    }
    # Admin console
    server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name admin.example.com;
    ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    location / {
    proxy_pass http://127.0.0.1:9091;
    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;
    }
    location /.well-known/acme-challenge/ {
    root /var/www/certbot;
    }
    }
    # HTTP: Let's Encrypt's challenge, and a redirect to HTTPS for everything else
    server {
    listen 80;
    listen [::]:80;
    server_name auth.example.com admin.example.com;
    location /.well-known/acme-challenge/ {
    root /var/www/certbot;
    }
    location / {
    return 301 https://$host$request_uri;
    }
    }

    http2 on; needs Nginx 1.25.1 or later, and nginx -v shows yours. On an older one, delete it and write listen 443 ssl http2; and listen [::]:443 ssl http2; instead.

    Terminal window
    sudo nginx -t && sudo systemctl reload nginx
  5. Open only 80 and 443, besides SSH. With ufw, allow your SSH port first, so turning the firewall on keeps your access.

    Set SSH_PORT to the port your SSH server listens on. In an SSH session, it’s the last field of $SSH_CONNECTION:

    Terminal window
    SSH_PORT="${SSH_CONNECTION##* }"; echo "SSH port: ${SSH_PORT:-unknown}"

    If that prints unknown, or you’re in a sudo -i or su shell, at a console, or in a tmux, screen or mosh session, which can keep an old value, look the port up and set it yourself, such as SSH_PORT=2222. sudo ss -tlnp shows SSH listening as sshd, or as systemd on a host that starts it through socket activation, as recent Ubuntu releases do, where systemctl cat ssh.socket shows its ListenStream.

    Then add the rules. Nothing changes unless SSH_PORT is a port from 1 to 65535 written without a leading zero, and each command runs only if the one before it succeeded. It runs the same in Bash and in sh:

    Terminal window
    case "$SSH_PORT" in
    [1-9] | [1-9][0-9] | [1-9][0-9][0-9] | [1-9][0-9][0-9][0-9] | [1-9][0-9][0-9][0-9][0-9]) port_ok=yes ;;
    *) port_ok=no ;;
    esac
    if [ "$port_ok" = yes ] && [ "$SSH_PORT" -le 65535 ]; then
    sudo ufw allow "$SSH_PORT"/tcp && sudo ufw allow 80/tcp && sudo ufw allow 443/tcp && sudo ufw enable
    else
    echo "SSH_PORT isn't a port from 1 to 65535; nothing was changed."
    fi

    Before you close this session, open a second SSH connection to check that you still get in. ufw comes with Ubuntu; on Debian, sudo apt-get install ufw first, or open the same ports in the firewall you use.

    Never open 9090 or 9091: a caller that reaches Goiabada around Nginx picks the IP address it’s rate limited and audited under.

  6. Check the renewals. Let’s Encrypt certificates last 90 days, and the certbot package renews them on a timer:

    Terminal window
    sudo certbot renew --dry-run
    systemctl list-timers | grep certbot
  7. Sign in at https://admin.example.com, as First sign-in describes.

  • Nginx terminates TLS. Goiabada serves plain HTTP on 127.0.0.1, which only processes on the host reach. Goiabada marks its cookies Secure because its URLs are https://, whatever Nginx speaks to it.
  • Each server sees the client’s address, from the X-Forwarded-For entry Nginx appends: see Client IP and proxy trust.
  • The admin console reaches the auth server inside the Compose network, not through Nginx: see The hop to the auth server.
  • Goiabada sets its own security headers, Strict-Transport-Security included once its URLs are https://, so Nginx adds none. A header Nginx added as well would reach the browser twice.