diff --git a/.gitignore b/.gitignore index 82ba10d..c7924ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -inventaire +.docker +.env +.eslintcache +certbot data log -configs/docker.ini - diff --git a/README.md b/README.md index 56be882..48115b5 100644 --- a/README.md +++ b/README.md @@ -1,202 +1,109 @@ -Run [Inventaire](https://github.com/inventaire/inventaire) in Docker +# Inventaire Suite -This repository is meant to support running Inventaire for testing and development. For production, see [inventaire-deploy](https://github.com/inventaire/inventaire-deploy). +The Inventaire Suite is a containerized, production-ready Inventaire system that allows you to self-host a knowledge graph similar to [inventaire.io](https://inventaire.io). + +It is composed of several services: +* **[Inventaire](https://hub.docker.com/r/inventaire/inventaire)**: a Docker image packaging: + * the Inventaire [server](https://git.inventaire.io/inventaire/), which comes with its embedded database: LevelDB + * the Inventaire [client](https://git.inventaire.io/inventaire-client/) +* **[CouchDB](https://hub.docker.com/_/couchdb)**: the primary database used by the Inventaire server +* **[Elasticsearch](https://hub.docker.com/_/elasticsearch)**: a secondary database used by Inventaire for text and geographic search features +* **[Nginx](https://hub.docker.com/_/nginx)**: a reverse proxy with TLS termination thank to Let's Encrypt [certbot](https://hub.docker.com/r/certbot/certbot). + +The service orchestration is implemented using Docker Compose. + +> 🔧 This document is for people wanting to self-host the full Inventaire Suite. If you are looking for the individual Inventaire image, head over to [hub.docker.com/r/inventaire/inventaire](https://hub.docker.com/r/inventaire/inventaire). + +> 💡 This document presumes familiarity with basic Linux administration tasks and with Docker and Docker Compose. ## Summary -- [Requirements](#requirements) -- [Install](#install) +- [Quickstart](#quickstart) + - [Requirements](#requirements) + - [Hardware](#hardware) + - [Software](#software) + - [Domain name](#domain-name) + - [Open ports](#open-ports) +- [Initial setup](#initial-setup) + - [Download this repository](#download-this-repository) + - [Initial configuration](#initial-configuration) + - [Generate a TLS certificate](#generate-a-tls-certificate) - [Usage](#usage) - [Tips](#tips) - - [Fixtures](#fixtures) - - [Tests](#tests) - - [Push git commits](#push-git-commits) - - [Rootless Docker](#rootless-docker) - - [Run inventaire server and client outside of Docker](#run-inventaire-server-and-client-outside-of-docker) - [Troubleshooting](#troubleshooting) - [Elasticsearch errors](#elasticsearch-errors) - - [Quieting CouchDB notice](#quieting-couchdb-notice) -## Requirements +## Quickstart +### Requirements +#### Hardware +* Network connection with a public IP address +* 4 GB RAM +* 10 GB free disk space -- [docker-compose](https://docs.docker.com/compose/gettingstarted/) up and ready -- git +#### Software +* [Docker](https://docs.docker.com/get-started/get-docker/) >= v22.0 +* [Docker compose](https://docs.docker.com/compose/gettingstarted/) >= v2 +* [git](https://git-scm.com/) -## Install +#### Domain name +> Ignore this section if you are just testing on your local machine +You need a DNS records that resolves to your machine's public IP address + +#### Open ports +> Ignore this section if you are just testing on your local machine + +Your machine's firewall should let the http ports (`80` and `443`) open. + +## Initial setup + +### Download this repository ```sh -git clone https://github.com/inventaire/docker-inventaire.git +git clone https://git.inventaire.io/docker-inventaire.git cd docker-inventaire ``` -Clone `inventaire` core application [server](https://github.com/inventaire/inventaire) +### Initial configuration +Copy the `dotenv` file to `.env` +```sh +cp dotenv .env +``` +and open this new `.env` file with a text editor to customize the variables (mainly adding your own domain name, and setup a couchdb password) + +#### Generate a TLS certificate +> Ignore this section if you are just testing on your local machine + +Generate the first TLS certificate with Let's Encrypt ```sh -git clone https://github.com/inventaire/inventaire.git +docker run -it --rm --name certbot -p 80:80 -v "$(pwd)/certbot/conf:/etc/letsencrypt" certbot/certbot certonly --standalone ``` -Build - -```sh -docker-compose build -``` - -Download Node dependencies and install the [client repository](https://github.com/inventaire/inventaire-client): - -```sh -docker-compose run --rm inventaire npm install -``` - -Configure inventaire so that it can connect to CouchDB. For that, create a file `config/local.cjs` with the following command: - -```sh -echo "module.exports = { - db: { - username: 'yourcouchdbusername', - password: 'yourcouchdbpassword' - } -} -" > ./inventaire/config/local.cjs -``` - -NB: Those username and password should match the `COUCHDB_USER` and `COUCHDB_PASSWORD` environment variables set in `docker-compose.yml` - ## Usage -Start CouchDB, Elasticsearch, and the Inventaire [server](https://github.com/inventaire/inventaire) in development mode (modifications to the server files will reload the server), by default on port 3006 +Start all the services (Nginx, CouchDB, Elasticsearch, and the Inventaire [server](https://git.inventaire.io/inventaire)) in production mode: ```sh -docker-compose up +docker compose up -d ``` -To also work on the [client](https://github.com/inventaire/inventaire-client), you need to also start the webpack dev server: +Alternatively, to test locally, you can start only Inventaire and its dependencies (CouchDB and Elasticsearch) without Nginx, with the following command: ```sh -cd inventaire/client -npm run watch +docker compose up inventaire ``` ## Tips -General tips on how to run Inventaire can be found in the [server repository docs](https://github.com/inventaire/inventaire/tree/main/docs). Here after are some additional Docker-specific tips. - -### Fixtures - -In case you would like to play with out-of-the-box data. - -Run api tests to populate tests dbs (see Tests section) - -```sh -docker-compose -f docker-compose.yml -f docker-compose.test.yml exec inventaire npm run test-api -``` - -- Replicate `*-tests` dbs documents into `*` dbs - -```sh -`docker-compose exec inventaire npm run replicate-tests-db` -``` - -### Tests - -Start services with test environnement with [multiple compose files](https://docs.docker.com/compose/extends/#understanding-multiple-compose-files) - -```sh -docker-compose -f docker-compose.yml -f docker-compose.test.yml up -``` - -Execute tests script - -```sh -docker-compose exec inventaire npm run test-api -``` - -or execute directly the test command - -```sh -docker-compose exec inventaire npm test /opt/inventaire/path/to/test/file -``` - -Tip : create a symbolic link on your machine between the inventaire folder and docker working directory on your machine at `/opt/`, in order to autocomplete path to test file to execute - -```sh -sudo ln ~/path/to/inventaire-docker/inventaire /opt -s -``` - -Alternatively, as root in inventaire container: - -```sh -mkdir /supervisor/path/to/inventaire -ln -s /opt/ /supervisor/path/to/inventaire -``` - -### Push git commits - -To keep things simple, this installation steps above clone repositories in https, but if you want to push to a branch with ssh, you will probably need to change the repositories `origin`: -```sh -cd inventaire -git remote set-url origin git@github.com:inventaire/inventaire.git -cd client -git remote set-url origin git@github.com:inventaire/inventaire-client.git -``` - -### Rootless Docker - -Docker Engine v20.10 is now available in rootless mode. If you would like to try it, you may follow the [official guide](https://docs.docker.com/engine/security/rootless/) (including command `export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock`). - -Start the inventaire install steps above, before installing dependencies, make sure that the owner of inventaire folder is the same as the owner inside the container. - -Delete `network_host` occurences from `docker-compose.yml` and adapt the `config/local.cjs` in consequence: - -```js -module.exports = { - protocol: 'http', - port: 3006, - host: 'inventaire', - db: { - username: 'couchdb', - password: 'password', - protocol: 'http', - hostname: 'couch' - }, - elasticsearch: { - host:'http://elasticsearch:9200' - } -} -``` - -### Run inventaire server and client outside of Docker - -It can sometimes be more convenient to keep CouchDB and Elasticsearch in Docker, but to run the Inventaire server and client outside. For this you will need to: -- have [NodeJS](https://nodejs.org/) >= v16 installed on your machine, which should make both `node` and `npm` executables accessible in your terminal - -Then you can start CouchDB and Elasticsearch in the background -```sh -docker-compose up couchdb elasticsearch -d -``` - -Start the Inventaire server in development mode -```sh -cd inventaire -npm run watch -``` - -And in another terminal, start the client Webpack dev server -```sh -cd inventaire/client -npm run watch -``` +General tips on how to run Inventaire can be found in the [server repository docs](https://git.inventaire.io/inventaire/tree/main/docs). ## Troubleshooting ### Elasticsearch errors + - `max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]`: fix by running the command `sudo sysctl -w vm.max_map_count=262144` on your host machine See also [Elasticsearch with Docker](https://www.elastic.co/guide/en/elasticsearch/reference/7.9/docker.html) - -### Quieting CouchDB notice -CouchDB may warn constantly that `_users` database does not exist, [as documented](https://docs.couchdb.org/en/latest/setup/single-node.html), you can create de database with: - -`curl -X PUT http://127.0.0.1:5984/_users` diff --git a/configs/0_local.ini b/configs/0_local.ini deleted file mode 100644 index c9cc7e4..0000000 --- a/configs/0_local.ini +++ /dev/null @@ -1,6 +0,0 @@ -; CouchDB configuration file, named to have a lower priority than ./docker.ini -; See https://docs.couchdb.org/en/stable/config/index.html - -[fabric] -; Prevent "No DB shards could be opened" errors, see https://github.com/apache/couchdb/issues/4497 -shard_timeout_min_msec = 5000 diff --git a/couchdb/Dockerfile.couchdb b/couchdb/Dockerfile.couchdb new file mode 100644 index 0000000..bfb5c62 --- /dev/null +++ b/couchdb/Dockerfile.couchdb @@ -0,0 +1,5 @@ +FROM couchdb:3.4.2 + +COPY local.ini /opt/couchdb/etc/local.d/local.ini + +# Inherit ENTRYPOINT and CMD from couchdb image diff --git a/couchdb/local.ini b/couchdb/local.ini new file mode 100644 index 0000000..92f6b96 --- /dev/null +++ b/couchdb/local.ini @@ -0,0 +1,8 @@ +[couchdb] +; Automatically create the system databases on startup (in particular `_users`) +; see https://docs.couchdb.org/en/stable/config/couchdb.html#couchdb/single_node +single_node = true + +[fabric] +; Prevent "No DB shards could be opened" errors, see https://github.com/apache/couchdb/issues/4497 +shard_timeout_min_msec = 5000 diff --git a/docker-compose.yml b/docker-compose.yml index 682c185..191810a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,28 +1,86 @@ -version: '3' services: + inventaire: + image: inventaire/inventaire:latest + env_file: .env + # You may also build image from sources with: + # build: + # context: ./inventaire + # dockerfile: Dockerfile.inventaire + depends_on: + - couchdb + - elasticsearch + # Uncomment if you want to access the code: + volumes: + - inventaire-server:/opt/inventaire + logging: + options: + max-size: "10m" + max-file: "3" + restart: unless-stopped couchdb: - image: couchdb:3.4.2 - ports: - - "5984:5984" - environment: - COUCHDB_USER: "yourcouchdbusername" - COUCHDB_PASSWORD: "yourcouchdbpassword" + build: + context: ./couchdb + dockerfile: Dockerfile.couchdb + env_file: .env + # Uncomment ports to get access to the db + # ie. for database transformation, querying, UI access (http://localhost:5984/_utils/) + # /!\ Beware that exposing container ports like this might bypass your firewall rules + # See https://docs.docker.com/engine/install/ubuntu/#firewall-limitations + # Keeping it bound to localhost (which is the default) should be safe though + # (that is, contrary to binding to 0.0.0.0) + # ports: + # - "5984:5984" volumes: - 'couchdb:/opt/couchdb/data' - - './configs:/opt/couchdb/etc/local.d' - tty: true + restart: unless-stopped elasticsearch: - image: elasticsearch:7.16.2 + image: elasticsearch:7.17.28 environment: - - 'http.host=0.0.0.0' - - 'transport.host=127.0.0.1' + - 'http.host=elasticsearch' + - 'transport.host=elasticsearch' # See https://www.elastic.co/guide/en/elasticsearch/reference/7.16/docker.html - 'discovery.type=single-node' - - 'ES_JAVA_OPTS=-Xms1g -Xmx1g' + # Limit memory usage to 512MB, that should be enough at first + # See https://www.elastic.co/guide/en/elasticsearch/reference/current/advanced-configuration.html + - 'ES_JAVA_OPTS=-Xms512m -Xmx512m' + - 'xpack.security.enabled=false' + - 'ingest.geoip.downloader.enabled=false' + - 'logger.org.elasticsearch=ERROR' + - 'logger.com.azure.core=ERROR' + - 'logger.org.apache=ERROR' volumes: - 'elasticsearch:/usr/share/elasticsearch/data' - ports : - - '127.0.0.1:9200:9200' + restart: unless-stopped + nginx: + build: + context: ./nginx + dockerfile: Dockerfile.nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/templates:/etc/nginx/templates/ + - ./nginx/snippets:/etc/nginx/snippets + - inventaire-server:/opt/inventaire + - certbot-www:/var/www/certbot + - ./certbot/conf:/etc/letsencrypt + env_file: .env + depends_on: + # Required to be able to define the `inventaire` host as an upstream + - inventaire + restart: unless-stopped + certbot: + image: certbot/certbot:latest + volumes: + - ./certbot/conf:/etc/letsencrypt + - certbot-www:/var/www/certbot + restart: unless-stopped + entrypoint: /bin/sh -c "trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot; sleep 12h & wait $${!}; done;" + depends_on: + - nginx + volumes: couchdb: - elasticsearch: \ No newline at end of file + elasticsearch: + certbot-www: + inventaire-server: diff --git a/dotenv b/dotenv new file mode 100644 index 0000000..c70bda5 --- /dev/null +++ b/dotenv @@ -0,0 +1,53 @@ +# Your website domain name +PUBLIC_HOSTNAME=an-inventaire-site.org + +# Generic user, to modify only for good reason +COUCHDB_USER=couchdb +# Consider passwords with no less than 32 charracters, for example: +# cat /dev/urandom | tr -dc A-Za-z0-9-_ | head -c 32 +COUCHDB_PASSWORD=your_password + +INSTANCE_NAME='My Inventaire Instance' + +# Will be displayed on landing screen +ORG_NAME='Example Organization' +ORG_URL='https://inventaire.example.org' + +# Users receiving emails from the instance can reply to this +CONTACT_ADDRESS='contact@inventaire.example.org' + +MAILER_SMTP_HOST=smtp.example.org +MAILER_SMTP_PORT=587 +MAILER_SMTP_USERNAME=your_username_on_smtp.example.org +MAILER_SMTP_PASSWORD=your_password_on_smtp.example.org + +# Required to use MapBox tiles within leaflet maps +# See https://console.mapbox.com/account/access-tokens/ +MAP_TILES_ACCESS_TOKEN='youraccesstoken' + +# [OPTIONAL] Setup Matomo analytics https://matomo.org/ +MOTOMO_ENABLED=false +MOTOMO_ENDPOINT=https://yourmatomoendpoint/matomo.php +MOTOMO_IDSITE=1 +MOTOMO_REC=1 + +MEDIA_STORAGE_MODE=local +# Set MEDIA_STORAGE_MODE=swift and the following variables +# to store users and groups images in an OpenStack Swift container +SWIFT_USERNAME=customizedInLocalConfig +SWIFT_PASSWORD=customizedInLocalConfig +SWIFT_AUTH_URL=https://openstackEndpointToCustomize +SWIFT_PUBLIC_URL=https://swiftPublicURL +SWIFT_TENANT_NAME=12345678 +SWIFT_REGION=SBG-1 + +######## +# Everything below is a default configuration +# Modify if you know what you are doing +######## + +NODE_ENV=production +NODE_APP_INSTANCE=federated + +# Conventional port for Inventaire server in federated mode +INVENTAIRE_PORT=3016 diff --git a/inventaire/Dockerfile.inventaire b/inventaire/Dockerfile.inventaire new file mode 100644 index 0000000..888c9b6 --- /dev/null +++ b/inventaire/Dockerfile.inventaire @@ -0,0 +1,39 @@ +FROM node:20-slim + +RUN apt-get update \ + && apt-get install -y curl git graphicsmagick inotify-tools jq \ + && apt-get clean && rm -rf /var/lib/apt/lists/* \ + && mkdir -p /opt/inventaire \ + && chown -R 1000:1000 /opt + +# Default to the same user as the host (override from command line if needed) +# Known benefits: +# - allows to handle leveldb with level-party from both the host and container at the same time +USER 1000:1000 + +WORKDIR /opt/inventaire + +# - Create the client folder to prevent the server postinstall to run `npm run install-client` as it does it with the wrong workdir and env +# - Create the public/sitemaps folder to prevent the client postinstall to run `npm run generate-sitemaps` (which needs to be updated to support non-inventaire.io instances) +RUN git clone http://github.com/inventaire/inventaire --depth 1 . \ + && mkdir -p /opt/inventaire/client \ + && npm ci --omit=dev \ + && npm run build \ + && git clone https://git.inventaire.io/inventaire-client.git ./client --branch docker --depth 1 \ + && mkdir -p /opt/inventaire/client/public/sitemaps + +WORKDIR /opt/inventaire/client + +# Include dev dependencies (webpack, svelte-checks) at first to be able to build during the postinstall script +RUN npm ci \ + && rm -rf node_modules \ + && npm ci --omit=dev --ignore-scripts \ + && npm cache clean --force + +COPY docker-entrypoint.sh /opt/docker-entrypoint.sh + +WORKDIR /opt/inventaire + +# Avoid using npm script to start the server +# See https://adambrodziak.pl/dockerfile-good-practices-for-node-and-npm#heading-use-node-not-npm-to-start-the-server +ENTRYPOINT [ "/opt/docker-entrypoint.sh" ] diff --git a/inventaire/docker-entrypoint.sh b/inventaire/docker-entrypoint.sh new file mode 100755 index 0000000..f1cbd75 --- /dev/null +++ b/inventaire/docker-entrypoint.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -eu + +cd /opt/inventaire + +# Overwrites the local config with environment variables every time the container is restarted +# This file can be itself overwritten either by ./config/local-production.cjs (assuming NODE_ENV=production) or by NODE_CONFIG +# See https://github.com/node-config/node-config/wiki/Configuration-Files#file-load-order +# and https://github.com/node-config/node-config/wiki/Environment-Variables#node_config +cat > "./config/local.cjs" << EOF +module.exports = { + hostname: 'inventaire', + port: '${INVENTAIRE_PORT}', + publicHostname: '${PUBLIC_HOSTNAME}', + instanceName: '${INSTANCE_NAME}', + orgName: '${ORG_NAME}', + orgUrl: '${ORG_URL}', + contactAddress: '${CONTACT_ADDRESS}', + db: { + username: '${COUCHDB_USER}', + password: '${COUCHDB_PASSWORD}', + hostname: 'couchdb', + }, + elasticsearch: { + origin: 'http://elasticsearch:9200', + }, + mailer: { + disabled: false, + nodemailer: { + host: '${MAILER_SMTP_HOST}', + port: ${MAILER_SMTP_PORT}, + auth: { + user: '${MAILER_SMTP_USERNAME}', + pass: '${MAILER_SMTP_PASSWORD}' + }, + }, + }, + mapTilesAccessToken: '${MAP_TILES_ACCESS_TOKEN}', + matomo: { + enabled: ${MOTOMO_ENABLED}, + endpoint: '${MOTOMO_ENDPOINT}', + idsite: ${MOTOMO_IDSITE}, + rec: ${MOTOMO_REC}, + }, + mediaStorage: { + mode: '${MEDIA_STORAGE_MODE}', + swift: { + username: '${SWIFT_USERNAME}', + password: '${SWIFT_PASSWORD}', + authUrl: '${SWIFT_AUTH_URL}', + publicURL: '${SWIFT_PUBLIC_URL}', + tenantName: '${SWIFT_TENANT_NAME}', + region: '${SWIFT_REGION}', + }, + }, + +} +EOF + +./scripts/typescript/start_built_server.sh diff --git a/nginx/Dockerfile.nginx b/nginx/Dockerfile.nginx new file mode 100644 index 0000000..5d4c89f --- /dev/null +++ b/nginx/Dockerfile.nginx @@ -0,0 +1,5 @@ +FROM nginx + +RUN [ ! -f /etc/nginx/dhparam.pem ] && openssl dhparam -out /etc/nginx/dhparam.pem 2048 + +RUN mkdir -p /tmp/nginx/tmp /tmp/nginx/resize/img/users /tmp/nginx/resize/img/groups /tmp/nginx/resize/img/entities /tmp/nginx/resize/img/remote /tmp/nginx/resize/img/assets && chown -R nginx:nginx /tmp/nginx diff --git a/nginx/snippets/security_headers.conf b/nginx/snippets/security_headers.conf new file mode 100755 index 0000000..7614ecc --- /dev/null +++ b/nginx/snippets/security_headers.conf @@ -0,0 +1,12 @@ +# add_header from parent blocks are ignored when the current block also calls add_header +# Thus the need for this snippet, to redefine the same headers in many blocks +# See http://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header +add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; +# opt out Google Floc see: https://plausible.io/blog/google-floc#how-to-opt-out-of-floc-as-a-web-developer-set-a-permissions-policy +add_header Permissions-Policy interest-cohort=(); +# source: https://gist.github.com/plentz/6737338 +add_header X-Frame-Options "SAMEORIGIN" always; +# source: https://scotthelme.co.uk/hardening-your-http-response-headers/#x-content-type-options +add_header X-Content-Type-Options 'nosniff' always; +# source: https://scotthelme.co.uk/a-new-security-header-referrer-policy/ +add_header Referrer-Policy 'strict-origin' always; diff --git a/nginx/snippets/ssl.conf b/nginx/snippets/ssl.conf new file mode 100644 index 0000000..434b7f7 --- /dev/null +++ b/nginx/snippets/ssl.conf @@ -0,0 +1,11 @@ +# Recommended by https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1f&guideline=5.6 + +ssl_session_timeout 1d; +ssl_session_cache shared:SSL:50m; +ssl_session_tickets off; +ssl_protocols TLSv1.2 TLSv1.3; +ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; +ssl_prefer_server_ciphers on; +ssl_stapling on; +ssl_stapling_verify on; +ssl_dhparam /etc/nginx/dhparam.pem; diff --git a/nginx/templates/default.conf.template b/nginx/templates/default.conf.template new file mode 100755 index 0000000..ffbcd04 --- /dev/null +++ b/nginx/templates/default.conf.template @@ -0,0 +1,207 @@ +# The INVENTAIRE_PORT and PUBLIC_HOSTNAME variables are set with nginx image function, +# which will extract environment variables before nginx starts +# See https://hub.docker.com/_/nginx + +upstream inv { + server inventaire:${INVENTAIRE_PORT} fail_timeout=5s; +} + +# Using error_page as a way to have a named location that can +# then be shared between several locations, see: +# https://serverfault.com/questions/908086/nginx-directly-send-from-location-to-another-named-location +# https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/#what-to-do-instead +# Contrary to what the documentation says, the HTTP verbs aren't all converted to GET +# http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page +error_page 543 = @invserver; + +server { + listen 80; + listen [::]:80; + + # Required to be able to run `certbot -w /var/www/html/` + location /.well-known/ { + root /var/www/html/; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + + http2 on; + + server_name ${PUBLIC_HOSTNAME}; + + ssl_certificate /etc/letsencrypt/live/${PUBLIC_HOSTNAME}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${PUBLIC_HOSTNAME}/privkey.pem; + + include /etc/nginx/snippets/ssl.conf; + + client_max_body_size 25M; + + # Disabling compression to mitigate BREACH exploit + # https://en.wikipedia.org/wiki/BREACH_(security_exploit)#Mitigation + # http://security.stackexchange.com/questions/39925/breach-a-new-attack-against-http-what-can-be-done + # until we can confidently say that HTTP/2 solves the issue? https://blog.cloudflare.com/hpack-the-silent-killer-feature-of-http-2 + gzip off; + + # On-The-Fly Image Resizer + + # URLs look like /img/users/300x1200/8185d4e039f52b4faa06a1c277133e9a8232551b + # for locally hosted images + # or /img/remote/300x1200/630022006?href=http%3A%2F%2Fescaped.url + # for remote images, with 630022006 being the hash of the passed href + # generated by [hashCode](https://git.inventaire.io/inventaire/blob/35b1e63/server/lib/utils/base.js#L69-L80) + + # The hack: I couldn't make the proxy_store work: it never hits the cache, but + # it does put the resized images in /tmp/nginx/resize, so using a try_files + # directive instead + + # Sometimes, for some unidentified reason, the cached files end up empty, so it can be useful to add a root cron to remove those files: + # 0 4 * * * /usr/bin/find /tmp/nginx -type f -size 0 -delete + + # Do not remove the (.*) capture group as it seems to be required by the try_files + location ~ ^/img/(groups|users|entities|assets)/(.*) { + include /etc/nginx/snippets/security_headers.conf; + root /tmp/nginx/resize; + default_type "image/jpeg"; + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header X-File-Cache "hit"; + add_header Content-Security-Policy "sandbox"; + try_files $uri @invimg; + limit_except GET { + deny all; + } + } + + # Same as above, but without the immutable + location ~ ^/img/remote/(.*) { + include /etc/nginx/snippets/security_headers.conf; + root /tmp/nginx/resize; + default_type "image/jpeg"; + add_header X-File-Cache "hit"; + add_header Content-Security-Policy "sandbox"; + try_files $uri @invimg; + limit_except GET { + deny all; + } + } + + location ~ ^/img/ { + return 404; + } + + location @invimg { + include /etc/nginx/snippets/security_headers.conf; + default_type "image/jpeg"; + # add_header Content-Type "image/jpeg"; + add_header X-File-Cache "miss"; + add_header Content-Security-Policy "sandbox"; + proxy_temp_path /tmp/nginx/tmp; + proxy_store /tmp/nginx/resize$uri; + proxy_store_access user:rw group:rw all:r; + proxy_http_version 1.1; + proxy_pass http://inv; + } + + root /opt/inventaire/client; + location /public { + include /etc/nginx/snippets/security_headers.conf; + limit_except GET { + deny all; + } + gzip_static on; + # Let resources that can't be cache busted + # - such as opensearch.xml or robots.txt - + # out of this caching policy + if ($uri ~ "^/public/(dist|fonts)/" ) { + include /etc/nginx/snippets/security_headers.conf; + add_header Cache-Control "public, max-age=31536000, immutable"; + # All headers that aren't in the last block won't be taken in account + # thus the need to have CORS headers here too + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET' always; + } + } + + # Pass the request to the node.js server + # with some correct headers for proxy-awareness + location /api { + return 543; + } + + location /.well-known/webfinger { + return 543; + } + + # Let the API server handle all but /public JSON and RSS requests + location ~ "^/[^p].*\.(json|rss)$" { + limit_except GET { + deny all; + } + return 543; + } + + location @invserver { + include /etc/nginx/snippets/security_headers.conf; + # Let the server decide when CORS headers should be added + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Host $host; + + # Set a large value to let the API determine the appropriate + # timeout per endpoint + # http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout + proxy_read_timeout 3600; + proxy_redirect off; + proxy_http_version 1.1; + proxy_pass http://inv; + } + + location = /favicon.ico { + include /etc/nginx/snippets/security_headers.conf; + try_files /public/$uri /public/images/$uri; + expires 30d; + add_header Cache-Control "public"; + } + + location = /robots.txt { + include /etc/nginx/snippets/security_headers.conf; + gzip_static on; + try_files /public/$uri /$uri; + expires 1d; + add_header Cache-Control "public"; + } + + # Prevent exposing git folders such as /public/i18n/.git + # For why this rule takes precedence over location /public/ + # see http://stackoverflow.com/a/34262192/3324977 + location ~ /\.git { + deny all; + } + + location ^~ '/.well-known/acme-challenge' { + include /etc/nginx/snippets/security_headers.conf; + default_type "text/plain"; + root /var/www/certbot; + } + + location / { + include /etc/nginx/snippets/security_headers.conf; + gzip_static on; + limit_except GET { + deny all; + } + # index.html should always be fresh out of the server + # time is negative => “Cache-Control: no-cache” + # http://nginx.org/en/docs/http/ngx_http_headers_module.html + # Those headers should be set here and not at "location /" as they would be ignored (cf http://serverfault.com/a/786248) + expires -1; + # The remaining routes (/users, /entity, etc) should be handled by the client router + rewrite .* /public/index.html break; + } +} diff --git a/scripts/docker_publish.sh b/scripts/docker_publish.sh new file mode 100755 index 0000000..4ca0c58 --- /dev/null +++ b/scripts/docker_publish.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -eu + +cwd="$PWD" + +cd ./inventaire + +echo -n "Enter version number (ex: 3.0.0-beta): " +read -r version + +docker build -t inventaire -f ./Dockerfile.inventaire . + +docker tag inventaire "inventaire/inventaire:${version}" +docker tag inventaire inventaire/inventaire:latest + +docker push "inventaire/inventaire:${version}" +docker push inventaire/inventaire:latest + +cd "$cwd"