frollo

Italian Software Engineer. Lazy poster. Follow me on the fediverse @frollo@write.as

Allora, partiamo dalle basi: come c'è scritto direttamente nella testata del blog, io sono pigro. Ma non pigro poco, pigro tanto. Quindi tutto questo è la soluzione meno sbatta e più plug and play che sono riuscito a costruirmi per hostarmi roba generica. Se volete meno libertà di riconfigurazione o meno servizi c'è quasi certamente di meglio.

Requisiti

Vi serve un server su cui giri docker e che sia raggiungibile da dove volete raggiungerlo. Io uso quattro macchine:

  • Una casalinga, per la roba che voglio raggiungere solo da dentro casa;
  • Un VPS (io uso IONOS perché è italiano e costa veramente poco) un po' cicciotto per le cose meno importanti ma che voglio raggiungere anche fuori casa (tipo il server degli audiolibri – voglio i miei audiolibri e non mi impedirete di ascoltarli ovunque);
  • Un VPSsino scrauso da 1€/mese per la VPN;
  • Un server un po' meno domestico, a casa dei miei, con su i loro servizi;

I due server domestici si parlano via VPN per tenere dei backup incrociati della roba importante, ma alla fine della fiera si può arrangiarsi anche con meno (la cosa importante è che i servizi accessibili all'esterno stiano fuori dalla VPN).

Tutte le macchine montano Ubuntu Server LTS senza snapd, che ho rimosso a mano perché faceva casino sui dischi. Quindi docker e docker-compose vanno installati alla vecchia maniera.

Le immagini

Ho deciso di pescare tutto da linuxserver.io per quattro motivi:

  1. Tutte le immagini hanno una base comune, il che riduce occupazione e download;
  2. Tutte le immagini hanno gli stessi manutentori, con le stesse logiche di fix e rilascio;
  3. Molte delle configurazioni sono comuni o almeno simili, il che riduce di molto lo sbattimento;
  4. La loro raccolta è grossa e variegata, quindi difficilmente avrò bisogno di staccarmi da loro;

Si tratta, in un certo senso, di vendor lock in, ma, come dicevo sopra, il mio obiettivo è quello di avere un sistema “plug and play”, che mi permetta di aggiungere o togliere servizi con uno sbattimento minimale.

Configurazione

Come dicevo sopra, le immagini di linuxserver hanno diversi campi in comune nella configurazione, quindi possiamo usare le estensioni per semplificare il nostro file:

version: '3.7'

x-linuxserver-conf: &linuxserver-conf
  PUID: 1000
  GUID: 1000
  TZ: Europe/Rome

services:
        grocy:
                image: ghcr.io/linuxserver/grocy
                container_name: grocy
                restart: unless-stopped
                environment:
                      <<: *linuxserver-conf
                expose:
                        - 9283
                volumes:
                        - "/root/library/grocy:/config"
        booksonic:
                image: ghcr.io/linuxserver/booksonic
                container_name: booksonic
                restart: unless-stopped
                environment:
                        <<: *linuxserver-conf
                expose:
                        - 4040
                volumes:
                        - "/root/library/booksonic/config:/config"
                        - "/root/library/booksonic/audiobooks:/audiobooks"
                        - "/root/library/booksonic/podcasts:/podcasts"
                        - "/root/library/booksonic/othermedia:/othermedia"

SWAG

Se esponete più di un servizio per macchina e volete usare anche HTTPS, c'è un'utilissima immagine di linuxserver, swag, che fa da reverse proxy e aggiunge i certificati. Swag è già attrezzato con i default per tutti i servizi di linuxserver, basta abilitarli, più un'altra manciata:

  • adguard
  • adminer
  • airsonic
  • archisteamfarm
  • authelia
  • bazarr
  • beets
  • bitwarden
  • boinc
  • booksonic
  • bookstack
  • calibre
  • calibre-web
  • chronograf
  • code-server
  • codimd
  • collabora
  • couchpotato
  • deluge
  • dillinger
  • documentserver
  • dokuwiki
  • domoticz
  • dozzle
  • drone
  • duplicati
  • emby
  • embystat
  • filebot
  • filebrowser
  • flexget
  • flood
  • foldingathome
  • freshrss
  • ghost
  • gitea
  • glances
  • gotify
  • grafana
  • grocy
  • guacamole
  • headphones
  • heimdall
  • homeassistant
  • huginn
  • jackett
  • jellyfin
  • jenkins
  • kanzi
  • komga
  • lazylibrarian
  • librespeed
  • lidarr
  • lychee
  • mailu
  • matomo
  • medusa
  • metube
  • miniflux
  • monitorr
  • mstream
  • mylar
  • mytinytodo
  • navidrome
  • netboot
  • netdata
  • nextcloud
  • nzbget
  • nzbhydra
  • ombi
  • openhab
  • openvpn-as
  • organizr
  • organizr-auth
  • osticket
  • overseerr
  • photoprism
  • phpmyadmin
  • picard
  • pihole
  • piwigo
  • pixelfed
  • plex
  • plexwebtools
  • podgrab
  • portainer
  • privatebin
  • prometheus
  • pydio
  • pydio-cells
  • pyload
  • qbittorrent
  • quassel-web
  • radarr
  • raneto
  • recipes
  • requestrr
  • resilio-sync
  • rutorrent
  • sabnzbd
  • scope
  • scrutiny
  • seafile
  • sickchill
  • sickrage
  • skyhook
  • smokeping
  • sonarr
  • statping
  • synclounge
  • syncthing
  • taisun
  • tautulli
  • tdarr
  • thelounge
  • transmission
  • ubooquity
  • unifi-controller
  • wallabag
  • yacht
  • youtube-dl
  • youtube-dl-server
  • znc

(no, non vi metto i link servizio per servizio, l'ho detto all'inizio di essere pigro!)

A questo punto, una volta configurati i DNS (SWAG supporta anche quelli dinamici) dovreste essere a cavallo.

Backup

Al momento il solo backup configurato è per i file di nextcloud, con un rsync cronnato due volte alla settimana (una volta per macchina), ma sto studiando anche un modo per tenere una seconda copia su Tarsnap. Siccome sono pigro, non credo che passerò anche questo su docker compose, ma mi limiterò a mettere tutto su cron.

Era una cosa che volevo fare da un po', perché con ActiveRecord in Ruby si fa abbastanza facilmente, ma ActiveJDBC, pur comodissimo, non permette di fare il provisioning del database all'avvio dell'applicativo. C'è un plugin che permette di farlo via Maven, ma sono piuttosto contrario all'usare Maven in giro sulle macchine di produzione e ogni applicativo ha diverse produzioni differenti. La soluzione ideale per il mio caso d'uso sarebbe di avere il tutto che gira ogni volta che parte l'applicativo, sia per mettere a posto eventuali aggiornamenti, sia per i nuovi deploy.

Dopo qualche mese di tentativi quando avevo tempo, ho finalmente trovato un workaround che funzioni (l'alternativa era farmi il tutto io parlando direttamente con JDBC, ma mi dava fastidio concettualmente l'idea di dover passare a uno strato inferiore solo per questa cosa, dovendomi smazzare io tutte le connessioni e gli errori). Per comodità mia e perché non si sa mai che possa servire a qualcun altro, riporto qua tutto il processo.

Il primo step è di aggiungere il plugin come dipendenza del codice (purtroppo non c'è un pacchetto a parte), quindi prendete il vostro pom.xml e aggiungete la seguente dipendenza:

 <dependency>
        <groupId>org.javalite</groupId>
        <artifactId>db-migrator-maven-plugin</artifactId>
        <version>${activejdbc.version}</version>
</dependency>

(Io uso la variabile activejdbc.version per tenere sincronizzate tutte le versioni della roba di ActiveJDBC che uso)

A questo punto, all'avvio del vostro applicativo (o nel momento in cui vi serve di più)

import org.javalite.db_migrator.MigrationManager;
import static org.javalite.db_migrator.DbUtils.*;
...
try{
    openConnection(driver, databaseUrl, databaseUser, databasePassword);
    MigrationManager migrator = new 
    MigrationManager("/path/to/migration/folder");
    migrator.migrate(logger, "utf-8");
}finally{
    closeConnection();
}

Inizialmente avevo trovato dei metodi statici per ottenere driver, databaseUrl, databaseUser e databasePassword, ma sono pensati per i test e ritornano dei valori fissi. Siccome database.properties (il file che usa ActiveJDBC per configurare la connessione al DB) deve essere comunque presente ed è piuttosto facile da leggere (io uso Config di TypeSafe), mi sono limitato a prenderlo su così e ad estrarre i dati.

Tenete conto che dovrete gestire delle eccezioni di tipo SQLException. Per sicurezza, io faccio fallire e fermare il processo in caso di eccezioni, perché se ci sono problemi di DB non ha senso che parta nient'altro e voglio un bell'alert grosso e inquietante che mi avvisi appena schiaccio il bottone “deploy”.

Un'unica precauzione che mi ha fatto bestemmiare per un po': MigrationManager, essendo pensata per un plugin Maven, si aspetta di ricevere un logger del tipo org.apache.maven.plugin.logging.Log, che tipicamente non usate in un applicativo. Io ho risolto creandomi una classe wrapper che rimappa le funzioni di Log sul logger che uso normalmente (io preferisco SLF4J). Se vi appoggiate a un IDE ci vuole relativamente poco, sfruttando l'autocompletamento, a risolvere il tutto.

L'ultimo step, siccome uso Docker, è di piazzare dentro il Dockerfile questa riga:

COPY src/migrations ./migrations

Dove src/migrations è il path che il plugin di ActiveJDBC usa per salvare le migrazioni (e alla fine io uso quello per generarle).

La storia dell'orrore

Due settimane fa, mi sono messo a fare manutenzione a un po' di codice Java al lavoro. Un processo scritto nella notte dei tempi, prima del mio arrivo in azienda (forse prima del mio arrivo nel mondo dello sviluppo) e che non aveva richiesto nemmeno un riavvio negli ultimi 700 giorni si era improvvisamente bloccato.

Siccome sono arrivato da meno di 700 giorni nella mia azienda, mi stavo inoltrando in un mondo ignoto. Dopo qualche ora di lavoro trovo il punto incriminato (un banalissimo puntamento hardcodato – peccato avessimo migrato il servizio). A metà modifica ho una curiosa sensazione di déjà-vù. Questa roba l'avevo già corretta da un'altra parte. E da un'altra parte ancora. Questo significa due cose: 1. Una sessione di 20 minuti di insulti verso chi ha hardcodato in giro puntamenti a servizi, senza neanche avere la creanza di lasciarmi della documentazione; 2. Che c'è del materiale per una libreria interna;

Finiti i 20 minuti di odio, mi appunto che prima o poi dovrò estrarre il codice. Settimana scorsa ho finalmente il tempo di farlo e mi ci metto. Ci va via tutta una settimana, tra l'identificazione delle classi, la brutale copia, la preparazione della documentazione e dei test (che mancavano) e la pipeline. Quando finalmente ho deployato la nuova libreria e comincio a sostituire le vecchie classi con la versione della libreria, cominciano a piovermi errori di compilazione.

Gli errori hanno poco senso, perché sembra che non ci sia compatibilità tra package.ClasseA e package.ClasseA, o che certi metodi della ClasseA abbiano una signature diversa. L'IDE (IntelliJ Idea) non aiuta, perché non li segna come errori, anzi, secondo lui va tutto che è un piacere – almeno finché non provo a compilare.

Dopo un'ora di lavoro, scopro che quelle famose classi ripetute più volte si ripetono almeno un'altra volta in una dipendenza del codice che sto modificando e che il mio paziente ogni tanto usava la sua versione interna, ogni tanto quella della dipendenza. Mi appunto di correggere anche quella ma, finché non avrò un'altra settimana da dedicarci, si mantiene la porcheria.

La morale

La prima cosa importante che ho imparato da questa disavventura è che le librerie vanno estratte il prima possibile. Se, al momento di riscrivere la stessa classe due volte, coscientemente, il mio predecessore avesse detto “Aspetta, qua serve una libreria”, il macello non sarebbe successo. Il codice duplicato in contesti differenti ha la tendenza a evolversi in modi leggermente differenti (nel mio caso, diverse interfacce avevano sviluppato metodi diversi – spesso molto simili ma non del tutto compatibili – tra un progetto e l'altro, buona fortuna con le implementazioni).

La seconda cosa importante è stata una lezione pratica sul concetto di contagio, che avevo già incontrato, teoricamente, in questo post, ma vederlo con mano è tutta un'altra cosa: fixare quel problema richiedeva una nuova classe, che ora esiste in progetti diversi e, magari, non sarà così identica tra un progetto e l'altro ora che riuscirò a tornare per estrarla. Anche se ci ho messo del mio meglio, devo considerare anche questa classe e i suoi cloni come debito tecnico – e non è ancora stata deployata!