Nginx Deployment

Introduction#

Although Sanic can be run directly on Internet, it may be useful to use a proxy server such as Nginx in front of it. This is particularly useful for running multiple virtual hosts on the same IP, serving NodeJS or other services beside a single Sanic app, and it also allows for efficient serving of static files. TLS and HTTP/2 are also easily implemented on such proxy.

We are setting the Sanic app to serve only locally at 127.0.0.1:8001, while the Nginx installation is responsible for providing the service to public Internet on domain example.com. Static files will be served by Nginx for maximal performance.

Proxied Sanic app#

from sanic import Sanic
from sanic.response import text

app = Sanic("proxied_example")

@app.get("/")
def index(request):
    # This should display external (public) addresses:
    return text(
        f"{request.remote_addr} connected to {request.url_for('index')}\n"
        f"Forwarded: {request.forwarded}\n"
    )

Since this is going to be a system service, save your code to /srv/sanicservice/proxied_example.py.

For testing, run your app in a terminal using the sanic CLI in the folder where you saved the file.

SANIC_FORWARDED_SECRET=_hostname sanic proxied_example --port 8001

We provide Sanic config FORWARDED_SECRET to identify which proxy it gets the remote addresses from. Note the _ in front of the local hostname. This gives basic protection against users spoofing these headers and faking their IP addresses and more.

SSL certificates#

Install Certbot and obtain a certicate for all your domains. This will spin up its own webserver on port 80 for a moment to verify you control the given domain names.

certbot -d example.com -d www.example.com

Nginx configuration#

Quite much configuration is required to allow fast transparent proxying, but for the most part these don't need to be modified, so bear with me.

Note

Separate upstream section, rather than simply adding the IP after proxy_pass as in most tutorials, is needed for HTTP keep-alive. We also enable streaming, WebSockets and Nginx serving static files.

The following config goes inside the http section of nginx.conf or if your system uses multiple config files, /etc/nginx/sites-available/default or your own files (be sure to symlink them to sites-enabled):

# Files managed by Certbot
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

# Sanic service
upstream example.com {
  keepalive 100;
  server 127.0.0.1:8001;
  #server unix:/tmp//sanic.sock;
}

server {
  server_name example.com;
  listen 443 ssl http2 default_server;
  listen [::]:443 ssl http2 default_server;
  # Serve static files if found, otherwise proxy to Sanic
  location / {
    root /srv/sanicexample/static;
    try_files $uri @sanic;
  }
  location @sanic {
    proxy_pass http://$server_name;
    # Allow fast streaming HTTP/1.1 pipes (keep-alive, unbuffered)
    proxy_http_version 1.1;
    proxy_request_buffering off;
    proxy_buffering off;
    proxy_set_header forwarded 'by=\"_$hostname\";$for_addr;proto=$scheme;host=\"$http_host\"';
    # Allow websockets and keep-alive (avoid connection: close)
    proxy_set_header connection "upgrade";
    proxy_set_header upgrade $http_upgrade;
  }
}

# Redirect WWW to no-WWW
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name ~^www\.(.*)$;
  return 308 $scheme://$1$request_uri;
}

# Redirect all HTTP to HTTPS with no-WWW
server {
  listen 80 default_server;
  listen [::]:80 default_server;
  server_name ~^(?:www\.)?(.*)$;
  return 308 https://$1$request_uri;
}

# Forwarded for= client IP address formatting
map $remote_addr $for_addr {
  ~^[0-9.]+$          "for=$remote_addr";        # IPv4 client address
  ~^[0-9A-Fa-f:.]+$   "for=\"[$remote_addr]\"";  # IPv6 bracketed and quoted
  default             "for=unknown";             # Unix socket
}

Start or restart Nginx for changes to take effect. E.g.

systemctl restart nginx

You should be able to connect your app on https://example.com. Any 404 errors and such will be handled by Sanic's error pages, and whenever a static file is present at a given path, it will be served by Nginx.

Running as a service#

This part is for Linux distributions based on systemd. Create a unit file /etc/systemd/system/sanicexample.service

[Unit]
Description=Sanic Example

[Service]
DynamicUser=Yes
WorkingDirectory=/srv/sanicservice
Environment=SANIC_PROXY_SECRET=_hostname
ExecStart=sanic proxied_example --port 8001 --fast
Restart=always

[Install]
WantedBy=multi-user.target

Then reload service files, start your service and enable it on boot:

systemctl daemon-reload
systemctl start sanicexample
systemctl enable sanicexample

Note

For brevity we skipped setting up a separate user account and a Python virtual environment or installing your app as a Python module. There are good tutorials on those topics elsewhere that easily apply to Sanic as well. The DynamicUser setting creates a strong sandbox which basically means your application cannot store its data in files, so you may consider setting User=sanicexample instead if you need that.