It's a start

This commit is contained in:
Frank "PHiAX" Weggelaar 2025-12-05 11:02:48 +01:00
parent 302b725dc1
commit e1a9018ca6
34 changed files with 774 additions and 1 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# Specify filepatterns you want git to ignore.
.env

View file

@ -17,4 +17,4 @@ The needed parts as of right now
- Ansible
- To automate the software layers and container deployments
- PXE
- For local cloud-init? with OpenTofu? (still researching this
- For local cloud-init? with OpenTofu? (still researching this)

View file

@ -0,0 +1 @@
Is this file needed?

View file

@ -0,0 +1,6 @@
namespace: phiax.nl
name: testicle
version: 1.0.0
author: Frank "PHiAX" Weggelaar
description: Ansible collection for doing all the things.
dependencies: []

View file

@ -0,0 +1,5 @@
raspberries:
hosts:
app-octoprint.phiax.nl:
sensor-adsb.phiax.nl:

View file

@ -0,0 +1,21 @@
---
- hosts: all
vars_prompt:
- name: "install_dir"
prompt: "Install directory"
private: false
default: "/opt/OctoPrint"
- name: "new_hostname"
prompt: "Hostname"
private: false
default: "octoprint.local"
- name: "appport"
prompt: "OctoPrint listening Port"
private: false
default: 5000
- name: "webcam_port"
prompt: "Webcam listening Port"
private: false
default: 8080
roles:
- octoprint

View file

@ -0,0 +1,29 @@
---
# Notice that "# noqa: package-latest" is included in this file. This disabled a specific check for the Ansible linter,
# see: https://ansible.readthedocs.io/projects/lint/usage/#muting-warnings-to-avoid-false-positives.
# For a purely reproducible build this would be a good suggestion but I'm willing to take the risk with the Pi.
- name: Install raspberry pi
hosts: all
# vars_files:
# - vault.yml
# - versions.yml
roles:
# These roles are disabled after they have being applied once for performance reasons, it should be safe to enable them again.
# Notice that this role changes some settings on reruns (on the "Change various sysctl-settings" task), doesn't seem problematic though.
- role: devsec.hardening.ssh_hardening
become: true
- role: packages
become: true
vars:
# devsec.hardening.ssh_hardening vars:
ssh_client_port: 22 # Default, but duplicated here for documentation purpose. Not changed because its only accessible via LAN.
ssh_client_password_login: false # Default, but duplicated here for documentation purpose.
ssh_allow_tcp_forwarding: true
tasks:
# This task can be handy for debugging gathered facts, uncomment it if necessary:
# - name: Store gathered facts in local file
# delegate_to: localhost
# ansible.builtin.copy:
# dest: './.ansible_facts.json'
# content: "{{ ansible_facts }}"
# mode: "0600"

View file

@ -0,0 +1,20 @@
- hosts: all
become: true
roles:
- role: system/ohmyzsh
vars:
target_user: "phiax"
ohmyzsh_theme: "agnoster"
ohmyzsh_plugins:
- git
- zsh-autosuggestions
http_fetcher: "curl"
####
# Notes:
#
# Role creates .oh-my-zsh by running upstream installer as the target user (RUNZSH=no, KEEP_ZSHRC=yes).
# Override variables in playbook/inventory to target different users, themes, or plugin lists.
# For systems where zsh path differs, override zsh_path.
#
####

View file

@ -0,0 +1,13 @@
---
collections:
# See: https://galaxy.ansible.com/ui/repo/published/devsec/hardening/
- name: devsec.hardening
version: 10.3.0
# See: https://galaxy.ansible.com/ui/repo/published/prometheus/prometheus/
# Docs: https://prometheus-community.github.io/ansible/branch/main/prometheus_role.html#ansible-collections-prometheus-prometheus-prometheus-role
- name: prometheus.prometheus
version: 0.26.0
roles:
# See: https://galaxy.ansible.com/ui/standalone/roles/geerlingguy/docker/
- name: geerlingguy.docker
version: 7.4.7

View file

@ -0,0 +1,14 @@
---
- name Backup Mikrotik Device Configs
hosts switches
gather_facts no
tasks
- name Fetch running config
mikrotik.cli_command
commands /export compact
register output
- name Save config to file
copy
content {{ output.stdout[0] }}
dest backups{{ inventory_hostname }}.cfg

View file

@ -0,0 +1,3 @@
---
dependencies:
- global

View file

@ -0,0 +1,26 @@
---
- name: Create new user
user: name={{ new_username }}
password={{ new_password |password_hash('sha512') }}
state=present
update_password=on_create
shell=/bin/bash
become: yes
become_method: sudo
- name: Copy sudoer dropin file
template: src=sudoers_dropin.j2
dest=/etc/sudoers.d/{{ new_username }}
owner=root
group=root
mode="0600"
become: yes
become_method: sudo
- name: Add public ssh key to new users authorized key list
authorized_key: user={{ username }}
key="{{ lookup('file', ssh_pub_key) }}"
state=present
become: yes
become_user: "{{ new_username }}"
when: add_ssh_pub_key |bool

View file

@ -0,0 +1,2 @@
Defaults>{{ new_username }} !lecture
{{ new_username }} ALL=(ALL) NOPASSWD:ALL

View file

@ -0,0 +1,11 @@
---
- name: Configure hostname
become: true
ansible.builtin.hostname:
name: "{{ hostname }}"
- name: Remove existing /etc/hosts entry for hostname and add FQDN name
become: true
ansible.builtin.lineinfile:
path: /etc/hosts
regexp: "^127.0.1.1.*$"
line: "127.0.0.1 {{ hostname }}.phiax.nl {{ hostname }}"

View file

@ -0,0 +1,3 @@
---
dependencies:
- global

View file

@ -0,0 +1,7 @@
---
- name: Set static ip address (replace /etc/dhcpcd.conf)
template: src=etc_dhcpcd.conf.j2
dest=/etc/dhcpcd.conf
backup=yes
become: yes
become_method: sudo

View file

@ -0,0 +1,47 @@
# A sample configuration for dhcpcd.
# See dhcpcd.conf(5) for details.
# Allow users of this group to interact with dhcpcd via the control socket.
#controlgroup wheel
# Inform the DHCP server of our hostname for DDNS.
hostname
# Use the hardware address of the interface for the Client ID.
clientid
# or
# Use the same DUID + IAID as set in DHCPv6 for DHCPv4 ClientID as per RFC4361.
#duid
# Persist interface configuration when dhcpcd exits.
persistent
# Rapid commit support.
# Safe to enable by default because it requires the equivalent option set
# on the server to actually work.
option rapid_commit
# A list of options to request from the DHCP server.
option domain_name_servers, domain_name, domain_search, host_name
option classless_static_routes
# Most distributions have NTP support.
option ntp_servers
# Respect the network MTU.
# Some interface drivers reset when changing the MTU so disabled by default.
#option interface_mtu
# A ServerID is required by RFC2131.
require dhcp_server_identifier
# Generate Stable Private IPv6 Addresses instead of hardware based ones
slaac private
# A hook script is provided to lookup the hostname if not set by the DHCP
# server, but it should not be run by default.
nohook lookup-hostname
interface {{iface}}
static ip_address={{new_ip_address}}
static routers={{gateway_address}}
static domain_name_servers={{dns_address}}

View file

@ -0,0 +1,117 @@
---
# The ZSH installation instructions are sourced from this blog:
# https://harshithashok.com/tools/oh-my-zsh-with-starship/
- name: Create the user
when: user_username is not undefined # Skip when no user is provided, we'll asume we're targetting the Ansible user.
block:
- name: Create a new user
become: true
ansible.builtin.user:
append: true
groups:
- users
name: "{{ user_username }}"
# Salt is necessary, see: https://stackoverflow.com/questions/56869949/ansible-user-module-always-shows-changed
password: "{{ user_password | password_hash('sha512', password_salt) }}"
- name: Ensure .ssh directory exists in user home
become: true
become_user: "{{ user_username }}"
ansible.builtin.file:
path: "/home/{{ user_username }}/.ssh"
state: directory
mode: "0700"
# We're assuming that the ansible user has its authorized keys setup before running the playbook and that all created users using this
# rule want the same machines to be able to access them.
- name: Copy over authorized keys from the main ansible user
become: true
ansible.builtin.copy:
remote_src: true
src: "/home/{{ ansible_facts['user_id'] }}/.ssh/authorized_keys"
dest: "/home/{{ user_username }}/.ssh/"
owner: "{{ user_username }}"
group: "{{ user_username }}"
mode: "0600"
- name: Set fact for defining the user which should run the next modules
ansible.builtin.set_fact:
target_user: "{{ ansible_facts['user_id'] if user_username is undefined else user_username }}"
# The "lingering" property seems to be important to Podman, otherwise errors are thrown as mentioned here:
# https://superuser.com/questions/1788594/podman-the-cgroupv2-manager-is-set-to-systemd-but-there-is-no-systemd-user-sess
- name: "Check if lingering is enabled (user: {{ target_user }})"
ansible.builtin.command:
cmd: "loginctl show-user {{ target_user }} --property=Linger"
register: linger_check
changed_when: false
failed_when: false
- name: "Enable systemd \"lingering\" (user: {{ target_user }})"
become: true
ansible.builtin.command:
cmd: "loginctl enable-linger {{ target_user }}"
when: linger_check.rc != 0
changed_when: true
- name: Ensuring ZSH is installed
become: true
ansible.builtin.apt:
pkg:
- acl # Needed to prevent this error: https://stackoverflow.com/questions/46352173/ansible-failed-to-set-permissions-on-the-temporary
- zsh
state: present
- name: Install Oh My ZSH # noqa: command-instead-of-module ignore error since we're removing the script after install.
become: true
become_user: "{{ target_user }}"
ansible.builtin.shell: |
wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh
chmod u+x install.sh
./install.sh --unattended
rm install.sh
args:
executable: /bin/bash
creates: ~/.oh-my-zsh
- name: Install Starship # noqa: command-instead-of-module ignore error since we're removing the script after install.
become: true
ansible.builtin.shell: |
wget https://starship.rs/install.sh
chmod u+x install.sh
./install.sh --yes
rm install.sh
args:
executable: /bin/bash
creates: /usr/local/bin/starship
- name: Install zsh-autosuggestions # noqa: command-instead-of-module ignore error since we're removing the script after install.
become: true
become_user: "{{ target_user }}"
ansible.builtin.command:
cmd: git clone https://github.com/zsh-users/zsh-autosuggestions ~/.oh-my-zsh/custom/plugins/zsh-autosuggestions
creates: ~/.oh-my-zsh/custom/plugins/zsh-autosuggestions
- name: Clear "ZSH_THEME" in ~/.zshrc
become: true
become_user: "{{ target_user }}"
ansible.builtin.lineinfile:
path: ~/.zshrc
regexp: '^ZSH_THEME="[^"]+"$'
line: ZSH_THEME=""
- name: Add the zsh-autosuggestions plugin in ~/.zshrc
become: true
become_user: "{{ target_user }}"
ansible.builtin.lineinfile:
path: ~/.zshrc
regexp: '^plugins=\((.*)(?<!zsh-autosuggestions)\)$'
line: 'plugins=(\1 zsh-autosuggestions)'
backrefs: true
# For some reason snap isn't properly configured and its bin directory isn't added to the $PATH variable.
# This probably has something to do with the hardening rules, instead we'll fix it here.
- name: Add Starship config and Snapcraft to ~/.zshrc
become: true
become_user: "{{ target_user }}"
ansible.builtin.blockinfile:
path: ~/.zshrc
block: |-
# Add Snapcraft to $PATH
export PATH=$PATH:/snap/bin
# Starship
eval "$(starship init zsh)"
- name: Change the default shell of the current user
become: true
ansible.builtin.user:
name: "{{ target_user }}"
shell: /usr/bin/zsh

View file

@ -0,0 +1,2 @@
---
user_add_to_docker_group: false

View file

@ -0,0 +1,10 @@
---
- name: Confirm Certbot plugin containment level
become: true
ansible.builtin.command:
cmd: snap set certbot trust-plugin-with-root=ok
- name: Restart Nginx
become: true
ansible.builtin.systemd:
name: nginx.service
state: restarted

View file

@ -0,0 +1,77 @@
---
- name: Add an apt key by id from a keyserver
become: true
ansible.builtin.apt_key:
url: https://nginx.org/keys/nginx_signing.key
state: present
- name: Add Nginx repository into sources list
become: true
ansible.builtin.apt_repository:
repo: deb https://nginx.org/packages/debian/ {{ ansible_facts['lsb']['codename'] }} nginx
state: present
- name: Add Nginx source repository into sources list
become: true
ansible.builtin.apt_repository:
repo: deb-src https://nginx.org/packages/debian/ {{ ansible_facts['lsb']['codename'] }} nginx
state: present
- name: Install Nginx
become: true
ansible.builtin.apt:
name: nginx # Creates the "nginx" user as well
state: present
- name: Remove default configuration
become: true
ansible.builtin.file:
path: /etc/nginx/conf.d/default.conf
state: absent
notify: Restart Nginx
# ---------- CERTBOT INSTALLATION ---------- #
# See the installation instructions here: https://certbot.eff.org/instructions?ws=nginx&os=debianbuster&tab=wildcard
- name: Install Certbot
become: true
community.general.snap:
name: certbot
classic: true
state: present
notify: Confirm Certbot plugin containment level
- name: Flush handlers # Makes sure that the handler runs
ansible.builtin.meta: flush_handlers
- name: Install Certbot DNS Cloudflare plugin
become: true
community.general.snap:
name: certbot-dns-cloudflare
classic: true
state: present
- name: Set cloudflare variable
ansible.builtin.set_fact:
cloudflare_credential_dir_path: "/root/.secrets/certbot"
cloudflare_credential_filename: cloudflare.ini
- name: Create Certbot credential directory
become: true
ansible.builtin.file:
path: "{{ cloudflare_credential_dir_path }}"
state: directory
mode: '0700'
- name: Place cloudflare credential in certbot user's file
become: true
ansible.builtin.template:
src: cloudflare.ini.j2
dest: "{{ cloudflare_credential_dir_path }}/{{ cloudflare_credential_filename }}"
mode: '0400'
- name: Install the certificate script
become: true
ansible.builtin.template:
src: register_certbot_domain.sh.j2
dest: /usr/local/bin/register_certbot_domain.sh
mode: '0500'
- name: Create the root certificate for my domain
become: true
ansible.builtin.command:
cmd: register_certbot_domain.sh kleinendorst.info
creates: /etc/letsencrypt/live/kleinendorst.info # The certificate directory
# END ------ CERTBOT INSTALLATION ------ END #
- name: Start Nginx
become: true
ansible.builtin.systemd:
name: nginx.service
state: started

View file

@ -0,0 +1,2 @@
# Cloudflare API token used by Certbot
dns_cloudflare_api_token = {{ dns_cloudflare_token }}

View file

@ -0,0 +1,9 @@
#!/bin/bash
# For the --post-hook argument see: https://stackoverflow.com/questions/70002636/https-certbot-certificate-is-renewed-but-connection-not-secure-till-you-restart
/snap/bin/certbot certonly \
--dns-cloudflare \
--dns-cloudflare-propagation-seconds 120 \
--dns-cloudflare-credentials '{{ cloudflare_credential_dir_path }}/{{ cloudflare_credential_filename }}' \
--post-hook "nginx -s reload" \
--agree-tos -m {{ administration_email }} \
-d $1

View file

@ -0,0 +1,3 @@
---
nginx_user: nginx # Created automatically by the apt installation
certbot_user: certbot

View file

@ -0,0 +1,3 @@
---
dependencies:
- global

View file

@ -0,0 +1,38 @@
---
- name: Installing required packages for haproxy
apt: name={{item}}
state=latest
with_items:
- haproxy
become: yes
become_method: sudo
tags:
- haproxy
- name: Copy haproxy.cfg
template: src=haproxy.cfg.j2
dest=/etc/haproxy/haproxy.cfg
become: yes
become_method: sudo
tags:
- haproxy
- name: Update /etc/default/haproxy
lineinfile:
dest: /etc/default/haproxy
state: present
regexp: '^#?\s*ENABLED\s*=\s*'
line: 'ENABLED=1'
become: yes
become_method: sudo
tags:
- haproxy
- name: Enable and start haproxy service
service: name=haproxy
enabled=yes
state=started
become: yes
become_method: sudo
tags:
- haproxy

View file

@ -0,0 +1,130 @@
---
- name: Gather the package facts
ansible.builtin.package_facts:
manager: auto
- name: Check whether a package called git is installed
apt: name={{item}}
state=latest
update_cache=yes
with_items:
- git
become: yes
become_method: sudo
when: "'git' not in ansible_facts.packages"
- set_fact:
virtualenv_dir: "{{ install_dir }}/.venv"
- name: Setting new hostname
hostname: name={{ new_hostname }}
become: yes
become_method: sudo
tags:
- init
- name: Create OctoPrint user group
group: name=OctoPrint
become: yes
become_method: sudo
- name: Add user {{ username }} to OctoPrint group
user: name={{ username }}
groups=OctoPrint
append=yes
become: yes
become_method: sudo
- name: Installing required packages
apt: name={{item}}
state=latest
update_cache=yes
with_items:
- python3-virtualenv
- python3-pip
- python3
- avahi-daemon
become: yes
become_method: sudo
tags:
- init
- name: Create {{ install_dir }} directory
file: path={{ install_dir }}
mode=0755
state=directory
owner={{ username }}
group=OctoPrint
become: yes
become_method: sudo
tags:
- init
- name: Download OctoPrint source
git: repo=https://github.com/foosel/OctoPrint.git
dest={{ install_dir }}
become: yes
become_user: "{{ username }}"
tags:
- code
- stat: path={{ virtualenv_dir }}/bin/activate
register: virtualenv_stats
tags:
- code
- name: Create virtualenv {{ virtualenv_dir }} (if it does not already exist)
command: "{{item}}"
with_items:
- virtualenv --prompt="(OctoPrint) " {{ virtualenv_dir }}
become: yes
become_user: "{{ username }}"
when: not virtualenv_stats.stat.exists
tags:
- code
- name: Update pip
pip: name=pip
state=latest
virtualenv={{ virtualenv_dir }}
become: yes
become_user: "{{ username }}"
tags:
- code
- name: Install OctoPrint
pip: name={{ install_dir }}
virtualenv={{ virtualenv_dir }}
extra_args="-e "
become: yes
become_user: "{{ username }}"
ignore_errors: yes
tags:
- code
- name: Install OctoPrint systemd Unit
template: src=octoprint.service.j2
dest=/etc/systemd/system/octoprint.service
become: yes
become_method: sudo
tags:
- service
- name: reload systemd
command: systemctl daemon-reload
become: yes
become_method: sudo
tags:
- service
- name: Enable and start OctoPrint service
service: name=octoprint
enabled=yes
state=restarted
become: yes
become_method: sudo
tags:
- service
- include_tasks: haproxy.yml
- include_tasks: webcam.yml

View file

@ -0,0 +1,35 @@
global
maxconn 4096
user haproxy
group haproxy
daemon
log 127.0.0.1 local0 debug
defaults
log global
mode http
option httplog
option dontlognull
retries 3
option redispatch
option http-server-close
option forwardfor
maxconn 2000
timeout connect 5s
timeout client 15m
timeout server 15m
frontend public
bind :::80 v4v6
use_backend webcam if { path_beg /webcam/ }
default_backend octoprint
backend octoprint
http-request replace-path ^([^\ :]*)\ /(.*) \1\ /\2
option forwardfor
server octoprint1 127.0.0.1:{{ appport }}
backend webcam
http-request replace-path ^([^\ :]*)\ /webcam/(.*) \1\ /\2
server webcam1 127.0.0.1:{{ webcam_port }}

View file

@ -0,0 +1,16 @@
[Unit]
Description=OctoPrint
Requires=network.target
After=network.target
[Service]
Environment=PORT={{ appport }}
Environment=BASEDIR={{ install_dir }}/.octoprint
Environment=CONFIGFILE={{ install_dir }}/.octoprint/config.yaml
User={{ username }}
Group=OctoPrint
Nice=-2
ExecStart={{ virtualenv_dir }}/bin/octoprint serve --basedir ${BASEDIR} --config ${CONFIGFILE} --port ${PORT}
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,7 @@
---
galaxy_info:
author: Frank "PHiAX" Weggelaar
description: Install zsh and oh-my-zsh for a given user
license: MIT
min_ansible_version: 2.9
dependencies: []

View file

@ -0,0 +1,77 @@
---
- name: Ensure zsh is installed
package:
name: "{{ zsh_package | default('zsh') }}"
state: present
- name: Ensure git is installed (required for oh-my-zsh)
package:
name: "{{ git_package | default('git') }}"
state: present
- name: Ensure curl or wget is installed
package:
name: "{{ http_fetcher_package | default('curl') }}"
state: present
when: http_fetcher is not defined or http_fetcher == 'curl'
- name: Ensure wget is installed (if selected)
package:
name: "{{ http_fetcher_package | default('wget') }}"
state: present
when: http_fetcher == 'wget'
- name: Create zsh user config directory
file:
path: "{{ ansible_user_dir | default('/home/' + (ansible_user_id | default(ansible_user))) }}/.oh-my-zsh"
state: directory
owner: "{{ target_user | default(ansible_user_id | default(ansible_user)) }}"
group: "{{ target_group | default(target_user | default(ansible_user_id | default(ansible_user))) }}"
mode: "0755"
become: true
when: not oh_my_zsh_custom_install
- name: Install oh-my-zsh (unattended) via curl
become: true
become_user: "{{ target_user | default(ansible_user_id | default(ansible_user)) }}"
shell: |
export RUNZSH=no
export KEEP_ZSHRC=yes
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
args:
creates: "{{ ansible_user_dir | default('/home/' + (ansible_user_id | default(ansible_user))) }}/.oh-my-zsh"
when:
- not oh_my_zsh_custom_install
- http_fetcher is not defined or http_fetcher == 'curl'
- name: Install oh-my-zsh (unattended) via wget
become: true
become_user: "{{ target_user | default(ansible_user_id | default(ansible_user)) }}"
shell: |
export RUNZSH=no
export KEEP_ZSHRC=yes
sh -c "$(wget -qO- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
args:
creates: "{{ ansible_user_dir | default('/home/' + (ansible_user_id | default(ansible_user))) }}/.oh-my-zsh"
when:
- not oh_my_zsh_custom_install
- http_fetcher == 'wget'
- name: Ensure .zshrc exists
copy:
dest: "{{ ansible_user_dir | default('/home/' + (ansible_user_id | default(ansible_user))) }}/.zshrc"
content: |
export ZSH="{{ ansible_user_dir | default('/home/' + (ansible_user_id | default(ansible_user))) }}/.oh-my-zsh"
ZSH_THEME="{{ ohmyzsh_theme | default('robbyrussell') }}"
plugins=({{ ohmyzsh_plugins | default(['git']) | join(' ') }})
source $ZSH/oh-my-zsh.sh
owner: "{{ target_user | default(ansible_user_id | default(ansible_user)) }}"
group: "{{ target_group | default(target_user | default(ansible_user_id | default(ansible_user))) }}"
mode: "0644"
force: no
- name: Change default shell to zsh for user
user:
name: "{{ target_user | default(ansible_user_id | default(ansible_user)) }}"
shell: "{{ zsh_path | default('/bin/zsh') }}"
become: true

View file

@ -0,0 +1,13 @@
---
# Defaults you can override in playbook or inventory
zsh_package: "zsh"
git_package: "git"
http_fetcher: "curl" # or "wget"
http_fetcher_package: "{{ 'curl' if http_fetcher == 'curl' else 'wget' }}"
ohmyzsh_theme: "robbyrussell"
ohmyzsh_plugins:
- git
target_user: "{{ lookup('env','SUDO_USER') | default(ansible_user_id | default(ansible_user)) }}"
target_group: "{{ target_user }}"
zsh_path: "/bin/zsh"
oh_my_zsh_custom_install: false # set true if you will manage oh-my-zsh yourself

22
docker/calibre.yaml Normal file
View file

@ -0,0 +1,22 @@
########################################################################
# #
# Description: Calibre-Web is a self-hosted e-book manager #
# ReferenceURL: https://docs.linuxserver.io/images/docker-calibre-web/ #
# #
########################################################################
services:
calibre-web:
image: lscr.io/linuxserver/calibre-web:latest
container_name: calibre-web
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- OAUTHLIB_RELAX_TOKEN_SCOPE=1 #optional
volumes:
- /path/to/calibre-web/data:/config
- /path/to/calibre/library:/books
ports:
- 8083:8083
restart: unless-stopped