# py-push-server ## Docker Build & Deploy ``` export PUSH_SERVER_VERSION=0.1 # This command is also in build.sh # Note that there is a user with the uid 1000 inside the Dockerfile: make sure that such a user exists on the server and has write access to the database. docker build --tag py-push-server:amd-$PUSH_SERVER_VERSION --platform linux/amd64 . docker save -o ~/dl/py-push-server-amd-$PUSH_SERVER_VERSION.tar py-push-server:amd-$PUSH_SERVER_VERSION bzip2 ~/dl/py-push-server-amd-$PUSH_SERVER_VERSION.tar ``` ... and then on the server after transferring that file: ``` export PUSH_SERVER_VERSION=0.1 bzip2 -d py-push-server-amd-$PUSH_SERVER_VERSION.tar.bz2 sudo docker load -i py-push-server-amd-$PUSH_SERVER_VERSION.tar sudo docker run -d -p 8900:3000 -v ~/py-push-server-db:/app/instance/data --name py-push-server-$PUSH_SERVER_VERSION py-push-server:amd-$PUSH_SERVER_VERSION ``` Finally, generate a new VAPID by hitting the `regenerate_vapid` endpoint with a POST, eg. `curl -X POST localhost:8080/web-push/regenerate_vapid` ## Docker Compose & HAProxy Setup On first run you need to: `docker network create phoenix-network` ### HAProxy setup ... in docker-compose.yml ... ``` default-backend: container_name: 'default-backend' image: nginx:1.22.0-alpine volumes: - /docker-volumes/haproxy-config/core/nginx/html:/usr/share/nginx/html restart: always networks: - phoenix-network rsyslog: container_name: 'rsyslog' hostname: 'rsyslog' image: alpine-rsyslog build: context: ./alpine-rsyslog volumes: - $PWD/haproxy-config/core/haproxy.conf:/etc/rsyslog.d/haproxy.conf - $PWD/haproxy-config/log:/var/log - $PWD/haproxy-config/spool:/var/spool - $PWD/rsyslog/rsyslog.conf:/etc/rsyslog.conf ports: - '127.0.0.1:514:514' networks: - phoenix-network haproxy: container_name: 'haproxy' hostname: 'haproxy' image: haproxytech/haproxy-alpine:latest ports: - 443:443 - 80:80 depends_on: - default-backend - rsyslog volumes: - $PWD/haproxy-config/log:/var/log - $PWD/haproxy-config/certs:/usr/local/etc/haproxy/certs:ro - $PWD/haproxy-config/core:/usr/local/etc/haproxy/core:ro - $PWD/haproxy-config/maps:/usr/local/etc/haproxy/maps:ro - $PWD/haproxy-config/sites:/usr/local/etc/haproxy/sites:ro command: "haproxy -f /usr/local/etc/haproxy/core/haproxy.cfg -f /usr/local/etc/haproxy/sites/" networks: - phoenix-network ``` in `haproxy-config/core/haproxy.conf`: ``` $UDPServerAddress 127.0.0.1 $UDPServerRun 514 local0.* /var/log/haproxy.log & ~ ``` in `haproxy-config/core/haproxy.cfg`: ``` global tune.ssl.default-dh-param 2048 log rsyslog:514 local0 maxconn 4096 defaults option httplog option forwardfor except 127.0.0.1 option forwardfor header X-Real-IP option http-no-delay log global mode http retries 10 option redispatch timeout connect 4000 timeout client 600000 timeout server 600000 timeout queue 10s frontend default_frontend mode http bind *:80 bind *:443 ssl crt /usr/local/etc/haproxy/certs alpn h2,http/1.1 # redirect non-www # http-request redirect prefix https://%[hdr(Host),regsub(^www\.,,i)] code 301 if { hdr_beg(host) -i www. } # Make a rule that the server cannot be directly accessed by IP address acl has_domain hdr(Host),map_str(/usr/local/etc/haproxy/maps/domains.map) -m found http-request deny if !has_domain # ACME challenge rule? acl letsencrypt-acl path_beg /.well-known/acme-challenge/ redirect code 301 scheme https if !{ ssl_fc && letsencrypt-acl } compression algo gzip compression type text/css text/html text/javascript application/javascript text/plain text/xml application/json image/svg+xml acl is_content_type_html res.hdr(Content-Type) -i text/html http-response set-header Content-Type text/html;\ charset=UTF-8 if is_content_type_html http-response set-header Cache-Control no-cache,\ max-age=31536000 http-response set-header Expires %[date(3600),http_date] http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" http-response set-header X-XSS-Protection "1; mode=block" http-response set-header X-Content-Type-Options "nosniff" http-response set-header Referrer-Policy "strict-origin-when-cross-origin" use_backend %[base,lower,regsub(^www\.,,i),map_beg(/usr/local/etc/haproxy/maps/sites.map,default_backend)] listen stats bind *:9999 mode http log global stats enable stats realm Haproxy\ Statistics stats uri /haproxy_stats stats hide-version backend haproxy_stats_backend http-request auth realm haproxy-stats unless { http_auth_group(basic-auth-list) is-haproxy-stats } mode http compression algo gzip compression offload server server_nginx localhost:9999 userlist basic-auth-list group is-guest group is-haproxy-stats user guest password $5$N7CpS0mo$FyJtlwQOwzAi5HnCISumyBKWyPu6DhBO7eGzUUyWoJ7 groups is-guest ``` ... in `haproxy-config/sites/web-push.anomalistlabs.com.cfg` ... define an HAProxy backend and map it to the Docker host and port NOTE: this also turned off CORS origin rule ``` backend web_push_backend mode http compression algo gzip compression offload http-response set-header Access-Control-Allow-Origin "*" server server_nginx endorser-push-server:3000 ``` ... in `haproxy-config/maps/domains.map` ... add a domain that will be used as a base ``` timesafari.anomalistlabs.com ``` ... in `haproxy-config/maps/sites.map` ... map the `/web-push` path to the `web_push_backend` NOTE: `timesafari-pwa.anomalistlabs.com` PWA sits on the root ``` timesafari-pwa.anomalistlabs.com/web-push/ web_push_backend ``` ### The rest .. `docker-compose up -d` should just work :-) ## Run the server outside Docker Run the app: ```commandline sh <(curl https://pkgx.sh) +python.org +virtualenv.pypa.io sh pip install gunicorn source venv/bin/activate gunicorn -b 0.0.0.0:3000 --log-level=debug --workers=3 app:app # ... and see the results in a browser: http://localhost:3000/web-push/vapid # See Troubleshooting below if that doesn't work out of the box. ``` Run a test: ```commandline python webpush.py ``` Run haproxy (on a Mac): * Create "haproxy-config" directory for those files above, eg. /usr/local/etc/haproxy * Comment out the `log rsyslog` and `bind *:443` lines in /usr/local/etc/haproxy/haproxy.cfg and then run: `haproxy -f /usr/local/etc/haproxy/haproxy.cfg` Troubleshooting * If you get "no such table: vapid_key" then your file pointers are probably wrong. Check that the "docker run" mounted volume matches the SQLALCHEMY_DATABASE_URI in the app.py file. * If you get "unable to open database file", you can provide the app.py with SQLALCHEMY_DATABASE_URI with `sqlite:////...` with the full path to the data/webpush.db file. (Why does the relative path of `sqlite:///...` not work for a relative path?)