[{"data":1,"prerenderedAt":903},["ShallowReactive",2],{"navigation":3,"\u002Fposts\u002Fhomelab_automation-content":42,"\u002Fposts\u002Fhomelab_automation-surround":898},[4],{"title":5,"path":6,"stem":7,"children":8,"page":41},"Posts","\u002Fposts","posts",[9,13,17,21,25,29,33,37],{"title":10,"path":11,"stem":12},"Agents, MCP, RAG, Knowledge Graphs, all open source and local","\u002Fposts\u002Fagents_mcp_rag_local_foss","posts\u002Fagents_mcp_rag_local_foss",{"title":14,"path":15,"stem":16},"DeGoogle your phone","\u002Fposts\u002Fdegoogle_your_phone","posts\u002Fdegoogle_your_phone",{"title":18,"path":19,"stem":20},"Homelab Automation","\u002Fposts\u002Fhomelab_automation","posts\u002Fhomelab_automation",{"title":22,"path":23,"stem":24},"How I discovered Static Site Generators","\u002Fposts\u002Fhow_i_discovered_ssg","posts\u002Fhow_i_discovered_ssg",{"title":26,"path":27,"stem":28},"My Linux Journey","\u002Fposts\u002Fmy_switch_to_linux","posts\u002Fmy_switch_to_linux",{"title":30,"path":31,"stem":32},"Own your Data","\u002Fposts\u002Fown_your_data","posts\u002Fown_your_data",{"title":34,"path":35,"stem":36},"Self-host your AI assistant","\u002Fposts\u002Fself_host_your_ai_assistant","posts\u002Fself_host_your_ai_assistant",{"title":38,"path":39,"stem":40},"The move to Vue.js","\u002Fposts\u002Fthe_move_to_vuejs","posts\u002Fthe_move_to_vuejs",false,{"id":43,"title":18,"body":44,"date":883,"description":884,"extension":885,"image":886,"meta":887,"navigation":426,"path":19,"readingTime":447,"seo":888,"stem":20,"tags":889,"__hash__":897},"content\u002Fposts\u002Fhomelab_automation.md",{"type":45,"value":46,"toc":869},"minimark",[47,52,89,94,104,130,134,137,156,164,168,183,340,346,517,524,528,531,535,546,606,616,642,648,652,665,671,674,703,709,718,732,736,739,748,759,762,778,781,790,797,800,804,807,814,848,852,862,865],[48,49,51],"h2",{"id":50},"from-chaos-to-order-with-renovate-gitlab-cicd","From chaos to order with Renovate & Gitlab CI\u002FCD",[53,54,55,56,60,61,64,65,72,73,78,79,83,84,88],"p",{},"I think everyone self-hosting eventually has been there. You’re setting up your first homelab, the excitement is high,\nand your ",[57,58,59],"code",{},"compose.yaml"," is littered with the most dangerous word in DevOps: ",[57,62,63],{},":latest",".\nIt works perfectly until it doesn't. One morning you wake up, look at your ",[66,67,71],"a",{"href":68,"rel":69},"https:\u002F\u002Fgithub.com\u002Fcontainrrr\u002Fwatchtower",[70],"nofollow","watchtower","\nservice updating everything, and your entire stack in ",[66,74,77],{"href":75,"rel":76},"https:\u002F\u002Fwww.portainer.io\u002F",[70],"Portainer"," is a smoking crater...\nI realized that I had to stop being lazy and start treating it like some kind of production.\nHere is how I moved from ",[80,81,82],"em",{},"beginner’s luck"," to a fully automated, ",[85,86,87],"strong",{},"GitLab CI\u002FCD and Renovate"," pipeline.",[90,91,93],"h3",{"id":92},"the-root-cause-the-latest-trap","The Root Cause: The \"Latest\" Trap",[53,95,96,97,99,100,103],{},"The mistake wasn't just using ",[57,98,63],{},"; it was the ",[85,101,102],{},"lack of intent",". Using generic tags means you never have to pay attention.\nTo have a more controlled setup, I needed three things:",[105,106,107,118,124],"ul",{},[108,109,110,113,114,117],"li",{},[85,111,112],{},"Pinning:"," Total control over what version is running (e.g., sticking to ",[85,115,116],{},"3.6"," until I’ve personally verified the upgrade path).",[108,119,120,123],{},[85,121,122],{},"Visibility:"," To be notified when a new version exists without manually checking dozens of GitHub repositories.",[108,125,126,129],{},[85,127,128],{},"Automation:"," A way to deploy those updates that didn't involve me manually SSH-ing into a terminal for every minor patch.\nBut keeping the option just in case...",[90,131,133],{"id":132},"a-solution-renovate","A Solution: Renovate",[53,135,136],{},"I was already tracking docker compose and configuration for services in a private repository in my gitlab.com account.\nIt seemed a perfectly decent way to keep track of configuration, and also some documentation by service in a few Markdown files.\nIn the end, it turned out as a perfect first step to work on GitOps and server admin tasks.",[53,138,139,140,143,144,147,148,151,152,155],{},"I also read about renovate, and it's Gitlab integration.\nBasically, it can scan my homelab repository and look for outdated Docker tags.\nInstead of just breaking things, Renovate opens a ",[85,141,142],{},"Merge Request",".\nIt’s like a librarian handing me a book and saying, ",[80,145,146],{},"\"Hey, there's a new version. I've gathered the release notes\nand changelogs for you (if you add a GitHub API token). Do you want to merge this?\"","\nThis allows for ",[85,149,150],{},"Intentional Upgrading",". If I see an update for a critical service, I can read or search for breaking changes\nbefore clicking ",[80,153,154],{},"Merge",".",[53,157,158],{},[159,160],"img",{"alt":161,"src":162,"title":163},"Mend Renovate CLI","\u002Fposts\u002Fhomelab_automation\u002Fmend-renovate-cli-banner.jpg","Renovate, the game changer.",[90,165,167],{"id":166},"renovate-gitlab-pipeline-in-practice","Renovate GitLab Pipeline in practice",[53,169,170,171,174,175,178,179,182],{},"Fortunately, Gitlab offers a built-in ",[85,172,173],{},"CI\u002FCD pipeline"," that includes Renovate to simplify the configuration. First,\nyou need to produce an access token with scopes are read_api, read_repository and write_repository for renovate.\nYou can do this in Gitlab's UI under Settings > Access Tokens. After that, add a variable to your Gitlab CI\u002FCD pipeline\nnamed ",[57,176,177],{},"RENOVATE_TOKEN"," and set it to your token. Then, you just need to add a few lines of\nconfiguration in your ",[57,180,181],{},".gitlab\u002Frenovate.json"," file:",[184,185,190],"pre",{"className":186,"code":187,"language":188,"meta":189,"style":189},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"$schema\": \"https:\u002F\u002Fdocs.renovatebot.com\u002Frenovate-schema.json\",\n  \"extends\": [\"config:base\"],\n  \"enabledManagers\": [\"docker-compose\"],\n  \"assignees\": [\"YourGitlabUsername\"],\n  \"labels\": [\"dependencies\", \"renovate\"]\n}\n","json","",[57,191,192,201,229,254,277,300,334],{"__ignoreMap":189},[193,194,197],"span",{"class":195,"line":196},"line",1,[193,198,200],{"class":199},"sMK4o","{\n",[193,202,204,207,211,214,217,220,224,226],{"class":195,"line":203},2,[193,205,206],{"class":199},"  \"",[193,208,210],{"class":209},"spNyl","$schema",[193,212,213],{"class":199},"\"",[193,215,216],{"class":199},":",[193,218,219],{"class":199}," \"",[193,221,223],{"class":222},"sfazB","https:\u002F\u002Fdocs.renovatebot.com\u002Frenovate-schema.json",[193,225,213],{"class":199},[193,227,228],{"class":199},",\n",[193,230,232,234,237,239,241,244,246,249,251],{"class":195,"line":231},3,[193,233,206],{"class":199},[193,235,236],{"class":209},"extends",[193,238,213],{"class":199},[193,240,216],{"class":199},[193,242,243],{"class":199}," [",[193,245,213],{"class":199},[193,247,248],{"class":222},"config:base",[193,250,213],{"class":199},[193,252,253],{"class":199},"],\n",[193,255,257,259,262,264,266,268,270,273,275],{"class":195,"line":256},4,[193,258,206],{"class":199},[193,260,261],{"class":209},"enabledManagers",[193,263,213],{"class":199},[193,265,216],{"class":199},[193,267,243],{"class":199},[193,269,213],{"class":199},[193,271,272],{"class":222},"docker-compose",[193,274,213],{"class":199},[193,276,253],{"class":199},[193,278,280,282,285,287,289,291,293,296,298],{"class":195,"line":279},5,[193,281,206],{"class":199},[193,283,284],{"class":209},"assignees",[193,286,213],{"class":199},[193,288,216],{"class":199},[193,290,243],{"class":199},[193,292,213],{"class":199},[193,294,295],{"class":222},"YourGitlabUsername",[193,297,213],{"class":199},[193,299,253],{"class":199},[193,301,303,305,308,310,312,314,316,319,321,324,326,329,331],{"class":195,"line":302},6,[193,304,206],{"class":199},[193,306,307],{"class":209},"labels",[193,309,213],{"class":199},[193,311,216],{"class":199},[193,313,243],{"class":199},[193,315,213],{"class":199},[193,317,318],{"class":222},"dependencies",[193,320,213],{"class":199},[193,322,323],{"class":199},",",[193,325,219],{"class":199},[193,327,328],{"class":222},"renovate",[193,330,213],{"class":199},[193,332,333],{"class":199},"]\n",[193,335,337],{"class":195,"line":336},7,[193,338,339],{"class":199},"}\n",[53,341,342,343,182],{},"and add jobs to your ",[57,344,345],{},".gitlab-ci.yml",[184,347,351],{"className":348,"code":349,"language":350,"meta":189,"style":189},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","include:\n  - project: 'renovate-bot\u002Frenovate-runner'\n    file: '\u002Ftemplates\u002Frenovate-config-validator.gitlab-ci.yml'\n  - project: 'renovate-bot\u002Frenovate-runner'\n    file: '\u002Ftemplates\u002Frenovate.gitlab-ci.yml'\n\nrenovate:\n  stage: renovate\n  variables:\n    RENOVATE_EXTRA_FLAGS: --autodiscover=false\n    RENOVATE_ALLOW_POST_UPGRADE_COMMANDS: \"true\"\n    RENOVATE_REPOSITORIES: $CI_PROJECT_PATH\n  rules:\n    - if: '$CI_PIPELINE_SOURCE == \"schedule\"'\n","yaml",[57,352,353,362,381,395,409,422,428,434,445,453,464,480,491,499],{"__ignoreMap":189},[193,354,355,359],{"class":195,"line":196},[193,356,358],{"class":357},"swJcz","include",[193,360,361],{"class":199},":\n",[193,363,364,367,370,372,375,378],{"class":195,"line":203},[193,365,366],{"class":199},"  -",[193,368,369],{"class":357}," project",[193,371,216],{"class":199},[193,373,374],{"class":199}," '",[193,376,377],{"class":222},"renovate-bot\u002Frenovate-runner",[193,379,380],{"class":199},"'\n",[193,382,383,386,388,390,393],{"class":195,"line":231},[193,384,385],{"class":357},"    file",[193,387,216],{"class":199},[193,389,374],{"class":199},[193,391,392],{"class":222},"\u002Ftemplates\u002Frenovate-config-validator.gitlab-ci.yml",[193,394,380],{"class":199},[193,396,397,399,401,403,405,407],{"class":195,"line":256},[193,398,366],{"class":199},[193,400,369],{"class":357},[193,402,216],{"class":199},[193,404,374],{"class":199},[193,406,377],{"class":222},[193,408,380],{"class":199},[193,410,411,413,415,417,420],{"class":195,"line":279},[193,412,385],{"class":357},[193,414,216],{"class":199},[193,416,374],{"class":199},[193,418,419],{"class":222},"\u002Ftemplates\u002Frenovate.gitlab-ci.yml",[193,421,380],{"class":199},[193,423,424],{"class":195,"line":302},[193,425,427],{"emptyLinePlaceholder":426},true,"\n",[193,429,430,432],{"class":195,"line":336},[193,431,328],{"class":357},[193,433,361],{"class":199},[193,435,437,440,442],{"class":195,"line":436},8,[193,438,439],{"class":357},"  stage",[193,441,216],{"class":199},[193,443,444],{"class":222}," renovate\n",[193,446,448,451],{"class":195,"line":447},9,[193,449,450],{"class":357},"  variables",[193,452,361],{"class":199},[193,454,456,459,461],{"class":195,"line":455},10,[193,457,458],{"class":357},"    RENOVATE_EXTRA_FLAGS",[193,460,216],{"class":199},[193,462,463],{"class":222}," --autodiscover=false\n",[193,465,467,470,472,474,477],{"class":195,"line":466},11,[193,468,469],{"class":357},"    RENOVATE_ALLOW_POST_UPGRADE_COMMANDS",[193,471,216],{"class":199},[193,473,219],{"class":199},[193,475,476],{"class":222},"true",[193,478,479],{"class":199},"\"\n",[193,481,483,486,488],{"class":195,"line":482},12,[193,484,485],{"class":357},"    RENOVATE_REPOSITORIES",[193,487,216],{"class":199},[193,489,490],{"class":222}," $CI_PROJECT_PATH\n",[193,492,494,497],{"class":195,"line":493},13,[193,495,496],{"class":357},"  rules",[193,498,361],{"class":199},[193,500,502,505,508,510,512,515],{"class":195,"line":501},14,[193,503,504],{"class":199},"    -",[193,506,507],{"class":357}," if",[193,509,216],{"class":199},[193,511,374],{"class":199},[193,513,514],{"class":222},"$CI_PIPELINE_SOURCE == \"schedule\"",[193,516,380],{"class":199},[53,518,519,520,523],{},"Simple right ? The last piece is how you want the pipeline to run. Here as you can see, I decided to go for a ",[85,521,522],{},"schedule","\nthat you can add in your project's CI\u002FCD menu. But obviously, you could set it up on push, or on a manual action in the UI.",[48,525,527],{"id":526},"wait-how-do-i-update-my-server-now","Wait, how do I update my server now ?",[53,529,530],{},"Obviously, merging things on Gitlab doesn't make it, at least not at the automation level of watchtower. With it, I had nothing to\ndo. My goal is to have better control but not at the cost of laziness... Here again Gitlab will come to the rescue.",[90,532,534],{"id":533},"makefile-single-source-of-truth","Makefile - Single Source of Truth",[53,536,537,538,541,542,545],{},"I decided to use a ",[85,539,540],{},"Makefile"," as ",[85,543,544],{},"source of truth",", which is a common practice. It keeps the deployment configuration in\none place, making it easier to manage and maintain. Here's an example of what it might look like:",[184,547,551],{"className":548,"code":549,"language":550,"meta":189,"style":189},"language-makefile shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","SERVICES = traefik authentik nextcloud jellyfin # whatever you have...\n\npull:\n    @echo \"--- Synchronizing with GitLab (Hard Reset) ---\"\n    git fetch origin main\n    git reset --hard origin\u002Fmain\n\nupdate-%:\n    @echo \"--- Updating service: $* ---\"\n    @cd $* && docker compose pull -q\n    @cd $* && docker compose up -d --remove-orphans --wait\n","makefile",[57,552,553,558,562,567,572,577,582,586,591,596,601],{"__ignoreMap":189},[193,554,555],{"class":195,"line":196},[193,556,557],{},"SERVICES = traefik authentik nextcloud jellyfin # whatever you have...\n",[193,559,560],{"class":195,"line":203},[193,561,427],{"emptyLinePlaceholder":426},[193,563,564],{"class":195,"line":231},[193,565,566],{},"pull:\n",[193,568,569],{"class":195,"line":256},[193,570,571],{},"    @echo \"--- Synchronizing with GitLab (Hard Reset) ---\"\n",[193,573,574],{"class":195,"line":279},[193,575,576],{},"    git fetch origin main\n",[193,578,579],{"class":195,"line":302},[193,580,581],{},"    git reset --hard origin\u002Fmain\n",[193,583,584],{"class":195,"line":336},[193,585,427],{"emptyLinePlaceholder":426},[193,587,588],{"class":195,"line":436},[193,589,590],{},"update-%:\n",[193,592,593],{"class":195,"line":447},[193,594,595],{},"    @echo \"--- Updating service: $* ---\"\n",[193,597,598],{"class":195,"line":455},[193,599,600],{},"    @cd $* && docker compose pull -q\n",[193,602,603],{"class":195,"line":466},[193,604,605],{},"    @cd $* && docker compose up -d --remove-orphans --wait\n",[53,607,608,611,612,615],{},[85,609,610],{},"Note",": Using ",[57,613,614],{},"git reset --hard"," ensures the local state never drifts from the repo.",[53,617,618,619,622,623,626,627,629,630,633,634,637,638,641],{},"With this, updating a service is just a matter of running ",[57,620,621],{},"make pull"," and ",[57,624,625],{},"sudo make update-\u003Cservice_name>",".\nEasy, quick and efficient. After any update on a ",[57,628,59],{},", I have to ssh into the server and\nrun make commands. It is rather secure because I control the access to the server and the ssh ports\nare not open thus I can log in only ",[85,631,632],{},"locally"," via my ",[85,635,636],{},"ssh key",". My docker install is requiring ",[85,639,640],{},"sudo"," as well so at this point,\neverything seems fine.",[53,643,644,645,155],{},"But obviously, it's not automated ! And as I understood while trying to automate this process,\nthere are some challenges regarding ",[85,646,647],{},"security",[90,649,651],{"id":650},"the-trade-off-security-vs-convenience","The Trade-off: Security vs. Convenience",[53,653,654,655,660,661,664],{},"To further automate the updating process, I planned on using a ",[66,656,659],{"href":657,"rel":658},"https:\u002F\u002Fdocs.gitlab.com\u002Frunner\u002F",[70],"Gitlab runner","\ninstalled on the server that executes the ",[57,662,663],{},"make"," commands on my server.\nI'll explain the runner configurations later but let's first discuss the security implications of this approach.",[53,666,667,668,670],{},"Executing any docker command on my server requires ",[80,669,640],{},". This is a quite normal and secure configuration,\nbut it makes the automation process harder.",[53,672,673],{},"It seems there are essentially 3 options to mitigate this:",[675,676,677,691,697],"ol",{},[108,678,679,682,683,686,687,690],{},[85,680,681],{},"Run Docker commands as a non-root user",": adding the ",[57,684,685],{},"gitlab-runner"," ",[80,688,689],{},"user"," to the docker group and running Docker commands as that user.",[108,692,693,696],{},[85,694,695],{},"Run Docker in rootless mode",": running Docker without root privileges. This is a more secure option but requires\nsome additional configuration steps.",[108,698,699,702],{},[85,700,701],{},"Run Docker with root privileges",": running Docker normally, but allow the connection of the gitlab.com runner via ssh and handle secrets there.",[53,704,705,706,155],{},"This is always the same dilemma: ",[85,707,708],{},"Security vs. Convenience",[53,710,711,712,717],{},"Giving a user access to the Docker socket is generally considered a security risk, as it allows to execute\narbitrary commands with elevated privileges. For more details on the security risks see the\n",[66,713,716],{"href":714,"rel":715},"https:\u002F\u002Fdocs.docker.com\u002Fengine\u002Fsecurity\u002F#docker-daemon-attack-surface",[70],"Docker documentation",".\nIf the GitLab instance or the runner is compromised, the attacker has a path to the host in this case.",[53,719,720,723,724,727,728,731],{},[85,721,722],{},"Why I chose it anyway:"," For a homelab, I prioritized ",[85,725,726],{},"minimizing the network attack surface",".\nBy using a local shell runner, the server doesn't need to expose SSH to the wider network or manage external credentials.\nThe runner stays ",[80,729,730],{},"inside the house",", pulling instructions down from GitLab rather than having GitLab push them into my server.\nIt’s a calculated risk: I traded \"Internal Privilege Escalation Risk\" for \"External Network Exposure Risk.\"",[90,733,735],{"id":734},"gitlab-runners","Gitlab Runners",[53,737,738],{},"To bridge the gap between a \"Merge\" button on GitLab.com and your local terminal, you need a GitLab Runner acting\nas a local agent. Since we decided on a shell-based approach to trigger our Makefile, here is how to get it running:",[53,740,741,742,747],{},"First, install the runner on your server following the official ",[66,743,746],{"href":744,"rel":745},"https:\u002F\u002Fdocs.gitlab.com\u002Frunner\u002Finstall\u002F",[70],"GitLab documentation",".\nOnce installed, you need to link it to your project:",[184,749,753],{"className":750,"code":751,"language":752,"meta":189,"style":189},"language-Bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","sudo gitlab-runner register\n","Bash",[57,754,755],{"__ignoreMap":189},[193,756,757],{"class":195,"line":196},[193,758,751],{},[53,760,761],{},"During the prompt:",[105,763,764,772,775],{},[108,765,766,767,771],{},"GitLab Instance URL: this is ",[66,768,769],{"href":769,"rel":770},"https:\u002F\u002Fgitlab.com\u002F",[70]," (unless you are self-hosting GitLab).",[108,773,774],{},"Registration Token: Grab this from your project under Settings > CICD > Runners.",[108,776,777],{},"Executor: I chose shell. This allows the runner to execute commands directly on the host's terminal.",[53,779,780],{},"By default, the runner creates a system user named gitlab-runner. For our Makefile to work without manual intervention,\nthis user needs specific permissions. To allow the runner to manage containers without sudo, we need to add it to the docker group:",[184,782,784],{"className":750,"code":783,"language":752,"meta":189,"style":189},"sudo usermod -aG docker gitlab-runner\n",[57,785,786],{"__ignoreMap":189},[193,787,788],{"class":195,"line":196},[193,789,783],{},[53,791,792,793,796],{},"This is the ",[80,794,795],{},"unsafe"," decision we discussed earlier.",[53,798,799],{},"The runner also needs a workspace. While GitLab CI usually clones the repo into a temporary build folder, I also wanted\nto be able to manually deploy, just in case. I am managing the local git repository into my home folder, so I needed to grant\nsome access permission to the gitlab-runner user to my directories. This allows the runner to clone the repo and run the make commands.",[90,801,803],{"id":802},"the-workflow-in-action","The Workflow in Action",[53,805,806],{},"Now, the \"New Version\" flow looks like this:",[53,808,809],{},[159,810],{"alt":811,"src":812,"title":813},"GitOps Workflow","\u002Fposts\u002Fhomelab_automation\u002Fgitops_workflow.png","My *GitOps* workflow.",[675,815,816,822,829,834,845],{},[108,817,818,821],{},[85,819,820],{},"Renovate"," detects an update and opens a GitLab MR.",[108,823,824,825,828],{},"I review the ",[85,826,827],{},"Changelog"," directly in the MR description.",[108,830,831,832,155],{},"I click ",[85,833,154],{},[108,835,836,837,840,841,844],{},"The ",[85,838,839],{},"Local Shell Runner"," triggers a job that runs ",[57,842,843],{},"docker compose pull && docker compose up -d"," via make commands.",[108,846,847],{},"Everything is updated in seconds, with a full audit trail in Git.",[48,849,851],{"id":850},"conclusion","Conclusion",[53,853,854,855,858,859,861],{},"Moving to this setup felt a bit like ",[80,856,857],{},"growing up",". It’s not just about the automation; it’s about the peace of mind that comes with\na controlled process. The ability to roll back changes quickly and easily is invaluable.\nIf you are tired of your homelab breaking behind your back, stop using ",[57,860,63],{}," and start building a pipeline.\nYour future self (who just wants things to work) will thank you.",[53,863,864],{},"I'll definitely have a look at the docker executor gitlab runners. It could be the missing piece that ensures the complete\nsecurity of my setup. Stay tuned !",[866,867,868],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"title":189,"searchDepth":203,"depth":203,"links":870},[871,876,882],{"id":50,"depth":203,"text":51,"children":872},[873,874,875],{"id":92,"depth":231,"text":93},{"id":132,"depth":231,"text":133},{"id":166,"depth":231,"text":167},{"id":526,"depth":203,"text":527,"children":877},[878,879,880,881],{"id":533,"depth":231,"text":534},{"id":650,"depth":231,"text":651},{"id":734,"depth":231,"text":735},{"id":802,"depth":231,"text":803},{"id":850,"depth":203,"text":851},"2026-04-04","It seems everyone building a homelab goes through this phase. This is my take on homelab automation, extensively using gitlab CI\u002FCD powers and Renovate bot.","md","\u002Fposts\u002Fhomelab_automation\u002Ffeatured.svg",{},{"title":18,"description":884},[890,891,892,893,894,895,896,820],"Gitlab","Automation","Homelab","Docker","CI\u002FCD","GitOps","Docker-compose","j5cCcaI9Os-pcBkw7Mnwhoq8kZwda5lVwnR2RqqKGhw",[899,901],{"title":10,"path":11,"stem":12,"description":900,"children":-1},"Agentic AI and MCP are the new thing in 2025. I figured it's time to try them out and share the results. Witness the rise of my AI Agent, with Web Search, Data Analysis and Knowledge Graph enhanced RAG.",{"title":38,"path":39,"stem":40,"description":902,"children":-1},"A while back, I decided to learn some JavaScript to go along my improvements on the FastAPI backend side. I started a few things, never finished. But trecently I got back into it and the result are worth sharing.",1777387548248]