Skip to content

Instantly share code, notes, and snippets.

@neurobin
Forked from sambeirne/deploy.md
Created January 18, 2024 12:55
Show Gist options
  • Save neurobin/c9bcf2be0ff6a0071e53030a24befe6d to your computer and use it in GitHub Desktop.
Save neurobin/c9bcf2be0ff6a0071e53030a24befe6d to your computer and use it in GitHub Desktop.
Deploy a Python WebSocket Server on Debian 10 ("buster") with systemd and nginx

Deploy a Python WebSocket Server on Debian 10 ("buster") with systemd and nginx

Following are configuration files and instructions for using systemd to create Unix domain sockets to handle incoming websocket requests. Websocket requests are proxied by nginx.

While these instructions are for Debian 10, they should largely work on other Linux distributions. It may be necessary to change paths and the nginx username, which is www-data when installed with the Debian package manager.

Install requirements

user@host:~$ sudo apt update
user@host:~$ sudo apt install nginx python3 wget

Set up the WebSocket server

user@host:~$ sudo mkdir -p /srv/websocket
user@host:~$ sudo chown user:user /srv/websocket/
user@host:~$ wget -O /srv/websocket/server.py https://gist.githubusercontent.com/sambeirne/7a1ee751b3b3da80106c67f7c69582a8/raw/06f0e881e4da79043d67ca41c294038164eef6b5/server.py
user@host:~$ python3 -m venv /srv/websocket/venv
user@host:~$ source /srv/websocket/venv/bin/activate
user@host:~$ pip install websockets

Configure systemd

user@host:~$ sudo wget -O /etc/systemd/system/websocket@.socket https://gist.githubusercontent.com/sambeirne/7a1ee751b3b3da80106c67f7c69582a8/raw/06f0e881e4da79043d67ca41c294038164eef6b5/websocket@.socket
user@host:~$ sudo wget -O /etc/systemd/system/websocket@.service https://gist.githubusercontent.com/sambeirne/7a1ee751b3b3da80106c67f7c69582a8/raw/06f0e881e4da79043d67ca41c294038164eef6b5/websocket@.service
user@host:~$ sudo systemctl daemon-reload
user@host:~$ sudo systemctl enable --now websocket@{1..2}.socket
user@host:~$ systemctl status websocket@1.socket
● websocket@1.socket - websocket socket 1
   Loaded: loaded (/etc/systemd/system/websocket@.socket; enabled; vendor preset: enabled)
   Active: active (listening) since Wed 2020-12-23 19:50:11 UTC; 12s ago
   Listen: /run/websocket/websocket1.sock (Stream)
    Tasks: 0 (limit: 4915)
   CGroup: /system.slice/system-websocket.slice/websocket@1.socket
user@host:~$ systemctl status websocket@1.service
● websocket@1.service - websocket daemon 1
   Loaded: loaded (/etc/systemd/system/websocket@.service; disabled; vendor preset: enabled)
   Active: inactive (dead)

Two sockets are listening for connections. systemd will start the associated services when a client connects.

Configure nginx

user@host:~$ sudo wget -O /etc/nginx/sites-available/example.com https://gist.githubusercontent.com/sambeirne/7a1ee751b3b3da80106c67f7c69582a8/raw/06f0e881e4da79043d67ca41c294038164eef6b5/example.com
user@host:~$ sudo rm /etc/nginx/sites-enabled/*
user@host:~$ sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
user@host:~$ sudo nginx -s reload

Test the WebSocket server

user@host:~$ python -m websockets ws://localhost/socket/
Connected to ws://localhost/socket/.
> hello
< hello
Connection closed: code = 1000 (OK), no reason.

Messages entered at the interactive prompt should be echoed. Use Ctrl+C to quit.

upstream websocket {
least_conn;
server unix:/run/websocket/websocket1.sock;
server unix:/run/websocket/websocket2.sock;
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name example.com;
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
location / {
try_files $uri $uri/ =404;
}
location /socket/ {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
import asyncio
import logging
import os
import signal
import socket
import websockets
logger: logging.Logger = logging.getLogger(__name__)
# https://www.freedesktop.org/software/systemd/man/sd_listen_fds.html
LISTEN_FDS: int = int(os.getenv("LISTEN_FDS", 0))
assert LISTEN_FDS == 1
SD_LISTEN_FDS_START: int = 3
async def echo(websocket: websockets.WebSocketServerProtocol, path: str):
async for message in websocket:
await websocket.send(message)
async def run():
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
shutdown: asyncio.Future = loop.create_future()
for s in [signal.SIGINT, signal.SIGTERM]:
loop.add_signal_handler(s, shutdown.set_result, None)
sock: socket.socket = socket.socket(fileno=SD_LISTEN_FDS_START)
async with websockets.unix_serve(echo, path=None, sock=sock):
logger.info(f"Serving on {sock.getsockname()}")
await shutdown
logger.info("Server stopped")
asyncio.run(run())
[Unit]
Description=websocket daemon %i
PartOf=websocket@.socket
After=network.target
[Service]
DynamicUser=yes
ExecStart=/srv/websocket/venv/bin/python /srv/websocket/server.py
ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target
[Unit]
Description=websocket socket %i
[Socket]
RuntimeDirectory=websocket
ListenStream=/run/websocket/websocket%i.sock
SocketUser=www-data
SocketMode=0600
[Install]
WantedBy=sockets.target
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment