Skip to content

Instantly share code, notes, and snippets.

@CalebSargeant
Last active November 25, 2023 07:15
Show Gist options
  • Save CalebSargeant/44d89787513685ef4e3ff50d3994d0d5 to your computer and use it in GitHub Desktop.
Save CalebSargeant/44d89787513685ef4e3ff50d3994d0d5 to your computer and use it in GitHub Desktop.
Firefly III Ansible

Firefly III Ansible

Please see https://github.com/TempoChocolate/ansible-docker-firefly-iii instead of going through this gist.

In this tutorial I will be showing you how to deploy Firefly III via Ansible with Docker behind a containerised Nginx reverse proxy (so you can securely access Firefly III publically). I use Cloudflare as my DNS provider, so I will be showing you how to get hold of a certificate from letsencrypt via a Cloudflare DNS challenge.

If you have any comments/suggestions/questions, please do post them below and I will be happy to answer, embrace change, or help :)

Folder Structure

You can either follow the folder structure I use, or merge all the tasks, vars, etc., however, I prefer to separate roles, tasks, vars, and templates, as it makes writing and using playbooks easier to understand and makes the experience much more modular.

You will most likely need to refer to the folder structure as you go through each file.

ansible
 |__roles
 |   |__letsencrypt
 |   |   |__tasks
 |   |   |   |__main.yml
 |   |   |__templates
 |   |       |__cloudflare.ini.j2
 |   |__docker-firefly
 |   |    |__tasks
 |   |    |   |__main.yml
 |   |    |__templates
 |   |        |__.env.j2
 |   |        |__firefly.conf.j2
 |   |        |__nginx.conf
 |   |__docker-install
 |       |__tasks
 |           |__main.yml
 |__vars
 |    |__letsencrypt
 |    |   |__cloudflare.yml
 |    |__docker-firefly
 |        |__firefly.yml
 |__firefly-ansible.yml
 |__hosts.yml

Files

Roles

letsencrypt -> tasks -> main.yml

NOTHING TO MODIFY - In this task we are installing certbot and setting up letsencrypt on the host machine. I prefer to do it this way, so that I can use the certificate for other instances if I so choose. Alternatively, you can have a look at creating/using a docker certbot container. There's a method to get hold of the certificate natively in ansible, however, I chose to use the command module, as this task creates a monthly certbot cron job to renew the certificates and did not want to confuse the directory locations of the generated/obtained certificates.

---
- name: Add Certbot Repository
  apt_repository:
    repo: ppa:certbot/certbot
    state: present

- name: Apt Update
  apt:
    update_cache: yes
    force_apt_get: yes

- name: Install Required System Packages
  apt:
    name: "{{ packages }}"
    update_cache: yes
    state: latest
  vars:
    packages:
    - certbot
    - python3-certbot-dns-cloudflare

- name: Copy cloudflare.ini
  template:
    src: templates/cloudflare.ini.j2
    dest: /etc/letsencrypt/cloudflare.ini
    mode: '0400'

- name: Letsencrypt Certificate
  command: /usr/bin/certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini -d {{ DOMAIN }},*.{{ DOMAIN }} --preferred-challenges dns-01
  register: res
  failed_when: false
  changed_when: false

- name: Cert Renewal Cron Job
  cron:
    name: "Cert Renewal"
    minute: "0"
    hour: "0"
    day: "1"
    month: "*"
    weekday: "*"
    job: /usr/bin/certbot renew --quiet --post-hook "/usr/bin/docker exec firefly_iii_nginx nginx -s reload" > /dev/null 2>&1
...

letsencrypt -> templates -> cloudflare.ini.j2

NOTHING TO MODIFY - This template is used in creating your wildcard certificate in the letsencrypt - Letsencrypt Certificate play. It is also used in future for when the cron job runs to renew your certificate monthly.

dns_cloudflare_email = "{{ CF_EMAIL }}"
dns_cloudflare_api_key = "{{ CF_TOKEN }}"

docker-firefly -> tasks -> main.yml

NOTHING TO MODIFY - In this task we do the bulk of the work. The (official) Postgres DB, (official) firefly application, and (official) Nginx proxy containers are deployed to your host. Nginx is also configured and the container restarted to reflect on the changes (you could just docker exec -it firefly_iii_nginx nginx -s reload on the host, or I could have just used the command ansible module, but whatever).

