荐《给阿嬷的情书》

<p> 看过电影,时间已经临近十二点,我走在回家的路上,不禁思绪万千。我要说些什么?说与谁听呢?不知道从哪里听来的:表达是有对象的。当我在写这篇文章时,我希望写给谁?</p> <p> 曾经写文章只为我自己,因为我是个怀旧的人,当自己韶华已逝之时,我希望自己有些东西可以缅怀。</p> <p> 如今我发现可以真正分享自己的情绪给对方,让她感受我所感受的、体会我体会的,我觉得这是很美好的事情,我很想永远珍藏这种美好。</p> <p> 年少私奔,木生与淑柔情定终生,新育一女二子,奈时局艰,木生迫而下南洋,颠沛流离,从马来亚至暹罗,十年而已。修路三轮货船,终苦尽甘来时,竟遭歹人害命。可怜淑柔拉扯子女,幸得南枝相助十八载,方得儿孙满堂。</p> <p> 木生去世六十年,淑柔才知道事情原来是这样的——原来是南枝在默默帮助自己啊。</p> <p> 那封封家书,道不尽的纸短情长,情义满溢指尖。</p> <blockquote> <p>吾妻淑柔,展信安康。随信寄两百元,我一切无恙,生意昌顺。行船入夜,恰江上升明月,圆如玉坠,仿若身在故乡,似与你并肩共享。江海万里,心中念你,便不觉遥远。湄南河畔木棉花盛开,像极了家乡的春天,压了一朵在信中,望你也能闻到花香。近来握笔练字,学会了你的名,虽然潦草,努力数日定会成功。纸短情长,伏惟珍重。——夫 木生</p> </blockquote> <blockquote> <p>木生吾夫,五十元收到。听到要去汕头市区照相,三个小孩高兴得睡不着,天未亮就出门。穿衣服的是大弟,白衣服的是小弟,大妹最聪明,折了飞机和船摆姿势,还叮嘱我一定要把飞机寄给你。想爸爸时,一眨眼就能飞回来。</p> </blockquote> <blockquote> <p>信件及家用已悉数收到,家中诸事皆顺,庄稼长势尤好,番薯肥美壮实。昨日将其烤作晚餐,孩子们十分喜爱。大弟最馋嘴,半夜抹黑,又偷啃番薯,声响吵醒我们。大妹气得直跺脚,细弟更是严厉,强其开口,务必查明罪证,终于水落石出。不过多时,三人又嬉闹起来,待鸡鸣声响,方才睡去。望你见此,定能感同身受,因这三个可爱的孩子备感幸福。</p> </blockquote> <p> 书信一来一往,道不尽情深。</p> <p> 片中不乏让人发笑的场景,我记下的有这么一刻:</p> <p> 木生因殴打纵火印度商人入狱,嘱托南枝替自己收债五十港币寄给家里,但并未收够五十。南枝爸拿自己私房钱凑给木生。南枝对爸说:你长大了:)</p> <p> 南枝如姨两人脚踩洗衣的样子真可爱呀!</p> <p> 什么是银信局?银指钱,信即家书,一起来理解就是帮人往家里寄钱和家书的地方。</p> <p> 什么是无米粿?又称韭菜粿,一种潮汕小吃,主要用番薯粉和韭菜制作。外形似饼,分为甜馅和咸馅两种。蒸熟后粿皮呈半透明,可看见里面的馅料。</p> <p> 什么是爱情?爱,对对方之爱,情,对对方之情。情爱者,用心至深也。</p> <p> 那声声情话,诉的是火热的思恋,怨的是不能长久在一起。既然不能相见,就让这情思附着纸上,飘洋过海,代我表达相思。</p> <p> 现在的社会,物质生活太过丰富,人浸在物欲中太久会忘记从哪里来,要到哪里去。时刻需要某些人和事来提醒自己,人生在世最重要的是什么——觅一伴侣,友二三,父母安康,阅天下风景,如此而已。</p>

2026/7/6
阅读更多

夏雨

<p> 七点十一起床,七点二十六开始跑步,跑了四十分钟,7.02 公里。和上周日比起来,这次跑前二十分钟要轻松一些,后二十分钟有些突破极限了,因为我的腰部开始有痛的感觉,这不是病痛,而是当跑步没有成为习惯时的一种身体反应。当我继续跑时,那种痛感自然消退了。</p> <p> 我的运气还算不错,当我跑完走路休息时,开始下雨了。雨一开始几滴,几分钟后开始下毛毛雨,再过一分钟雨越下越急,再然后雨就大了起来。我在雨中走着,满是凉意的雨滴在我滚烫的手臂脸上身上,好舒服啊。在这深圳的夏天,有什么能比跑一场大汗淋漓的步后被雨洗澡更快乐的事情呢!</p> <p> 走在雨中,望向远处的湖,我想要不要拍个照呢?犹豫间身体已经走过去,在大雨下,我拿着淋雨的手机拍了两张湖面下雨的照片,留下一张看着更大的。</p> <p> 看到雨中有人仍在跑步,我突然好奇:雨中跑步什么感觉呢?于是,开始跑,跑了一段路,畅快无比。雨中跑步同样别有一番滋味啊。</p>

2026/7/4
阅读更多

我思

<p> 思考不必局限于形式。我开始写下心中所想。</p> <p> 病痛往往会让人思考自己的处境,我的处境让我有些麻木和难受。咳嗽的毛病到今天已经六天,当拖到无可再拖时,我终于选择去医院看看。挂号,医生问了情况,让去拍CT、抽血化验。拍CT的仪器以前从未见过,好像一个甜甜圈。抽血时当针头刺进血管那一刻,我感受到轻微的刺痛,好在这痛很快就结束了。</p> <p> 结果在半小时后出来,我又去挂号医生处,询问如何解决我的问题。他说可以吃药,也可以住院挂吊水,我问住院要多久,他说要五六天,我说那不行我还要工作还是只开些药吧。总共花了119.3元,医保报销了350.87,嗯医保省钱啊。</p> <p> 我本来请半天假,但是工作上的问题没处理完只得又多工作了一个半小时才走。</p> <p> 这一周我很明显地感受到:自己的身体在虚弱,和咳嗽有关,和工作强度有关。但是好像也无解。有多大本事,做什么工作。在没法选择工作时,至少保证自己能够生存下去。埋怨只会激发更多的不满,于事无补。</p> <p> 张云帆在知行小酒馆播客讲的两期内容,我现在又翻看,觉得可以藉此改变自己的生活。时间管理是人世俗成功的不二法门,他如是说。我开始践行他的见解。</p> <p> 当我开始记录时间,我发现:时间如沙,我让它溜走却做不成任何事。很多时间都被浪费在无谓的事情上,这实在是不该。</p> <p> 但是,肉体的疲惫无法拯救精神危机,可能只会让肉体更加堕落。我想阻挡,却有些无可奈何。</p>

2026/6/24
阅读更多

园区的猫

<p> 时常见到工业区里的一只猫</p> <p> 猫在园区里游荡</p> <p> 一会儿在宿舍楼下</p> <p> 一会儿在工厂楼下</p> <p> 四处游荡并不怕人</p> <p> 曾经她来到我五楼的家</p> <p> 对着我喵喵地叫</p> <p> 以为她饿给她火腿肠</p> <p> 不吃</p> <p> 只是在叫</p> <p> 语言不通真恼人</p> <p> 为何人和猫不能对话</p> <p> 为何人和人能对话</p> <p> 但好像隔了层磨砂玻璃</p>

2026/6/21
阅读更多

去深圳国际美术馆——艺术源于生活,是对生活的总结和升华

<p> 我发现自己在选择这种开拓眼界的活动时很随机,往往是在某处看到文章或者知道消息,觉得有意思就想去了。这次也是,我有时会看一下保利票务上有没有感兴趣的音乐剧或其它演出,周三那天很偶然,就看到了<a href="https://mp.weixin.qq.com/s/UDeaLcja7d0Ix7Jmot_0vA">深圳国际美术馆要办七大展览</a>的消息。想去看是因为最近总是高压的工作让我有些崩溃,想要感受些不一样的事物调节一下。这次经历让我想到自己去年去看<a href="/posts/september-and-october/">印象派画展</a>时的事情,那时欣赏了很多梵高、莫奈等大师的作品。</p> <p> 工作上从周三开始去了加工厂出差,在惠州的一个鸟不拉屎的地方,坐公交坐到火车站竟然需要从起点坐到终点站,要花两个小时,无奈只能坐顺风车。昨天才回来,打车回来花了将近两百块,而且这个司机开车不行,头晕难受一路子,到家后悲催地发现:竟然不能开发票(高德打车顺风车开不了发票),也就无法报销。本来工作是赚钱的,我还倒贴两百块,可想而知我有多郁闷。下次再让我出去,绝对不去了。</p> <p> 早上睡到九点多,简单吃个早餐(两片全麦面包+一盒纯牛奶)就出发了。到地方后,按照主题开始观看:</p> <ol> <li>大师之境——布达佩斯艺术博物馆馆藏</li> <li>东方根脉与当代转化——中国抽象派画作</li> <li>闪烁的1/∞——科技与艺术的结合</li> <li>维素绵延——中国传统水墨画</li> <li>徐冰:字在乐园——《凤凰》作品印象深刻</li> <li>新链:艺术的未来——艺术的未来和对科技的巧妙运用分不开关系</li> </ol> <div id="outline-container-headline-1" class="outline-2"> <h2 id="headline-1"> 大师之境 </h2> <div id="outline-text-headline-1" class="outline-text-2"> <p> 我觉得这是几个展中最易懂的一个,因为画的都是自然中存在的事物,而非抽象的符号。即使不在自己的生活范畴里,发挥些想象力也是能够体会到的。我觉得自己能够从这些画作中体会到一种独属于自己的理解。</p> <ul> <li>夏,秋两幅作品,作者是<a href="https://www.mfab.hu/artworks/?artwork_author=veronese-bonifazio">Bonifazio Veronese</a>。当我想到夏,想到秋,该怎么表达出来,用画面的形式?作者为什么选择人作为描绘季节的核心?孩子代表着怎样的含义?</li> <li><a href="https://www.mfab.hu/artworks/8872/">圣父</a>,作者是Giovanni Domenico Tiepolo。这幅画吸引我的是它用到了那种蓝色,那种能够看出衣物质感的蓝色,这样一种上帝的形象,看起来还行</li> <li><a href="https://www.mfab.hu/artworks/517/">Ondine</a>,作者是Jules Lefebvre。这幅画吸引人的地方不言而喻——年轻貌美的裸体女子形象。等身的高度,红棕色的头发垂到膝盖,单手触摸着墙壁单手抚发。对置式(重心在一条腿上,另外一条腿弯曲。整个身体呈现S形)站姿让女人好像要动起来。光滑的身体雕塑般的质感,描绘出一种理想美。</li> <li>飞鸽传书。这幅画过于鲜艳了,这种鲜艳让我觉得女孩是很真实的,手握着信鸽,期待它将自己的爱意传递给远方的恋人。</li> <li>红罂粟。罂粟花盛开在绿色的原野上,有毒的花朵却是这样的美丽。</li> <li>阿利加的悬崖岩岸。阳光穿过云层,射下一道道光辉,让我觉得:希望就在这里。</li> </ul> </div> </div> <div id="outline-container-headline-2" class="outline-2"> <h2 id="headline-2"> 中国抽象画作 </h2> <div id="outline-text-headline-2" class="outline-text-2"> <p> 抽象派画作,欣赏这种作品,更具个性体会,不同的颜色线条搭配出不一样的氛围,欣赏的人注视到不同的色调搭配,会有不一样的感受。</p> <ul> <li>荷塘,作者是吴冠中。色块或深或浅地铺叠在画布上,黑色、棕色、黄色、绿色、粉红,没有逻辑可言,不规则的形状,涂鸦般的线条,花非花,物非物</li> <li>灵魂之光,作者是朱德俊。很喜欢这幅作品,混乱不成人形,这是灵魂的形状?蒙眬迷茫,不知所云</li> <li>2019,作者是余有涵。近乎圆形的,波浪分割了几种颜色,界限分明,但是墨迹自然滴落,划过那分界,沾染着什么</li> </ul> </div> </div> <div id="outline-container-headline-3" class="outline-2"> <h2 id="headline-3"> 科技与艺术 </h2> <div id="outline-text-headline-3" class="outline-text-2"> <p> 最令我印象深刻的有几个:</p> <ol> <li>一个圆形,上面布满了各种不同时刻的钟,将一个人从早到晚的时间都囊括进去</li> <li>利用透镜和投影仪,表现出日冕的形态,很逼真</li> <li>星空。星星点缀着漆黑的夜空,深色背景下的,那一颗颗耀眼的星辰,好像我的思想,多密且乱</li> <li><a href="https://calculatingempires.net/">计算帝国</a>。作者探讨了从1500年到今天,技术与社会结构是怎样变化的</li> <li>叶锦添的存而有疑,说实话没看懂。现场的黑暗氛围,再加上视频里深沉的讲述,让我觉得有些害怕</li> </ol> <hr> <p> 艺术源于生活,如果一幅画不能引起自己的共鸣或者引发一些思考,那么这部作品对于我来说便一文不值。即使这部作品很有名。</p> <p> 艺术来源于生活,是对生活的总结和升华。日常的琐碎与不安,让人烦躁无法专注。有些情绪会在画作中得到深刻体现,能让人透过画作感悟自己的心绪。</p> <p> 艺术对于我的意义——溺水时好不容易鼻子接触到空气,呼吸的那第一口氧气。</p> <p> 生活是一天接着一天的,如果工作不是自己感兴趣的,把上班看成坐牢也很正常。而穿插于工作的一些小的爱好,像是真正的自己想追求的生活。艺术是我的一个爱好,欣赏那些艺术作品,让我有种焕然一新的感觉,不再是被工作摧残得不成样子的我,而是沉迷于画中的世界,欣赏那画的美与真。</p> <p> 一些燥热的不安的想法,向我弥漫而来,我无法阻挡只能任由对方侵蚀。我的精神比我的身体先投降,我的肉体也逐渐枯萎,人的与生俱来的意义在这一刻仿佛烟消云散。</p> </div> </div>

2026/5/30
阅读更多

Evening Run

