diff --git a/Caddyfile b/Caddyfile index cad756b..4a3c3a8 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,27 +1,14 @@ # Production Caddyfile for appointment booking application -# Replace your-domain.com with your actual domain +# Replace your-admin-domain.com with your actual admin domain { on_demand_tls { ask http://app:3000/api/public/domains - burst 5 - interval 60s } } :443 { reverse_proxy app:3000 - # Security headers - header { - -Server - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" - X-Content-Type-Options "nosniff" - X-Frame-Options "DENY" - X-XSS-Protection "1; mode=block" - Referrer-Policy "strict-origin-when-cross-origin" - Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'" - } - encode gzip log { @@ -41,17 +28,6 @@ your-admin-domain.com { reverse_proxy app:3000 - # Security headers - header { - -Server - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" - X-Content-Type-Options "nosniff" - X-Frame-Options "DENY" - X-XSS-Protection "1; mode=block" - Referrer-Policy "strict-origin-when-cross-origin" - Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'" - } - encode gzip log { diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 65cda14..db303b1 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,9 +1,8 @@ services: postgres: image: postgres:16-alpine - container_name: open-reception-postgres + container_name: postgres restart: unless-stopped - user: postgres environment: POSTGRES_DB_FILE: /run/secrets/postgres_db POSTGRES_USER_FILE: /run/secrets/postgres_user @@ -15,7 +14,6 @@ services: - postgres_password volumes: - postgres_data:/var/lib/postgresql/data - - ./init-db:/docker-entrypoint-initdb.d:ro networks: - open-reception-internal healthcheck: @@ -24,10 +22,10 @@ services: "CMD-SHELL", "pg_isready -U $$(cat /run/secrets/postgres_user) -d $$(cat /run/secrets/postgres_db)", ] - interval: 10s - timeout: 5s - retries: 5 - start_period: 30s + interval: 5s + timeout: 3s + retries: 10 + start_period: 60s security_opt: - no-new-privileges:true cap_drop: @@ -44,23 +42,25 @@ services: read_only: true app: - image: openreception/open-reception:${VERSION:-latest} - container_name: open-reception-app + # use specific version here to watch for breaking changes + image: openreception/open-reception:latest + container_name: app restart: unless-stopped user: "1001:1001" environment: NODE_ENV: production - SMTP_HOST: /run/secrets/smtp_host - SMTP_PORT: /run/secrets/smtp_port - SMTP_SECURE: /run/secrets/smtp_secure - SMTP_USER: /run/secrets/smtp_user - SMTP_PASS: /run/secrets/smtp_pass - SMTP_FROM_NAME: /run/secrets/smtp_from_name - SMTP_FROM_EMAIL: /run/secrets/smtp_from_email secrets: - postgres_db - postgres_user - postgres_password + - smtp_host + - smtp_port + - smtp_secure + - smtp_user + - smtp_pass + - smtp_from_name + - smtp_from_email + - jwt_secret depends_on: postgres: condition: service_healthy @@ -71,8 +71,8 @@ services: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/api/health"] interval: 30s timeout: 10s - retries: 3 - start_period: 40s + retries: 5 + start_period: 60s security_opt: - no-new-privileges:true cap_drop: @@ -85,26 +85,30 @@ services: export POSTGRES_USER=$$(cat /run/secrets/postgres_user) export POSTGRES_PASSWORD=$$(cat /run/secrets/postgres_password) export DATABASE_URL=\"postgres://$$POSTGRES_USER:$$POSTGRES_PASSWORD@postgres:5432/$$POSTGRES_DB\" + export SMTP_HOST=$$(cat /run/secrets/smtp_host) + export SMTP_PORT=$$(cat /run/secrets/smtp_port) + export SMTP_SECURE=$$(cat /run/secrets/smtp_secure) + export SMTP_USER=$$(cat /run/secrets/smtp_user) + export SMTP_PASS=$$(cat /run/secrets/smtp_pass) + export SMTP_FROM_NAME=$$(cat /run/secrets/smtp_from_name) + export SMTP_FROM_EMAIL=$$(cat /run/secrets/smtp_from_email) + export JWT_SECRET=$$(cat /run/secrets/jwt_secret) exec node build/index.js " read_only: true caddy: - image: caddy:2-alpine - container_name: open-reception-caddy + image: caddy:2 + container_name: caddy restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config + - caddy-data:/data" networks: - open-reception-internal - depends_on: - app: - condition: service_healthy security_opt: - no-new-privileges:true cap_drop: @@ -119,10 +123,10 @@ secrets: file: ./secrets/postgres_user.txt postgres_password: file: ./secrets/postgres_password.txt - smtp_port: - file: ./secrets/smtp_port.txt smtp_host: file: ./secrets/smtp_host.txt + smtp_port: + file: ./secrets/smtp_port.txt smtp_secure: file: ./secrets/smtp_secure.txt smtp_user: @@ -133,14 +137,22 @@ secrets: file: ./secrets/smtp_from_name.txt smtp_from_email: file: ./secrets/smtp_from_email.txt + jwt_secret: + file: ./secrets/jwt_secret.txt volumes: postgres_data: driver: local - caddy_data: - driver: local - caddy_config: + driver_opts: + type: none + o: bind + device: /opt/openreception/postgres + caddy-data: driver: local + driver_opts: + type: "none" + o: "bind" + device: "/opt/openreception/caddy/data" networks: open-reception-internal: diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..20af379 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,29 @@ +# Deploy OpenReception to a single server + +This guide will lead you through setting up OpenReception on linux a server of your choice. + +> Too complicated? Managed Hosting is available at [open-reception.com](https://open-reception.com). + +## Requirements + +This setup requires + +- shell access to a root server +- docker with docker compose installed +- a small server with 2 cores and 2gb of ram should get you started + +## Recommendations/ Considerations + +- Update your operating system on a regular basis. Use automatic security updates like `unattended-upgrades` for the very short term issues. +- Encrypt the drive where your database is stored to prevent data theft by just pulling the hard drive. You can use `luks`. +- Protect access with `fail2ban`, `ufw`, `crowdsec`, proper user permissions and other best-practices. +- Make regular backups of your database and test them regularly. You can use `restic`. +- Monitor your server loads and logs over time to detect peak usages (with insufficient ram or cpu power) and malicious activities. + +## Setup + +1. Use our [production docker-compose example](../docker-compose.prod.yml) +1. Use our [Caddyfile example](../Caddyfile) +1. Adjust the settings in these two files above to your needs. +1. Run `docker-compose up -d` or `docker compose up -d` depending on your docker installation. +1. Proceed to secure your instance by opening your admin domain in a browser.