---
- name: Copy .env file for firefly_iii_app
  template:
    src: templates/.env.j2
    dest: /tmp/.env

- name: Create Docker Network
  docker_network:
    name: firefly_iii_net

- name: Create firefly_iii_db container
  docker_container:
    name: firefly_iii_db
    image: "{{ DOCKER_IMAGE_POSTGRES }}"
    networks:
      - name: "{{ DOCKER_NETWORK_NAME }}"
    purge_networks: yes
    networks_cli_compatible: no
    volumes:
      - firefly_iii_db:/var/lib/postgresql/data
    env:
      POSTGRES_PASSWORD: "{{ DB_PASSWORD }}"
      POSTGRES_USER: "firefly"

- name: Create firefly_iii_app container
  docker_container:
    name: firefly_iii_app
    image: "{{ DOCKER_IMAGE_FIREFLY }}"
    published_ports: "81:80"
    networks:
      - name: "{{ DOCKER_NETWORK_NAME }}"
    purge_networks: yes
    networks_cli_compatible: no
    volumes:
      - firefly_iii_export:/var/www/firefly-iii/storage/export
      - firefly_iii_upload:/var/www/firefly-iii/storage/upload
    env_file: /tmp/.env

- name: Create firefly_iii_nginx container
  docker_container:
    name: firefly_iii_nginx
    image: "{{ DOCKER_IMAGE_NGINX }}"
    published_ports: "443:443"
    networks:
      - name: "{{ DOCKER_NETWORK_NAME }}"
    purge_networks: yes
    networks_cli_compatible: no
    volumes:
      - firefly_iii_nginx:/etc/nginx
      - /etc/letsencrypt:/etc/letsencrypt
    env:
      VIRTUAL_HOST: "{{ FQDN }}"

- name: Create sites-enabled Directory
  file:
    path: /var/lib/docker/volumes/firefly_iii_nginx/_data/sites-enabled
    state: directory

- name: Copy Nginx Config
  synchronize:
    src: templates/nginx.conf
    dest: /var/lib/docker/volumes/firefly_iii_nginx/_data/nginx.conf

- name: Copy Nginx VHOST Config
  template:
    src: templates/firefly.conf.j2
    dest: /var/lib/docker/volumes/firefly_iii_nginx/_data/sites-enabled/firefly.conf

- name: Restart Nginx Container
  docker_container:
    name: firefly_iii_nginx
    state: started
    restart: yes
...

docker-firefly -> templates -> .env.j2

NOTHING TO MODFIY - This is basically a direct copy and paste from the official .env file. I just modified it to make use of the Jinja2 templating.

# You can leave this on "local". If you change it to production most console commands will ask for extra confirmation.
# Never set it to "testing".
APP_ENV=local

# Set to true if you want to see debug information in error screens.
APP_DEBUG=false

# This should be your email address
SITE_OWNER={{ EMAIL }}

# The encryption key for your sessions. Keep this very secure.
# If you generate a new one existing data must be considered LOST.
# Change it to a string of exactly 32 chars or use command `php artisan key:generate` to generate it
APP_KEY={{ APP_KEY }}

# Change this value to your preferred time zone.
# Example: Europe/Amsterdam
TZ={{ TIMEZONE }}

# This variable must match your installation's external address but keep in mind that
# it's only used on the command line as a fallback value.
APP_URL={{ APP_URL }}

# TRUSTED_PROXIES is a useful variable when using Docker and/or a reverse proxy.
# Set it to ** and reverse proxies work just fine.
TRUSTED_PROXIES=**

# The log channel defines where your log entries go to.
# - If you use DOCKER, use 'docker_out'
# - For everything else, use 'daily'

# Several other options exist. You can use 'single' for one big fat error log (not recommended).
# Also available are 'syslog', 'errorlog' and 'stdout' which will log to the system itself.
LOG_CHANNEL={{ LOG_CHANNEL }}

# Log level. You can set this from least severe to most severe:
# debug, info, notice, warning, error, critical, alert, emergency
# If you set it to debug your logs will grow large, and fast. If you set it to emergency probably
# nothing will get logged, ever.
APP_LOG_LEVEL=notice