<p> I got off work at 5:30, went home, changed into a sleeveless shirt, shorts, and running shoes, and headed out for a run on Phoenix Mountain across the street. Walking there, I couldn&#39;t help but think about what lies ahead. When you stay in a not-so-great environment for too long, your mindset turns negative, your behavior slowly becomes undisciplined, your body and mind weaken, and self-destructive habits start creeping in one after another. It&#39;s a vicious cycle. If you don&#39;t break it in time, you&#39;ll regret it when it&#39;s too late.</p> <p> As my work pace shifts, so does my state of mind. My ability to think clearly comes and goes. When work exhausts me to the point where I don&#39;t want to think at all, I turn into a soulless zombie—walking, eating, sleeping on autopilot. Maybe I&#39;ve been in this environment for too long, so long that I feel deep down: if things keep going like this, my future won&#39;t be what I imagine.</p> <p> Change doesn&#39;t happen overnight. It takes small, daily efforts and repeatedly pulling yourself out of despair. I&#39;m grateful that I can still write these words, still think. My environment is something I create for myself. If the objective environment doesn&#39;t suit me, then it&#39;s only natural to move to a better one.</p> <p> But changing jobs isn&#39;t easy. It comes with the risk of being unemployed for months, and I need to save up enough to cover those months&#39; expenses. Without savings, I can&#39;t make the leap.</p> <p> So I started running.</p> <p> As I run, I pay attention to how I&#39;m doing—whether my heart is beating strongly, whether my knees feel uncomfortable like before, whether my feet ache, whether they can handle the pressure from my shoes. It&#39;s cloudy today, but there&#39;s a breeze. As I run, the wind brushes past my ears and sides, slowly carrying away the heat building up in my body. I want to finish one full lap. It&#39;s been ten days since my last run, and I stayed up until 3 a.m. last night, so I was a little worried I might not be in good shape.</p> <p> Thankfully, I managed to finish the full three kilometers.</p> <p> After the run, I walked along the road, enjoying the feel of my new shoes. They cost me nearly two hundred yuan. Only after buying them did I realize they&#39;re not breathable. But I didn&#39;t return them—I just wear them. Running in cushioned shoes feels completely different from running without cushioning. It&#39;s so much more comfortable.</p> <p> Walking on the road after the run, with the wind blowing against my sweaty body—it felt amazing.</p> <p> Let me know if you&#39;d like an even shorter or more conversational version.</p>

2026/5/22
阅读更多

傍晚跑步

<p> 五点半下班,回家换上无袖衫、短裤和运动鞋,去对面的凤凰山跑步。走在路上,想着接下来的事情。当一个人长久地处在一个不那么好的环境中,人的心态会变得消极,人的行为会渐渐地无节制,人的身体与精神会日渐萎靡,自我伤害的行为一个接一个地。出现。这是一个恶性循环,如果不及时打破,待到无法挽回时悔之晚矣。</p> <p> 伴随着工作节奏的变化,我的状态会随之改变。清醒思考的能力也会时有时无。当工作挤压得我不想再思考时,我如同没有灵魂的行尸,在行走、吃饭、睡觉。或许是待在这种环境太久,久到我自己都觉得再这么下去,自己的未来不会是我想象的那个样子。</p> <p> 改变不是朝夕就见成效的事,每天的点滴努力,一次次地从沉沦中惊醒。我庆幸自己还能写下这些文字,还能思考。我的环境是由我自己创造的,如果客观环境不适合我,那换个更适合自己的环境就变得理所当然。</p> <p> 可是换并不容易。因为要承担几个月找不到工作的风险,而这几个月的开销是我要积攒出来的。如果没有积蓄就没法换工作。</p> <p> 我开始跑步。</p> <p> 一边跑一边感受自己的状态,感受自己的心脏是否在有力地跳动、膝盖是否会出现之前那样的不舒服感觉、脚部是否会有疼痛、鞋子传递到脚的压力脚能否承受。今天虽然阴天但是有风,跑步时风从耳边吹过、从身侧吹过,因跑步带来的热量在缓慢减少。我想跑完一圈,距离上次跑步已经有十天了,而且凌晨熬夜到三点,担心自己状态不佳。</p> <p> 还好,完整地跑完了三公里。</p> <p> 跑完,我走在路上,感受着新鞋带给我的感觉。买这双鞋花了将近两百,买来后才发现这鞋不透气。但是也没退就这样穿着。带缓震的鞋跑起来和之前不带的感觉不一样。带缓震的跑起来更舒适。</p> <p> 跑完,走在路上,风吹过满身是汗的身体,真舒服呀。</p>

2026/5/22
阅读更多

How to deploy Hermes Agent on Ubuntu

<div id="outline-container-headline-1" class="outline-2"> <h2 id="headline-1"> Phase 1: Server Initialization &amp; Base Setup </h2> <div id="outline-text-headline-1" class="outline-text-2"> <ol> <li>Purchase a VPS, deploy Ubuntu 24.04 LTS</li> <li>Create system user <code class="verbatim">tianhe</code>, add to sudo group</li> <li>Go to <a href="https://hermes-agent.nousresearch.com/"><a href="https://hermes-agent.nousresearch.com/">https://hermes-agent.nousresearch.com/</a></a>, run the install script to complete setup</li> </ol> </div> </div> <div id="outline-container-headline-2" class="outline-2"> <h2 id="headline-2"> Phase 2: Data Migration (from old Railway deployment) </h2> <div id="outline-text-headline-2" class="outline-text-2"> <ol> <li>Download backup from Cloudflare R2 via AWS CLI</li> <li>Restore <code class="verbatim">.hermes</code> directory from backup tarball</li> <li>Rebuild corrupted <code class="verbatim">state.db</code> — reimport from sessions/*.json files</li> <li>Honcho was originally on Railway; later migrated to local Docker Compose(PostgreSQL + pgvector + Redis)</li> </ol> </div> </div> <div id="outline-container-headline-3" class="outline-2"> <h2 id="headline-3"> Phase 3: R2 Backup Script Adaptation </h2> <div id="outline-text-headline-3" class="outline-text-2"> <ol> <li>Update <code class="verbatim">backup-to-r2.py</code> paths from old user home to <code class="verbatim">/home/tianhe</code></li> <li>Set up cron: daily at 03:00 UTC, keep 14 most recent backups</li> <li>Optimize backup: exclude venv/node_modules (119 MB → 27.5 MB)</li> </ol> </div> </div> <div id="outline-container-headline-4" class="outline-2"> <h2 id="headline-4"> Phase 4: Security Hardening </h2> <div id="outline-text-headline-4" class="outline-text-2"> <ol> <li>Bind Dashboard to <code class="verbatim">0.0.0.0</code> with <code class="verbatim">--insecure</code> flag (due to DNS rebinding protection interfering with reverse proxy)</li> <li>Harden SSH: <code class="verbatim">PermitRootLogin prohibit-password</code>, force publickey auth, disable password auth, disable X11 forwarding, limit MaxAuthTries to 3</li> <li>Enable UFW firewall: allow only 22/tcp (SSH) and 443/tcp (HTTPS)</li> <li>Configure fail2ban: 3 failures → 24-hour ban((sshd jail only))</li> <li>Enable unattended-upgrades for automatic security patches</li> </ol> </div> </div> <div id="outline-container-headline-5" class="outline-2"> <h2 id="headline-5"> Phase 5: Dashboard + Caddy Reverse Proxy </h2> <div id="outline-text-headline-5" class="outline-text-2"> <ol> <li>Build Web UI: npm install → npm run build</li> <li>Create <code class="verbatim">systemd</code> service for hermes-dashboard on <code class="verbatim">127.0.0.1:9119</code></li> <li>Configure Caddy reverse proxy: <code class="verbatim">herma.tianheg.co:443</code> → localhost:9119</li> <li>Host Header issue: Dashboard&#39;s built-in DNS rebinding protection rejects the forwarded hostname. Bind <code class="verbatim">0.0.0.0</code> with <code class="verbatim">--insecure</code> flag</li> <li>Integrate Cloudflare: SSL mode <code class="verbatim">Full</code>, Cloudflare Access auth layer</li> </ol> </div> </div> <div id="outline-container-headline-6" class="outline-2"> <h2 id="headline-6"> Phase 6: Gateway / Telegram Integration </h2> <div id="outline-text-headline-6" class="outline-text-2"> <ol> <li>Clear stale <code class="verbatim">gateway_state.json</code> (draining state from old environment)</li> <li>Install Gateway as user-level systemd service; fix service file path (was written to wrong location by <code class="verbatim">hermes gateway install</code>)</li> <li>Configure Telegram Bot Token in <code class="verbatim">.env</code></li> <li>Enable linger (<code class="verbatim">loginctl enable-linger</code>) to keep user services alive after SSH logout</li> <li>Verify: send /start to the bot, confirm auto-response and zero errors</li> </ol> </div> </div> <div id="outline-container-headline-7" class="outline-2"> <h2 id="headline-7"> Phase 7: Cleanup </h2> <div id="outline-text-headline-7" class="outline-text-2"> <ol> <li>Remove Railway leftovers: RAILWAY_TOKEN env var, railway CLI binary(11 MB)</li> <li>Create / update DevOps skills (security audit, Dashboard, Caddy proxy, Gateway service, data maintenance, R2 ops — 6 skills total)</li> </ol> </div> </div> <div id="outline-container-headline-8" class="outline-2"> <h2 id="headline-8"> Phase 8: Hermes Desktop Remote Access </h2> <div id="outline-text-headline-8" class="outline-text-2"> <p> <a href="https://hermes-agent.nousresearch.com/desktop">Hermes Desktop</a> is an Electron GUI for Hermes Agent. Shares the same <code class="verbatim">~/.hermes/</code> config, sessions, skills, and memory as the CLI. Runs on macOS, Windows, Linux.</p> <p> Desktop defaults to local mode (starts a local <code class="verbatim">hermes dashboard</code> backend). Remote mode turns Desktop into a thin client — agent runs on VPS, Desktop renders UI via WebSocket.</p> <p> <strong><strong>Server-side Setup</strong></strong></p> <p> The dashboard must bind to <code class="verbatim">0.0.0.0</code> (not <code class="verbatim">127.0.0.1</code>) so Tailscale IPs can reach the WebSocket endpoint. <code class="verbatim">--insecure</code> disables Host header validation which rejects non-loopback connections.</p> <ol> <li>Restart dashboard on 0.0.0.0:</li> </ol> <div class="src src-bash"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>hermes dashboard --host 0.0.0.0 --port <span style="color:#bd93f9">9120</span> --no-open --insecure</span></span></code></pre></div> </div> <ol> <li>Get session token (used by Desktop for auth):</li> </ol> <div class="src src-bash"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># from terminal (run on VPS):</span> </span></span><span style="display:flex;"><span>curl -s http://127.0.0.1:9120/ | grep -oP <span style="color:#f1fa8c">&#39;__HERMES_SESSION_TOKEN__=&#34;\\K[^&#34;]+&#39;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># or open http://127.0.0.1:9120/ in browser, open console, type:</span> </span></span><span style="display:flex;"><span>__HERMES_SESSION_TOKEN__</span></span></code></pre></div> </div> <ol> <li>Pin the token (optional — avoids token change after restart):</li> </ol> <div class="src src-bash"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># Add to ~/.hermes/.env:</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">HERMES_DASHBOARD_SESSION_TOKEN</span><span style="color:#ff79c6">=</span>&lt;token-from-step-2&gt;</span></span></code></pre></div> </div> <p> Fixed token lets Desktop use a stable <code class="verbatim">HERMES_DESKTOP_REMOTE_TOKEN</code> that survives dashboard restarts.</p> <p> <strong><strong>Network: Tailscale</strong></strong></p> <p> SSH tunnel (<code class="verbatim">ssh -L 9120:localhost:9120</code>) works but breaks on disconnect. Tailscale is more stable — zero port exposure, auto-reconnection.</p> <p> Install native Tailscale on VPS:</p> <div class="src src-bash"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -fsSL https://tailscale.com/install.sh | sh </span></span><span style="display:flex;"><span>sudo tailscale up</span></span></code></pre></div> </div> <p> Desktop connects via <code class="verbatim">http://&lt;tailscale-ip&gt;:9120</code>. No basic auth needed — port is only reachable inside tailnet.</p> <p> <strong><strong>tailscale serve trap</strong></strong>: If you previously ran <code class="verbatim">tailscale serve</code> on this port, the rule persists in state and intercepts traffic silently, returning 404. Fix:</p> <div class="src src-bash"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo tailscale serve --http<span style="color:#ff79c6">=</span><span style="color:#bd93f9">9120</span> off</span></span></code></pre></div> </div> <p> <strong><strong>Clash Verge conflict</strong></strong>: If you use Clash Verge TUN mode on the same machine, it may hijack Tailscale traffic (100.64.0.0/10) and route it through the proxy, making the Tailscale IP unreachable. Fix: add bypass rules at the top of Clash config:</p> <div class="src src-yaml"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>IP-CIDR,100.64.0.0/10,DIRECT,no-resolve </span></span><span style="display:flex;"><span>DOMAIN-SUFFIX,ts.net,DIRECT</span></span></code></pre></div> </div> <p>If using Fake-IP DNS mode, also add <code class="verbatim">*.ts.net</code> to <code class="verbatim">fake-ip-filter</code>.</p> <p> <strong><strong>Desktop Client Setup</strong></strong></p> <p> On Arch (CLI already installed):</p> <div class="src src-bash"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>hermes desktop</span></span></code></pre></div> </div> <p> On Windows: download .exe from <code class="verbatim">https://hermes-agent.nousresearch.com/desktop</code>.</p> <p> In Desktop settings → Gateway → Remote Gateway:</p> <ul> <li>Remote URL: <code class="verbatim">http://&lt;tailscale-ip&gt;:9120</code></li> <li>No auth (open inside tailnet), or use token from step 2</li> </ul> <p><strong><strong>Experience Notes</strong></strong></p> <ul> <li>First connection: session list loads slowly (initial sync of all history)</li> <li>After warm-up: chat is smooth, no perceptible latency</li> <li>Both Arch and Windows work</li> <li>Thin client — all tools (terminal, file ops) execute on VPS</li> </ul> </div> </div> <div id="outline-container-headline-9" class="outline-2"> <h2 id="headline-9"> Final System State </h2> <div id="outline-text-headline-9" class="outline-text-2"> <ul> <li>8 services active (Dashboard, Gateway, Caddy, SSH, fail2ban, UFW, Tailscale, Desktop)</li> <li>Dashboard: <code class="verbatim">0.0.0.0:9120</code> with <code class="verbatim">--insecure</code> (Tailscale-only, no public reverse proxy)</li> <li>Telegram platform: connected and responding</li> <li>Hermes Desktop connected via Tailscale: Arch + Windows clients</li> <li>Backup: daily at 03:00 UTC via Hermes internal cron (successful)</li> <li>Daily automatic backup to Cloudflare R2 at 03:00 UTC</li> <li>Memory provider: Honcho (self-hosted on local Docker)</li> </ul> </div> </div>

2026/5/19
阅读更多

May Day memories--Always remember to look up at the stars

