diff --git a/.envs/.production/.postgres b/.envs/.production/.postgres index 3c79c761..9e6b4ffd 100644 --- a/.envs/.production/.postgres +++ b/.envs/.production/.postgres @@ -1,6 +1,7 @@ # PostgreSQL # ------------------------------------------------------------------------------ -POSTGRES_HOST=kc-med-oslerdb.kc.umkc.edu +POSTGRES_HOST=postgres POSTGRES_PORT=5432 POSTGRES_DB=osler POSTGRES_USER=django +# the following are missing: POSTGRES_PASSWORD diff --git a/.envs/.secrets/.gitignore b/.envs/.secrets/.gitignore new file mode 100644 index 00000000..5e7d2734 --- /dev/null +++ b/.envs/.secrets/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/.gitignore b/.gitignore index 27c2ec48..2982a236 100644 --- a/.gitignore +++ b/.gitignore @@ -278,4 +278,7 @@ osler/media/ !.envs/.local/ !.envs/.production/ +# secrets has a .gitignore file that ignores everything inside it +!.envs/.secrets/ + .vscode/* diff --git a/compose/production/nginx/certs/.gitignore b/compose/production/nginx/certs/.gitignore new file mode 100644 index 00000000..5e7d2734 --- /dev/null +++ b/compose/production/nginx/certs/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/compose/production/postgres/Dockerfile b/compose/production/postgres/Dockerfile index 7cf4173d..bd416859 100644 --- a/compose/production/postgres/Dockerfile +++ b/compose/production/postgres/Dockerfile @@ -1,4 +1,4 @@ -FROM postgres:11.3 +FROM postgres:11.12 COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance RUN chmod +x /usr/local/bin/maintenance/* diff --git a/compose/production/traefik/Dockerfile b/compose/production/traefik/Dockerfile deleted file mode 100644 index 746aa2b4..00000000 --- a/compose/production/traefik/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM traefik:v2.0 -RUN mkdir -p /etc/traefik/acme -RUN touch /etc/traefik/acme/acme.json -RUN chmod 600 /etc/traefik/acme/acme.json -COPY ./compose/production/traefik/traefik.yml /etc/traefik diff --git a/compose/production/traefik/traefik.yml b/compose/production/traefik/traefik.yml deleted file mode 100644 index 7d58c4f5..00000000 --- a/compose/production/traefik/traefik.yml +++ /dev/null @@ -1,69 +0,0 @@ -log: - level: INFO - -entryPoints: - web: - # http - address: ":80" - - web-secure: - # https - address: ":443" - -certificatesResolvers: - letsencrypt: - # https://docs.traefik.io/master/https/acme/#lets-encrypt - acme: - email: "contact@oslerproject.org" - storage: /etc/traefik/acme/acme.json - # https://docs.traefik.io/master/https/acme/#httpchallenge - httpChallenge: - entryPoint: web - -http: - routers: - web-router: - rule: "Host(`oslerproject.org`) || Host(`www.oslerproject.org`)" - - entryPoints: - - web - middlewares: - - redirect - - csrf - service: django - - web-secure-router: - rule: "Host(`oslerproject.org`) || Host(`www.oslerproject.org`)" - - entryPoints: - - web-secure - middlewares: - - csrf - service: django - tls: - # https://docs.traefik.io/master/routing/routers/#certresolver - certResolver: letsencrypt - - middlewares: - redirect: - # https://docs.traefik.io/master/middlewares/redirectscheme/ - redirectScheme: - scheme: https - permanent: true - csrf: - # https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders - # https://docs.djangoproject.com/en/dev/ref/csrf/#ajax - headers: - hostsProxyHeaders: ["X-CSRFToken"] - - services: - django: - loadBalancer: - servers: - - url: http://django:5000 - -providers: - # https://docs.traefik.io/master/providers/file/ - file: - filename: /etc/traefik/traefik.yml - watch: true diff --git a/config/settings/demo.py b/config/settings/demo.py new file mode 100644 index 00000000..e6f52148 --- /dev/null +++ b/config/settings/demo.py @@ -0,0 +1,33 @@ +# Custom settings for demo deployment +# ----------------------------------------------------------------------------- +from .base import env + +ALLOWED_HOSTS = ['demo.llemrconspiracy.org', '0.0.0.0'] + +TIME_ZONE = "America/Chicago" +LANGUAGE_CODE = "en-us" + +OSLER_ROLE_DASHBOARDS = { + 'Attending': 'dashboard-attending', + 'Physician': 'dashboard-attending', +} + +OSLER_DISPLAY_REFERRALS = False +OSLER_DISPLAY_APPOINTMENTS = False +OSLER_DISPLAY_CASE_MANAGERS = False +OSLER_DISPLAY_ATTESTABLE_BASIC_NOTE = False +OSLER_DISPLAY_DIAGNOSIS = False +OSLER_DISPLAY_VOUCHERS = False +OSLER_DISPLAY_WILL_RETURN = False +OSLER_DISPLAY_ATTENDANCE = True +OSLER_DISPLAY_FOLLOWUP = False +OSLER_DISPLAY_VACCINE = False + +OSLER_DEFAULT_CITY = "Gotham" +OSLER_DEFAULT_STATE = "New Jersey" +OSLER_DEFAULT_ZIP_CODE = "00000" +OSLER_DEFAULT_COUNTRY = "USA" +OSLER_DEFAULT_ADDRESS = "" + +OSLER_ABOUT_NAME = "About" +OSLER_ABOUT_URL = "https://llemrconspiracy.org" diff --git a/config/settings/production-demo.py b/config/settings/production-demo.py new file mode 100644 index 00000000..486b973e --- /dev/null +++ b/config/settings/production-demo.py @@ -0,0 +1,2 @@ +from .production import * +from .demo import * diff --git a/docs/running-in-production.rst b/docs/running-in-production.rst new file mode 100644 index 00000000..cabef6a7 --- /dev/null +++ b/docs/running-in-production.rst @@ -0,0 +1,159 @@ +Running in Production +==================================== + +Set up a docker compose file +---------------------------- + +The global configuration of your docker compose swarm is set up in `production.yml`. +We have a number of examples, including the demo (`production-demo.yml`). These set up +virtual machines for each of the elements of the web app. + +The Database +------------ + +We use PostgresQL. The database container is named `postgres`. Here is an example of a configuration: + +.. code-block:: yaml + + postgres: + build: + context: . + dockerfile: ./compose/production/postgres/Dockerfile + image: llemr_production_postgres + container_name: postgres + volumes: + - production_postgres_data:/var/lib/postgresql/data + - production_postgres_data_backups:/backups + env_file: + - ./.envs/.production/.postgres + - ./.envs/.secrets/.postgres + networks: + - database_network + +The key here is the `env_file` section, which sets some important environment variables: + +.. code-block:: bash + + POSTGRES_HOST=postgres + POSTGRES_PORT=5432 + POSTGRES_DB=llemr + POSTGRES_USER=django + +Furthermore, the file `./.envs/.secrets/.postgres` *does not exist*, and must be created. Create a file that looks something like: + +.. code-block:: bash + + POSTGRES_PASSWORD=FybL7H4ftzJPiEWQrWMDuogLUcLgv47iw78vUqHexPPnJGd9EJDPeDXH9RGdiTBC + + +.. warning:: + POSTGRES_PASSWORD is **priviledged information** and should be kept secret. Do not check it in to your git repository. The string provided here is an example--do not use it yourself! Generate a long, random string yourself and use it instead. + + +The Web App +----------- + +The web app is run with gunicorn in a custom Dockerfile. This guy accounts for by far the majority of the runtime of `docker-compose build`. + +.. note:: + We provide the postgres configuration environment files + (`.envs/.production/.postgres` and `./.envs/.secrets/.postgres`) to _both_ + the django container and the postgres container. This is because the + django container needs to be able to connect and authenticate to the + postgres container! + +.. code-block:: yaml + + django: + build: + context: . + dockerfile: ./compose/production/django/Dockerfile + image: llemr_production_django + container_name: django + ports: + - 5000:5000 + depends_on: + - postgres + - redis + environment: + - DJANGO_SETTINGS_MODULE=config.settings.production-demo + env_file: + - ./.envs/.production/.django + - ./.envs/.production/.postgres + - ./.envs/.secrets/.postgres + - ./.envs/.secrets/.django + command: /start + networks: + - nginx_network + - database_network + +Notice that we use the `environment` section to provide `DJANGO_SETTINGS_MODULE`, which points to `config/settings/production-demo.py`. This file contains: + +.. code-block:: python + + from .production import * + from .demo import * + +Thus, it combines the configurations listed in `config/settings/production.py` and `config/settings/demo.py`, with those in `demo.py` overriding anything in `production.py` (since `demo.py` comes second). Most of the settings in `production.py` are strong recommendations for production, whereas those in `demo.py` are likely to be configured by you. + +.. code-block:: python + from .base import env + + TIME_ZONE = "America/Chicago" + LANGUAGE_CODE = "en-us" + + OSLER_ROLE_DASHBOARDS = { + 'Attending': 'dashboard-attending', + 'Physician': 'dashboard-attending', + } + + OSLER_DISPLAY_REFERRALS = False + OSLER_DISPLAY_APPOINTMENTS = False + OSLER_DISPLAY_CASE_MANAGERS = False + OSLER_DISPLAY_ATTESTABLE_BASIC_NOTE = False + OSLER_DISPLAY_DIAGNOSIS = False + OSLER_DISPLAY_VOUCHERS = False + OSLER_DISPLAY_WILL_RETURN = False + OSLER_DISPLAY_ATTENDANCE = True + OSLER_DISPLAY_FOLLOWUP = False + OSLER_DISPLAY_VACCINE = False + + OSLER_DEFAULT_CITY = "Gotham" + OSLER_DEFAULT_STATE = "New Jersey" + OSLER_DEFAULT_ZIP_CODE = "00000" + OSLER_DEFAULT_COUNTRY = "USA" + OSLER_DEFAULT_ADDRESS = "" + + OSLER_ABOUT_NAME = "About" + OSLER_ABOUT_URL = "https://llemrconspiracy.org" + + +The Web Server +-------------- + +The web server we use is nginx. It's responsible for serving static files, terminating SSL, and passing data to gunicorn. The pertinent part of the docker compose file is here: + +.. code-block:: yaml + + nginx: + image: nginx:1.19 + container_name: nginx + ports: + - 80:80 + - 443:443 + env_file: + - ./.envs/.production/.nginx + volumes: + - ./compose/production/nginx/templates:/etc/nginx/templates + - ./compose/production/nginx/certs:/etc/nginx/certs + depends_on: + - django + networks: + - nginx_network + +To get this working, you need to put an SSL certificate named `cert.crt` in `compose/production/nginx/certs`. SSL certificates get kind of complicated, but you can usually get one from Let's Encrypt (https://letsencrypt.org/) or, if you're part of an organization with an IT department like a university, you can ask your friendly local IT professional. In a pinch, just to get things running, you can make a self-signed one like so: + +.. code-block:: console + + $ mkdir -p ./compose/production/nginx/certs + $ openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout ./compose/production/nginx/certs/cert.key -out ./compose/production/nginx/certs/cert.crt diff --git a/production-demo.yml b/production-demo.yml new file mode 100644 index 00000000..799a2a88 --- /dev/null +++ b/production-demo.yml @@ -0,0 +1,69 @@ +version: "3" + +volumes: + production_postgres_data: {} + production_postgres_data_backups: {} + +services: + django: + build: + context: . + dockerfile: ./compose/production/django/Dockerfile + image: llemr_production_django + container_name: django + ports: + - 5000:5000 + depends_on: + - postgres + - redis + environment: + - DJANGO_SETTINGS_MODULE=config.settings.production-demo + env_file: + - ./.envs/.production/.django + - ./.envs/.production/.postgres + - ./.envs/.secrets/.postgres + - ./.envs/.secrets/.django + command: /start + networks: + - nginx_network + - database_network + + postgres: + build: + context: . + dockerfile: ./compose/production/postgres/Dockerfile + image: llemr_production_postgres + container_name: postgres + volumes: + - production_postgres_data:/var/lib/postgresql/data + - production_postgres_data_backups:/backups + env_file: + - ./.envs/.production/.postgres + - ./.envs/.secrets/.postgres + networks: + - database_network + + nginx: + image: nginx:1.21 + container_name: nginx + ports: + - 80:80 + - 443:443 + env_file: + - ./.envs/.production/.nginx + volumes: + - ./compose/production/nginx/templates:/etc/nginx/templates + - ./compose/production/nginx/certs:/etc/nginx/certs + depends_on: + - django + networks: + - nginx_network + + redis: + image: redis:5.0 + +networks: + nginx_network: + driver: bridge + database_network: + driver: bridge diff --git a/production-umkc.yml b/production-umkc.yml index 00ed5dbc..b6c067cd 100644 --- a/production-umkc.yml +++ b/production-umkc.yml @@ -3,7 +3,6 @@ version: '3' volumes: production_postgres_data: {} production_postgres_data_backups: {} - production_traefik: {} networks: default: @@ -20,13 +19,17 @@ services: dockerfile: ./compose/production/django/Dockerfile image: osler_production_django depends_on: - # - postgres - redis env_file: - ./.envs/.production/.django - - ./.envs/.production/.postgres - ./.envs/.production/.secrets - ./.envs/.production/.umkc + environment: + - POSTGRES_HOST=kc-med-oslerdb.kc.umkc.edu + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - POSTGRES_DB=osler + - POSTGRES_USER=django command: /start ports: - "5000:5000" diff --git a/production.yml b/production.yml deleted file mode 100644 index ac31aeee..00000000 --- a/production.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: '3' - -volumes: - production_postgres_data: {} - production_postgres_data_backups: {} - production_traefik: {} - -services: - django: - build: - context: . - dockerfile: ./compose/production/django/Dockerfile - image: osler_production_django - depends_on: - - postgres - - redis - env_file: - - ./.envs/.production/.django - - ./.envs/.production/.postgres - command: /start - - postgres: - build: - context: . - dockerfile: ./compose/production/postgres/Dockerfile - image: osler_production_postgres - volumes: - - production_postgres_data:/var/lib/postgresql/data - - production_postgres_data_backups:/backups - env_file: - - ./.envs/.production/.postgres - - traefik: - build: - context: . - dockerfile: ./compose/production/traefik/Dockerfile - image: osler_production_traefik - depends_on: - - django - volumes: - - production_traefik:/etc/traefik/acme - ports: - - "0.0.0.0:8084:80" - - "0.0.0.0:443:443" - - redis: - image: redis:5.0 - -