# Database credentials. Make sure the database exists. I recommend a dedicated user for Firefly III
# For other database types, please see the FAQ: https://docs.firefly-iii.org/support/faq
DB_CONNECTION=pgsql
DB_HOST=firefly_iii_db
DB_PORT=5432
DB_DATABASE=firefly
DB_USERNAME=firefly
DB_PASSWORD={{ DB_PASSWORD }}

# PostgreSQL supports SSL. You can configure it here.
PGSQL_SSL_MODE=prefer
PGSQL_SSL_ROOT_CERT=null
PGSQL_SSL_CERT=null
PGSQL_SSL_KEY=null
PGSQL_SSL_CRL_FILE=null

# If you're looking for performance improvements, you could install memcached.
CACHE_DRIVER=file
SESSION_DRIVER=file

# You can configure another file storage backend if you cannot use the local storage option.
# To set this up, fill in the following variables. The upload path is used to store uploaded
# files and the export path is to store exported data (before download).
SFTP_HOST=
SFTP_PORT=
SFTP_UPLOAD_PATH=
SFTP_EXPORT_PATH=

# SFTP uses either the username/password combination or the private key to authenticate.
SFTP_USERNAME=
SFTP_PASSWORD=
SFTP_PRIV_KEY=

# Cookie settings. Should not be necessary to change these.
COOKIE_PATH="/"
COOKIE_DOMAIN=
COOKIE_SECURE=false

# If you want Firefly III to mail you, update these settings
# For instructions, see: https://docs.firefly-iii.org/advanced-installation/email
MAIL_DRIVER={{ MAIL_DRIVER }}
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_FROM=changeme@example.com
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

# Other mail drivers:
MAILGUN_DOMAIN=
MAILGUN_SECRET=
# If you are on EU region in mailgun, use api.eu.mailgun.net, otherwise use api.mailgun.net
MAILGUN_ENDPOINT=api.mailgun.net
MANDRILL_SECRET=
SPARKPOST_SECRET=

# Firefly III can send you the following messages
SEND_REGISTRATION_MAIL=true
SEND_ERROR_MESSAGE=true

# These messages contain (sensitive) transaction information:
SEND_REPORT_JOURNALS=true

# Set a Mapbox API key here (see mapbox.com) so there might be a map available at various places.
MAPBOX_API_KEY=

# Firefly III currently supports two provider for live Currency Exchange Rates:
# "fixer", and "ratesapi".
# RatesApi.IO (see https://ratesapi.io) is a FREE and OPEN SOURCE live currency exchange rates,
# built compatible with Fixer.IO, based on data published by European Central Bank, and doesn't require API key.
CER_PROVIDER=ratesapi

# If you have select "fixer" as default currency exchange rates,
# set a Fixer IO API key here (see https://fixer.io) to enable live currency exchange rates.
# Please note that this WILL ONLY WORK FOR PAID fixer.io accounts because they severely limited
# the free API up to the point where you might as well offer nothing.
FIXER_API_KEY=

# If you wish to track your own behavior over Firefly III, set a valid analytics tracker ID here.
ANALYTICS_ID=

# Firefly III has two options for user authentication. "eloquent" is the default,
# and "ldap" for LDAP servers.
# For full instructions on these settings please visit:
# https://docs.firefly-iii.org/advanced-installation/authentication
LOGIN_PROVIDER=eloquent

# LDAP connection configuration
# OpenLDAP, FreeIPA or ActiveDirectory
ADLDAP_CONNECTION_SCHEME=OpenLDAP
ADLDAP_AUTO_CONNECT=true

# LDAP connection settings
ADLDAP_CONTROLLERS=
ADLDAP_PORT=389
ADLDAP_TIMEOUT=5
ADLDAP_BASEDN=""
ADLDAP_FOLLOW_REFFERALS=false
ADLDAP_USE_SSL=false
ADLDAP_USE_TLS=false

ADLDAP_ADMIN_USERNAME=
ADLDAP_ADMIN_PASSWORD=

ADLDAP_ACCOUNT_PREFIX=
ADLDAP_ACCOUNT_SUFFIX=

# LDAP authentication settings.
ADLDAP_PASSWORD_SYNC=false
ADLDAP_LOGIN_FALLBACK=false

ADLDAP_DISCOVER_FIELD=distinguishedname
ADLDAP_AUTH_FIELD=distinguishedname

# Will allow SSO if your server provides an AUTH_USER field.
WINDOWS_SSO_DISCOVER=samaccountname
WINDOWS_SSO_KEY=AUTH_USER

