Skip to content
This repository has been archived by the owner on Dec 31, 2023. It is now read-only.

Commit

Permalink
Caddy Deployment added, Nginx rewritten (#168)
Browse files Browse the repository at this point in the history
Co-authored-by: L. Kärkkäinen <[email protected]>
  • Loading branch information
Tronic and Tronic committed Jul 13, 2023
1 parent c861736 commit 584e8c7
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 116 deletions.
1 change: 1 addition & 0 deletions src/.vuepress/locales/details/sidebar/sidebar_en.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const userGuideFull = [
"/en/guide/deployment/app-loader.md",
"/en/guide/deployment/inspector.md",
// "/guide/deployment/server-choice.md",
"/en/guide/deployment/caddy.md",
"/en/guide/deployment/nginx.md",
"/en/guide/deployment/docker.md",
// "/guide/deployment/kubernetes.md",
Expand Down
74 changes: 74 additions & 0 deletions src/en/guide/deployment/caddy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Caddy Deployment

## Introduction

Caddy is a state-of-the-art web server and proxy that supports up to HTTP/3. Its simplicity lies in its minimalistic configuration and the inbuilt ability to automatically procure TLS certificates for your domains from Let's Encrypt. In this setup, we will configure the Sanic application to serve locally at 127.0.0.1:8001, with Caddy playing the role of the public-facing server for the domain example.com.

You may install Caddy from your favorite package menager on Windows, Linux and Mac. The package is named `caddy`.

## Proxied Sanic app

```python
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"
)
```

To run this application, save as `proxied_example.py`, and use the sanic command-line interface as follows:

```bash
SANIC_PROXIES_COUNT=1 sanic proxied_example --port 8001
```

Setting the SANIC_PROXIES_COUNT environment variable instructs Sanic to trust the X-Forwarded-* headers sent by Caddy, allowing it to correctly identify the client's IP address and other information.

## Caddy is simple

If you have no other web servers running, you can simply run Caddy CLI (needs `sudo` on Linux):

```bash
caddy reverse-proxy --from example.com --to :8001
```

This is a complete server that includes a certificate for your domain, http-to-https redirect, proxy headers, streaming and WebSockets. Your Sanic application should now be available on the domain you specified by HTTP versions 1, 2 and 3. Remember to open up UDP/443 on your firewall to enable H3 communications.

All done?

Soon enough you'll be needing more than one server, or more control over details, which is where the configuration files come in. The above command is equivalent to this `Caddyfile`, serving as a good starting point for your install:

```caddy
example.com {
reverse_proxy localhost:8001
}
```

Some Linux distributions install Caddy such that it reads configuration from `/etc/caddy/Caddyfile`, which `import /etc/caddy/conf.d/*` for each site you are running. If not, you'll need to manually run `caddy run` as a system service, pointing it at the proper config file. Alternatively, use Caddy API mode with `caddy run --resume` for persistent config changes. Note that any Caddyfile loading will replace all prior configuration and thus `caddy-api` is not configurable in this traditional manner.

## Advanced configuration

At times, you might need to mix static files and handlers at the site root for cleaner URLs. In Sanic, you'd use `app.static("/", "static", index="index.html")` to achieve this. However, for improved performance, you can offload serving static files to Caddy:

```caddy
app.example.com {
# Look for static files first, proxy to Sanic if not found
route {
file_server {
root /srv/sanicexample/static
precompress br # brotli your large scripts and styles
pass_thru
}
reverse_proxy unix//tmp/sanic.socket # sanic --unix /tmp/sanic.socket
}
}
```

Please refer to [Caddy documentation](https://caddyserver.com/docs/) for more options.
183 changes: 67 additions & 116 deletions src/en/guide/deployment/nginx.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,24 @@

## 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.
SSL and HTTP/2 are also easily implemented on such proxy.
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:8000`, while the
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 from `/var/www/`.

on domain example.com. Static files will be served by Nginx for maximal
performance.

## Proxied Sanic app

The app needs to be setup with a secret key used to identify a trusted proxy,
so that real client IP and other information can be identified. This protects
against anyone on the Internet sending fake headers to spoof their IP addresses
and other details. Choose any random string and configure it both on the app
and in Nginx config.

```python
from sanic import Sanic
from sanic.response import text

app = Sanic("proxied_example")
app.config.FORWARDED_SECRET = "YOUR SECRET"

@app.get("/")
def index(request):
Expand All @@ -36,37 +28,55 @@ def index(request):
f"{request.remote_addr} connected to {request.url_for('index')}\n"
f"Forwarded: {request.forwarded}\n"
)

if __name__ == "__main__":
app.run(host="127.0.0.1", port=8000, workers=8, access_log=False)
```

Since this is going to be a system service, save your code to
`/srv/sanicexample/sanicexample.py`.
`/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.

```bash
SANIC_FORWARDED_SECRET=_hostname sanic proxied_example --port 8001
```

For testing, run your app in a terminal.
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.

```bash
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.

Upstream servers need to be configured in a separate `upstream` block to enable
HTTP keep-alive, which can drastically improve performance, so we use this
instead of directly providing an upstream address in `proxy_pass` directive. In
this example, the upstream section is named by `server_name`, i.e. the public
domain name, which then also gets passed to Sanic in the `Host` header. You may
change the naming as you see fit. Multiple servers may also be provided for
load balancing and failover.
::: tip 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.
:::

Change the two occurrences of `example.com` to your true domain name, and
instead of `YOUR SECRET` use the secret you chose for your app.
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`):

```nginx
# 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:8000;
#server unix:/tmp/sanic.sock;
server 127.0.0.1:8001;
#server unix:/tmp//sanic.sock;
}
server {
Expand All @@ -75,7 +85,7 @@ server {
listen [::]:443 ssl http2 default_server;
# Serve static files if found, otherwise proxy to Sanic
location / {
root /var/www;
root /srv/sanicexample/static;
try_files $uri @sanic;
}
location @sanic {
Expand All @@ -84,125 +94,62 @@ server {
proxy_http_version 1.1;
proxy_request_buffering off;
proxy_buffering off;
# Proxy forwarding (password configured in app.config.FORWARDED_SECRET)
proxy_set_header forwarded "$proxy_forwarded;secret=\"YOUR SECRET\"";
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;
}
}
```

To avoid cookie visibility issues and inconsistent addresses on search engines,
it is a good idea to redirect all visitors to one true domain, always using
HTTPS:

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

The above config sections may be placed in `/etc/nginx/sites-available/default`
or in other site configs (be sure to symlink them to `sites-enabled` if you
create new ones).

Make sure that your SSL certificates are configured in the main config, or
add the `ssl_certificate` and `ssl_certificate_key` directives to each
`server` section that listens on SSL.
Additionally, copy&paste all of this into `nginx/conf.d/forwarded.conf`:

```nginx
# RFC 7239 Forwarded header for Nginx proxy_pass
# Add within your server or location block:
# proxy_set_header forwarded "$proxy_forwarded;secret=\"YOUR SECRET\"";
# Configure your upstream web server to identify this proxy by that password
# because otherwise anyone on the Internet could spoof these headers and fake
# their real IP address and other information to your service.
# Provide the full proxy chain in $proxy_forwarded
map $proxy_add_forwarded $proxy_forwarded {
default "$proxy_add_forwarded;by=\"_$hostname\";proto=$scheme;host=\"$http_host\";path=\"$request_uri\"";
}
# The following mappings are based on
# https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/
map $remote_addr $proxy_forwarded_elem {
# IPv4 addresses can be sent as-is
~^[0-9.]+$ "for=$remote_addr";
# IPv6 addresses need to be bracketed and quoted
~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\"";
# Unix domain socket names cannot be represented in RFC 7239 syntax
default "for=unknown";
# 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;
}
map $http_forwarded $proxy_add_forwarded {
# If the incoming Forwarded header is syntactically valid, append to it
"~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem";
# Otherwise, replace it
default "$proxy_forwarded_elem";
# 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
}
```

::: tip Note
For installs that don't use `conf.d` and `sites-available`, all of the above
configs may also be placed inside the `http` section of the main `nginx.conf`.
:::

Reload Nginx config after changes:
Start or restart Nginx for changes to take effect. E.g.

```bash
sudo nginx -s reload
systemctl restart nginx
```

Now you should be able to connect your app on `https://example.com/`. Any 404
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.

## SSL certificates

If you haven't already configured valid certificates on your server, now is a
good time to do so. Install `certbot` and `python3-certbot-nginx`, then run

```bash
certbot --nginx -d example.com -d www.example.com
```

Reference: [Using Free Let’s Encrypt SSL/TLS Certificates with NGINX](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/)

## Running as a service

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

```text
```systemd
[Unit]
Description=Sanic Example
[Service]
User=nobody
WorkingDirectory=/srv/sanicexample
ExecStart=/usr/bin/env python3 sanicexample.py
DynamicUser=Yes
WorkingDirectory=/srv/sanicservice
Environment=SANIC_PROXY_SECRET=_hostname
ExecStart=sanic proxied_example --port 8001 --fast
Restart=always
[Install]
Expand All @@ -212,7 +159,11 @@ WantedBy=multi-user.target
Then reload service files, start your service and enable it on boot:

```bash
sudo systemctl daemon-reload
sudo systemctl start sanicexample
sudo systemctl enable sanicexample
systemctl daemon-reload
systemctl start sanicexample
systemctl enable sanicexample
```

::: tip 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.
:::

0 comments on commit 584e8c7

Please sign in to comment.