<div id="outline-container-headline-1" class="outline-2"> <h2 id="headline-1"> Shenzhen to Hangzhou </h2> <div id="outline-text-headline-1" class="outline-text-2"> <p> On the evening of April 29th, I set off for home, carrying an old backpack. I couldn&#39;t remember how many years I&#39;d had it — I only knew that this trip nearly tore its handle off. Inside were clothes I&#39;d changed out of and hadn&#39;t washed yet, plus a change of clothes (a T-shirt, shorts, underwear, and socks), along with an extra pair of shorts, a pair of long pants, and a razor. I also had a small crossbody bag containing an e-ink reader, a power bank, and the keys to my Shenzhen apartment.</p> <p> I didn&#39;t work late that night. A little past eight, almost nine, I finished dinner, packed up, and left. The plan was to take the bus from Fufeng Road South to the metro station. But I stood there waiting for maybe ten or fifteen minutes before I checked the navigation app and realized: the bus I wanted had already stopped running. I couldn&#39;t help but blame myself for not checking the route sooner. I had no choice but to take a taxi — eleven yuan, not too bad. I took the metro to Bao&#39;an Airport Terminal 3. It was my first time there, and I couldn&#39;t help feeling a little nervous. I had to ask the staff how to get my boarding pass. Airports are complicated places where people from all kinds of financial backgrounds come together. It made me a bit uncomfortable, but I&#39;d been through this kind of situation a few times before, and after a while, I stopped minding. The security check made me feel insecure and distrusted — when was the last time someone invaded my privacy like this?</p> <p> Waiting at the gate was agonizing. My flight was delayed. It was supposed to take off at eleven at night but got pushed past midnight. I had enough energy to read at first, but later, though exhausted, I just couldn&#39;t fall asleep. I was afraid I&#39;d miss the flight — I was alone, and no one would think to remind me. Yawn after yawn, I couldn&#39;t stop.</p> <p> Once I boarded, I found my seat and sat down. I read the safety card in the seat pocket in front of me. I stared out the window, lost in thought. In the distance, the boarding tunnel had a sign that read &#34;AI? Use Alibaba Cloud.&#34; Okay then, I thought — I won&#39;t. I wasn&#39;t interested in the screen in front of me at first, but the woman next to me opened up some kind of flight tracking display. I found it interesting and started fiddling with mine too.</p> <p> The plane took off. The two drops right after liftoff made me a bit nervous. Then the plane gradually climbed, adjusting its angle. Watching the massive objects on the ground shrink into tiny points of light, I thought: this is only the second time I&#39;ve been this far from the earth. The plane slowly drifted through the clouds, rising to cruising altitude. At one moment I saw the altitude read 8,871 meters — already higher than Mount Everest. I still felt dizzy and uncomfortable on the plane. There was this constant beeping sound that annoyed me. But when the plane bumped through turbulence, I wasn&#39;t scared. There was a strange calm inside me, as if I knew I wasn&#39;t going to die. And even if I did — wouldn&#39;t there be compensation? :)</p> </div> </div> <div id="outline-container-headline-2" class="outline-2"> <h2 id="headline-2"> Hangzhou&#39;s cold caught me off guard </h2> <div id="outline-text-headline-2" class="outline-text-2"> <p> The plane landed at Hangzhou Xiaoshan Airport at 2:40 in the morning. The moment I stepped out into the skywalk, I regretted everything: why didn&#39;t I bring a long-sleeved jacket? Watching everyone else bundle themselves up tightly, I looked like an idiot in my Shenzhen outfit. I walked over to an area with benches where people were sleeping — each person curled up on two seats, looking as uncomfortable as could be. I still had a pair of long pants in my bag. I glanced around; people were sleeping, no one was watching me. I pulled out the pants and put them on over my shorts. Finally, a little warmth. I was cold and tired, so I followed their example, curled myself into a ball, and tucked myself into the seat. But I couldn&#39;t get comfortable. I drifted in and out of a daze until sometime past five in the morning.</p> <p> Then I walked out of the airport onto the road. From the terminal to the metro entrance — at most a hundred meters — but it felt like a kilometer. Every skeletal muscle in my body was shivering, trying to generate heat, but it wasn&#39;t enough. I walked while looking for food. Eventually I found a place selling wontons. Forty-nine yuan for a bowl of wontons and a chicken leg — what the hell, that&#39;s way too expensive! But then I thought about my shivering body. Expensive or not, I had to eat. Better than freezing to death.</p> <p> I took the metro from Xiaoshan to Fengqi Road. It was 7:10 in the morning. I wanted to take a stroll around West Lake. Back in 2022, when I was <a href="/posts/where-have-been-when-delivering/">delivering food</a>, I&#39;d pass by this area sometimes, but I&#39;d never actually gone in. I walked clockwise along the lake. There were lots of people there — some walking, some jogging, some dancing, some practicing with staffs. I just walked, unhurriedly, watching the distant Su Causeway stretch across the view like a wavy line, splitting the scenery into an upper and lower half. The morning sun shone on my body, but it didn&#39;t bring any warmth.</p> <p> I kept walking, and found a wooden bench by the lake. I sat down to rest. I looked at the surface of the lake, at the sightseeing boats drifting on the water, and my thoughts began to scatter in all directions. I got up from the bench and walked a bit further. Without really knowing why, I felt like leaving. I opened the map to figure out how to get to West Lake Cultural Square. Oh — I&#39;d need to take the metro from Ding&#39;an Road. So I walked. Along the way, I passed plenty of shops selling silk cheongsams and fabrics — they looked beautiful and delicate, and definitely not cheap. I walked past one shop, but then a thought suddenly flashed through my mind: why not buy a gift for Mom? I turned back and asked the shopkeeper how much the scarves were. There were several price ranges. I picked one with a nice pattern that seemed like good value for money. When I got home and gave it to her, Mom was really happy.</p> <p> By the time I reached West Lake Cultural Square, the sun had started to warm up. Finally, some real heat. I wandered around the square, but even resting in the shade of the trees, I could still feel a chill. Later I walked over to the Grand Canal, drawn by the sound of elderly men playing the saxophone. That bright, crisp sound stopped me in my tracks. If it weren&#39;t for that damn work nagging at me, I probably could have enjoyed it even more. In April and May in Hangzhou, trees along the canal were in bloom, and beneath them an old man was playing a love song with deep emotion. Who was he playing for?</p> <p> I walked to Midu Bridge Road, hoping to find the fast-food place I used to eat at back then. It was gone. But Song Sun Bakery was still there. And the Sha County小吃 shop was still there. Time lets the things that can endure keep on enduring.</p> </div> </div> <div id="outline-container-headline-3" class="outline-2"> <h2 id="headline-3"> Going home, going back to the countryside </h2> <div id="outline-text-headline-3" class="outline-text-2"> <p> I took the high-speed train home. The moment I walked through the door and saw my parents, it felt both strange and natural. I hadn&#39;t set foot in this house for over a year. Like old friends reunited after a long separation, I naturally slipped right back in. During the May Day holiday, I went back to the village to see my grandpa, who&#39;s in his nineties now. His hearing isn&#39;t what it used to be — you have to speak louder for him to hear. I looked out at the endless wheat fields and felt so much that was fresh and new, filling me with curiosity.</p> <p> I took a lot of photos: a single stalk of wheat standing upright like a peacock with its tail not yet fanned out; a bee&#39;s slender legs gripping an awn of wheat; a bee working hard to pollinate flowers in a patch of wildflowers; my dad riding his homemade electric tricycle with a canopy, turning a corner; the endless wheat fields captured from just the right angle; glistening green chili seedlings freshly watered; tiny cucumber seedlings that looked like bean sprouts; an unassuming little four-petal flower — one white petal, three gradient blue; the persimmon tree in the courtyard; the big tree next to Grandpa&#39;s kitchen; fennel with its clearly defined veins; the harmonious scene of a big tree and a house together; a fava bean pod resting in the palm of my hand.</p> <p> I took so many photos. It made me rediscover the beauty of the countryside.</p> </div> </div> <div id="outline-container-headline-4" class="outline-2"> <h2 id="headline-4"> Meeting my middle school classmate Li </h2> <div id="outline-text-headline-4" class="outline-text-2"> <p> He and I meet every year — have a meal, catch up on life. I didn&#39;t come home for the New Year holiday this year, so I was glad we could make up for it during May Day. We agreed to meet at the gate of our high school (we went to the same high school, though in different classes). We walked and talked — about our plans for the future, about the latest applications of AI technology, about family stuff, about what we&#39;d been reading lately, about how we&#39;d each grown. Life keeps pushing us forward, whether we like it or not. All we can do is face it with the best version of ourselves. Only then can we truly say: I&#39;ve lived my life fully enough, richly enough.</p> </div> </div> <div id="outline-container-headline-5" class="outline-2"> <h2 id="headline-5"> Going for a run at the sports field </h2> <div id="outline-text-headline-5" class="outline-text-2"> <p> On May 1st and May 3rd, I went running at the sports field near our house in the county town. On the 1st, I only managed two kilometers before my knees started aching. On the 3rd — that&#39;s today — I ran three intervals, totaling 2.6 kilometers. Some of it was a slow jog, some was pushing myself to find my limits, and some was to feel what they call &#34;the strong wind blowing against you.&#34; The wind rushing past my ears, that whooshing sound, the exhilaration of sprinting at full speed — it captivated me.</p> <p> When I run, the wind flows along my body. In the moment of running, everything seems to stop. There&#39;s only one goal: keep moving forward.</p> </div> </div> <div id="outline-container-headline-6" class="outline-2"> <h2 id="headline-6"> Always remember to look up at the stars </h2> <div id="outline-text-headline-6" class="outline-text-2"> <p> On the night of the 1st, I looked up at the sky — no stars yet. But today, the day before I leave, I can see the sky full of stars. That makes me genuinely happy.</p> <p> I still remember when I was younger — I could sprint a full four hundred meters at full speed without stopping, and still have energy to keep going. Now I can only manage two-thirds of that. My body isn&#39;t what it used to be. As I get older, there are more and more things to deal with, and I&#39;ve been paying less attention to my physical health. That&#39;s not good. If this keeps up, my body will only get slower and duller, and my spirit will grow numb, unable to feel the beauty of life. That kind of decline is like a nightmare to me.</p> <p> After my run, I went to buy some street food — some grilled gluten skewers, two pancakes. While eating, I stood a little way off from the roadside and noticed three people on tricycles, planning to sell snacks by the street. Two women and a man — they looked pretty young. I got curious and kept watching. A couple came by to buy something. I noticed the look on their faces as they bought the food — they seemed happy. It suddenly made me think: if you want to win a woman&#39;s heart, winning her stomach is a pretty good way to go. I should work on my cooking skills from now on. And then I started wondering: what does romance between two people really look like?</p> </div> </div>

2026/5/2
阅读更多

五一回忆——永远记得抬头看天上的星星