# field to sync as local username.
ADLDAP_SYNC_FIELD=userprincipalname

# You can disable the X-Frame-Options header if it interfears with tools like
# Organizr. This is at your own risk.
DISABLE_FRAME_HEADER=false

# Leave the following configuration vars as is.
# Unless you like to tinker and know what you're doing.
APP_NAME=FireflyIII
ADLDAP_CONNECTION=default
BROADCAST_DRIVER=log
QUEUE_DRIVER=sync
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
CACHE_PREFIX=firefly
SEARCH_RESULT_LIMIT=50
PUSHER_KEY=
PUSHER_SECRET=
PUSHER_ID=
DEMO_USERNAME=
DEMO_PASSWORD=
USE_ENCRYPTION=false
IS_SANDSTORM=false
IS_DOCKER=false
IS_HEROKU=false
BUNQ_USE_SANDBOX=false
FFIII_LAYOUT=v1

docker-firefly -> templates -> firefly.conf.j2

NOTHING TO MODIFY - This file is a Jinja2 templated Nginx virtual host file that gets copied to your server. This will allow you to use HTTPS to connect to your firefly instance. Note that this is copied directly from firefly-iii/firefly-iii#2109 (comment) (thank you!).

server {
    server_name {{ FQDN }};
    add_header Referrer-Policy "no-referrer";

    location / {
        proxy_pass http://{{ HOST_IP }}:81/;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/{{ DOMAIN }}/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/{{ DOMAIN }}/privkey.pem; # managed by Certbot
#    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
#    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

server {
    if ($host = {{ FQDN }}) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    server_name {{ FQDN }};
    return 404; # managed by Certbot
}

docker-firefly -> templates -> nginx.conf

NOTHING TO MODIFY - This is the nginx server configuration. Note that this is copied directly from firefly-iii/firefly-iii#2109 (comment) (thank you!).

user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
        # multi_accept on;
}

http {

        ##
        # Basic Settings
        ##
        server_tokens off;
        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;
        # server_tokens off;

        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        ##
        # SSL Settings
        ##
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        ##
        # Logging Settings
        ##
        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        ##
        # Gzip Settings
        ##
        gzip on;

        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}

Vars

letsencrypt -> cloudflare.yml

MODIFY ME! - This var file is used for the cloudflare.ini file for generating/renewing certificates.

CF_EMAIL: mycloudflareemail@example.com
CF_TOKEN: mycloudflareapikey

docker-firefly -> firefly.yml

MODIFY ME! - This var file is used for the docker container deployments. CHANGE - pretty much everything - see official documentation. I deployed this to a Raspberry Pi 3B running Ubuntu 18 Server, so I had to use the arm image. If you are deploying to a normal server, change the image to jc5x/firefly-iii:latest.

---
# Changeable .env variables
HOST: firefly
FQDN: firefly.example.com
DOMAIN: example.com
EMAIL: myemail@example.com
APP_KEY: R@nd0m32Chars
TIMEZONE: Africa/Johannesburg
APP_URL: http://firefly
MAIL_DRIVER: sendmail
LOG_CHANNEL: stdout
HOST_IP: x.x.x.x

# Docker & related
DOCKER_IMAGE_FIREFLY: jc5x/firefly-iii:latest-arm
DOCKER_IMAGE_POSTGRES: postgres:10
DOCKER_IMAGE_NGINX: nginx:latest
DOCKER_NETWORK_NAME: firefly_iii_net
DB_PASSWORD: firefly
...

Playbook & Hosts Files

hosts.yml

MODIFY ME! - This file contains all your hosts that ansible will therefore know about. Note that we are using ssh keys to connect to our host. CHANGE - x.x.x.x to your host's IP Address and ansible_ssh_private_key_file to the name and location of your ssh key.

[firefly]
x.x.x.x

[linux_servers:children]
firefly

[linux_servers:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/firefly

firefly-ansible.yml

NOTHING TO MODIFY - This is the playbook that will tell ansible to connect to your server, reference the vars and run the plays/roles.

---
- hosts: firefly
  become: yes
  become_method: sudo
  vars_files:
  - vars/letsencrypt/cloudflare.yml
  - vars/docker-firefly/firefly.yml
  roles:
    - docker-install
    - letsencrypt
    - docker-firefly
...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment