jar-lid-archive: a staging deployment on its own box

Jar Lid Archive (Django + Postgres 18 + SeaweedFS) on jar-lid-archive-staging-box
(nodito VM 3300, 1 core / 1 GB / 10 GB), public at jars-staging.contrapeso.xyz
through vipy's Caddy. New role roles/jar_lid_archive; per-host values in
host_vars, so a production host is a new [jar_lid_archive] entry, not a new
playbook.

── Containers, following :master ──────────────────────────────────────────
The same three services as the app repo's compose.yaml, so staging rehearses
what dev and CI run. Every playbook run pulls :master and `up -d --wait`s;
compose recreates web only when the digest moved. --wait gates on web's
/health/, which checks the database and the bucket, so a green run means the
app serves. Upgrading is re-running the playbook.

── web is on the host network ─────────────────────────────────────────────
Docker's published ports are DNATed before UFW sees them, so `8000:8000` would
answer on the LAN whatever UFW says, and binding to the tailnet IP races
tailscaled at boot. On the host network gunicorn's port is an ordinary one: UFW
default-deny covers the LAN, tailscaled's ts-input accepts tailscale0 ahead of
UFW (checked on mempool-box), and the headscale ACL grants tag:prd-vipy exactly
:8000. db and storage publish on 127.0.0.1 only. Verified: LAN times out, vipy
gets 200.

That matters for HTTPS: the app trusts X-Forwarded-Proto
(DJANGO_BEHIND_TLS_PROXY=1), which is only safe while Caddy is the sole client.
Login POST through the public URL passes CSRF; cookies are Secure.

── Secrets ────────────────────────────────────────────────────────────────
Generated straight into host_vars/jar_lid_staging_local/vault.yml, never
printed: Django key, Postgres and S3 secrets, and seed passwords for the
fixture's alice/bob/admin usernames - never the fixture's published passwords,
since the site is public. The group vault gains one line, this host's Gatus push
token; decrypted, it differs from before by exactly that line.

── Backups, and the outage the first one caused ───────────────────────────
One uncompressed tar (pg_dump -Fc + the SeaweedFS data dir), age-encrypted on
the box at 02:20, pulled by small-backups-box; 3 days at both ends. Verified end
to end, including decrypting a pulled artefact on lapy.

The first test run stopped web AND storage and left the app down 26 minutes:
SeaweedFS outlasted its 150s healthcheck budget on this disk, compose marked it
unhealthy, and web's depends_on never started it. Now only web is stopped - it
is the only writer to both stores, so they are still captured at one instant -
and the healthchecks have start_periods sized for the disk.

Also inherited from the app's compose.yaml: storage writes /tmp/s3.json and
chowns it to seaweed, so on a RESTART of the same container root cannot
re-create it (sticky /tmp + protected_regular): "Permission denied". Fixed here
with rm -f first; the app repo needs the same line.

── The disk is the problem ────────────────────────────────────────────────
The VM disk is on proxmox-tank-1, the HDD mirror, which knots-box's random reads
(~6 MB/s) keep near saturation. Inside this VM: ~96% "full" I/O pressure while
idle, ~5 min to start web, sudo occasionally over 12s. Moving it to local-zfs
(NVMe) is the fix; noted in the README, not done.

── Also ───────────────────────────────────────────────────────────────────
- lan_ip read from the Proxmox guest agent; host key checked against the one
  the agent reads off the disk before it went into known_hosts.
- Monitoring: liveness + disk (400), probe_jar-lid-archive-staging (403), public
  /health/ + DNS (402), backup-dump/backup-store.
- tofu: the VM definition.
- Deployed v2026.10.03.1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
counterweight 2026-10-03 23:56:30 +02:00
parent 97c7d580bc
commit c3a7ca0ba7
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
22 changed files with 929 additions and 215 deletions

View file

@ -62,6 +62,11 @@ subdomains:
# has nowhere to carry a second credential. Same call made for monero above. # has nowhere to carry a second credential. Same call made for monero above.
music: music music: music
# Jar Lid Archive staging (jar-lid-archive-staging-box, proxied via vipy).
# Public with no basic auth: the one-off photo links are for people without
# an account, and the app has its own login for everything else.
jar_lid_archive_staging: jars-staging
# Read by plays across several groups, so global rather than group_vars/<group>. # Read by plays across several groups, so global rather than group_vars/<group>.
headscale_namespace: counter-net headscale_namespace: counter-net

View file

@ -1,214 +1,218 @@
$ANSIBLE_VAULT;1.1;AES256 $ANSIBLE_VAULT;1.1;AES256
66356262646437623239383039643762363139653565356263623438303833663431396566353163 33653731643464333331646636613630383836323134363761333038383334353230663631376666
3631323563343638346464386661346235656130383238370a353236623366313561646334363639 3861376232363238613234663263646137336662666263360a633661313435616338383337396664
35346232376138666430363437346638326264646562386436323962633630663136626462343762 31316234616365326363353739323230326264623562313061303339373265663435313938326634
3862333139646636650a656337306533313436333239653561333231363030633339303835396637 3464636635386664620a663330396463653963613537653230333430326137333964386530343636
61633763313464353933306333383061346563623637383166646332303237653065346233623134 62383336343031363537383362643934643839653731346364636534373430303636353431646534
39386130613866386235376266643062653833643132633235393931636366376164306331303762 66313264343338623333633964376137623833613664393137343432666264656466353830346631
33393562303738393364323465653163633730633636343764646337383733643362396132303063 62616639343265646661373062376138363232376562316134303730323431336533306565646438
32336461396561663063633137613234356535383863346334666465643437613962376439356433 62666132613930646564393162316461643431666664356436356264633839333338356561333038
66353532333933343462636163353637613637356530333331636363616265376263636538656566 65333466396165363030653137663939303838663030666235356639616362346330346437306133
62643137623933363237343865613131383766633561636664616434393538646332346132336630 61653130363136643038626562653437303939393133613733326530656163323531386436656363
62333361626339626638333666393735373834396431343965386532313834353837336664306530 37303930613063366537623435396564643431346266373363333332663632663939346131373565
36303138373734343735653637663466353435306166316661353738353035366535333265626330 32663430643034663530643431346335623636663265316431336361363233383761333130353530
36396538353566316634376662383564323861303063386163663061323435656261316439336539 30373566316336646536313938353663666436643532303433643139376232646566616430366330
65323063343961646435373134303039373139333265633838313636373463316231633832306261 36646433393037373733633562306631623164356237326532653833633031633735653463343461
36623934343731343938306332356261646264613063393834336332666230626534653464363335 65646664383531346531353133313334663130393237353436623638333731326139653363383032
66356331666434623735613333303466656263313964363030316231303330303735373135653763 63356235646366333534366666383961636361626636616237323237346234383035623365376137
30323635303435656639653037366430316330333033306339366361353765656337316561366263 34646162663761343662353631393131313562326138336565383764666333383731353730376135
38613265363565373832636239396362616435303536646465326132626239353361613137626632 64656231613630646330643133373766643330656166346134363263373966363665323964396233
30353764323430656631366134373263343861376666346166363131333137376466393438346461 64333038373565343330626633323933333239623634333436666338633539316263616438626461
31613435363637306432343437653636356665373838626361306332316664343266356666353238 30643566656534666533373365613861383231373062613831653537623532353561633736633866
35303464326633326438353730393462373437313864326638323262323034346464643038386462 61383532633430323330646137613633383830393136303332643531383466353039353564393232
34613435326432663566386331333430663336656239353131633261626637613035636662346136 39326130333336663038643766303464363664333261313137663863616466666534383831613732
33626131626435316566393831333534393663326465633531373932626631613063666165613766 35303831633232306131636137656461653361346134373935333462306232323165313265323239
63636463656435386333323037656533306331646561363061613537663738326162656332393161 36343864306130303537386164653866656632633861363736336362656161356463386265336430
62313338376630643031666630663337646439623862306663303335303232306161353336643334 38323739336364303061363437346137633436653566353464323462643838313631393235643635
30386537336265383334336431653637303836376261303038653739396263666161653365356133 65626461643263656537616638623066346636663335383861376336623932343439353231343739
62636638393465363239616264316331316139363063313431323165313237646437643361653539 64663263613862626266616362313136373630636262633036386462666632623336343431333231
63393932623934313062376137626266363330623766646231353561646332353565643638356633 31333164356538616562353335313161353339653262616439636134383063346566663261323938
39656565363962653038393430363666643361343131323231663566343035643631646433306464 36633631636265396230313033373230373635643165613239623532613436376464646635326535
64343735393937333036623139303531343338373162396635613863316466393235343865333065 39333433336361343530623434356636363433653035333934653162363135626564346161346130
63353662313737353737353736316261346466366666666536653064356130326334326137383933 65313130393639393539633534653330343935376365656162383132643635313034383538333333
64306331663430313838376266613763343537613965303937393364396238663266363539313531 64346336346231313939396463643166363165393439626431386334393563646337383361326431
33616133646365396336613234636630396239316633333832653138393964616261643531663836 34346462333732643365303230383937316462623566346238393431656433323535376361623561
36666265386134383430626637643766636439393636336436366265396239303335306563653962 66333530346638623332633832623532393035343930663434343737643032303764343138326638
62323763626265373037343766386433646637633838643933633738333037633336646663316335 33386666656162393633636437656564326336343661643137323362653437323939663033316230
36373534663439306137353236343565636538636333313239373936653263366637613335663233 61643437633665643439663566663232643162656139383236356538663139333162383665343766
32306536643866656461343666396565373661353561393665643162613539633634666138626266 31326539313163333034666233316634396434316535313264313430313432313938383534663333
35613663376465626363373864626266393335396461316338653262316131643633666361333537 33653165303864353638303833646134363362306632336133306430353165363063353735336464
64366533633535313633306535393732383436333762616539646463366665623866323463336263 31303536333964333833343566383238636233366233656531343664346331386230316365626662
38313365326338386664363538633232343636303135356230393065613238386366323435633931 30316635353262303962316364386263636531396636306534306464336536343138663933326236
37353537313330396463383233616532653139376263346264623366383339306139386231636466 38653563633663356339633335376361326132356334626163616361383134306239643365343562
35383761373733386133386436316431613265306264656630353561333736613734653164333031 34323031383638313336663937663564376362663534373464386635653164636430643266343766
32643237366534643062653065333632343563633034663266393961353763663534353466383561 62326134623638343735613562366232653137613265653836326461386539646133303638613639
38373332323135353932343231353433366637333437636238623235663836393735346230623438 31373465353533383237393933626333383839313736636132646564666639336530386537663239
37616639643231396564373033636162616234366262333461343139363539316332333431636565 34653533643039633337353535393832343439346536336232373837363734366430336539373732
64396639383462386239613035303565376163633637653137373631383864353532373465626630 66646339616366313834333562303466373962656435373530666466383339653064666532663236
63636531653063306131623735393963623836313566643565386336636565633130383264316533 64393032623234623561313766343233643232303630613165383761323435313130303361346237
33343030316365373164663661306161363062646361646332393935373533626331623163323038 62323430353333383539613536646439336466633435383134626530326531643135326264376662
61316666366263313531373530366639333364336563623162323938356662626565383363303533 37393832643662386137613230633964396338616638313764373464616665316531396462326431
61323730313161633035646435346234366661386339346663363438663862636361343033306134 36666338666536613666643032376331626132316433653036656330353834323865663865366535
66396365653634373235623837666537323230616334663263633135316433313734663863333561 36313937346136353534373631396630633432313065666639623538343332636665313236336366
34326134613335346661316632353764343235326630316535306666326662613436383764646366 35613330336136623331346636646134316265613863323730663837303032303131376435643131
33616135333038333834656439383733656436663433303536313961333662366564376338326661 39353965376565356136613533623235303031646331663166396631336434623561643464656263
35393839643663333130383965663538393931363662336165636435643132353461343266363265 64376636376337336661363133663139343935613033333266663439326239646434633131316637
66346635313337346661646461386336386461346133323435643939616234666665616261313731 38643164383136343939663332653433376362363432333265623166643831303639376637366363
38383265643830646364373164656133396132666236313265346337643939613831303366393530 37336361336461363437646234663330393830393861373162663463366533366637646530373838
63333330346131656134366237323661316534323330306330616564393130353136306461666361 32633764373164646339653931653066333833363534663234663864336465333164353231663032
62376637646230623264363437313161396130616231346133346665303834623833663062663436 36333666653633643362343334303035656539636663306333613964663837366338613565626635
30643736376638303065356161366561353565343139343731313263393933313239363363636638 34393964303866313835373936376130626665303031376637396639353734373337336534376563
32663932626332626366333635616335316235363561373539623265663733373030373439373831 30643261663235323831366537613434376566323837633136306663663331333863643037336133
65333763653635613964396437333833626535643666643433393031336461366562333934666234 63623035376233393431373361653964323833396362646661343235343332636435356365346330
62383632303436633965643430363763353361613762306361613066356136383131383232313961 39663533353631643062613839643761633866373539613536326132386336373731633239323362
30626531353933336563656139316338616565303261363933306164393966343037353431653233 31383863333330366665393666363864613632636537363436613230623030353436373061633037
32636631373966323838333666313964323933306638653162646166623237343662646134383337 36326633356533666438353034343966366136316331376431396536393261383633373131656530
37383039383531656337626334386638613133343133633637343738343731326663356666333161 36336364653962303730393338313435316162393561383161646232663430616239623964623431
61323765636135306563323064346465633537613361383761393361353363396237333438306166 38383563386266343935666463636533353361623965326232663937326462366433366534306534
35386338356135633365396661356535623232613765653931303063353538643130323439366438 66323139346238343031313762353166646235313463363435393236633337303561333930376439
39363936316263333562646133646464346362343761336662353764613261396364626164313664 37613630386636636634396437623866326639343863353134346438663033353232646566336164
31383839303361323461663662393736616363646232666633323134653138306161323735386638 63376535363235653362623630636661636162313462303664393032326538613139653039653065
30333365663064366535656134323338663566616163333634353939363533356364393563363333 38626234353034643330333861316665306665313933363234623639373765303065376632643233
65623838323034336530326363383237373563326334343066323738663833353930663930373965 39633764346132356564333236306333373664366134363037633730653732643130616665343466
30363766633137396561376636636566363862353062663038653534623430643766633631356562 62356138656331626437666465663132663562336365353834353632383062326164356161366138
63623762343433626363353239376539633136333038623338303961373938396632313966666532 66663532373464316264366661343863363265616438323132646361346135653063303031323833
33633865626462636137343436386661353561623035656435623566613530623163316465383664 33306231353231316165373137616134326163313430613963653532316331323738633639356565
37346261616239653138623262623930316432633531313037353938396164646534316564613238 38343537616330376263636165396139396365643663393637626132306564613263323362663637
36643137303965316238346134323639386261633338363130323134336632636564636638653861 64356665646532326466393733303130386230363831666265396635363630336331326662363864
38346566643966386465366535393166626463313039613236393831393732613537336530393936 39383432346265376137636639613162643135343433303435336436376161303766383232656166
31613234333836333732323165653931336565316564346239336663363736323763623962383261 32323462313432643330303039373239323939626266383232393433366232636134643636646431
65626332626565636162336264303964323139303831656262373161303861393366353237393136 37376330343461393766643664393864633265393631383566396530393364346634626535373133
62303361636131646335633733373966626135396630623133623733373565306234646165656138 61623931386433396630616463353236393630343838383865666466383632363631373136393930
35383065613365363438396332306236646432663731326666613838643434393866623163316631 35633934393963626263646137623262666335306233356439633334313765353161653566613833
33633931373135336438643735646266383462386431666330363066386538386335383231653334 32346637393530313730376564656434393737383037613664666338666435373066303338623930
64323366363032386662333662613562333864356238626161366566356564343339343838373437 63333763633533373437363666616263646564613466613830336134353933626336343036646137
30363133363136663036656461343534666636343932313336336637376665393564313234373430 32633265636134333438386330663764633432376633383930333835363362613738353464636330
39356362616539366237646363646364326664653232313235653831663539353530393261346532 38613135326635643436346565633833623630623736623231306237313662666666653532333234
38653134623732313466386635313730303639613561323665356633653836393237333737323864 62383866373939326335653461653238346336636636383961613534326336653030386534653934
36323736326239636565346536646566656138666630396366353930383237333338626532663131 39323266373664346437613362373162643532636566623563303139353430343638656130303866
64366339303230663530626632623066613030663533366539313866646466373439663831363936 63346537323431613434643865353865633331653233343839376237646337613566663130306537
36366636653032343064383661383664636463343165326532653861663537633238323937363839 39343735626236653636393465633038323465656661626561656438373835343361363735386638
65333030653566306465643035633162393534323731346330333861353236393732346130313034 39623830303864636333373534396562623630653362366166386264643438373435386438383831
64303736323030643533633539623638643232306435663238393566376564313162633334613462 34633264613230643434313037323538346661346464393362613434303135313136376131666530
33363931383664633433363032643536356135626662653733303339656339373735643732626535 36373866313739323962333061343836363661346334383033613534393666636631363262323238
35316131356135393933393462336163373062653133356133393830393638313838396361336635 64313330316464336533663764386666313732616338393162393434613437623730613038356663
62323463333464353939346233333462646161633036643332313337353065376561336630383163 39383236316664313731626330323039373937366638373461373736633239666438306437666666
38306666666634393766366436653436623138363830343636323833353365333762343533633835 66316639346432316631616261626262666238346530353563356436353332313362353431646430
66363730383932303661313831663566613830393530616164323963636339393339623065316534 64643366656466333363313135653331303764633839316239333630353665666465393263633333
38373662636330336632653335316463623935653961363566396132616433633831636334306236 31306663326366643032653762646464396530643662303636393432373966393934396634343032
34393761663962623563333534383739613965353033646235653532303330343961656639333337 31346365633933313436643937626634656535323431616231393866663334303632613237633666
31643462663538313930383038343964326463363935396365343466643236653738626639616434 65396262363665353837366166633435326133356330383066613537653736656565306434303663
62343337356239316162383564666131663535313633373062353030336439396533376235386131 38313837303965633236366533386661366464613262326162316365626534356237663035396264
31653865643931323364356566316435333336316166643063613164643266313834323235353863 33356165623232323664363731383962616465653230373234323961646236356132383031346465
34623936613366303161353165386662376465656366656561613466383830326162336238656236 61396136636137303933346462356339393236313361316465346631666432626434636366396531
30646632396231653263373631346330376461303231373733646231373462623131303633616166 35313264323766663436666430313864303065313633396631656438666564313038386161313433
37366435353830303234393962663334656335356661333437363637663664323430623135346262 66383734373435633838666463643636376366633334626130333730366639663961666630383234
33303233343362336261633663336635366233313466636233333836663464633637383964313133 35343964363931663431373162653333613563366462333037363838623662303837626237396132
31363461336237373337353438396132616134303432336533653737333538383534363735316334 36366162313766366237306337333338623937363165306433616530326435363063333938393939
62303666363532383131383833383536333733306163366339653038313930646531653565386335 61373433666233393962633263386534333735303564326435623363663334313163326239653966
38313561303062336232333634343866393763636131333665396430306432373734613834316636 30656132353938326530306663613637373838323432323762306263623136653937383465613038
61306138626530616230343462613036323166656334656637616139663030623932386132393762 66346661646231623161373233653337353162636334376137373138613864353036663964336362
36363364343737613165376365323361363233616532643866303632653063343431613338373633 64303362306239633339313666303636626264353833333333613132316261333830616232333662
63303263626434326539316439323239616632653632346538336535373336623739323931623462 64623061633466336336623734616632303265363630356665303663636334656136636339353534
35656365336630323035616161656237353135373366376466326264323863336166633438663834 65636564636462346133666438623461653630333833643230623632333266663066376461316661
63646562386339363235396163646334333238323932383230643964366466323064313138323464 37623637356333386638343937613361343265663033326337666138393233646639656232333864
62636239663635336332633334656631383261306437333963363464303430666462633333666638 37343733646432353564316237363964656532386561666538633737323739383766316337386137
37633830663439396133323861303735346561376434386131363864316332623137376139376231 65656231336463623965653232323162363866373732656534376430356432653539393939343661
30303765646132306261363335366565306564373261663336613336336632616231363366316135 30626130336436343033386637393334363237373737613761363633316533623033343731356338
65303966616439643863326662656439623938373833323738646535636230663136653266653533 38363231303637323861376662356564323032316130356461653466336165363363383637313862
63326235376633333366373436333534336436366434363461393763633437616262326133623563 35666338383164666135633734386432353137363464376538663465353330376237363333653566
37613161303161356632316335616639373564306161623339353066383163303431313333306635 32376462383732656135333861323465396664353032393432373861353836376239346338393230
36326435376661343237303633316462613532313232643461313065376134393338613130336161 39623532336232343761303235393963333065653935616230656236646330323465303263663236
38326463303562666662613361656161306139653431386630303561626666613037393238313463 39336233303030656161343961353566626462616333643235623032623338646163346564383034
62623533396532326332326636346134616236653638363837643131343166333564323538653638 63333439653936653731653337396231396562383565363837643138373864613338386164346262
63383261383265353331663864643933373562396565346163396434383765343962336363353261 31613133303833323238343665353238323335623162353362646337336335323639363730386462
38353034643436626632623235396261326235323937646135326532313561396361363062333366 61633966313435313165643537626330663864623762633330383730613131396463313231343837
32343666343163636233646466613633383034653438646262666133643830333834383233343136 61623965393836303230626463363438343239363562643334653733343865313334383233343932
33646437396137613466643031323538626332613066333065643237353263636435393964356464 33313930666136613766376435636432303466626565326434636561373565306532376239643735
34383265303364663233326432313230336436373866303435336666343832383737303030306534 63613037616534656265663536373164643866653466326361313965383635626331636463383363
36383036363937393965386431343561333961663565333565333261323033616630343130613934 35356533393461376132626431383839393132663161373831366130663964313839613566643165
32383166333063623235333464343864326531333165346261643131393264346534393035393431 63353139316333666337636635303236643034623830353462626535623064653065316135343564
64636139383232383332623866323363623666326361386134353030376366323039393033653061 64363963383230313765643262396330663938366631623834376532643937316236623865613665
63383338303734323637386366646365663032636463336261643533663638343661613462373736 34653331333336333364396330646632326266626638316566363539313532323762383034323036
32663566666133656165396233643334343335333731333938376236633330366262366139326133 38343838616264333737376432366535643339323734316563386439313339653763343139306565
32653163343963333739303061643936363861313938646164316564393261663765393462613837 61383538613761353733653933316634316138313565393932623638636135303962366562623231
31646636326330663464363034376230393739343961653130303166366233396262656634313061 38346365616434663362306138373634613334613037613961383032326661356163626238666233
65646664373161356134616165383265346633376136376132643234623334353563323539333065 31653938633938366632326439333237663539383461646236306265326536626135343161396137
61623265343730326164373063383937323766363466343466316364373935626138343938323261 34616636663639396534363431666464306538623632623133623133643864313839653865623266
38323739663131346138623663393832346236356631633133313436363364663536316561623631 35643865366465346532386130353565633333613061396139383265663734323139663532376638
32393562646666343537663039363165646337653564363065353433643031353865386361613534 38623966306639363938643564343330636363346261333166613938623936303966613037663365
37633639303636313261613563353136346465326163623361323230306364396362373966336262 62376334613363323563363332356463376532323964333338623239326663343031373635663032
31353565623239343564333533656236656666316638646666376138373164663138633835366162 33626432356662636130363761323862616131653630396164623435333061366564626236643936
37633036346562663261343731366161613361656534343839303062373232636339643961666432 65316562633036366233303538373530343932616533346637656464346166306438396435666533
61396665656638333435386338383638323664313139656331336131326563373235646539633835 37313766366636336535363536333061373264663163303162666330633061356464353164636239
30393235316564656131303932386336366661373737393264613364663030363862656462646463 39623836663066623431343835623264373239363766323338353633336338363536643462666236
33303766303166336132626265653431313263363131653835336634356632666230666235383262 35313031633561353066306336336362643863393730613938373539363831336266353266633036
31653837356536666266383562633437376161653664353533656331303563626539626161653562 30313039336631396664383434316462643366643532353638396339353430303063303734343830
64333337626430616330346233613566353330383361353234376236656336653735663838343333 31323761643235663061336237333866323139613533366463353433613832316232323938323331
37643162376630373531396138643861326562323133303363363837306138373434306364356364 39356261636231353936313033363962353232663831316634653634323965313035663537323864
64623037643737663138626262326630323638376336663062393930663566316632323862653135 33633138383431376131616462363337313035326339646661643730666337393266316562333138
66363664336431373966663133353030393863313538366632643932363438613035326563323163 39616636323630356631653834313466393533333761666534333239306666356166363433326434
63343565373862316230643166336438393464323032666334653631393761303337303861666134 33643566323438333934363236313633666465663236353034653439323338613433333538336535
36663637346136623564613233343432383930616339626161623933323565623861653939326165 30326435383362343065356463373761333565396131336562636236353863613635636436326131
37383436353533613736323237666433316130613464396333343965393031333138396461376632 37353238656337633135643430306532393530653032663437303139323463613136656563613633
61373166643766376234646436653566313637326434363261333831336136306137333061323035 39303463656631336231643733323439376331653732376639303231333533373264623034316435
66303335363035666366386364303861383231366339643666313636623733616562333539643465 64386635636565333961663936356131643536636235666433356338646531306535653366363362
32353566326262326637393434656133313963646435333135613630313865373430366265666632 64336634396666303364663033383034616639646532333238336138343032376631326139646664
37316539363439663131323835313761313765653564663337346462653332613430373037333064 37323432613664343032313838663561373735323938666339336133653131383263366665656537
64323731623566326664613634626265383661346462323463303638346464643366663333336339 61363139356666303438316434626536323362306539613930353263663263353933353664393666
65626265663230346338633764633436653339373031623438633638326137613635633239343834 37663763616635306665386163333034643465376130313861633435366666653535383234363438
35373132376133363861373062386235353836633030323236343666323134326135336231613566 34326565373461343035616162316532333739386562383466626436366434306261363931653464
34326263643163633935643065306165643564663831633561353061653331326566633664376432 63306238663038373631363834313261323337393263323362306636663765393535303433643562
33353835366265346266663836326465356166626432356663366566313962663862363930336334 63616336383563343732653034316133343662333733323231623064386561623535376235336432
65323337386231376361343933353865633330613466323337306137633762653931313331313339 38646537303136653939346335653031636266343132626262643439363564613132356634666435
36663965353832386662666433383934303737656336383432633632646135383835336631373861 34626562336630363363393261643735353263653462353336383630366335643966396434363034
63646164373137316662373330333465343134633466396566333334663036663039373934333832 65623233346335343637633564393466616365626361613134306362663333353730336264616664
35343661303565646464373834646234623837363562386463383237373533316638666663613633 33656361323631663639303033663632663262313461663133646663633833383833636661336463
30313437626231343062636437363836386635333862373362643532646335353863323936663034 37633266616639663830343333336362653838316363303637333535663136336633393339666263
30316461373330623130353563643035303561303666393464636533626562633165393236663936 37313434303638616530336366303865333038326534656135613136613930376635363062353962
31313061303465633332646363643866306439383636356366623565663437313365376331656666 38323339356235663965383633323665313932643632363132386666363239633666313566653063
34333930333235306636646664326561613165633233353937666238646536323131313232626366 64643433363234396563356435383335623133613364313834396236376339633738326235363737
36623237613635343134313366346661656433643136353433393532303165326564646331333035 31303566613461386434646233306261393835613739353831306164343164616432656466636139
35353365633039353161663337306463303535643062366232336239656162653532306539333035 62393132636465663965633432353665643535643233313738323236383166653838663338323462
65373735373832326337333962643330363338656134303637313337393466366263363264343631 61383035636632323863333361643333363238636436616562393465326230373164366261313564
30656433643562393537326361323733643639396537303065623936336230346664616534323262 39343861333562373762623437613334396634613462373132386564616462333638373161653737
36313864396364353836373538333130303731383437376137383938383633373063323936613464 38313366373864333064646133393561396537353333626462666361326532323239376431326330
37613166323432363232313834326565663265336138396335313230303935396266646438323833 30373734346566663235616235316633636263373431326166623734303734383337643562666536
65306666636338373239346332613464373235613137643133626261313031326462356138356632 61353130646537623936646639393439306639356130373431623431643834386632616165383133
66363465346165306533613636383938636531656631306139666237396632646634333937623032 61666165396634613763306561356533663263386639643766306166303162376366363530396361
33613439656637383537356364336265653931613835333734383836343930313838626564333065 65356538663638633261636332386262343236663731313932656339323239343938666662343064
35363735313162616238643438316535356663396339316331633232393232316131396432653639 66353234346530366163656437356464376136663261343938636566333865313261396164316137
36303335643534376536383331323562353139613130383330636334636261326462656565643138 38663238656365383131363239333537646333656136656465636464656530306137363436636231
63326365363163383932653562363833643761653839306131343737353633313737353732633463 39366339663630653461383461323234363936346230643266393833356432396661356263333238
62326563336339383661376665353230613366646132653364396338663337326136333263363838 38656530306133363733336335386238626132356237313334633634393634333937643631623963
35626537666238383435613431666365303133373166386136353061373765313632343665306439 36326364306664613832363734336463303635373262653632356263336665353739373231316664
34356139363636623639626633623030363933303539626665383733363638303832333561653130 34633036383439393339363364346165376261613339356236323330306432616665343265363564
64393437386362333731643131663263626434666661326533386362306362653732376366626139 66666137346231383465353666633835393737313261643935656530356162613538363864343830
65626662323232636134653334366639386164653535323435613863353332366630303636663339 35323865613563366130323132326238346338303761353433656532643131333962373066336233
34333633373737333732363664353437613762383064316134393139393539306564396237343332 34303734396235303137616362653037333138306132393537646531616635636561343831366531
64636339643030353365356136366264353935643830633237316532316265316635393664326533 36333932323234643237346266323466303736663665363362373465333139393934623939313535
64343030396334326135336465336637623832363065353163316465303537643861306632336535 34316433323136316533313663393238663530386565376131366631393031643362346239316430
31366338323263313965643565656430366566393130326333383862313239356437623232613865 64303636343765326465616464663234373137393137666635306261653466323733303962393461
63383863316639346632323834633739356464313865376463323633393365653033376431373138 61356438316631333231636465653933343264356230313937636431336261306464316532343335
61363035653233373036666531303331383337353839363563386333313730363664666463333735 32383732363866383531373136353331623566636332643332636237653737313064393566373837
61383638363164336265383664376463616536383533666265383961353961303833353265366633 37333031643034343637333832363931343938303764313836373230353931613535313765393065
32646330333336613631633830633837346332653437356536363532396438326364623465383133 30623234653039316461626635366337633265623236333963323934313065623732353362303338
36656231343734643830383335623930343132386336666366623161666637623862363033373236 64663130383030613536383739646562636662656234363036613162666461373834333530363363
32666332316266623230666231663162643262653231313965323137383532633663323239396139 62383166353333663232363737653639316235303535616536383962653039386464303435356538
37656533373065623161623766333335353965373731346639613566343236363239313537636239 61653166633162616563336663323830383732666264393262323139303936626464373132303738
34626533346138613764346161383337623265353563343066303236333534316137356636396565 30343061313535363838653930306164303438353736393862643461363333396539326633386236
32616136306665346566616565346162613736333836623363346232373932366365663531653435 63623666343330363166373037636332313966303565303061663361386239353962666164383435
37623539366233366539396361386163643161653731353232313034613135373039653339346637 64626663383735393431623436323862656262396461393663616530353132646234353838633537
31623138326133366333656661393837653939343064656638616630346462363065623139323131 66313437306330653963613663373235383338383439636563643333323265626132326639386639
65363036383433663566626336353432613535363239373264356237323032323634383662613763 38663762326666343935393664643761393236323266336265393033643734333439353538613062
39646161363639326330346332353965366330346632303939623136303238323431346361656462 38626438663431366464343937316165313937323732643061643334643437353764333231646362
61623838626139636163353430636461666239343337373132643334616234616339313761353331 65393566663530383133393430383237633638363363313963383230663434353635353364333830
36643165373834373437363165303861306135383236303030626463373235393234313765343563 30306535353962633865346131333035353638336539336635643238383762303563323663616164
37353932366430666539623230373161303935633565323366363232336264643961383835333066 63356630393664653637613935363536656239616239316337353730666632633234613331336136
32623366663965316662383036306462616465353139336333643936363838313538363832363330 34383036616432306533663164316163303332313239646662656536333564643539343533383939
33313861313636373831656431383434363664663734626363646365666135376331366335653064 32303935313565626661393334303564633136323164623965663364323966346636396532336535
30386235313037363365363232316338613634643235383037393635333166346632356435373039 38373232616265346131633930333134666333663961383063666237653462623635323537393837
66663862373939346434333130383065386135306661353066343330666563386136376562316134 30373438616561353430633139323163353138363735636530643630346566633631383931363638
63346435643132316434363666343964303335663131383831373430653632663638396333393932 66633266643762626530613238343663326236363134316337653861376561383631663937373833
36383664376666393761643730306336333538376637633135616166346432636464336434323031 32663063356164333731343263393837386332623665313135396665373436323366613331363333
3966 65393533353933643134643362366330653261313064663963336539653134396437383430313666
33663234616461626630646563616561313139396630646461386338623266666434376335306435
36643430663131373064313464633733356131386338643439653032353436656661356366626533
35343434396361303634636438633036386563383664646561663566613339323865363136333230
37616265613030633061376639386461383062353135666534346632353064353231

View file

@ -0,0 +1,28 @@
---
# Jar Lid Archive STAGING. Secrets are in vault.yml beside this file.
#
# These live here rather than in the role defaults because three plays read
# them: the deploy on this box, the Caddy vhost on the edge host (port, domain)
# and the backup (retention). A role default cannot reach a second play.
# Names this deployment everywhere it must be unique across hosts: the Caddy site
# file, the Gatus probe (probe_<instance>), the backup artefacts and the
# backup-dump/backup-store endpoints.
jar_lid_archive_instance: jar-lid-archive-staging
# Fixed by the image's CMD (gunicorn --bind 0.0.0.0:8000); changing this moves
# what Caddy and the probe call, not where gunicorn listens.
jar_lid_archive_port: 8000
jar_lid_archive_domain: "{{ subdomains.jar_lid_archive_staging }}.{{ root_domain }}"
# Staging data is disposable: three days locally and on small-backups-box.
jar_lid_archive_backup_retention_days: 3
jar_lid_archive_seed_users:
- {username: alice, email: alice@example.com, password: "{{ jar_lid_archive_seed_password_alice }}"}
- {username: bob, email: bob@example.com, password: "{{ jar_lid_archive_seed_password_bob }}"}
- {username: admin, email: admin@example.com, password: "{{ jar_lid_archive_seed_password_admin }}", is_superuser: true}
# No systemd units of ours run here - the stack is docker compose - so infra/401
# has nothing to watch. The probe in the deploy playbook calls /health/, which
# checks the database and the bucket as well as gunicorn.

View file

@ -0,0 +1,34 @@
$ANSIBLE_VAULT;1.1;AES256
37383535306133363031316535656461613038653562373839363432623163356234303061643566
6533333535613064653336313664643063326562666639640a373530386539316363346332663137
31303230323065623733663666383039346337343239646465393166663239643836356639373765
3564313065363266360a643239333330616463616533623435656164386264303634356134303831
36356131323637363734316161383065653663623732386638363165633265356362386538643039
64636464323730316638316430643339356662353736366332663738656566663137366166393735
34633430363931323330393234336661383532653366633735313365363562396137356438353330
65313737313131326361326631363864396536343633623963303632623465363964373964393430
61323161663939346435386435616364653336646165356232346335353235356339656336306138
65613636353636623562613362633761333637613066663838396638613161663532393030303035
35316464356539666631636265646433353537396564366538643962373364383730626164316265
63393034663835346362636564316135326430633963393139646365333666663630336462323361
31353830633335366635366435643864313665366563396134313938666537663232343162383366
34636231636261663739383366363962366438323962393732393864623238343961363035333866
61386630396334626564306536336561333265383464623730386263373132613438396332356631
30386435333739653637633032313966626437303634613838653735373666343565306663376230
61303734623865353835626464613033346437623234626136353664303434386262346634356463
34396433353431393935386136333331643539633138636135353339323930386634393639666665
36643462653061363062383039313936303062393364353366623965313530303031306336396336
65643663343163636338393666386134366337633064376530303463393737396165663930383562
64316363663636383764656139366433393766613262613433623637393538633130616232373136
31376636633666626365376533356262393065656539613266326465643663383439373661393631
30636136363066653936653837376634356333353036633165316662383337636435323162316430
30316331353361396132653233623034376539323064363135306262376638353664643163653334
37643638323038663531376336313064383238366430663363353733356530393661646138303230
30633862626166653362346632626261636531373130353034656532386230616330643134383631
63396138326336626431656130306531626539323934386332306361396239376466623866613364
36666632316331326361373136326465323365653034363337313135656262623465306439646362
30636639663163643430363165316231366232623332393635623461306339313762323265623732
30633464343235323566356434376535313238663533633630356163336532303765633366316166
38633462303762323930393337306265393033653061616337646163333335333737666332666632
35613132646636333335313263343136323337616662623465653939336637653134383766336265
35633638663636353537333238643236623033636163333164663062353863643164

View file

@ -37,6 +37,7 @@
- {sub: "{{ subdomains.monero }}", host: vipy, interval: '24h'} - {sub: "{{ subdomains.monero }}", host: vipy, interval: '24h'}
- {sub: "{{ subdomains.monero_explorer }}", host: vipy, interval: '24h'} - {sub: "{{ subdomains.monero_explorer }}", host: vipy, interval: '24h'}
- {sub: "{{ subdomains.music }}", host: vipy} - {sub: "{{ subdomains.music }}", host: vipy}
- {sub: "{{ subdomains.jar_lid_archive_staging }}", host: vipy}
# A public resolver on purpose: this must test what the internet sees, not # A public resolver on purpose: this must test what the internet sees, not
# what a local cache or the tailnet's MagicDNS happens to answer. # what a local cache or the tailnet's MagicDNS happens to answer.
@ -75,6 +76,10 @@
# API the mobile clients speak cannot carry a proxy credential, so its # API the mobile clients speak cannot carry a proxy credential, so its
# own login is the gate. See roles/navidrome/README.md. # own login is the gate. See roles/navidrome/README.md.
- {name: music, sub: "{{ subdomains.music }}", path: "/app/", status: 200} - {name: music, sub: "{{ subdomains.music }}", path: "/app/", status: 200}
# /health/ through the whole public path: DNS, vipy's Caddy and cert, the
# tailnet hop, gunicorn, and the database and bucket behind it (503 if
# either is down). "/" would only prove the login redirect works.
- {name: jars-staging, sub: "{{ subdomains.jar_lid_archive_staging }}", path: "/health/", status: 200}
# Ports published from the edge host by socket_proxy. # Ports published from the edge host by socket_proxy.
public_tcp: public_tcp:

View file

@ -55,6 +55,7 @@
- {name: monerod, host: monerod_box_local, heartbeat: '26h'} - {name: monerod, host: monerod_box_local, heartbeat: '26h'}
- {name: xmrblocks, host: monerod_box_local, heartbeat: '26h'} - {name: xmrblocks, host: monerod_box_local, heartbeat: '26h'}
- {name: navidrome, host: music_box_local} - {name: navidrome, host: music_box_local}
- {name: jar-lid-archive-staging, host: jar_lid_staging_local}
tasks: tasks:
- name: Build the probe endpoint list - name: Build the probe endpoint list

View file

@ -21,6 +21,10 @@ monerod_box_local ansible_host=monerod-box lan_ip=192.168.1.146 ansible_user=cou
# key was still in known_hosts and SSH refused the connection, which is the # key was still in known_hosts and SSH refused the connection, which is the
# designed behaviour described in group_vars/nodito_vms.yml. # designed behaviour described in group_vars/nodito_vms.yml.
music_box_local ansible_host=music-box lan_ip=192.168.1.149 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua music_box_local ansible_host=music-box lan_ip=192.168.1.149 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua
# lan_ip read from the Proxmox guest agent for VM 3300 (qm guest cmd
# network-get-interfaces), and the host key checked against the one the agent
# reads off the disk before it went into known_hosts.
jar_lid_staging_local ansible_host=jar-lid-archive-staging-box lan_ip=192.168.1.151 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua
# Local connection to laptop: this assumes you're running ansible commands from your personal laptop # Local connection to laptop: this assumes you're running ansible commands from your personal laptop
[lapy] [lapy]
@ -63,6 +67,10 @@ memos_box_local
[music] [music]
music_box_local music_box_local
# Staging only for now; a production host joins this group with its own host_vars.
[jar_lid_archive]
jar_lid_staging_local
[ci_runner] [ci_runner]
forgejo_runner_local forgejo_runner_local

View file

@ -30,6 +30,10 @@
- name: forgejo - name: forgejo
source: "backup-pull@prd-vipy:/opt/backups/forgejo/" source: "backup-pull@prd-vipy:/opt/backups/forgejo/"
retention_days: 14 retention_days: 14
# Staging: disposable data, so the long tail is as short as the local one.
- name: "{{ hostvars['jar_lid_staging_local'].jar_lid_archive_instance }}"
source: "backup-pull@{{ hostvars['jar_lid_staging_local'].ansible_host }}:/opt/backups/{{ hostvars['jar_lid_staging_local'].jar_lid_archive_instance }}/"
retention_days: "{{ hostvars['jar_lid_staging_local'].jar_lid_archive_backup_retention_days }}"
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Register the backup checks with Gatus. # Register the backup checks with Gatus.
@ -62,7 +66,8 @@
- {name: vaultwarden, host: vipy} - {name: vaultwarden, host: vipy}
- {name: lnbits, host: vipy} - {name: lnbits, host: vipy}
- {name: forgejo, host: vipy} - {name: forgejo, host: vipy}
store_sources: [arbret, headscale, memos, vaultwarden, lnbits, forgejo] - {name: jar-lid-archive-staging, host: jar_lid_staging_local}
store_sources: [arbret, headscale, memos, vaultwarden, lnbits, forgejo, jar-lid-archive-staging]
tasks: tasks:
# 26h, not 7h: the DUMP is genuinely daily, so the window cannot be tighter # 26h, not 7h: the DUMP is genuinely daily, so the window cannot be tighter

View file

@ -0,0 +1,121 @@
# `jar_lid_archive`
Runs [Jar Lid Archive](https://forgejo.contrapeso.xyz/counterweight/jar-lid-archive)
as a docker compose stack: Django under gunicorn (`web`), Postgres 18 (`db`) and
SeaweedFS as the S3 photo store (`storage`). The same three services as the app
repo's own `compose.yaml`, so staging rehearses what dev and CI run.
Deployed by `services/jar-lid-archive/deploy_jar_lid_archive_playbook.yml` to
every host in `[jar_lid_archive]`. Today that is `jar_lid_staging_local`
(jar-lid-archive-staging-box, nodito VM 3300), public at
`jars-staging.contrapeso.xyz` through vipy's Caddy.
Per-host values are in `host_vars/<host>/main.yml`, and secrets are in `vault.yml`
beside it. A production host would be a second inventory entry with its own
`jar_lid_archive_instance`, domain and vault, not a second playbook.
## Updates: follows `:master`
CI pushes `:master` on every green build of master. Every run of the playbook
runs `docker compose pull` and `up -d --wait`, and compose recreates `web` only
if the digest moved. There is no watchtower, so **a merge reaches staging when
the playbook runs**, not on its own. The run ends by printing `/health/`, which
includes the deployed version.
`up --wait` only returns once every container's healthcheck passes. `web`'s is
`/health/`, which runs `SELECT 1` and touches the bucket, so a green run means
the app actually serves.
## Networking: why `web` is on the host network
Docker's published ports are DNATed in the `nat` table, before UFW's `INPUT`
rules ever see the packet. A `ports: "8000:8000"` would answer on the LAN as well
as the tailnet, whatever UFW says. Binding the publish to the tailnet IP avoids
that, but at boot Docker races tailscaled for the address and loses.
So `web` uses `network_mode: host`, and gunicorn's port is an ordinary host
port:
- **LAN:** UFW default-deny blocks it.
- **Tailnet:** tailscaled's `ts-input` chain accepts `tailscale0` ahead of UFW
(checked on mempool-box), so the headscale ACL is the real control. It grants
`tag:prd-vipy` exactly `tag:jar-lid-archive-staging-box:8000`.
- `db` and `storage` publish on `127.0.0.1` only, which is how `web` reaches
them from the host network.
This is load-bearing for HTTPS. The app is told to trust `X-Forwarded-Proto`
(`DJANGO_BEHIND_TLS_PROXY=1`), which is only safe while Caddy is the only
thing that can reach gunicorn. Caddy overwrites whatever value the client sent.
## Secrets and seed users
The Django secret key, the Postgres password, the S3 secret and the three seed
passwords were generated into `host_vars/jar_lid_staging_local/vault.yml`. They
reach the box as `/opt/jar-lid-archive/.env` (0600, root), which compose
interpolates. `docker-compose.yml` itself has no secrets.
The seed users are alice, bob and admin (superuser), with the fixture's usernames
and **vault** passwords. The fixture's `*-test-pass` passwords are published in
the app repo and this site is public. `seed_users` runs on every deploy and
resets each listed user's password, so rotating one means editing the vault and
re-running the playbook.
```sh
ansible-vault view host_vars/jar_lid_staging_local/vault.yml # from ansible/
```
## Backups
`services/jar-lid-archive/setup_backup_jar_lid_archive.yml` uses
`backup_source`. At 02:20 it stops `web` only, and
`/usr/local/bin/jar-lid-archive-dump.sh` writes one tar with:
- `db.dump`: `pg_dump -Fc`
- `seaweedfs/`: the photo store's data directory, as files
`web` is the only writer to the database and the photo store. With it stopped,
both are quiet and are captured at the same instant. `storage` is deliberately
left running: on this disk it can take minutes to restart, and the first test run
that stopped it left the app down for 26 minutes. Uploads fail while `web` is
down.
The tar is not gzipped, because the bulk is JPEGs. Three days are kept locally,
and small-backups-box keeps three as well. Staging data is disposable.
### Restore
```sh
age -d -i ~/.age/counterweight_age jar-lid-archive-staging_<ts>.tar.age | tar -xf - -C /tmp/restore
# on the box, stack down:
docker compose -f /opt/jar-lid-archive/docker-compose.yml down
rm -rf /opt/jar-lid-archive/seaweedfs && cp -a /tmp/restore/seaweedfs /opt/jar-lid-archive/
docker compose -f /opt/jar-lid-archive/docker-compose.yml up -d db
docker compose -f /opt/jar-lid-archive/docker-compose.yml exec -T db \
pg_restore -U jarlid -d jarlid --clean --if-exists < /tmp/restore/db.dump
docker compose -f /opt/jar-lid-archive/docker-compose.yml up -d --wait
```
## Monitoring
| What | Where |
|---|---|
| Host liveness and disk | infra/400 (automatic for `managed`) |
| `/health/` from the box, every 5 min | `probe_jar-lid-archive-staging` (403) |
| `/health/` through DNS, Caddy, TLS and the tailnet | `jars-staging` public site (402) |
| Dump ran / artefact arrived | `backup-dump_` / `backup-store_jar-lid-archive-staging` |
infra/401 has nothing to watch here, because no systemd units of ours run on
this box.
## Size and disk
The VM's disk is on `proxmox-tank-1`, nodito's mirror of two HDDs. knots-box's
random reads keep that mirror close to saturated, and this box has measured
**~96% "full" I/O pressure** with nothing of its own running. Everything is slow
here, but especially container starts. The healthcheck `start_period`s are sized
for that. Moving the disk to `local-zfs` (NVMe) would fix it.
1 core, 1 GB RAM and 10 GB disk, by choice, for staging. The disk holds Debian,
the images, the photos and three days of local backups, and each backup is
about the size of the whole photo store. infra/400's disk check alarms at 85%.

View file

@ -0,0 +1,57 @@
---
# Jar Lid Archive: Django (gunicorn) + Postgres + SeaweedFS (S3) as one compose
# stack. Source and CI live in forgejo.contrapeso.xyz/counterweight/jar-lid-archive.
#
# Per-deployment values (domain, port, retention) live in the host's host_vars,
# because the Caddy play on the edge host and the backup plays read them too and
# a role default cannot reach a second play.
jar_lid_archive_dir: /opt/jar-lid-archive
# ── Images ───────────────────────────────────────────────────────────────────
# The app follows a moving tag on purpose: every run pulls it, and compose
# recreates `web` only when the digest actually changed. CI pushes :master on
# every green build of master, plus :<sha>.
jar_lid_archive_image: forgejo.contrapeso.xyz/counterweight/jar-lid-archive
jar_lid_archive_image_tag: master
# Pinned to what the app's own compose.yaml and CI run, so staging rehearses the
# same stack. Bump them together with the app repo, not separately.
jar_lid_archive_postgres_image: "postgres:18"
jar_lid_archive_seaweedfs_image: "chrislusf/seaweedfs:4.47"
# Registry login. Empty = pull anonymously, which works today: the package is
# owner-scoped in Forgejo and public even though the repository is private.
jar_lid_archive_registry: forgejo.contrapeso.xyz
jar_lid_archive_registry_user: ""
jar_lid_archive_registry_token: ""
# ── Networking ───────────────────────────────────────────────────────────────
# gunicorn listens on the HOST network on this port. See the README for why host
# networking rather than a published port: Docker's published ports bypass UFW.
jar_lid_archive_port: 8000
# Loopback-only ports for the two backing services, so the web container (on the
# host network) can reach them and nothing else can.
jar_lid_archive_postgres_port: 5432
jar_lid_archive_s3_port: 8333
jar_lid_archive_domain: "" # e.g. jars-staging.contrapeso.xyz
# ── App settings ─────────────────────────────────────────────────────────────
jar_lid_archive_postgres_db: jarlid
jar_lid_archive_postgres_user: jarlid
jar_lid_archive_s3_access_key: jarlid
jar_lid_archive_s3_bucket: photos
# Secrets: pass from the vault. The assert in tasks/deploy.yml refuses empties.
jar_lid_archive_django_secret_key: ""
jar_lid_archive_postgres_password: ""
jar_lid_archive_s3_secret_key: ""
# ── Seed users ───────────────────────────────────────────────────────────────
# Loaded with `manage.py seed_users -` on every run. That command RESETS each
# listed user's password and flags, so the vault stays the source of truth.
# Same usernames as the app's tests/fixtures/users.json, never its passwords:
# this deployment is reachable from the internet.
# - {username: alice, password: "...", email: "...", is_superuser: false}
jar_lid_archive_seed_users: []

View file

@ -0,0 +1,100 @@
---
- name: Assert the deployment is fully specified
ansible.builtin.assert:
that:
- jar_lid_archive_domain | length > 0
- jar_lid_archive_django_secret_key | length >= 50
- jar_lid_archive_postgres_password | length > 0
- jar_lid_archive_s3_secret_key | length > 0
fail_msg: >-
jar_lid_archive needs a domain and its three secrets from the vault. The
app falls back to development defaults for every one of them, so an empty
value here would not fail - it would deploy with a published secret key.
no_log: true
- name: Create the deployment directory
ansible.builtin.file:
path: "{{ jar_lid_archive_dir }}"
state: directory
owner: root
group: root
mode: '0755'
# Existence only, no owner: the postgres and seaweedfs entrypoints chown their
# own data directories to the uid inside the container. Declaring an owner here
# would fight them on every run - the mempool MariaDB lesson.
- name: Ensure the data directories exist
ansible.builtin.file:
path: "{{ jar_lid_archive_dir }}/{{ item }}"
state: directory
loop:
- postgres
- seaweedfs
# diff: false, because --diff prints rendered content and this is all secrets.
- name: Write the compose environment file
ansible.builtin.template:
src: env.j2
dest: "{{ jar_lid_archive_dir }}/.env"
owner: root
group: root
mode: '0600'
diff: false
- name: Write docker-compose.yml
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ jar_lid_archive_dir }}/docker-compose.yml"
owner: root
group: root
mode: '0644'
- name: Log in to the container registry
ansible.builtin.command:
cmd: "docker login {{ jar_lid_archive_registry }} -u {{ jar_lid_archive_registry_user }} --password-stdin"
stdin: "{{ jar_lid_archive_registry_token }}"
when: jar_lid_archive_registry_token | length > 0
changed_when: false
no_log: true
# Pulled on every run: staging follows :master. `up` below recreates `web` only
# when the pulled digest differs from the running one.
- name: Pull images
ansible.builtin.command: docker compose pull --quiet
args:
chdir: "{{ jar_lid_archive_dir }}"
changed_when: false
# --wait blocks until every service's healthcheck passes, and `web`'s is
# /health/, which itself checks the database and the bucket - so this task
# succeeding means the app actually serves, not merely that containers exist.
- name: Start the stack and wait for it to be healthy
ansible.builtin.command: docker compose up -d --remove-orphans --wait --wait-timeout 300
args:
chdir: "{{ jar_lid_archive_dir }}"
register: jar_lid_archive_up
changed_when: jar_lid_archive_up.stderr is search('Created|Recreated|Started|Removed')
- name: Read the deployed version
ansible.builtin.uri:
url: "http://127.0.0.1:{{ jar_lid_archive_port }}/health/"
return_content: true
register: jar_lid_archive_health
# Under --check the stack was never started, so there is nothing to ask.
when: not ansible_check_mode
- name: Show the deployed version
ansible.builtin.debug:
msg: "{{ jar_lid_archive_health.json }}"
when: not ansible_check_mode
# Used by the backup (services/jar-lid-archive/setup_backup_jar_lid_archive.yml).
# Lives here because what to dump is a fact about this stack's layout.
- name: Install the dump script
ansible.builtin.template:
src: dump.sh.j2
dest: /usr/local/bin/jar-lid-archive-dump.sh
owner: root
group: root
mode: '0755'
validate: "bash -n %s"

View file

@ -0,0 +1,74 @@
---
# Same Docker CE install as roles/mempool/tasks/docker.yml. infra/910_docker
# targets every managed host, so each Docker role installs its own engine.
- name: Remove old Docker-related packages
apt:
name:
- docker.io
- docker-doc
- docker-compose
- podman-docker
- containerd
- runc
state: absent
purge: yes
autoremove: yes
- name: Update apt cache
apt:
update_cache: yes
- name: Install prerequisites
apt:
name:
- ca-certificates
- curl
state: present
- name: Create directory for Docker GPG key
file:
path: /etc/apt/keyrings
state: directory
mode: '0755'
- name: Download Docker GPG key
get_url:
url: https://download.docker.com/linux/debian/gpg
dest: /etc/apt/keyrings/docker.asc
mode: '0644'
- name: Get Debian architecture
command: dpkg --print-architecture
register: deb_arch
changed_when: false
- name: Add Docker repository
apt_repository:
repo: "deb [arch={{ deb_arch.stdout }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian {{ ansible_distribution_release }} stable"
filename: docker
state: present
update_cache: yes
- name: Install Docker packages
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: present
update_cache: yes
- name: Ensure Docker is started and enabled
systemd:
name: docker
enabled: yes
state: started
- name: Add user to docker group
user:
name: "{{ ansible_user }}"
groups: docker
append: yes

View file

@ -0,0 +1,6 @@
---
# import_tasks, not include_tasks: static imports stay visible to --list-tasks,
# which is the main verification tool in this repo.
- ansible.builtin.import_tasks: docker.yml
- ansible.builtin.import_tasks: deploy.yml
- ansible.builtin.import_tasks: seed.yml

View file

@ -0,0 +1,13 @@
---
# seed_users is idempotent but RESETS each listed user's password and flags, so
# re-running it is how a rotated vault password reaches the app. It reports
# "N created, M updated"; only a creation counts as a change.
- name: Seed users
ansible.builtin.command:
cmd: docker compose exec -T web python manage.py seed_users -
chdir: "{{ jar_lid_archive_dir }}"
stdin: "{{ jar_lid_archive_seed_users | to_json }}"
register: jar_lid_archive_seed
changed_when: "jar_lid_archive_seed.stdout is not search('Seeded users: 0 created')"
when: jar_lid_archive_seed_users | length > 0
no_log: true

View file

@ -0,0 +1,123 @@
# Managed by Ansible - roles/jar_lid_archive. Do not edit on the host.
#
# Same three services as the app repo's compose.yaml, with three differences:
# - `web` runs the CI-built image instead of `build: .`
# - `web` is on the HOST network, so UFW and the tailnet ACL decide who reaches
# gunicorn. A published port would be DNATed by Docker straight past UFW and
# answer on the LAN too. See the role README.
# - data is bind-mounted under {{ jar_lid_archive_dir }} rather than in named
# volumes, so the backup tars a path it can name.
#
# Secrets are interpolated from .env (0600) beside this file; this file has none.
name: jar-lid-archive
x-logging: &logging
# Docker's default json-file driver never rotates, on a 10 GB disk.
driver: json-file
options:
max-size: "10m"
max-file: "3"
x-postgres-env: &postgres-env
POSTGRES_DB: "{{ jar_lid_archive_postgres_db }}"
POSTGRES_USER: "{{ jar_lid_archive_postgres_user }}"
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
x-s3-env: &s3-env
S3_ACCESS_KEY: "{{ jar_lid_archive_s3_access_key }}"
S3_SECRET_KEY: ${S3_SECRET_KEY:?}
services:
web:
image: "{{ jar_lid_archive_image }}:{{ jar_lid_archive_image_tag }}"
# gunicorn binds 0.0.0.0:{{ jar_lid_archive_port }} - the port is fixed by the image's CMD.
network_mode: host
environment:
<<: [*postgres-env, *s3-env]
POSTGRES_HOST: 127.0.0.1
POSTGRES_PORT: "{{ jar_lid_archive_postgres_port }}"
S3_ENDPOINT_URL: "http://127.0.0.1:{{ jar_lid_archive_s3_port }}"
S3_BUCKET: "{{ jar_lid_archive_s3_bucket }}"
DJANGO_DEBUG: "0"
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:?}
# 127.0.0.1 for the container healthcheck and the probe, which both call
# /health/ directly rather than through Caddy.
DJANGO_ALLOWED_HOSTS: "{{ jar_lid_archive_domain }},127.0.0.1,localhost"
# Caddy terminates TLS and forwards plain HTTP with X-Forwarded-Proto. The
# app must trust that header or every POST fails CSRF and share links come
# out http://. Safe only because nothing but Caddy can reach gunicorn.
DJANGO_BEHIND_TLS_PROXY: "1"
DJANGO_CSRF_TRUSTED_ORIGINS: "https://{{ jar_lid_archive_domain }}"
depends_on:
db:
condition: service_healthy
storage:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:{{ jar_lid_archive_port }}/health/')"]
interval: 30s
timeout: 5s
retries: 5
start_period: 180s
logging: *logging
restart: unless-stopped
db:
image: "{{ jar_lid_archive_postgres_image }}"
environment: *postgres-env
ports:
- "127.0.0.1:{{ jar_lid_archive_postgres_port }}:5432"
volumes:
- ./postgres:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 15
start_period: 120s
logging: *logging
restart: unless-stopped
storage:
image: "{{ jar_lid_archive_seaweedfs_image }}"
environment: *s3-env
# Copied from the app's compose.yaml: write the S3 identity from the
# environment, then hand over to the image's entrypoint, which drops root
# and runs master + volume + filer + S3 gateway.
#
# The `rm -f` is ours, and load-bearing. On a RESTART of the same container
# (a reboot, a backup's stop/start) /tmp/s3.json survives, owned by seaweed
# from the chown below. Root cannot then re-create it: /tmp is sticky and
# the kernel's protected_regular refuses O_CREAT on another user's file
# there. The result was "can't create /tmp/s3.json: Permission denied", an
# unhealthy storage container, and web never starting - found on the first
# backup run.
entrypoint: ["sh", "-c"]
command:
- |
rm -f /tmp/s3.json
umask 077
cat > /tmp/s3.json <<EOF
{"identities": [{"name": "app",
"credentials": [{"accessKey": "$$S3_ACCESS_KEY", "secretKey": "$$S3_SECRET_KEY"}],
"actions": ["Admin", "Read", "Write", "List", "Tagging"]}]}
EOF
chown seaweed /tmp/s3.json
exec /entrypoint.sh server -s3 -s3.config=/tmp/s3.json
ports:
- "127.0.0.1:{{ jar_lid_archive_s3_port }}:8333"
volumes:
- ./seaweedfs:/data
# start_period, because this VM's disk is on nodito's HDD mirror, shared
# with knots-box. Under that contention SeaweedFS has taken over the 150s
# that retries x interval allowed; compose then marked it unhealthy, and
# web's depends_on gave up and never started it - a 26-minute outage.
# Failures inside start_period do not count.
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8333/healthz"]
interval: 5s
timeout: 3s
retries: 30
start_period: 300s
logging: *logging
restart: unless-stopped

View file

@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Jar Lid Archive dump - managed by Ansible (roles/jar_lid_archive).
#
# Writes ONE uncompressed tar to STDOUT, for roles/backup_source to encrypt:
# db.dump pg_dump custom format -> pg_restore -d jarlid db.dump
# seaweedfs/ the photo store, as files
#
# Called with `web` stopped (the backup playbook arranges it). web is the only
# writer to db and storage, so both are quiescent and agree with each other: no
# row points at a photo the tar does not have. storage keeps running; with no
# writes arriving its volume files do not change under tar.
#
# The pg_dump lands in a temp dir on the same disk as the live database, which
# is already plaintext; it is gone before this script exits.
#
# tar, not tar.gz: the bulk is JPEGs, which do not compress, and gzip on one
# core would only make the window longer.
set -euo pipefail
cd {{ jar_lid_archive_dir }}
tmp=$(mktemp -d {{ jar_lid_archive_dir }}/.dump.XXXXXX)
trap 'rm -rf "$tmp"' EXIT
docker compose exec -T db pg_dump -U {{ jar_lid_archive_postgres_user }} -d {{ jar_lid_archive_postgres_db }} -Fc > "$tmp/db.dump"
tar -cf - -C "$tmp" db.dump -C {{ jar_lid_archive_dir }} seaweedfs

View file

@ -0,0 +1,5 @@
# Managed by Ansible - roles/jar_lid_archive. Do not edit on the host.
# Read by docker compose to interpolate docker-compose.yml. 0600: secrets.
POSTGRES_PASSWORD={{ jar_lid_archive_postgres_password }}
S3_SECRET_KEY={{ jar_lid_archive_s3_secret_key }}
DJANGO_SECRET_KEY={{ jar_lid_archive_django_secret_key }}

View file

@ -43,6 +43,7 @@
"tag:mempool-box:8080", "tag:mempool-box:8080",
"tag:memos-box:5230", "tag:memos-box:5230",
"tag:music-box:4533", "tag:music-box:4533",
"tag:jar-lid-archive-staging-box:8000",
"tag:monerod-box:18089", "tag:monerod-box:18089",
"tag:monerod-box:8081", "tag:monerod-box:8081",
"tag:arbret-staging-box:80", "tag:arbret-staging-box:80",
@ -82,7 +83,8 @@
"tag:prd-arbret:22", "tag:prd-arbret:22",
"tag:prd-vipy:22", "tag:prd-vipy:22",
"tag:memos-box:22", "tag:memos-box:22",
"tag:stalwart-box:22" "tag:stalwart-box:22",
"tag:jar-lid-archive-staging-box:22"
], ],
"src": [ "src": [
"tag:small-backups-box" "tag:small-backups-box"
@ -102,6 +104,9 @@
"tag:fulcrum-box": [ "tag:fulcrum-box": [
"{{ headscale_namespace }}@" "{{ headscale_namespace }}@"
], ],
"tag:jar-lid-archive-staging-box": [
"{{ headscale_namespace }}@"
],
"tag:knots-box": [ "tag:knots-box": [
"{{ headscale_namespace }}@" "{{ headscale_namespace }}@"
], ],

View file

@ -0,0 +1,51 @@
---
# Jar Lid Archive on its own box, published from the edge host.
#
# See roles/jar_lid_archive/README.md. Per-host values (domain, port, seed users,
# secrets) are in host_vars/<host>/, so a production host is a new inventory
# entry in [jar_lid_archive], not a new playbook.
- name: Deploy Jar Lid Archive
hosts: jar_lid_archive
become: yes
roles:
- jar_lid_archive
# "Does the app answer", as opposed to "are the containers up". /health/
# runs SELECT 1 and touches the bucket, and returns 503 if either fails.
- role: healthcheck
vars:
healthcheck_name: jar-lid-archive
healthcheck_description: "Jar Lid Archive /health/ (app, database and photo store)"
healthcheck_command: "curl -fsS --max-time 10 -o /dev/null http://127.0.0.1:{{ jar_lid_archive_port }}/health/"
healthcheck_push_url: "https://{{ subdomains.gatus }}.{{ root_domain }}/api/v1/endpoints/probe_{{ jar_lid_archive_instance }}/external"
healthcheck_push_token: "{{ gatus_push_tokens[inventory_hostname] }}"
- name: Publish Jar Lid Archive from the edge host
hosts: edge
become: yes
tasks:
# No basic auth: the one-off links (/links/...) are opened by people who have
# no account and no proxy credential. The app's own login guards the rest.
#
# Caddy sets X-Forwarded-Proto: https and overwrites any value the client
# sent, which is what lets the app trust it (DJANGO_BEHIND_TLS_PROXY).
#
# No request size limit is set, deliberately: Caddy has none by default, and
# phones upload multi-megabyte photos.
- name: Expose Jar Lid Archive over HTTPS
ansible.builtin.include_role:
name: caddy_site
vars:
caddy_site_name: "{{ hostvars[jla_host].jar_lid_archive_instance }}"
caddy_site_domain: "{{ hostvars[jla_host].jar_lid_archive_domain }}"
# Tailscale MagicDNS: the box has no public address.
caddy_site_upstream: "{{ hostvars[jla_host].ansible_host }}:{{ hostvars[jla_host].jar_lid_archive_port }}"
caddy_site_resolvers: "100.100.100.100"
loop: "{{ groups['jar_lid_archive'] }}"
# Not `item`: the role's own tasks loop, and would shadow it.
loop_control:
loop_var: jla_host
# The per-site validate cannot catch a conflict between sites.
- name: Validate the whole Caddyfile
ansible.builtin.command: caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
changed_when: false

View file

@ -0,0 +1,34 @@
---
# Jar Lid Archive backup: the database and the photo store, as ONE artefact,
# dumped and age-encrypted on the box itself. small-backups-box pulls it.
#
# Only `web` is stopped. It is the only writer to both the database and the
# photo store, so with it down both are quiescent and are captured at the same
# instant - no row points at a photo the tar lacks. db stays up for pg_dump.
#
# storage is deliberately NOT stopped, though an earlier version did: on this
# VM's contended HDD-backed disk SeaweedFS can take minutes to come back, and the
# first test run left the app down for 26 minutes waiting on it.
- name: Configure the Jar Lid Archive backup on its own host
hosts: jar_lid_archive
become: yes
tasks:
- name: Ensure Jar Lid Archive dumps itself, encrypted, on a timer
ansible.builtin.include_role:
name: backup_source
vars:
backup_source_name: "{{ jar_lid_archive_instance }}"
backup_source_description: "Jar Lid Archive ({{ jar_lid_archive_instance }})"
backup_source_dump_command: /usr/local/bin/jar-lid-archive-dump.sh
# Uncompressed: the bulk is JPEGs. See roles/jar_lid_archive/templates/dump.sh.j2.
backup_source_artifact_suffix: "tar.age"
backup_source_stop_command: "docker compose -f /opt/jar-lid-archive/docker-compose.yml stop web"
# `up -d --wait`, not `start`: it also repairs a db or storage that died
# meanwhile, and only returns once /health/ passes.
backup_source_start_command: "docker compose -f /opt/jar-lid-archive/docker-compose.yml up -d --wait --wait-timeout 300"
backup_source_retention_days: "{{ jar_lid_archive_backup_retention_days }}"
backup_source_on_calendar: "*-*-* 02:20:00"
# Reported to Gatus as backup-dump_<instance>.
backup_source_push_url: "https://{{ subdomains.gatus }}.{{ root_domain }}/api/v1/endpoints/backup-dump_{{ jar_lid_archive_instance }}/external"
backup_source_push_token: "{{ gatus_push_tokens[inventory_hostname] }}"

View file

@ -57,6 +57,7 @@
- import_playbook: services/xmrblocks/deploy_xmrblocks_playbook.yml - import_playbook: services/xmrblocks/deploy_xmrblocks_playbook.yml
- import_playbook: services/memos/deploy_memos_playbook.yml - import_playbook: services/memos/deploy_memos_playbook.yml
- import_playbook: services/navidrome/deploy_navidrome_playbook.yml - import_playbook: services/navidrome/deploy_navidrome_playbook.yml
- import_playbook: services/jar-lid-archive/deploy_jar_lid_archive_playbook.yml
- import_playbook: services/forgejo-runner/deploy_forgejo_runner_playbook.yml - import_playbook: services/forgejo-runner/deploy_forgejo_runner_playbook.yml
- import_playbook: services/phoenixd/deploy_phoenixd_playbook.yml - import_playbook: services/phoenixd/deploy_phoenixd_playbook.yml
- import_playbook: services/headscale/deploy_headscale_playbook.yml - import_playbook: services/headscale/deploy_headscale_playbook.yml
@ -72,6 +73,7 @@
- import_playbook: services/forgejo/setup_backup_forgejo.yml - import_playbook: services/forgejo/setup_backup_forgejo.yml
- import_playbook: services/lnbits/setup_backup_lnbits.yml - import_playbook: services/lnbits/setup_backup_lnbits.yml
- import_playbook: services/memos/setup_backup_memos.yml - import_playbook: services/memos/setup_backup_memos.yml
- import_playbook: services/jar-lid-archive/setup_backup_jar_lid_archive.yml
- import_playbook: playbooks/backups.yml - import_playbook: playbooks/backups.yml
# Deliberately not here. Every playbook in the repo is either imported above or # Deliberately not here. Every playbook in the repo is either imported above or

View file

@ -90,6 +90,14 @@ vms = {
disk_size_gb = 20 disk_size_gb = 20
ipconfig0 = "ip=dhcp" ipconfig0 = "ip=dhcp"
}, },
"jar-lid-archive-staging-box" = {
vmid = 3300
name = "jar-lid-archive-staging-box"
cores = 1
memory_mb = 1024
disk_size_gb = 10
ipconfig0 = "ip=dhcp"
},
# disk_size_gb here is the music library's ceiling, not just the OS: the # disk_size_gb here is the music library's ceiling, not just the OS: the
# library lives on the root disk at /srv/music. ~45 GB is usable after # library lives on the root disk at /srv/music. ~45 GB is usable after
# Debian, so roughly 300-450 albums in FLAC. Growing it means a Proxmox # Debian, so roughly 300-450 albums in FLAC. Growing it means a Proxmox