<div id="outline-container-headline-1" class="outline-2"> <h2 id="headline-1"> 深圳飞杭州 </h2> <div id="outline-text-headline-1" class="outline-text-2"> <p> 四月二十九日晚我启程回家,背了一个旧书包,不知道用了几年,只知道这次回来差点把它的拎手扯断。包里装着换下还没洗的衣服,还有一套换洗衣服(短袖、短裤、内裤和袜子),外加一条短裤、长裤和一个剃须刀。除此之外,还背了一个斜挎小包,里面装着墨水屏阅读器、3C充电宝、深圳家的钥匙。</p> <p> 晚上没加班,八点多快九点吃完饭收拾下衣物就出发了。计划从福凤路南坐公交到地铁站,但我在那里等了大概十几分钟,直到我用导航app才发现:已经没有我想坐的那班车了。不禁埋怨自己为什么不早点看导航。只能打车了,还好只花十一块。从地铁站坐到宝安机场T3航站楼,我是第一次来到这个地方,难免有些紧张。询问工作人员后才知道如何取机票。机场是个复杂的地方,各种金钱阶层的人汇聚在一起,这让我有些不自在,但是我已经见过几次这种场合了,待得时间久些也就不在意了。安检让我很没安全感且不被信任——我上次被人如此侵犯隐私是在什么时候?</p> <p> 候机时的等待是难熬的,我的航班晚点了。本来晚上十一点起飞,硬生生拖到凌晨十二点多才起飞。一开始我还有精神看会儿书,后面虽然疲惫但就是睡不着。担心睡着会错过航班,因为我只有一个人,没人会想着提醒我。哈欠一个接一个,总也止不住。</p> <p> 登机后,找到座位,坐下。读了前面布袋里的安全提醒。望着窗外发呆,远处的登机通道写着“AI就用阿里云”,嗯,好的,我不用。本来对面前的屏幕没什么兴趣,邻座的女人打开了一个航班导航的东西,我觉得挺有趣,也开始摆弄起来。</p> <p> 飞机起飞了,刚飞时的两次下坠让我有点慌,后面飞机慢慢上升,机身调整姿势。看着地面上的庞然大物变成一个个明亮的点,我想:这是我第二次离大地那么遥远。飞机慢慢穿过云层,上升到适合巡航的高度。某一瞬间我看到飞机海拔8871m,这已经很高了,比珠穆朗玛峰都高。坐飞机还是会头晕,不舒服。飞机上一直会有一个声音嘀嘀地叫,让我很烦。感受到飞机的颠簸,我并不害怕。内心有一种平静感,似乎是知道自己不会死掉,即使死掉不是还有赔偿吗:)</p> </div> </div> <div id="outline-container-headline-2" class="outline-2"> <h2 id="headline-2"> 杭州的冷出人意料 </h2> <div id="outline-text-headline-2" class="outline-text-2"> <p> 飞机抵达杭州萧山机场是凌晨两点四十,一走出舱门,来到廊桥,我就后悔了:怎么没带长袖褂子呢?看着别人把自己上下裹得严实,我像个傻子一样完全是深圳装扮。走到机场一处供人睡觉的座位处,看到人们一人占两个座位那样蜷缩着睡,要多难受有多难受。包里还有条长裤子,扫视了周围,人们都在睡觉,没有人注意我。我拿出长裤直接套在我的短裤外面,终于感到一丝温暖了。我既冷又困,也有样学样,把自己缩成一个球,蜷在座位上。可是怎么也睡不踏实,就这样我迷迷糊糊地混到了五点多。</p> <p> 然后,走出机场来到马路上,从机场走出来到地铁站入口,顶多一百米。我走起来却感觉有一公里那么远,全身的骨骼肌都在战栗想要为我提供热量,但还是入不敷出。边走边找吃的,终于找到一家卖馄饨的,一看价格四十九,就一碗馄饨加一个鸡腿,这也太他妈贵了!一想到不断颤抖的身体,贵也要吃,总比冻死强。</p> <p> 我从萧山地铁站坐到凤起路,这时是七点十分,想去西湖转转。<a href="/posts/where-have-been-when-delivering/">22年我送卖时</a>,有时会经过这里,但从来没有进去看过。我沿着湖顺时针走,很多人在这里:或是散步、或是跑步、或是跳舞、或是舞棍。我就走着,不紧不慢地走,看着遥远的苏堤像一条波浪线把我眼前的风景隔成上下两半。早晨的阳光照耀在身上,但是并不给人温暖。</p> <p> 我继续走,走到一处湖边的木椅,我坐下休息。看着湖面,看着湖面上的游船,思绪开始四散而去。离开木椅,我继续走了段距离。不知什么原因就有了离开的想法,开始看导航怎么去西湖文化广场。哦,要从定安路坐地铁。我就走啊走,一路上碰到很多卖旗袍丝绸织物的店,看起来很漂亮、精致,价格肯定不便宜。我路过一家店,本来都走过去了,脑子里突然闪现一个念头:要不给妈妈买份礼物吧。我折返去问店家方巾怎么卖。有很多价位,我选了一款花纹好看且性价比高的。到家后妈妈收到礼物时很开心。</p> <p> 当我到西湖文化广场时,太阳开始发热了,终于让我感到了温度。在广场上转悠,在林荫下休息时依然能感受到凉意。后面走到运河,被老人们吹奏萨克斯的声音吸引。那嘹亮清脆的声音,让我驻足倾听。如果不是该死的工作打扰,我说不定能更加投入呢!四五月的杭州,运河旁的树开着花,树下老人在深情地演奏一首情歌,他在吹给谁听?</p> <p> 我走到密渡桥路想要寻找当年吃过的快餐店,可惜已经找不到了。但是,宋太阳面包店还在,沙县小吃店还在。时间会让能够存在的事物继续存在下去。</p> </div> </div> <div id="outline-container-headline-3" class="outline-2"> <h2 id="headline-3"> 回家、回老家 </h2> <div id="outline-text-headline-3" class="outline-text-2"> <p> 坐高铁回家了,进门时看到父母的那一刻,觉得陌生而自然。我已经一年多没有踏进这个家门了。像阔别的老友,很自然地我就融入其中了。五一回老家村子里,见到了九十多岁的爷爷,他的耳朵已经有些听不见了,和他说话要大点声才行。我看着一眼望不到头的麦田,感受到很多新鲜的、令我好奇的地方。</p> <p> 我拍了一些照片:一束麦穗竖着立在那里像没开屏的孔雀;蜜蜂纤细的足握住麦芒;一处野花丛中蜜蜂辛苦地传粉;父亲骑着自己改装的带篷电动三轮车在转弯;通过一定视角拍出的无尽的麦田;刚浇完水的鲜嫩欲滴的辣椒苗;小小的像豆苗一样的黄瓜苗;一处不起眼的四叶花、一瓣白色三瓣渐变蓝色;院落里的柿子树;爷爷灶屋旁的大树;纹路分明的茴香;大树和屋舍构成的和谐图景;手掌中的一个蚕豆荚。</p> <p> 拍了很多照片,让我重新发现了农村的美。</p> </div> </div> <div id="outline-container-headline-4" class="outline-2"> <h2 id="headline-4"> 见初中同学李 </h2> <div id="outline-text-headline-4" class="outline-text-2"> <p> 我和他每年都会见上一面,吃饭聊近况。今年年初过年时没回家,还好这次五一补上了。我们约好在高中校园(我俩高中也在同一个学校虽然不同班级)门口见面。我们边走边聊,聊自己对未来的打算、聊关于当前AI技术的应用、聊家庭琐事、聊自己最近读了什么、聊自己有哪些成长。生活在不断地裹挟着我们向前,我们能做的就是以自己最积极的一面去面对。只有这样,我们才能够说:我的生活过得足够充实、足够精彩。</p> </div> </div> <div id="outline-container-headline-5" class="outline-2"> <h2 id="headline-5"> 去体育场跑步 </h2> <div id="outline-text-headline-5" class="outline-text-2"> <p> 一号和三号都去县城家附近的体育场跑步了,一号只跑了两公里便停了下来,膝盖有些酸了。三号也就是今天跑的时候,跑了三次,总共二点六公里。其中有慢跑也有为了探索自己的极限而跑,也有为了体验什么叫“强风吹拂”。风掠过耳边,那哗哗的响声,全力向前冲刺的快感,让我着迷。</p> <p> 当我跑的时候,风沿着我的身体流动。跑动的刹那,一切仿佛都停止了,只有一个目标,那就是不断向前。</p> </div> </div> <div id="outline-container-headline-6" class="outline-2"> <h2 id="headline-6"> 永远记得抬头看天上的星星 </h2> <div id="outline-text-headline-6" class="outline-text-2"> <p> 一号晚上看天,天上还没有星星,明天就要走了,今天能看到漫天星星,这是一件令人开心的事。</p> <p> 还记得更年轻的时候,我能一口气全力跑完四百米都不用停,甚至还有余力继续跑。现在我只能跑三分之二,身体不如从前了。随着年龄的增加,要处理的事情越来越多,对身体健康的关注不如从前了。这样持续下去并不好,长期下来,身体会越来越迟钝,精神也会变得麻木,无法感受到生活的美好。这种恶化对我来说是噩梦一样的事情。</p> <p> 跑完步去买点小吃,买了面筋、两个煎饼,吃的时候我站在离路边有段距离的地方,看到有三个人骑着三轮车,打算在路边卖小吃。两女一男,看起来蛮年轻的,不禁好奇,就一直在关注。有一对情侣来买东西,我留意到他们买东西的神情,应该是快乐的。突然就联想到:要抓住女人的心,抓住女人的胃是一个不错的方式。以后要锻炼自己的厨艺了。进而又在想:两个人的浪漫究竟是什么样的状态?</p> </div> </div>

2026/5/2
阅读更多

How to Deploy LobeHub on Railway

<div id="outline-container-headline-1" class="outline-2"> <h2 id="headline-1"> How to Deploy LobeHub on Railway </h2> <div id="outline-text-headline-1" class="outline-text-2"> <p> I finally figured out how to deploy LobeHub on Railway. LobeHub is an open-source AI agent platform that helps me build my own knowledgebase. Railway is my main SaaS platform for deploying Docker apps.</p> <p> I did find some templates on the <a href="https://railway.com/deploy?q=lobe">Railway Marketplace</a>. However, I didn&#39;t find the right one for me, so I chose to create one myself. I used LobeHub(v2.1.56) to build it.</p> <p> Choosing the database was a bit troublesome. I couldn&#39;t use the Railway official PostgreSQL template, because it doesn&#39;t support the <code class="verbatim">pg_search</code> extension. After some research, I chose <a href="https://github.com/paradedb/paradedb">ParadeDB</a> for my PostgreSQL database, since it supports. I use the Docker image <code class="verbatim">lobehub/lobehub</code> for faster build times. The variables are listed below:</p> <div class="src src-conf"> <pre tabindex="0"><code class="language-conf" data-lang="conf">APP_URL=&#34;https://${{RAILWAY_PUBLIC_DOMAIN}}&#34; DATABASE_URL=&#34;postgres://${{ParadeDB.PGUSER}}:${{ParadeDB.PGPASSWORD}}@${{ParadeDB.PGHOST}}:${{ParadeDB.PGPORT}}/${{ParadeDB.PGDATABASE}}&#34; QSTASH_TOKEN=&#34;&#34; ## deployed a SearXNG instance on Railway SEARCH_PROVIDERS=&#34;searxng&#34; SEARXNG_URL=&#34;&#34; ## the following can be obtained from https://lobehub.com/docs/self-hosting/platform/docker#deploying-on-a-linux-server KEY_VAULTS_SECRET=&#34;&#34; AUTH_SECRET=&#34;&#34; JWKS_KEY=&#34;&#34; ## chose Cloudflare R2 S3_ACCESS_KEY_ID=&#34;&#34; S3_SECRET_ACCESS_KEY=&#34;&#34; S3_ENDPOINT=&#34;&#34; S3_BUCKET=&#34;&#34;</code></pre> </div> <p> Then set the port in the networking settings to <code class="verbatim">8080</code> .</p> <p> After the above setup, you will have a LobeHub instance up and running!</p> <p> However, I can&#39;t run commands on my self-hosted instance because the market authorization failed:</p> <blockquote> <p>Market authorization expired. An authorization dialog has been shown to the user. Please wait for the user to complete authorization and then retry the current task.</p> </blockquote> <p> I&#39;m thinking about how to remove that part so that I can run cmds freely. I&#39;m working on it.</p> </div> </div> <div id="outline-container-headline-2" class="outline-2"> <h2 id="headline-2"> Add third-party sandbox(AIO Sandbox) to LobeHub </h2> <div id="outline-text-headline-2" class="outline-text-2"> <p> I&#39;m not going to remove the official cloud-sandbox – it&#39;s too much trouble. I deployed <a href="https://github.com/agent-infra/sandbox">AIO sandbox</a> on Railway, accessing it through MCP(streamable HTTP). But it needs a lot of memory to run, so I need to enable serverless mode to reduce resource usage. And it takes about 20 minutes to deploy, so don&#39;t worry if it doesn&#39;t deploy quickly.</p> </div> </div>

2026/4/30
阅读更多

Watch spoken drama 'Rhinoceros in Love'

<p> A ticket bought out of curiosity, prompted by a fleeting conversation last year with an acquaintance about wanting to see this play. I never made it. This time, I went and watched it to the end. It was worth it.</p> <p> What does love look like? I believe everyone has their own answer. What is mine? Plainness, joy, passion, happiness, quarrels, anger, compromise, submission, building together.</p> <p> These are the words that shape my current imagination of love. I have never been in a relationship, so I have no first‑hand experience. These are simply the shades I suspect love might hold.</p> <p> Watching Malu try so desperately to show himself to Mingming, only to be met with her utter indifference—a spontaneous sadness welled up from my heart. The saddest thing in the world, perhaps, is loving a woman who does not love you back.</p> <p> When Malu poured out everything he had before Mingming, and she still refused, still loved another man, I felt a kind of dread: What if one day I meet a woman to whom I would willingly give all that I am, yet her heart belongs to someone else?</p> <p> This is only a specter of the imagination. I hope I am lucky enough to meet someone whom I love, and who loves me in return.</p> <hr> <p> Songs that moved me during the performance</p> <p> <em>Rhinoceros in Love</em></p> <blockquote> <p>The birds have all flown south</p> <p> I am not a bird, I have no need for the south</p> <p> The leaves all turn to face the sun</p> <p> I am not a leaf, I have no need for sunlight</p> <p> How lonely I am, how brave I am</p> <p> I am a rhinoceros sick with longing</p> <p> How lonely I am, how brave I am</p> <p> I am a rhinoceros in love</p> <p> The train has pulled into the platform</p> <p> I am not a train, I need no terminus</p> <p> The rain has soaked through my clothes</p> <p> I am not the rain, I need not stay up in the sky</p> <p> I am a rhinoceros in love</p> </blockquote> <p> To love without being loved in return, yet to call oneself &#34;in love&#34;—nothing but a brave face over a broken smile.</p> <p> <em>Oxygen — Mingming&#39;s Song</em></p> <blockquote> <p>Smile at me, smile</p> <p> As if we were meeting for the first time</p> <p> Speak to me, speak</p> <p> Even if the vows will change by tomorrow</p> <p> Enjoy me now</p> <p> Life drifts so weightlessly</p> <p> Remember me in the years to come</p> <p> In the year you grow old</p> <p> The years gone by will surely pass</p> <p> With you, my final love</p> <p> The years gone by will surely pass</p> <p> With you, beside me at last</p> <p> All the light rushes toward me</p> <p> All the oxygen is breathed in by me</p> <p> All objects lose their weight</p> <p> I have nearly reached the end of every road</p> </blockquote> <p> When Mingming sang this song, a shiver ran through me.</p> <p> <em>A Poem for You — Malu&#39;s Song</em></p> <blockquote> <p>Everything white, when it meets you,</p> <p> Turns to black ink drained of light</p> <p> Every bird and beast despairs at being unable to speak your name</p> <p> Every intersection lights up green so you may pass freely</p> <p> Every compass needle points out your direction for me</p> <p> You are the wind that leaves no trace</p> <p> You are the wind that brushes gently past the body</p> <p> You are the wind that leaves no trail</p> <p> You are the wind that is everywhere and at all times</p> <p> My love, my love,</p> <p> My love, my love,</p> <p> My love, my love,</p> <p> You are my warm gloves,</p> <p> The ice‑cold beer,</p> <p> The shirt that carries the scent of sunlight,</p> <p> My day‑after‑day dream</p> <p> You are pure, innocent, glass‑like,</p> <p> You are pure, innocent, water‑like,</p> <p> You are pure, innocent, and nothing can change that</p> <p> Sunlight passes through you</p> <p> And alters its own course</p> </blockquote> <p> On the way home, listening to this song, I wept in silence, letting the tears trace down my cheeks.</p> <p> Ah, Mingming, Mingming. Beloved one, how can you be so merciless? How can you let a heart that loves you slowly grow cold?</p> <p> Ah, Malu, Malu. Pitiable man, the taste of unrequited love—how it aches, how it aches.</p>

2026/4/10
阅读更多

看话剧《恋爱的犀牛》

<p> 一次好奇心的驱使让我买了这张票,去年曾经和一位聊过几句的朋友说想看,最后也没成行。这次我看完了,我觉得值得。</p> <p> 爱情是什么样子,我相信每个人都有自己的答案,我的答案是什么?平淡、快乐、激情、幸福、争吵、愤怒、妥协、顺从、共筑。</p> <p> 我此刻对爱情的想象,就是这些词汇。我是个没谈过恋爱的人,所以没有什么一手经验。这些词是我觉得爱情里可能会有的样子。</p> <p> 当我看到马路那么努力地向明明展示自己,明明依然不为所动时,一种自然生发的悲伤情绪就从心间流淌。世界上最悲哀的事情,莫过于爱上一个不爱自己的女人。</p> <p> 当马路那么用力地把自己有的都给明明,明明依然拒绝,依然去爱着另一个男人。我有一种恐惧:当我真的遇到一个女人,遇到一个我愿意献出我的一切去爱的女人,她却爱着别人。</p> <p> 这只是想象。我希望自己足够幸运,遇到一个人,我爱她,她也同样爱我。</p> <hr> <p> 剧中打动我的音乐:</p> <p> <em>恋爱中的犀牛</em></p> <blockquote> <p>鸟儿 全飞向南方</p> <p> 我不是鸟儿不需要南方</p> <p> 树叶 都面对着阳光</p> <p> 我不是树叶不需要阳光</p> <p> 我多么孤单 我多么勇敢</p> <p> 我是一只害相思的犀牛</p> <p> 我多么孤单 我多么勇敢</p> <p> 我是一只恋爱中的犀牛</p> <p> 火车 已驶进了站台</p> <p> 我不是火车不需要终点</p> <p> 雨水 已打湿了衣裳</p> <p> 我不是雨水不需要呆在天上</p> <p> 我是一只恋爱中的犀牛</p> </blockquote> <p> 爱而不得,却说“恋爱中”。强颜欢笑罢了</p> <p> <em>氧气-明明之歌</em></p> <blockquote> <p>对我笑吧 笑吧</p> <p> 就像你我初次见面</p> <p> 对我说吧 说吧</p> <p> 即使誓言明天就变</p> <p> 享用我吧 现在</p> <p> 人生如此飘忽不定</p> <p> 想起我吧 将来</p> <p> 在你变老的那一年</p> <p> 过去岁月总会过去</p> <p> 有你最后的爱情</p> <p> 过去岁月总会过去</p> <p> 有你最后和我一起</p> <p> 所有的光芒都向我涌来</p> <p> 所有的氧气都被我吸光</p> <p> 所有的物体都失去重量</p> <p> 我都快已经走到了所有路的尽头</p> </blockquote> <p> 当明明唱这首歌时,我有种战栗感。</p> <p> <em>给你的诗-马路之歌</em></p> <blockquote> <p>一切白的东西和你相遇</p> <p> 都成了黑墨水暗淡无光</p> <p> 一切鸟兽因为不能说出你的名字而万分绝望</p> <p> 一切路口亮起绿灯让你随意通行</p> <p> 一切指南针为我指出你的方向</p> <p> 你是不露痕迹的风</p> <p> 你是轻轻掠过身体的风</p> <p> 你是不露行踪的风</p> <p> 你是无时不在无处不在的风</p> <p> 我的爱人我的爱人</p> <p> 我的爱人我的爱人</p> <p> 我的爱人我的爱人</p> <p> 你是我温暖的手套</p> <p> 冰冷的啤酒</p> <p> 带着太阳光气息的衬衫</p> <p> 日复一日的梦想</p> <p> 你是纯洁的天真的玻璃一样的</p> <p> 你是纯洁的天真的水流一样的</p> <p> 你是纯洁的天真的什么也改变不了</p> <p> 阳光穿过你</p> <p> 却改变了自己的方向</p> </blockquote> <p> 在回家的路上,我听着这首歌,我静静地哭了,任泪水顺着脸颊流淌。</p> <p> 啊,明明,明明。被爱着的你,为何能这样的狠心,为何能放任一颗爱你的心,渐渐冷却?</p> <p> 啊,马路,马路。可怜的你,爱而不得的感觉,好痛,好痛苦。</p>

2026/4/9
阅读更多

What am I really leaving behind when I put pen to paper?

<p> You should keep a journal, I advise you so. Because it can bring you many benefits. Now let me share the benefits that journaling has brought me.</p> <p> I started journaling in 2020. I no longer remember what prompted me to begin, but from that moment on, the idea of keeping a journal took root in my mind. From the very beginning, I abandoned traditional paper diaries and fully embraced digital journaling. I use plain text format (<a href="https://orgmode.org/">Orgmode</a>) for my entries and maintain backups.</p> <p> A journal is a way to build memories. The thoughts and reflections of the moment, recorded casually, become the sole truth when memory fails to recall them later. Memories fade with time, but if text is properly preserved, it will not be lost for a long time.</p> <p> A journal is a great aid for reflection. Often we are lost in the midst of situations; looking back at what happened, we realize how we could have done better, making change easier.</p> <p> A journal is a grave for pain. Much suffering cannot or should not be shared with others. Recorded in the journal and buried there, tomorrow brings a brand new me. Past pains are forgotten.</p> <p> Journaling gives me a sense of groundedness, because I know what I did today—whether they were meaningless trivialities or new insights about life.</p> <p> What the journal records is a more authentic self. Through it, I am able to face who I truly am.</p>

2026/2/28
阅读更多

当我写日记时,我在记录什么

<p> 你应该记日记,我如是建议你。因为它能给你带来很多好处。接下来我就说说记日记给我带来的好处。</p> <p> 我是2020年开始记日记的,记日记的起因已经不记得了,但是从那时起要记日记的想法在我的心里扎下了根。从一开始我就抛弃了传统的纸质日记,全面拥抱电子日记。使用纯文本格式(<a href="https://orgmode.org/">Orgmode</a>)记日记,并保留备份。</p> <p> 日记是建立回忆的一种方式。当时的所思所想,随手记下来,事后回忆不起来时,日记便成为唯一的真实。记忆会随时间消失,但文本若得到妥善保存,则很长的一段时间内不会丢失。</p> <p> 日记是反思的好帮手。很多时候当局者迷,事后回想当时发生的种种,会觉得自己有哪些地方可以做得更好,让自己更容易改变。</p> <p> 日记是痛苦的坟墓。很多苦痛不能不想和人诉说,记在日记里,埋葬下来,明天又是一个全新的我。过往的苦痛已被忘却。</p> <p> 记日记给我一种踏实的感觉,因为我知道今天都做了什么,是都是一些无意义的小事,还是自己又对生活有了新的感悟。</p> <p> 日记所记的是一个更为真实的自我,从日记中我得以面对真实的自己。</p>

2026/2/28
阅读更多

My relationship with computers

<p> I&#39;ve read countless stories of successful people&#39;s early encounters with computers, and their experiences are mostly similar: initially driven by interest, they gradually learned and eventually chose computers as a career. I didn&#39;t really get into computers until I was in university, on New Year&#39;s Day 2019, when my father took me to an HP store to buy a laptop. I&#39;ve used it for both study and work ever since. I had some early exposure to computers, but it was all superficial at school because we didn&#39;t have a computer at home, so there wasn&#39;t an environment for learning programming. Although the laptop sometimes lags, it&#39;s still usable, and in these times of economic hardship, not buying a new computer will save a lot of money. I had my laptop professionally cleaned by the official after-sales service, replacing the old SATA mechanical hard drive with a SATA solid-state drive, and I&#39;m using the new SSD as my system drive.</p> <p> My operating system is Arch Linux, which has been with me for five years, and I&#39;m very grateful for its existence. My daily desktop environment is KDE, which suits my usage habits, and I&#39;ve always used it. When I left Windows, my first operating system was Ubuntu, using the GNOME desktop environment, which I used for six months. I eventually stopped using it, probably because I disliked Ubuntu&#39;s overall design philosophy and some aspects of GNOME as a desktop environment that I found unpleasant. I heard about Arch Linux naturally because of its biggest feature—rolling releases, meaning the software is always up-to-date.</p> <p> After getting into this hobby, I encountered quite a few problems in the early stages, but in the last two years, I haven&#39;t had any major issues affecting my use when upgrading the system. I feel very lucky about that.</p> <p> I admire those technically skilled developers. They are more passionate about technology and programming than I am, and they have a stronger desire to improve their programs. I feel like I&#39;ll always be chasing after them, imitating them, and learning from them. I can&#39;t understand their code because I lack their deep knowledge. The vast ocean of computer science may contain some boring and tedious things, but those truly captivating, sparkling gems keep me learning and understanding the principles behind the technology.</p> <p> Code is pure and logical; developers write code to express their thoughts about the world—it&#39;s an incredibly romantic thing. I&#39;m deeply drawn to this romance. In such a world, I can escape worldly affairs and immerse myself in the ocean of knowledge. I think I&#39;ll never want to leave this atmosphere unless I die.</p> <p> When I read Ruan Yifeng&#39;s Weekly Tech Enthusiast (Issue 386), I was startled by his description of what might be the future: programs mobilizing human resources to automate processes. In the AI ​​era, it&#39;s AI models that mobilize human resources. Humans will no longer be the core of the socio-economic system; AI will be. This possible future makes me feel that I need a long time to adapt to this change, even though I don&#39;t know when it will begin.</p> <p> When circumstances change, people&#39;s first thought is: What should I do? I&#39;m the same way. I don&#39;t want to, and can&#39;t, just sit and wait for things to change. I have parents to support, and I might get married in the future. What can I do to maintain my competitiveness? I will continue to think about this question.</p>

2026/2/27
阅读更多

我与计算机之缘

<p> 读过太多厉害人物早期接触计算机的故事,经历大多相像:一开始因为对计算机感兴趣,后来慢慢学习直到选择计算机作为职业。我真正玩计算机时已经在读大学,那是2019年元旦,父亲带着我去一家HP专卖店买了一台笔记本电脑。从那时至今我一直用它学习工作。早年也接触过电脑但那都是在学校的浅尝辄止,因为家里没有电脑所以没有学习编程的环境。笔记本电脑虽然有时会卡顿但还能继续使用,在现在这个经济不好的时期不买新电脑会省下一大笔钱。我给笔记本做过一次官方售后清洁,把老旧的SATA机械盘换成了SATA固态,并把新固态作为系统盘使用。</p> <p> 我用的操作系统是Arch Linux,它已经陪伴我五年时间了,我很感激能有这个操作系统存在。日常使用的桌面环境是KDE,满足了我的使用习惯,我一直都在用它。当初我从Windows逃离时,第一个操作系统选择的是Ubuntu,使用的GNOME桌面,使用了半年时间。大概是因为不喜欢Ubuntu整体的设计思路以及GNOME作为桌面环境有些让我觉得不爽的地方,后来就弃用了。听说Arch Linux自然是因为它最大的特点——滚动发行、软件一直都是最新的。</p> <p> 入坑以后,初期阶段踩坑不少,最近两年升级系统时从未出现过影响我使用的大问题。我为此觉得很幸运。</p> <p> 我很羡慕那些技术厉害的开发者们,他们比我对技术更为沉迷、比我更热爱编程,也有着想让程序更好的意愿。我觉得自己可能永远都只是在追赶他们,在模仿、在向他们学习。我看不懂他们写的代码,因为我并不具备他们的深厚积累。计算机科学浩瀚的知识海洋里总有些无聊乏味的东西,但是那些真正吸引我的、珍珠般闪耀的事物,让我不断地去学习去了解技术背后的道理。</p> <p> 代码是纯粹的、逻辑的,开发者书写代码表达自己对于世界的思考,这是极浪漫的事情。我也深深地被这种浪漫所吸引。在这样的世界里,我能远离世俗的人情世故,尽情徜徉在智识的海洋。我想我永远也不愿离开这种氛围,除非我死掉了。</p> <p> 当我读到阮一峰的科技爱好者周刊(第386期)时,我被他所描绘的、可能就是未来图景的样子吓到了:程序在调动人力来完成自动化流程。AI时代调动人力的就是那些AI模型。人不再是社会经济的核心,AI是。这种可能的未来让我觉得,我需要很长时间来适应这种变化,尽管我并不知道这种变化什么时候开始。</p> <p> 当环境发生变化时,人们首先会想:我该怎么办?我也是这样。我不想也不能坐以待毙,我还有父母要养,未来可能会结婚。我要做什么来保持我的竞争力?我会持续地思考这个问题。</p>

2026/2/27
阅读更多

The taste of sea

<p> It was around noon. I had just gotten off the bus and was looking for a place to eat. Walking along the road, I saw the Dark Sky Planetarium. At the intersection behind it, I saw some people eating. I asked if I could buy food from them. They said yes. The owner handed me a menu; everything was around 30 or 40 yuan. I asked if they had anything else. He said they had noodles. I asked what came with the noodles. He replied that there were vegetables and meat. I asked how much it cost. He said 25 yuan. Okay, I&#39;ll take that. When the noodles were served, I was surprised to find they were served in a large bowl, with noodles, meat, and vegetables. I was very satisfied and ate with great enjoyment.</p> <p> After eating, I checked the navigation and decided to go to the beach. I asked for directions and walked along the road for a while, only to find that I had to buy a ticket, which cost 35 yuan. I went to Xichong No. 2 Beach. When I went, there were almost no people there because it was midday, and the sun was high overhead, but it wasn&#39;t hot.</p> <p> After eating, I checked the navigation and decided to go to the beach. I asked for directions and walked along the road for a while, only to find that I had to buy a ticket, which cost 35 yuan. I went to Xichong No. 2 Beach. When I went, there were almost no people there because it was midday, and the sun was high overhead, but it wasn&#39;t hot.</p> <p> After splashing around in the water a few times, my pants got wet, and so did the papers I&#39;d brought—the ones I used to jot down notes. So I stopped playing. I wanted to walk along the line where the shore met the sea until I saw the observatory. So I went back to where I&#39;d taken off my shoes, and put my socks inside them so they wouldn&#39;t fall out while I walked. And so, I walked, observing the sea and the people on the shore. The water was constantly churning, and sometimes I felt dizzy from the sloshing, so I&#39;d rest in a spot where the water wouldn&#39;t get in, and then continue walking.</p> <p> During my walk, I found several beautiful seashells. Their surfaces, smoothed by the long wash of the sea, were irresistible to me. Moreover, the even color of these smooth shells created a sense of harmony. This is another reason why I love them. Seeing these seashells reminded me of Newton&#39;s famous quote: &#34;I do not know what I am in the eyes of others, but to my own eyes I have only seen myself as a boy playing on the seashore, occasionally pleased with the discovery of a smoother shell or a prettier one, while remaining completely oblivious to the vast ocean of truth that lay before me.&#34; I also saw mushroom-shaped transparent objects; I didn&#39;t know what they were, but I guessed they might be jellyfish.</p> <p> And so I walked and walked. From beach number 2 to number 3, and from number 3 to number 4. I put on socks and shoes without washing my feet because I couldn&#39;t find a place to wash them. Then, I decided to go to the observatory. So I chose a path, but unexpectedly, it wasn&#39;t easy to walk on.</p> <p> I walked the Kunpeng Path, and halfway through, I saw a silver pole with the words &#34;Kunpeng Path&#34; written on it. This path leads from the beach to the boardwalk at the observatory. I walked carefully, tense my body, step by step, afraid of stepping on a loose rock and twisting my ankle, or falling and scraping myself. Fortunately, I wasn&#39;t injured. When I reached the boardwalk, I was dizzy and needed to rest for a while to recover. The view of the mountains and sea merging in the distance from there was breathtaking. The blue-green sea was so beautiful, I could never get enough of it.</p> <p> I started walking on the boardwalk, resting for a while after each step, and looking at the distant sea view, I felt that my trip hadn&#39;t been in vain.</p>

2026/2/21
阅读更多

海的味道

<p> 那是在中午时分,我刚下公交,四处找吃饭的地方。沿着路走看到了暗夜天文馆,在后面的路口,我看到有人在吃饭,我问能买饭吗?对方答可以的。老板递给我菜单,全都是三四十块钱的,我问他还有其他的吗?他说还有面,我问面配什么呢?他回有菜和肉。我问多少钱?他回二十五。嗯,我就要它了。当面条端给我时,我发现竟是用海碗盛的,面条肉菜都有,我很满意,吃得很痛快。</p> <p> 吃完,看了导航,想去沙滩。问了路,沿着公路走了一段距离,没想到还要买票,花了三十五。我去的是西涌2号沙滩。去的时候几乎没人,因为是中午呀,太阳高挂在头顶,但是并不热。</p> <p> 一开始我还犹豫到底要不要脱鞋脱袜子,后来我看到别人也脱了,我也脱了。不得不说,还是脱鞋玩得爽。我走到海边,那里的沙子因为浸润了海水凉凉的,很舒服。我向海走去,让海水没过我的脚,那股凉意我现在还能忆起。我看着海,浪一波接一波,永远不停歇。我看着那浪在远处形成,声势浩大,等到岸边时,那浪已渐停歇。像只温和的小猫,轻轻从我脚边掠过,为我带来海的清凉。沿着岸往前走,看着那浪,我玩心突起,在浪到脚边时,我猛地捧起那浪往海的方向扬起,水花四溅,溅到了我的嘴里。我终于尝到了海的味道。</p> <p> 这样击水玩了几次,我的裤子湿了,把我带来的纸都弄湿了。那是我用来随手记的纸。这样我停止了玩耍。我想沿着岸与海的交接行走,直到看到天文台为止。于是,我回到脱鞋的位置,把袜子放到鞋的最里面,省得走路的过程中掉出来。就这样,我一边走一边四处打量着这海这海边的人们。海水晃动不止,我有时被晃得头晕,就会到海水沾不到的地方休息一下,然后继续走。</p> <p> 在行走的途中,我发现了几个好看的贝壳,贝壳的外表经过长久海水的冲刷已经足够光滑,让我爱不释手。而且,这种光滑贝壳的颜色也很均匀,看起来有种和谐感。这也是我喜欢这些贝壳的原因。当看到贝壳时,我想到牛顿的那句名言:“我不知道在世人眼里我是什么样的人,但在我自己看来,我不过像一个在海边玩耍的孩子,偶尔为发现一块更光滑的贝壳或一颗更美丽的贝壳而沾沾自喜,而对于展现在我面前的浩瀚的真理海洋,却全然没有发现”。我还看到蘑菇形状的透明物体,不知道是什么,我猜可能是水母。</p> <p> 就这样,我走啊走,走啊走。从2号沙滩走到3号、3号走到4号。我没洗脚就穿了袜子、鞋,因为我没找到洗脚的地方。然后,我想去天文台看看。于是就选了一条路,没想到,这条路并不好走。</p> <p> 我走的是鲲鹏径,走到路途一半时看到了一个银色杆子,杆子上写这是鲲鹏径。这条路从沙滩通到天文台的栈道。我小心翼翼、绷住身体,一步一步走着,生怕自己踩到某块活动石头崴到脚,或者摔倒被石头磕破。好在,我没有受伤,当我到栈道时,我头是懵的,休息了好一会儿才缓过来。从那里看远处的山海相连,美极了。蓝绿色的大海,怎么也看不够。</p> <p> 我开始上栈道,走一段休息一会儿,看着远处的海景,自己没白来啊,心里这样想着。</p>

2026/2/21
阅读更多

How to self-host Forgejo with docker on Ubuntu 24.04

<p> I want to deploy <a href="https://forgejo.org/">Forgejo</a> for about two years, never want to write a post about it. Now I finally deploy it. Why I want to self-host this GitHub alternative? Because I hate complexity, I donot like GitHub which has more &#39;features&#39; but users donot benefit from them. I want something certain, I&#39;m sure I own the data when I have a Forgejo instance. If I host my code on GitHub, I have to deal with the terible UX. I couldn&#39;t stand with it anymore.</p> <p> Now let me show the way how I deploy Forgejo.</p> <div id="outline-container-headline-1" class="outline-2"> <h2 id="headline-1"> Prepare the server </h2> <div id="outline-text-headline-1" class="outline-text-2"> <p> Use a Hetzner server(Ubuntu 24.04 LTS) deploy Forgejo. Donot forget to add SSH key.</p> <div id="outline-container-headline-2" class="outline-3"> <h3 id="headline-2"> 1. Ubuntu initial setup </h3> <div id="outline-text-headline-2" class="outline-text-3"> <p> Edit local <code class="verbatim">~/.ssh/config</code>:</p> <div class="src src-conf"> <pre tabindex="0"><code class="language-conf" data-lang="conf">Host forgejo Hostname IP User root IdentityFile ~/.ssh/id_ed25519</code></pre> </div> <p> Then run <code class="verbatim">ssh forgejo</code> login the server. Run below cmd to complete initial setup:</p> <div class="src src-sh"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>apt-get update <span style="color:#ff79c6">&amp;&amp;</span> apt-get upgrade </span></span><span style="display:flex;"><span><span style="color:#6272a4">## rm snapd, donot like it, never use it</span> </span></span><span style="display:flex;"><span>apt autoremove snapd --purge -y </span></span><span style="display:flex;"><span><span style="color:#6272a4">## install Docker</span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository</span> </span></span><span style="display:flex;"><span>install -m <span style="color:#bd93f9">0755</span> -d /etc/apt/keyrings </span></span><span style="display:flex;"><span>curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc </span></span><span style="display:flex;"><span>chmod a+r /etc/apt/keyrings/docker.asc </span></span><span style="display:flex;"><span>tee /etc/apt/sources.list.d/docker.sources <span style="color:#f1fa8c">&lt;&lt;EOF </span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">Types: deb </span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">URIs: https://download.docker.com/linux/ubuntu </span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">Suites: $(. /etc/os-release &amp;&amp; echo &#34;${UBUNTU_CODENAME:-$VERSION_CODENAME}&#34;) </span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">Components: stable </span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">Signed-By: /etc/apt/keyrings/docker.asc </span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">EOF</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>apt-get update </span></span><span style="display:flex;"><span>apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin</span></span></code></pre></div> </div> </div> </div> <div id="outline-container-headline-3" class="outline-3"> <h3 id="headline-3"> 2. Create a git user </h3> <div id="outline-text-headline-3" class="outline-text-3"> <div class="src src-sh"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>adduser <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> --system <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> --group <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> --disabled-password <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> --shell /bin/bash <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> --home /home/git <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> git </span></span><span style="display:flex;"><span>usermod -aG docker git</span></span></code></pre></div> </div> </div> </div> </div> </div> <div id="outline-container-headline-4" class="outline-2"> <h2 id="headline-4"> Add a A DNS record </h2> <div id="outline-text-headline-4" class="outline-text-2"> <p> On the domain DNS manage site, mine is Cloudflare, add a A record to your domain that point to your server IPv4 IP.</p> </div> </div> <div id="outline-container-headline-5" class="outline-2"> <h2 id="headline-5"> Write docker-compose.yml file </h2> <div id="outline-text-headline-5" class="outline-text-2"> <p> Prepare the folder:</p> <div class="src src-sh"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>mkdir -p ./forgejo </span></span><span style="display:flex;"><span>sudo chown -R git:git ./forgejo </span></span><span style="display:flex;"><span>mkdir -p ./conf </span></span><span style="display:flex;"><span>sudo chown -R git:git ./conf </span></span><span style="display:flex;"><span>id git </span></span><span style="display:flex;"><span><span style="color:#6272a4"># uid=108(git) gid=110(git) groups=110(git),988(docker)</span></span></span></code></pre></div> </div> <p> Create <code class="verbatim">Caddyfile</code>:</p> <div class="src src-conf"> <pre tabindex="0"><code class="language-conf" data-lang="conf">git.example.com { encode gzip zstd reverse_proxy localhost:3001 { header_up Host {host} header_up X-Real-IP {remote_host} header_up X-Forwarded-For {remote_host} header_up X-Forwarded-Proto {scheme} } }</code></pre> </div> <p> Setup port forward, so that SSH url will be <code class="verbatim">ssh://git@git.example.com/xxx/xxx.git</code> not <code class="verbatim">ssh://git@git.example.com:2222/xxx/xxx.git</code>:</p> <div class="src src-sh"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>vim forgejo-shell.sh </span></span><span style="display:flex;"><span>chmod +x /path_to/forgejo-shell.sh </span></span><span style="display:flex;"><span>usermod -s /abs_path_to/forgejo-shell.sh git </span></span><span style="display:flex;"><span>vim /etc/ssh/sshd_config </span></span><span style="display:flex;"><span>systemctl restart ssh </span></span><span style="display:flex;"><span>sudo -u git /path_to/forgejo-shell.sh </span></span><span style="display:flex;"><span><span style="color:#6272a4"># enter forgejo container shell</span> </span></span><span style="display:flex;"><span>ssh -T git@git.example.com </span></span><span style="display:flex;"><span><span style="color:#6272a4"># run above cmd in local PC</span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># Hi there, youruser! You&#39;ve successfully authenticated with the key named xxxxxx, but Forgejo does not provide shell access.</span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># If this is unexpected, please log in with password and setup Forgejo under another user.</span></span></span></code></pre></div> </div> <div class="src src-sh"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#6272a4"># forgejo-shell.sh</span> </span></span><span style="display:flex;"><span><span style="color:#6272a4">#!/bin/sh</span> </span></span><span style="display:flex;"><span>/usr/bin/docker <span style="color:#8be9fd;font-style:italic">exec</span> -i --env <span style="color:#8be9fd;font-style:italic">SSH_ORIGINAL_COMMAND</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$SSH_ORIGINAL_COMMAND</span><span style="color:#f1fa8c">&#34;</span> forgejo sh <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$@</span><span style="color:#f1fa8c">&#34;</span></span></span></code></pre></div> </div> <div class="src src-sh"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#6272a4"># /etc/ssh/sshd_config</span> </span></span><span style="display:flex;"><span><span style="color:#6272a4">## add to bottom</span> </span></span><span style="display:flex;"><span>Match User git </span></span><span style="display:flex;"><span> AuthorizedKeysCommandUser git </span></span><span style="display:flex;"><span> AuthorizedKeysCommand /usr/bin/docker <span style="color:#8be9fd;font-style:italic">exec</span> -i forgejo /usr/local/bin/forgejo keys -u %u -t %t -k %k</span></span></code></pre></div> </div> <p> Create <code class="verbatim">docker-compose.yml</code>:</p> <div class="src src-yml"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yml" data-lang="yml"><span style="display:flex;"><span><span style="color:#ff79c6">services</span>: </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">forgejo</span>: </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">image</span>: codeberg.org/forgejo/forgejo:14-rootless </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">container_name</span>: forgejo </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">user</span>: <span style="color:#bd93f9">108</span>:<span style="color:#bd93f9">110</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">environment</span>: </span></span><span style="display:flex;"><span> - USER_UID=108 </span></span><span style="display:flex;"><span> - USER_GID=110 </span></span><span style="display:flex;"><span> - FORGEJO__database__DB_TYPE=sqlite3 </span></span><span style="display:flex;"><span> - FORGEJO__server__SSH_PORT=22 </span></span><span style="display:flex;"><span> - FORGEJO__server__SSH_LISTEN_PORT=2222 <span style="color:#6272a4"># port forward</span> </span></span><span style="display:flex;"><span> - FORGEJO__repository__DEFAULT_REPO_UNITS=repo.code </span></span><span style="display:flex;"><span> - FORGEJO__repository__DISABLE_MIGRATIONS=true </span></span><span style="display:flex;"><span> - FORGEJO__repository__DISABLE_STARS=true </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">restart</span>: unless-stopped </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">networks</span>: </span></span><span style="display:flex;"><span> - forgejo </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">volumes</span>: </span></span><span style="display:flex;"><span> - ./forgejo:/var/lib/gitea </span></span><span style="display:flex;"><span> - ./conf:/etc/gitea </span></span><span style="display:flex;"><span> - /etc/localtime:/etc/localtime:ro </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">ports</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f1fa8c">&#34;3000:3000&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">caddy</span>: </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">image</span>: caddy:2-alpine </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">container_name</span>: caddy </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">network_mode</span>: <span style="color:#f1fa8c">&#39;host&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">restart</span>: unless-stopped </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">logging</span>: </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">driver</span>: <span style="color:#f1fa8c">&#39;json-file&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">options</span>: </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">max-size</span>: <span style="color:#f1fa8c">&#39;10m&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">max-file</span>: <span style="color:#f1fa8c">&#39;10&#39;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">volumes</span>: </span></span><span style="display:flex;"><span> - ./Caddyfile:/Caddyfile:ro </span></span><span style="display:flex;"><span> - caddy_data:/data </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">command</span>: <span style="color:#f1fa8c">&#39;caddy run --config /Caddyfile --adapter caddyfile&#39;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">anubis</span>: </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">image</span>: ghcr.io/techarohq/anubis:latest </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">container_name</span>: anubis </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">restart</span>: unless-stopped </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">environment</span>: </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">BIND</span>: <span style="color:#f1fa8c">&#34;:3000&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">TARGET</span>: <span style="color:#f1fa8c">&#34;http://forgejo:3000&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">SERVE_ROBOTS_TXT</span>: <span style="color:#f1fa8c">&#34;true&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">TRUSTED_PROXIES</span>: <span style="color:#f1fa8c">&#34;127.0.0.1,::1&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">networks</span>: </span></span><span style="display:flex;"><span> - forgejo </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">ports</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f1fa8c">&#34;3001:3000&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">networks</span>: </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">forgejo</span>: </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">external</span>: <span style="color:#ff79c6">false</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">volumes</span>: </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">caddy_data</span>: </span></span><span style="display:flex;"><span> caddy_config:</span></span></code></pre></div> </div> </div> </div> <div id="outline-container-headline-6" class="outline-2"> <h2 id="headline-6"> Backup Forgejo </h2> <div id="outline-text-headline-6" class="outline-text-2"> <p> I backup data to Hetzner Storagebox.</p> <p> First, create SSH key, add pub key to Storagebox.</p> <div class="src src-sh"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>ssh-keygen -t ed25519 -f ~/.ssh/hetzner_storagebox </span></span><span style="display:flex;"><span>cat ~/.ssh/hetzner_storagebox.pub | ssh -p23 u000000@u000000.your-storagebox.de install-ssh-key </span></span><span style="display:flex;"><span><span style="color:#6272a4"># type passwd</span></span></span></code></pre></div> </div> <p> Note, if the key name is not id_ed25519, need add config in <code class="verbatim">~/.ssh/config</code>:</p> <div class="src src-conf"> <pre tabindex="0"><code class="language-conf" data-lang="conf">Host storagebox HostName u000000.your-storagebox.de User u000000 Port 23 IdentityFile ~/.ssh/hetzner_storagebox</code></pre> </div> <p> Second, run below script.</p> <div class="src src-sh"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#ff79c6">#!/bin/bash </span></span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># Configuration</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">STORAGE_USER</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;u000000&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">STORAGE_HOST</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;u000000.your-storagebox.de&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">STORAGE_PORT</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;23&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">REMOTE_DIR</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;forgejo_backup&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">LOCAL_BACKUP_DIR</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;/tmp&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;[</span><span style="color:#ff79c6">$(</span>date <span style="color:#f1fa8c">&#39;+%Y-%m-%d %H:%M:%S&#39;</span><span style="color:#ff79c6">)</span><span style="color:#f1fa8c">] === Starting Forgejo backup process ===&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># need at /home/git</span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">if</span> <span style="color:#ff79c6">[</span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$PWD</span><span style="color:#f1fa8c">&#34;</span> !<span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#34;/home/git&#34;</span> <span style="color:#ff79c6">]</span>; <span style="color:#ff79c6">then</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;Changing directory to /home/git&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">cd</span> /home/git </span></span><span style="display:flex;"><span><span style="color:#ff79c6">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># Stop service</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;Stopping Forgejo service (docker compose stop)...&#34;</span> </span></span><span style="display:flex;"><span>docker compose stop </span></span><span style="display:flex;"><span><span style="color:#ff79c6">if</span> <span style="color:#ff79c6">[</span> <span style="color:#8be9fd;font-style:italic">$?</span> -eq <span style="color:#bd93f9">0</span> <span style="color:#ff79c6">]</span>; <span style="color:#ff79c6">then</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;Service stopped successfully.&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">else</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;WARNING: Failed to stop service, continuing anyway...&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># Create timestamp and archive name</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">TIMESTAMP</span><span style="color:#ff79c6">=</span><span style="color:#ff79c6">$(</span>date +%Y%m%d-%H%M%S<span style="color:#ff79c6">)</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">ARCHIVE_NAME</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;forgejo-backup-</span><span style="color:#8be9fd;font-style:italic">$TIMESTAMP</span><span style="color:#f1fa8c">.tar.gz&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">ARCHIVE_PATH</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$LOCAL_BACKUP_DIR</span><span style="color:#f1fa8c">/</span><span style="color:#8be9fd;font-style:italic">$ARCHIVE_NAME</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># Collect SSH forwarding config (needs sudo to read /etc/ssh/sshd_config)</span> </span></span><span style="display:flex;"><span>sudo cp /etc/ssh/sshd_config ./sshd_config.backup 2&gt;/dev/null <span style="color:#ff79c6">||</span> <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;Warning: cannot copy /etc/ssh/sshd_config (run backup as root or add sudo)&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># Save current git user&#39;s shell setting for reference</span> </span></span><span style="display:flex;"><span>getent passwd git | cut -d: -f7 &gt; ./git-shell.backup 2&gt;/dev/null <span style="color:#ff79c6">||</span> <span style="color:#8be9fd;font-style:italic">true</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># Create compressed archive</span> </span></span><span style="display:flex;"><span>tar -czf <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$ARCHIVE_PATH</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> ./forgejo <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> ./conf <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> ./Caddyfile <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> ./docker-compose.yaml <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> ./forgejo-shell.sh <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> ./sshd_config.backup <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> ./git-shell.backup </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># Upload to Storage Box</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;Uploading archive to </span><span style="color:#8be9fd;font-style:italic">$STORAGE_USER</span><span style="color:#f1fa8c">@</span><span style="color:#8be9fd;font-style:italic">$STORAGE_HOST</span><span style="color:#f1fa8c">:</span><span style="color:#8be9fd;font-style:italic">$REMOTE_DIR</span><span style="color:#f1fa8c">/ using rsync (port </span><span style="color:#8be9fd;font-style:italic">$STORAGE_PORT</span><span style="color:#f1fa8c">)...&#34;</span> </span></span><span style="display:flex;"><span>rsync -avh --delete -e <span style="color:#f1fa8c">&#34;ssh -p</span><span style="color:#8be9fd;font-style:italic">$STORAGE_PORT</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$ARCHIVE_PATH</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$STORAGE_USER</span><span style="color:#f1fa8c">@</span><span style="color:#8be9fd;font-style:italic">$STORAGE_HOST</span><span style="color:#f1fa8c">:</span><span style="color:#8be9fd;font-style:italic">$REMOTE_DIR</span><span style="color:#f1fa8c">/&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">RSYNC_EXIT</span><span style="color:#ff79c6">=</span><span style="color:#8be9fd;font-style:italic">$?</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># Clean up temp files</span> </span></span><span style="display:flex;"><span>rm -f ./sshd_config.backup ./git-shell.backup </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">if</span> <span style="color:#ff79c6">[</span> <span style="color:#8be9fd;font-style:italic">$RSYNC_EXIT</span> -eq <span style="color:#bd93f9">0</span> <span style="color:#ff79c6">]</span>; <span style="color:#ff79c6">then</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;Upload completed successfully.&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#6272a4"># Remove local archive if upload succeeded</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;Removing local archive: </span><span style="color:#8be9fd;font-style:italic">$ARCHIVE_PATH</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> rm <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$ARCHIVE_PATH</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">if</span> <span style="color:#ff79c6">[</span> <span style="color:#8be9fd;font-style:italic">$?</span> -eq <span style="color:#bd93f9">0</span> <span style="color:#ff79c6">]</span>; <span style="color:#ff79c6">then</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;Local archive removed.&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">else</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;WARNING: Failed to remove local archive (permissions?).&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;Backup successful: </span><span style="color:#8be9fd;font-style:italic">$ARCHIVE_NAME</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">else</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;ERROR: Upload failed (rsync exit code: </span><span style="color:#8be9fd;font-style:italic">$RSYNC_EXIT</span><span style="color:#f1fa8c">). Keeping local archive at </span><span style="color:#8be9fd;font-style:italic">$ARCHIVE_PATH</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># Start service</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;Starting Forgejo service (docker compose start)...&#34;</span> </span></span><span style="display:flex;"><span>docker compose start </span></span><span style="display:flex;"><span><span style="color:#ff79c6">if</span> <span style="color:#ff79c6">[</span> <span style="color:#8be9fd;font-style:italic">$?</span> -eq <span style="color:#bd93f9">0</span> <span style="color:#ff79c6">]</span>; <span style="color:#ff79c6">then</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;Service started successfully.&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">else</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;ERROR: Failed to start service. Please check manually.&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">exit</span> <span style="color:#bd93f9">1</span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;[</span><span style="color:#ff79c6">$(</span>date <span style="color:#f1fa8c">&#39;+%Y-%m-%d %H:%M:%S&#39;</span><span style="color:#ff79c6">)</span><span style="color:#f1fa8c">] === Backup process finished ===&#34;</span></span></span></code></pre></div> </div> <p> Third, make it backup repeatly.</p> <div class="src src-sh"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>crontab -e </span></span><span style="display:flex;"><span><span style="color:#6272a4">## add below line to edit area</span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># 0 2 * * * /home/git/backup.sh &gt;&gt; /home/git/backup.log 2&gt;&amp;1</span> </span></span><span style="display:flex;"><span>crontab -l</span></span></code></pre></div> </div> </div> </div> <div id="outline-container-headline-7" class="outline-2"> <h2 id="headline-7"> Restore Forgejo </h2> <div id="outline-text-headline-7" class="outline-text-2"> <p> The backup script above creates a complete snapshot of the Forgejo instance (data, config, Caddyfile, compose file, SSH forwarding setup). When you need to restore on a new machine or recover from data loss, the script below pulls the latest backup from StorageBox and reinstates everything.</p> <p> Place <code class="verbatim">forgejo-restore.sh</code> at <code class="verbatim">/home/git/</code>:</p> <div class="src src-sh"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#ff79c6">#!/bin/bash </span></span></span><span style="display:flex;"><span><span style="color:#6272a4"># forgejo-restore.sh -- Restore Forgejo from Hetzner StorageBox backup</span> </span></span><span style="display:flex;"><span><span style="color:#6272a4">#</span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># Usage:</span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># ./forgejo-restore.sh --list # List available backups</span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># ./forgejo-restore.sh # Restore latest backup</span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># ./forgejo-restore.sh forgejo-backup-20260606-120000.tar.gz # Specific file</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">set</span> -euo pipefail </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># ── Config (adjust before using) ──</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">STORAGE_USER</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;u000000&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">STORAGE_HOST</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;u000000.your-storagebox.de&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">STORAGE_PORT</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;23&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">REMOTE_DIR</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;./backups/forgejo&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">SSH_KEY</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;/home/git/.ssh/hetzner_storagebox&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># ── Colors ──</span> </span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">RED</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;\033[0;31m&#39;</span>; <span style="color:#8be9fd;font-style:italic">GREEN</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;\033[0;32m&#39;</span>; <span style="color:#8be9fd;font-style:italic">YELLOW</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;\033[1;33m&#39;</span>; <span style="color:#8be9fd;font-style:italic">CYAN</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;\033[0;36m&#39;</span>; <span style="color:#8be9fd;font-style:italic">NC</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;\033[0m&#39;</span> </span></span><span style="display:flex;"><span>info<span style="color:#ff79c6">()</span> <span style="color:#ff79c6">{</span> <span style="color:#8be9fd;font-style:italic">echo</span> -e <span style="color:#f1fa8c">&#34;</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">CYAN</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">[INFO]</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">NC</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c"> </span><span style="color:#8be9fd;font-style:italic">$*</span><span style="color:#f1fa8c">&#34;</span>; <span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span>ok<span style="color:#ff79c6">()</span> <span style="color:#ff79c6">{</span> <span style="color:#8be9fd;font-style:italic">echo</span> -e <span style="color:#f1fa8c">&#34;</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">GREEN</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">[OK]</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">NC</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c"> </span><span style="color:#8be9fd;font-style:italic">$*</span><span style="color:#f1fa8c">&#34;</span>; <span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span>warn<span style="color:#ff79c6">()</span> <span style="color:#ff79c6">{</span> <span style="color:#8be9fd;font-style:italic">echo</span> -e <span style="color:#f1fa8c">&#34;</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">YELLOW</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">[WARN]</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">NC</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c"> </span><span style="color:#8be9fd;font-style:italic">$*</span><span style="color:#f1fa8c">&#34;</span>; <span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span>err<span style="color:#ff79c6">()</span> <span style="color:#ff79c6">{</span> <span style="color:#8be9fd;font-style:italic">echo</span> -e <span style="color:#f1fa8c">&#34;</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">RED</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">[ERR]</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">NC</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c"> </span><span style="color:#8be9fd;font-style:italic">$*</span><span style="color:#f1fa8c">&#34;</span>; <span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>confirm<span style="color:#ff79c6">()</span> <span style="color:#ff79c6">{</span> <span style="color:#8be9fd;font-style:italic">echo</span> -en <span style="color:#f1fa8c">&#34;</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">YELLOW</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">[?]</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">NC</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c"> </span><span style="color:#8be9fd;font-style:italic">$*</span><span style="color:#f1fa8c"> [y/N] &#34;</span>; <span style="color:#8be9fd;font-style:italic">read</span> -r resp; <span style="color:#ff79c6">case</span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$resp</span><span style="color:#f1fa8c">&#34;</span> in y|Y|yes|Yes<span style="color:#ff79c6">)</span> <span style="color:#ff79c6">return</span> <span style="color:#bd93f9">0</span> ;; *<span style="color:#ff79c6">)</span> <span style="color:#8be9fd;font-style:italic">exit</span> <span style="color:#bd93f9">1</span> ;; <span style="color:#ff79c6">esac</span>; <span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>check_prerequisites<span style="color:#ff79c6">()</span> <span style="color:#ff79c6">{</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">local</span> <span style="color:#8be9fd;font-style:italic">missing</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">command</span> -v docker &amp;&gt;/dev/null <span style="color:#ff79c6">||</span> <span style="color:#ff79c6">{</span> err <span style="color:#f1fa8c">&#34;Docker not found&#34;</span>; <span style="color:#8be9fd;font-style:italic">missing</span><span style="color:#ff79c6">=</span>1; <span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span> docker compose version &amp;&gt;/dev/null <span style="color:#ff79c6">||</span> <span style="color:#ff79c6">{</span> err <span style="color:#f1fa8c">&#34;Docker Compose not found&#34;</span>; <span style="color:#8be9fd;font-style:italic">missing</span><span style="color:#ff79c6">=</span>1; <span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">if</span> ! ssh -i <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$SSH_KEY</span><span style="color:#f1fa8c">&#34;</span> -p <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$STORAGE_PORT</span><span style="color:#f1fa8c">&#34;</span> -o <span style="color:#8be9fd;font-style:italic">BatchMode</span><span style="color:#ff79c6">=</span>yes -o <span style="color:#8be9fd;font-style:italic">ConnectTimeout</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">5</span> <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$STORAGE_USER</span><span style="color:#f1fa8c">@</span><span style="color:#8be9fd;font-style:italic">$STORAGE_HOST</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#8be9fd;font-style:italic">exit</span> 2&gt;/dev/null; <span style="color:#ff79c6">then</span> </span></span><span style="display:flex;"><span> warn <span style="color:#f1fa8c">&#34;Cannot reach StorageBox at </span><span style="color:#8be9fd;font-style:italic">$STORAGE_HOST</span><span style="color:#f1fa8c">:</span><span style="color:#8be9fd;font-style:italic">$STORAGE_PORT</span><span style="color:#f1fa8c">&#34;</span>; <span style="color:#8be9fd;font-style:italic">missing</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">fi</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">[</span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$missing</span><span style="color:#f1fa8c">&#34;</span> -eq <span style="color:#bd93f9">1</span> <span style="color:#ff79c6">]</span> <span style="color:#ff79c6">&amp;&amp;</span> <span style="color:#ff79c6">{</span> err <span style="color:#f1fa8c">&#34;Prerequisites not met. Aborting.&#34;</span>; <span style="color:#8be9fd;font-style:italic">exit</span> 1; <span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span> ok <span style="color:#f1fa8c">&#34;Prerequisites check passed&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>list_backups<span style="color:#ff79c6">()</span> <span style="color:#ff79c6">{</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;&#34;</span> </span></span><span style="display:flex;"><span> info <span style="color:#f1fa8c">&#34;Available backups on StorageBox:&#34;</span> </span></span><span style="display:flex;"><span> ssh -i <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$SSH_KEY</span><span style="color:#f1fa8c">&#34;</span> -p <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$STORAGE_PORT</span><span style="color:#f1fa8c">&#34;</span> -o <span style="color:#8be9fd;font-style:italic">BatchMode</span><span style="color:#ff79c6">=</span>yes <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$STORAGE_USER</span><span style="color:#f1fa8c">@</span><span style="color:#8be9fd;font-style:italic">$STORAGE_HOST</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> <span style="color:#f1fa8c">&#34;ls -lh </span><span style="color:#8be9fd;font-style:italic">$REMOTE_DIR</span><span style="color:#f1fa8c">/forgejo-backup-*.tar.gz 2&gt;/dev/null || echo &#39;(none)&#39;&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>fetch_backup<span style="color:#ff79c6">()</span> <span style="color:#ff79c6">{</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">local</span> <span style="color:#8be9fd;font-style:italic">archive_name</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$1</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#8be9fd;font-style:italic">dest</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$2</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> info <span style="color:#f1fa8c">&#34;Downloading </span><span style="color:#8be9fd;font-style:italic">$archive_name</span><span style="color:#f1fa8c">...&#34;</span> </span></span><span style="display:flex;"><span> rsync -avh --progress -e <span style="color:#f1fa8c">&#34;ssh -i </span><span style="color:#8be9fd;font-style:italic">$SSH_KEY</span><span style="color:#f1fa8c"> -p </span><span style="color:#8be9fd;font-style:italic">$STORAGE_PORT</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$STORAGE_USER</span><span style="color:#f1fa8c">@</span><span style="color:#8be9fd;font-style:italic">$STORAGE_HOST</span><span style="color:#f1fa8c">:</span><span style="color:#8be9fd;font-style:italic">$REMOTE_DIR</span><span style="color:#f1fa8c">/</span><span style="color:#8be9fd;font-style:italic">$archive_name</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$dest</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>restore<span style="color:#ff79c6">()</span> <span style="color:#ff79c6">{</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">local</span> <span style="color:#8be9fd;font-style:italic">archive_path</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$1</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">local</span> restore_dir </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">restore_dir</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;</span><span style="color:#ff79c6">$(</span>mktemp -d<span style="color:#ff79c6">)</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">trap</span> <span style="color:#f1fa8c">&#39;rm -rf &#34;$restore_dir&#34;&#39;</span> EXIT </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> info <span style="color:#f1fa8c">&#34;Extracting archive...&#34;</span> </span></span><span style="display:flex;"><span> tar -xzf <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$archive_path</span><span style="color:#f1fa8c">&#34;</span> -C <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$restore_dir</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#6272a4"># Detect compose filename</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">local</span> <span style="color:#8be9fd;font-style:italic">compose_file</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">for</span> f in docker-compose.yaml docker-compose.yml compose.yml; <span style="color:#ff79c6">do</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">[</span> -f <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$restore_dir</span><span style="color:#f1fa8c">/</span><span style="color:#8be9fd;font-style:italic">$f</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#ff79c6">]</span> <span style="color:#ff79c6">&amp;&amp;</span> <span style="color:#8be9fd;font-style:italic">compose_file</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$f</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#ff79c6">&amp;&amp;</span> <span style="color:#8be9fd;font-style:italic">break</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">done</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">[</span> -z <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$compose_file</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#ff79c6">]</span> <span style="color:#ff79c6">&amp;&amp;</span> <span style="color:#ff79c6">{</span> err <span style="color:#f1fa8c">&#34;No docker-compose file in backup&#34;</span>; <span style="color:#8be9fd;font-style:italic">exit</span> 1; <span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#6272a4"># Stop existing</span> </span></span><span style="display:flex;"><span> docker compose down 2&gt;/dev/null <span style="color:#ff79c6">||</span> <span style="color:#8be9fd;font-style:italic">true</span> </span></span><span style="display:flex;"><span> ok <span style="color:#f1fa8c">&#34;Forgejo stopped&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#6272a4"># Backup current data defensively</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">local</span> ts </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">ts</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;</span><span style="color:#ff79c6">$(</span>date +%Y%m%d-%H%M%S<span style="color:#ff79c6">)</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">if</span> <span style="color:#ff79c6">[</span> -d forgejo <span style="color:#ff79c6">]</span> <span style="color:#ff79c6">||</span> <span style="color:#ff79c6">[</span> -d conf <span style="color:#ff79c6">]</span>; <span style="color:#ff79c6">then</span> </span></span><span style="display:flex;"><span> confirm <span style="color:#f1fa8c">&#34;Existing data found, proceed with restore?&#34;</span> </span></span><span style="display:flex;"><span> mkdir -p <span style="color:#f1fa8c">&#34;/tmp/forgejo-pre-restore-</span><span style="color:#8be9fd;font-style:italic">$ts</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">[</span> -d forgejo <span style="color:#ff79c6">]</span> <span style="color:#ff79c6">&amp;&amp;</span> cp -a forgejo <span style="color:#f1fa8c">&#34;/tmp/forgejo-pre-restore-</span><span style="color:#8be9fd;font-style:italic">$ts</span><span style="color:#f1fa8c">/&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">[</span> -d conf <span style="color:#ff79c6">]</span> <span style="color:#ff79c6">&amp;&amp;</span> cp -a conf <span style="color:#f1fa8c">&#34;/tmp/forgejo-pre-restore-</span><span style="color:#8be9fd;font-style:italic">$ts</span><span style="color:#f1fa8c">/&#34;</span> </span></span><span style="display:flex;"><span> ok <span style="color:#f1fa8c">&#34;Current data backed up to /tmp/forgejo-pre-restore-</span><span style="color:#8be9fd;font-style:italic">$ts</span><span style="color:#f1fa8c">/&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#6272a4"># Restore core data</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">[</span> -d <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$restore_dir</span><span style="color:#f1fa8c">/forgejo&#34;</span> <span style="color:#ff79c6">]</span> <span style="color:#ff79c6">&amp;&amp;</span> cp -a <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$restore_dir</span><span style="color:#f1fa8c">/forgejo&#34;</span> ./forgejo </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">[</span> -d <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$restore_dir</span><span style="color:#f1fa8c">/conf&#34;</span> <span style="color:#ff79c6">]</span> <span style="color:#ff79c6">&amp;&amp;</span> cp -a <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$restore_dir</span><span style="color:#f1fa8c">/conf&#34;</span> ./conf </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">[</span> -f <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$restore_dir</span><span style="color:#f1fa8c">/Caddyfile&#34;</span> <span style="color:#ff79c6">]</span> <span style="color:#ff79c6">&amp;&amp;</span> cp <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$restore_dir</span><span style="color:#f1fa8c">/Caddyfile&#34;</span> ./ </span></span><span style="display:flex;"><span> cp <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$restore_dir</span><span style="color:#f1fa8c">/</span><span style="color:#8be9fd;font-style:italic">$compose_file</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#f1fa8c">&#34;./</span><span style="color:#8be9fd;font-style:italic">$compose_file</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#6272a4"># Fix ownership (Forgejo container runs as uid=108/gid=110)</span> </span></span><span style="display:flex;"><span> chown -R 108:110 ./forgejo ./conf 2&gt;/dev/null <span style="color:#ff79c6">||</span> <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> warn <span style="color:#f1fa8c">&#34;Could not set ownership (run as root to fix)&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> ok <span style="color:#f1fa8c">&#34;Data restored&#34;</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#6272a4"># Restore forgejo-shell.sh if present</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">if</span> <span style="color:#ff79c6">[</span> -f <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$restore_dir</span><span style="color:#f1fa8c">/forgejo-shell.sh&#34;</span> <span style="color:#ff79c6">]</span>; <span style="color:#ff79c6">then</span> </span></span><span style="display:flex;"><span> cp <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$restore_dir</span><span style="color:#f1fa8c">/forgejo-shell.sh&#34;</span> <span style="color:#f1fa8c">&#34;/home/git/forgejo-shell.sh&#34;</span> </span></span><span style="display:flex;"><span> chmod +x <span style="color:#f1fa8c">&#34;/home/git/forgejo-shell.sh&#34;</span> </span></span><span style="display:flex;"><span> chown git:git <span style="color:#f1fa8c">&#34;/home/git/forgejo-shell.sh&#34;</span> 2&gt;/dev/null <span style="color:#ff79c6">||</span> <span style="color:#8be9fd;font-style:italic">true</span> </span></span><span style="display:flex;"><span> ok <span style="color:#f1fa8c">&#34;forgejo-shell.sh restored&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#6272a4"># Start services</span> </span></span><span style="display:flex;"><span> docker compose up -d </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">for</span> i in <span style="color:#ff79c6">$(</span>seq <span style="color:#bd93f9">1</span> 30<span style="color:#ff79c6">)</span>; <span style="color:#ff79c6">do</span> </span></span><span style="display:flex;"><span> sleep <span style="color:#bd93f9">2</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">if</span> curl -sf -o /dev/null <span style="color:#f1fa8c">&#34;http://127.0.0.1:3000/api/healthz&#34;</span> 2&gt;/dev/null; <span style="color:#ff79c6">then</span> </span></span><span style="display:flex;"><span> ok <span style="color:#f1fa8c">&#34;Forgejo is healthy&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">break</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">fi</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">done</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;================================================================&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34; Data restore complete.&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34; SSH forwarding needs manual steps (requires root):&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">if</span> <span style="color:#ff79c6">[</span> -f <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$restore_dir</span><span style="color:#f1fa8c">/sshd_config.backup&#34;</span> <span style="color:#ff79c6">]</span>; <span style="color:#ff79c6">then</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34; 1. Append Match User git block to /etc/ssh/sshd_config:&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34; sudo tee -a /etc/ssh/sshd_config &lt; </span><span style="color:#8be9fd;font-style:italic">$restore_dir</span><span style="color:#f1fa8c">/sshd_config.backup&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">fi</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34; 2. Set git user shell:&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34; sudo usermod -s /home/git/forgejo-shell.sh git&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34; 3. Restart SSH:&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34; sudo systemctl restart ssh&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34; 4. Verify:&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34; ssh -T git@git.example.com&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;================================================================&#34;</span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>main<span style="color:#ff79c6">()</span> <span style="color:#ff79c6">{</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">if</span> <span style="color:#ff79c6">[</span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$1</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#34;--list&#34;</span> <span style="color:#ff79c6">]</span> <span style="color:#ff79c6">||</span> <span style="color:#ff79c6">[</span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$1</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#34;-l&#34;</span> <span style="color:#ff79c6">]</span>; <span style="color:#ff79c6">then</span> </span></span><span style="display:flex;"><span> check_prerequisites; list_backups; <span style="color:#ff79c6">return</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">fi</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> check_prerequisites </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">if</span> <span style="color:#ff79c6">[</span> -n <span style="color:#f1fa8c">&#34;</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">1</span><span style="color:#ff79c6">:-</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#ff79c6">]</span> <span style="color:#ff79c6">&amp;&amp;</span> <span style="color:#ff79c6">[</span> <span style="color:#f1fa8c">&#34;</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">1</span>#--<span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$1</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#ff79c6">]</span>; <span style="color:#ff79c6">then</span> </span></span><span style="display:flex;"><span> <span style="color:#6272a4"># Use local archive</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">[</span> -f <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$1</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#ff79c6">]</span> <span style="color:#ff79c6">||</span> <span style="color:#ff79c6">{</span> err <span style="color:#f1fa8c">&#34;File not found: </span><span style="color:#8be9fd;font-style:italic">$1</span><span style="color:#f1fa8c">&#34;</span>; <span style="color:#8be9fd;font-style:italic">exit</span> 1; <span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span> restore <span style="color:#f1fa8c">&#34;</span><span style="color:#ff79c6">$(</span>realpath <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$1</span><span style="color:#f1fa8c">&#34;</span><span style="color:#ff79c6">)</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">else</span> </span></span><span style="display:flex;"><span> <span style="color:#6272a4"># Fetch latest from StorageBox</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">local</span> latest </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">latest</span><span style="color:#ff79c6">=</span><span style="color:#ff79c6">$(</span>ssh -i <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$SSH_KEY</span><span style="color:#f1fa8c">&#34;</span> -p <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$STORAGE_PORT</span><span style="color:#f1fa8c">&#34;</span> -o <span style="color:#8be9fd;font-style:italic">BatchMode</span><span style="color:#ff79c6">=</span>yes <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$STORAGE_USER</span><span style="color:#f1fa8c">@</span><span style="color:#8be9fd;font-style:italic">$STORAGE_HOST</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#f1fa8c">\ </span></span></span><span style="display:flex;"><span> <span style="color:#f1fa8c">&#34;ls -t </span><span style="color:#8be9fd;font-style:italic">$REMOTE_DIR</span><span style="color:#f1fa8c">/forgejo-backup-*.tar.gz 2&gt;/dev/null | head -1&#34;</span> <span style="color:#ff79c6">||</span> <span style="color:#8be9fd;font-style:italic">true</span><span style="color:#ff79c6">)</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">[</span> -z <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$latest</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#ff79c6">]</span> <span style="color:#ff79c6">&amp;&amp;</span> <span style="color:#ff79c6">{</span> err <span style="color:#f1fa8c">&#34;No backups found&#34;</span>; <span style="color:#8be9fd;font-style:italic">exit</span> 1; <span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span> <span style="color:#8be9fd;font-style:italic">local</span> temp_dir; <span style="color:#8be9fd;font-style:italic">temp_dir</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;</span><span style="color:#ff79c6">$(</span>mktemp -d<span style="color:#ff79c6">)</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> fetch_backup <span style="color:#f1fa8c">&#34;</span><span style="color:#ff79c6">$(</span>basename <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$latest</span><span style="color:#f1fa8c">&#34;</span><span style="color:#ff79c6">)</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$temp_dir</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> restore <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$temp_dir</span><span style="color:#f1fa8c">/</span><span style="color:#ff79c6">$(</span>basename <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$latest</span><span style="color:#f1fa8c">&#34;</span><span style="color:#ff79c6">)</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> rm -rf <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$temp_dir</span><span style="color:#f1fa8c">&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#ff79c6">fi</span> </span></span><span style="display:flex;"><span><span style="color:#ff79c6">}</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>main <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$@</span><span style="color:#f1fa8c">&#34;</span></span></span></code></pre></div> </div> <p> Usage:</p> <div class="src src-sh"> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#6272a4"># List available backups</span> </span></span><span style="display:flex;"><span>./forgejo-restore.sh --list </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># Restore the latest backup</span> </span></span><span style="display:flex;"><span>./forgejo-restore.sh </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#6272a4"># Restore a specific backup archive</span> </span></span><span style="display:flex;"><span>./forgejo-restore.sh /path/to/forgejo-backup-20260606-120000.tar.gz</span></span></code></pre></div> </div> <p> The script handles the full restore automatically:</p> <table> <thead> <tr> <th>Step</th> <th>What it does</th> </tr> </thead> <tbody> <tr> <td>Prerequisites</td> <td>Checks Docker, Docker Compose, StorageBox connectivity</td> </tr> <tr> <td>Fetch</td> <td>Downloads the backup archive from StorageBox</td> </tr> <tr> <td>Extract</td> <td>Untars the archive to a temporary directory</td> </tr> <tr> <td>Stop</td> <td>Runs <code class="verbatim">docker compose down</code> to stop any running service</td> </tr> <tr> <td>Defensive copy</td> <td>Backs up current <code class="verbatim">./forgejo</code> and <code class="verbatim">./conf</code> before overwriting</td> </tr> <tr> <td>Restore data</td> <td>Copies forgejo/, conf/, Caddyfile, compose file back</td> </tr> <tr> <td>Restore shell</td> <td>Restores <code class="verbatim">forgejo-shell.sh</code> for SSH forwarding</td> </tr> <tr> <td>Fix ownership</td> <td>Sets <code class="verbatim">108:110</code> on data directories</td> </tr> <tr> <td>Start</td> <td>Runs <code class="verbatim">docker compose up -d</code> and waits for health check</td> </tr> <tr> <td>Print SSH steps</td> <td>Shows the remaining root-required SSH config steps</td> </tr> </tbody> </table> <p> After the script finishes, two SSH config steps are left for you to do manually (because they need root), but the script tells you exactly what to run.</p> </div> </div>

2026/2/13
阅读更多