[{"content":"","date":"20 June 2024","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"20 June 2024","externalUrl":null,"permalink":"/tags/eink/","section":"Tags","summary":"","title":"EInk","type":"tags"},{"content":"I love taking notes. I put my thoughts down on books, articles and as part of learning any topic. I usually keep everything in Obsidian and have set it up to ingest notes from a number of sources, but I\u0026rsquo;m limited to taking those notes and interacting with Obsidian through a laptop, phone or full fat tablet. What if there\u0026rsquo;s another way? What if I can get a pen on paper experience, but have all the benefits of having those notes automatically digitised and sorted into a usable workflow. eInk tablets promise this, but they fail to deliver.\nThe Current Landscape When looking for an eInk tablet, there seems to be three main players out there:\nOnyx Ratta reMarkable Each vendor provides a decent hardware platform with users reporting a great paper-like feel for taking notes. When deciding on a tablet, I don\u0026rsquo;t think hardware is the issue (mostly).\nWhy eInk Tablets are not the Notes Panacea When I think about what I would want as a notes taking device, the biggest requirements come from the software stack. At the end of the day, it\u0026rsquo;s the software that will actually make your notes usable. Many of the reviewers I\u0026rsquo;ve seen for these devices concentrate far too much on the hardware and not enough on the workflow making it seem that the main use case for these devices is to makes notes and regard them as the final output. I disagree, notes I take are transformed and used in a wider context for either building a knowledge repository or generating other outputs (such as documents, presentations, etc\u0026hellip;).\nIssues With Current eInk Tablets The issues that are stopping me going the eInk route are mostly dependent on workflow. What I would like to do is:\nTake handwritten notes and have them easily accessible from other devices Take notes on eBooks and be able to export notes and entire noted pages Add comments to web pages and export those marked up web pages So What\u0026rsquo;s stopping me?\nData Ingestion How do you get data onto your eInk tablet? Starting from a blank page and taking tome handwritten notes is pretty self explanatory and all devices support this. It\u0026rsquo;s an imitation of a workflow you would use with pen and paper, however the intrigue with these devices is to be able to do more than what you can do with a paper notebook.\neInk notes seem to rely on the PDF document standard, so anything you want to do with data should be in a PDF. This means with all three vendors, there is no easy way to take a web page and mark it up with comments. Even if you do create some convoluted workflow with third party applications to somehow create a PDF of a web page and sync it to the tablet, getting those notes off the tablet in a usable way is still a challenge (more on that in the next section).\nThe same point can be made for eBooks. People that consume eBooks would love to be able to highlight, take notes and generally doodle on a page and have those notes usable. With all three vendors, you can upload ebooks to them (usually manually) but the note taking experience is janky at best.\nThe vendors could take the approach that building all of that functionality into their software is far too much effort (and I think they would be right), however, these are problems that can be solved with openness. There are a multitude of applications out there for interacting with web pages (I use wallabag, but there\u0026rsquo;s pocket and linkwarden, etc\u0026hellip;). For eBooks, there\u0026rsquo;s the venerable Calibre. If the vendors took a more open approach to data access, the community would take alot of the heavy lifting in integrating with these apps and you would get the additional benefit that people already use these apps and have good workflows implemented out of the box. I am aware that with Onyx and Ratta, you install these apps onto the tablet, however they are not integrated at all into the pen input / note taking functionality of the tablets so they are all but useless.\nGetting Access to Notes All three vendors would prefer you to either use their own cloud sync service or connect the device over USB to get data on and off (with their own desktop application to actually read the notes). This is pretty poor in terms of integration with other applications, devices and your own personal workflow.\nOnyx and Ratta make concessions in this aspect by allowing you to use 3rd party file sharing services such as Dropbox and OneDrive or to install another app such as syncthing. Onyx has introduced the ability to sync with WebDav but reports are that it\u0026rsquo;s not very configurable and it doesn\u0026rsquo;t work very well.\nTaking aside the fact that I think there should be an option to sync all of your data to your own infrastructure from a privacy, cost and continuity perspective(self host all the things!), once you get your notes off the device, there\u0026rsquo;s not much you can do with them\u0026hellip;\nGetting Access to Data In Notes All the vendors have adopted a proprietary notes format. This means that even iof you do get access to your notes on other devices you are limited to interacting with them through the vendor applications, which do not gracefully integrate with any other application you may want to use. You are left with trying to deal with PDFs which are not the most editable or transformable file formats.\nOne awesome dev has developed a plugin to be able to view supernote files in obsidian, but this is only half the battle. How do you transform the note or grab data from the note to use in other places in obsidian?\nThis is another issue that could be solved by the community if openness is embraced with the file format.\nHardware Robustness I know I mentioned that the problems with eInk tablets were all around the software stack, however there is one niggling issue on the hardware side. The eInk technology as it stands can be quite fragile. A thin glass front is required to enable the pen input functionality and this can be easily broken if you are not careful. This is a step back from pen and paper where the ony time I suffered a critical failure in my notepad, was when I dropped a glass full of water on it.\nMaking things worse is that since these devices are still considered niche, there is no market for third parties to produce more rugged accessories such as cases and screen protectors. I currently have an android tabled, phone and a windows laptop that I travel with regularly. I don\u0026rsquo;t have any worries about babying any of these devices when I\u0026rsquo;m on the road.\nConclusion I love the idea of a note taking eInk tablet, but I don\u0026rsquo;t think the technology is there yet. I would like to see vendors concentrate far more on their software stack with a view on how do people use these devices and less on the hardware side. It may be easier to differentiate yourself in the marketplace with PPI or processor specs, but we are at the point where the hardware is good enough. It\u0026rsquo;s the software that needs to be overhauled.\n","date":"20 June 2024","externalUrl":null,"permalink":"/posts/einkthoughts/","section":"Posts","summary":"Taking notes on an eInk tablet sounds like a great idea… Maybe not yet","title":"My Thoughts on eInk Tablets","type":"posts"},{"content":"","date":"20 June 2024","externalUrl":null,"permalink":"/","section":"Omar's Blog","summary":"","title":"Omar's Blog","type":"page"},{"content":"","date":"20 June 2024","externalUrl":null,"permalink":"/categories/opinions/","section":"Categories","summary":"","title":"Opinions","type":"categories"},{"content":"","date":"20 June 2024","externalUrl":null,"permalink":"/tags/tablets/","section":"Tags","summary":"","title":"Tablets","type":"tags"},{"content":"","date":"20 June 2024","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"19 June 2024","externalUrl":null,"permalink":"/tags/gear/","section":"Tags","summary":"","title":"Gear","type":"tags"},{"content":"I travel quite a bit for my job which means I end up on customer sites and in hotels rooms armed with nothing but a laptop and my bluetooth mouse (Logitech MX Anywhere 3). The problem is, I hate laptop keyboards. All laptop keyboards. I don\u0026rsquo;t mind them for short bursts but anything more than a couple of hours and I start to get antsy.\nI\u0026rsquo;ve been keeping my eyes open for a decent low profile keyboard because when you travel often, bulk is the enemy and a full sized mechanical (even a 65%) is way too big. Consensus (Reddit!) says the best keyboards in this space are the lowfree flow84 and the nuphy air v2. The major downside to both of these is that they are reported to be quite noisy which is a pretty big issue for me sine I\u0026rsquo;ll be primarily using it in open plan offices.\nWhen searching Aliexpress, I came accross the XINMENG C68. In the listing it gives an option for \u0026ldquo;Mute Axis\u0026rdquo; which means silent switches! Now, I\u0026rsquo;ve never heard of Xianmeng and so have no idea about their quality and their reliability but for the reasonable price of $40 I thought I\u0026rsquo;d give them a go.\nNow that I\u0026rsquo;ve received it, I can say I\u0026rsquo;m pretty impressed. It feels decent to type on (if a bit mushy which is normal for silent switches), there is no rattling from stabilisers, the keycaps are ok and the battery life lasts more than 2 days so far on 2.4ghz.\nThis is a 68% layout so you lose the function row and don\u0026rsquo;t even think about a numpad, but I\u0026rsquo;m ok with that. I rarely use the F-keys and hitting Fn-num to get them is fine with me. There\u0026rsquo;s also very little customisation that you can do. The switches are not hot swappable and there is no software to change bindings. This is also fine with me for a work keyboard where I\u0026rsquo;m mostly working in the office suite with a touch of VS Code if things get spicy.\nAll in all, I would recommend giving it a try.\n","date":"19 June 2024","externalUrl":null,"permalink":"/posts/travelkeyboard/","section":"Posts","summary":"I took a chance on an unknown brand low profile travel keyboard and it worked out well","title":"Low-Profile Travel Keyboard Review","type":"posts"},{"content":"","date":"19 June 2024","externalUrl":null,"permalink":"/categories/reviews/","section":"Categories","summary":"","title":"Reviews","type":"categories"},{"content":"","date":"5 October 2023","externalUrl":null,"permalink":"/tags/automation/","section":"Tags","summary":"","title":"Automation","type":"tags"},{"content":"","date":"5 October 2023","externalUrl":null,"permalink":"/tags/cicd/","section":"Tags","summary":"","title":"CICD","type":"tags"},{"content":"","date":"5 October 2023","externalUrl":null,"permalink":"/tags/docker/","section":"Tags","summary":"","title":"Docker","type":"tags"},{"content":"","date":"5 October 2023","externalUrl":null,"permalink":"/tags/gitea/","section":"Tags","summary":"","title":"Gitea","type":"tags"},{"content":"","date":"5 October 2023","externalUrl":null,"permalink":"/categories/homelab/","section":"Categories","summary":"","title":"Homelab","type":"categories"},{"content":"","date":"5 October 2023","externalUrl":null,"permalink":"/tags/portainer/","section":"Tags","summary":"","title":"Portainer","type":"tags"},{"content":"I mostly self-host my software on docker. I have two ubuntu servers that I use as docker servers mostly for resiliency (I\u0026rsquo;m thinking of going to docker swarm, but I\u0026rsquo;ve not got up the motivation to dig into that yet).\nThe Old Way I used to manage these services through a massive docker-compose.yml file on each server. Before I streamlined things, the compose file was 1885 lines for about 70 containers. Anything I wanted to do, I would ssh into the machine and make the needed changes. The basic flow would be:\nflowchart LR; A[ssh docker host]--\u003eB[cd to Docker directory]--\u003eC[docker compose actions] I would keep the docker-compose.yml file in a git repository but that was only for versioning reasons. I posted a previous series on finding a CICD solution for my homelab and settled on woodpecker CI so I wanted to make use of this for my container environment.\nThe Plan I was already running portainer and they released an update to their business version that let you trigger changes through an api as well as trigger a redeploy of your containers through a webhook.\nWhat I wanted to achieve was to be able to:\nClone a repository containing the relevant docker-compose.yml file Make the required changes to my containers then commit and push the updated file back to the Gitea repository Have Gitea trigger a webhook to Portainer Have portainer make the requisite changes to my containers %%{init: {'themeVariables': { 'edgeLabelBackground': 'white'}}}%% flowchart LR; home[Local Directory] gitea[(Gitea Repository)] portainer[Portainer Instance] gitea--\u003e|1. git clone|home--\u003e|2. commit \u0026 push changes|gitea--\u003e|3. webhook|portainer In order for portainer to be able to control my containers, I would also have to create them through portainer. Luckily portainer supports reading a docker-compose.yml and .env file from a git repository to form a stack so there would not be much effort required there. However, it would be far easier to manage if I split my huge compose file into multiple more manageable files, each in their own repository.\nImplementation I started by going through all of the containers in my original compose file and splitting them out into meaningful separate compose stacks and saving them into their own Gitea repositories. I then had to configure portainer to be triggered from a webhook and Gitea to send a webhook at the time of a commit.\nThe configuration you need to do is shown in the flowchart below:\nMake sure you have your compose file and .env in a gitea repository Open Portainer and go to your docker instance and add a stack Make sure the stack is pulling from the gitea repository Copy the url for with webhook Create a webhook in your gitea repository with the portainer webhook URL Now whenever you modify your compose file and commit it to the gitea repository, portainer will redeploy the containers in that compose file with whatever changes you made.\nOther Interesting Things You can also call this webhook using curl - curl --request POST \u0026quot;\u0026lt;WEBHOOK URL\u0026gt;\u0026quot;. This means you can deploy changes from scripts, and in my case, from WoodpeckerCI pipelines.\nIn portainer, you can go to an individual container, enable gitops and get a webhook for that individual container rather than the whole compose file. I use this with woodpecker to manipulate my development environments when I need them.\n","date":"5 October 2023","externalUrl":null,"permalink":"/posts/portainercicd/","section":"Posts","summary":"Manipulating docker containers through portainer webhooks","title":"Using Portainer for a Docker CICD pipeline","type":"posts"},{"content":"","date":"5 October 2023","externalUrl":null,"permalink":"/tags/woodpeckerci/","section":"Tags","summary":"","title":"WoodpeckerCI","type":"tags"},{"content":"","date":"27 August 2023","externalUrl":null,"permalink":"/categories/blog-admin/","section":"Categories","summary":"","title":"Blog Admin","type":"categories"},{"content":"","date":"27 August 2023","externalUrl":null,"permalink":"/tags/fediverse/","section":"Tags","summary":"","title":"Fediverse","type":"tags"},{"content":"I\u0026rsquo;ve been wanting to show comments on my blog for a while now, but I didn\u0026rsquo;t want to throw some heavyweight commenting system onto the site and I also didn\u0026rsquo;t want to use anything that would claim ownership of any of the comments. My feeling is that there are far smarter people out there and the comments they make to my posts would be interesting and add mopre context to the posts themselves.\nSince I\u0026rsquo;m a big fan of the Fediverse and post links to my new posts there, it made sense to use that as my commenting system.\nThe Plan Since I\u0026rsquo;m not so good at web development, I needed the path of least resistance (the lazy way). There\u0026rsquo;s already been alot of good work done on the Indieweb and there was code out there to display webmentions on a hugo blog. This means the only thing I needed to do was to get fediverse comments and reactions to show up as webmentions and viola!\nThe path of the comments would be:\nI post a link to my post with my fediverse account Replys to that post would be turned into a webmention. For this I used a lovely free service called brid.gy Now the webmentions need to be collected somewhere (because they can be generated through other mechanisms). Another lovely free service is webmention.io To pull and display the webmentions from webmention.io I found some javascript that did the job The Build Webmention.io The first step is to sign up to webmention.io. For this you need to put two lines of code into the header of your blog.\n\u0026lt;link href=\u0026#34;mailto:\u0026lt;YOUR EMAIL\u0026gt;\u0026#34; rel=\u0026#34;me\u0026#34;\u0026gt; Putting this in your header and putting your email address between the \u0026lt;\u0026gt; will give webmention.io a way to authenticate you and prove you own the domain. They will send you an email and you will need to click on a link to validate yourself.\n\u0026lt;link rel=\u0026#34;webmention\u0026#34; href=\u0026#34;https://webmention.io/\u0026lt;USERNAME\u0026gt;/webmention\u0026#34; /\u0026gt; This line enables webmention.io to know that your site is accepting webmentions. They will provide you a username so that they can determine which webmentions are yours.\nBrid.gy With bridge, you sign up with any accounts you want webmentions from. I chose the mastodon one (which supported my Akkoma instance). You can chose to treat your blog as a seperate entity on the fediverse, but I instead opted to go for associating the blog with my account. That makes it easier for me to reply to comments and have them show up on my posts too. To make sure brid.gy works, you need to have a link to your site in your fediverse profile. Hugo Everyone\u0026rsquo;s hugo theme works differently so these instructions may not work for you, but I hope they provide a good starting point. To start you need to download webmention.min.js and put it into the /static/js/ directory of your blog. That pretty much enables your site to receive webmentions. You just need a way to display them! Blowfish is the theme I use and I can add a comment section to each of my posts by creating a comments.html file in the /layouts/partials directory of my blog. In that file, the only code I need is:\n\u0026lt;div id=\u0026#34;webmentions\u0026#34; \u0026gt; \u0026lt;/div\u0026gt; {{- $js := resources.GetRemote \u0026#34;https://omaramin.me/js/webmention.min.js\u0026#34; | js.Build (dict \u0026#34;minify\u0026#34; true) -}} \u0026lt;script type=\u0026#39;text/javascript\u0026#39; data-page-url=\u0026#34;https://omaramin.me{{ .RelPermalink}}\u0026#34;\u0026gt; {{ $js.Content | safeJS }} \u0026lt;/script\u0026gt; The \u0026lt;script\u0026gt; block looks complicated, but that is because I needed to pass it into my theme safely. You might be able to get away with only using the following:\n\u0026lt;div id=\u0026#34;webmentions\u0026#34; \u0026gt;\u0026lt;/div\u0026gt; \u0026lt;script src=\u0026#34;/path/to/webmention.min.js\u0026#34; async\u0026gt;\u0026lt;/script\u0026gt; In order to make the comments look nice, you will need some CSS to format them properly. In my theme, I just need to create a custom.css file in the /assets/css/ directory of my blog and put the code there.\n#webmentions .comments { overflow-x: hidden; overflow-y: hidden; font-size: 80%; max-width: 100%; } #webmentions h2 { font-size: larger; margin: 0; padding: 2px; } #webmentions .reacts img { margin: 5px -1ex 1px 0; } #webmentions img.missing { background: white; border: dashed black 1px; } #webmentions ul { list-style-type: none; margin: 2; padding: 4px; } #webmentions li { text-indent: -1em; padding-left: 1em; } #webmentions a.reaction { position: relative; text-decoration: none; margin-right: 0; letter-spacing: -1ex; margin-right: 2ex; } #webmentions a.reaction img { max-height: 2em; width: auto; margin-right: -1ex; border-radius: 25%; display: inline; } #webmentions a.reaction sub { font-size: 50%; } #webmentions .comments li { white-space: normal; text-overflow: ellipsis; overflow: hidden; margin-bottom: 1em; } #webmentions .comments li .text { color: #a9b3b8; font-style: italic; text-decoration: none; } #webmentions .comments li .name { color: #111; } Just make sure id=\u0026quot;webmentions\u0026quot; in your comments.html matches the #webmentions in your CSS.\n","date":"27 August 2023","externalUrl":null,"permalink":"/posts/fediversecomments/","section":"Posts","summary":"Indieweb and static sites are not an easy combination. I use webmentions to get commenting on my blog","title":"Getting Fediverse Comments on My Blog Posts","type":"posts"},{"content":"","date":"27 August 2023","externalUrl":null,"permalink":"/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"","date":"27 August 2023","externalUrl":null,"permalink":"/tags/indieweb/","section":"Tags","summary":"","title":"Indieweb","type":"tags"},{"content":"","date":"7 August 2023","externalUrl":null,"permalink":"/tags/audio/","section":"Tags","summary":"","title":"Audio","type":"tags"},{"content":"","date":"7 August 2023","externalUrl":null,"permalink":"/categories/making/","section":"Categories","summary":"","title":"Making","type":"categories"},{"content":"","date":"7 August 2023","externalUrl":null,"permalink":"/series/pi-streaming/","section":"Series","summary":"","title":"Pi Streaming","type":"series"},{"content":"","date":"7 August 2023","externalUrl":null,"permalink":"/tags/plexamp/","section":"Tags","summary":"","title":"Plexamp","type":"tags"},{"content":"","date":"7 August 2023","externalUrl":null,"permalink":"/tags/raspberrypi/","section":"Tags","summary":"","title":"Raspberrypi","type":"tags"},{"content":"","date":"7 August 2023","externalUrl":null,"permalink":"/tags/selfhosted/","section":"Tags","summary":"","title":"Selfhosted","type":"tags"},{"content":"","date":"7 August 2023","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"I want a nice device that can sit on my desk and control what I\u0026rsquo;m playing through plexamp and moode audio on all my connected audio systems. I also want to be able to move this device around with me because I have devices in different rooms. A cheap tablet like a Fire HD on a classy stand would probably do the trick, but I don\u0026rsquo;t have one of those lying around (and they don\u0026rsquo;t ship to my country) and what if I wanted to install more substantial software on the device to do\u0026hellip; THINGS! Raspberry Pi to the rescue.\nThe Plan I have a raspberry pi, touchscreen and some other bits and bobs that I think I can cobble together to make a touch screen device to basically show the web pages for plexamp and moode audio and with that I have a \u0026ldquo;Now Playing\u0026rdquo; display as well as the ability to control what is being cast to any of my pi streamers.\nThe Parts Raspberry pi 4b with 4 gig of ram LiPo battery hat for the pi Official Raspberry pi 7in touchscreen SmartPi Touch Pro case - the large one to accomodate hats SD card for the OS a short USB-A to Micro-USB cable to connect the battery hat to the display for power 4 M2.5 standoffs and screws for mounting (I didn\u0026rsquo;t measure the length before I closed everything up I\u0026rsquo;m afraid) LiPo Battery Hat This is a cool little hat I found on Aliexpress with a decently sized battery for not much money. It supports USB-C fast charging and can deliver power via the GPIO pins and USB-A output port.\nSmartpi Touch Pro Case This case is pretty well made, has a ton of space to hold the raspberry pi and multiple hats, supports a fan and a camera if you want too.\nThe Build Use the raspberry pi imager to put 64-bit raspbian onto the SD card and slot it into the pi. Once the pi is mounted in the case, you won\u0026rsquo;t have access to the SD card slot to do it later. Ask me how I know. Plug in the battery to the lipo hat and turn it off by double clicking the standby button Assemble the pi and hat into the case following the very good instructions Here\u0026rsquo;s what it should look like before you connect the fan and close up the back. Notice the short micro-USB cable connecting the lipo hat to the screen. Because of this, I used the GPIO pins to power the pi itself (The red and black wires) Turn on the lipo hat before closing up the back and check you can boot into a desktop Configuring the OS for Quality of Life Out of the box, using Raspbian OS with a touch screen is a pain because there\u0026rsquo;s no on screen keyboard and the touch display does not support all mouse actions through a touch interface. I used the instructions here to \u0026ldquo;Install a Virtual Keyboard\u0026rdquo; and \u0026ldquo;Right-Click\u0026rdquo;.\nThe screen kept going to sleep, which is bad for a display I want always on to show what\u0026rsquo;s playing. I disabled this by following these instructions\nAfter running a battery test I got about an hour and a half out of this setup with the screen at full brightness and constantly on. This was ok, but nowhere near the run-time I wanted. I lowered the screen brightness by running\nsudo sh -c \u0026quot;echo 50 \u0026gt; /sys/class/backlight/10-0045/brightness\u0026quot;\nI haven\u0026rsquo;t re-run the test yet because there\u0026rsquo;s some things I might be changing.\nHere\u0026rsquo;s the result: The Issues The major issue I have with the hardware setup is that every time I plug in, or unplug, the USB-C cable for charging, the pi reboots. I believe the hat cuts the power to the GPIO pins when it detects a change in charging states. I need to experiment further to see if there is a fix. I suspect that if I seat the hat on the pi, taking up all the GPIO pins, it might resolve this.\nI suspect battery life will still be too short, even with lowered screen brightness. I may be asking too much of 3000mah.\nRaspbian OS is a pain to use with the touch screen. It\u0026rsquo;s fine for the plexamp and moode audio interfaces over the web but if I install software locally, I think I\u0026rsquo;m going to get frustrated with it all.\nShould You Build One? Probably not.\nIt was a fun little project but ultimately, as I mentioned at the beginning, a tablet and stand gets you to the same place with better battery life and easier to use touch interface.\nWhere this may be useful is for people without an existing DAC in their audio setup. If you replace the lipo hat with a DAC hat and make this a permanent part of your audio stack, I think you would find it much more usable.\n","date":"7 August 2023","externalUrl":null,"permalink":"/posts/piscreen/","section":"Posts","summary":"I make a portable device to keep track of the music I’m playing that might be pointless… because, you know, tablets exist","title":"Touchscreen Interface to My Audio System","type":"posts"},{"content":"","date":"7 August 2023","externalUrl":null,"permalink":"/tags/moodeaudio/","section":"Tags","summary":"","title":"Moodeaudio","type":"tags"},{"content":"I have a few audio setups scattered around the house. The main ones I use are in my office, living room and kitchen. They\u0026rsquo;re all set up differently (e.g. my office has seperate amp and DAC, the living room is a sound-bar and the kitchen is a bluetooth speaker) but it would be nice to stream my digital music collection to each of them.\nMy music collection is all on Plex and I\u0026rsquo;ve happily used Plexamp on my phone when I\u0026rsquo;m out and about. I wanted the ability to play music on my office system so I gave a Wiim Mini a try. It\u0026rsquo;s an inexpensive little streaming box but it only really supports big subscription based streaming services like Spotify and Amazon Music. It did have the ability to act as a uPnP/DLNA endpoint though. The only problem, Plexamp does not support casting to uPnP/DLNA and does not have any plans to support this.\nEnter Symfonium, a nifty Android app that can connect to plex on the backend and cast to the Wiim mini via uPnp/DLNA. It even syncs playlists back and forth and works really well with gapless playback. I was just missing some of the nicer features provided by PLexamp - better playlist management and the DJ features at the top of my list. I would also really like to take the AI based playlist generation for a spin.\nThe Plexamp Way The easiest way forward for me would have been for Plexamp to support uPnP/DLNA casting and I could just use the standard app to connect to a bunch of Wiim minis scattered around my house. This however is not the Plexamp way:\nPlexamp is on the Fediverse (Awesome!) so I had a brief exchange with them about my set up and was told that the focus of the product is through their headless plexamp application. I\u0026rsquo;m fine with that, they can\u0026rsquo;t be expected to support every setup out there but they have a roadmap and that\u0026rsquo;s their focus, either I get on board or use something different.\nThe Plan I decided to fire up a raspberry pi and see if I could build a streamer to replace the Wiim. The build list is:\nRaspberry Pi 4b with 4gb of ram (probably overkill but it\u0026rsquo;s what I had) SD card for the OS Official raspberry pi case (again, had it lying around) USB-A to USB-B cable (I don\u0026rsquo;t need a USB DAC or DAC-hat as I have a Topping DX3 Pro+ in my system which takes a USB input for the DAC) USB-A to USB-C cable for the power to the pi (I have a USB hub that can deliver the power needed) As you can see it\u0026rsquo;s not an overly complex or expensive build.\nOS Choices The easiest way forward would be to flash raspbian lite onto the SD card, boot it up and install headless plexamp but where would the fun be? That setup would lose me alot of the functionality from Wiim such as airplay, bluetooth, radio stations and more. The great thing about the FOSS world is that we have choices for a specialised OS:\nraspbian lite - Boring and lacks features Volumio - This is the default choice for alot of people setting up streamers. It\u0026rsquo;s feature rich, supports many of the major streaming services (as well as local files and NAS) and seems pretty easy to set up. However, it\u0026rsquo;s starting to paywall a bunch of features with a subscription which is usually a death-knell of a project for me. Also, may of those paywalled features are provided with plaxamp and my lifetime plexpass. Moode Audio - The FOSS choice. Supports airplay, bluetooth, radio, a couple of the major streamers (as well as local files and NAS) and lets you tweak the audio to your audiophile heart\u0026rsquo;s content. Mopidy - The tinkerer\u0026rsquo;s choice. It has much of the same features as Moode but alot of the functionality is in the form of extensions that you would need to install. This means that new features can be added much faster, but there\u0026rsquo;s more to keep track of from a maintenance point of view. I installed both Moode and Mopidy and the whole process felt more smooth with Moode so that\u0026rsquo;s what I went for.\nThe Build The build process it\u0026rsquo;s pretty straight forward:\nUse the raspberry pi imager to put the moode audio player OS image onto an SD card (Choose OS - Media PLayer OS - Moode Audio OS). Make sure to customise the image with host name, wifi details and enable ssh. Boot the pi and plug it into you dac I followed these setup instructions to finish configuring moode I followed these setup instructions and made sure to start it as a service (details also in the link) At this point you should be able to go to the web interfaces of both moode audio and plexamp. I set each of these applications to use my dac as the audio output (you can have a look at the configuration guides for each applications to do this) Play audio! Niggling Issues There seems to be a problem at boot where moode and plexamp step on each other\u0026rsquo;s toes. Moode seems to hang until I stop plexamp, restart the moode server and restart plexamp. I\u0026rsquo;ve tried to change the /lib/systemd/system/plexamp.service to delay plexamp\u0026rsquo;s boot process with: ExecStartPre=/bin/sleep 30 ExecStart=/home/pi/.nvm/versions/node/v16.18.1/bin/node /home/pi/plexamp/js/index.js It hasn\u0026rsquo;t resolved the issue so I\u0026rsquo;ll need to continue investigating\nPodcasts! I currently have no way of streaming podcasts. The most important past of a podcast manager is to manage podcasts. I need a service to download the latest episodes, keep my listening synch\u0026rsquo;d over the devices I use to listen and delete them when I listen to them. Ideally any client I sue should be able to download copies of the podcast locally and it would be great to have some smart playlist features to allow me to set up a custom order to listening to the podcasts. I\u0026rsquo;ve looked at Gpodder, podgrab, podfetch and audiobookshelf and none of them let me do that while being able to stream to the pi. I guess I\u0026rsquo;ll have to keep podcasts on my phone and connect via bluetooth. ","date":"7 August 2023","externalUrl":null,"permalink":"/posts/pistreamer/","section":"Posts","summary":"My music is in Plex, but getting that music to play on my audio equipment around the house was turning into a brittle encantation of uPnP, DLNA and prayers. Let’s see what Plex’s supported approach looks like on a Pi","title":"Streaming music with Raspberry Pi, Plexamp \u0026 Moode Audio","type":"posts"},{"content":"","date":"20 March 2023","externalUrl":null,"permalink":"/categories/entertainment/","section":"Categories","summary":"","title":"Entertainment","type":"categories"},{"content":"","date":"20 March 2023","externalUrl":null,"permalink":"/tags/moviereview/","section":"Tags","summary":"","title":"MovieReview","type":"tags"},{"content":"","date":"20 March 2023","externalUrl":null,"permalink":"/tags/movies/","section":"Tags","summary":"","title":"Movies","type":"tags"},{"content":" Post Contains Major Spoilers This may seem an odd title to be my first movie review, but it\u0026rsquo;s the last one I watched so here we go\u0026hellip;\nI usually quite enjoy a Guy Ritchie movie as a bit of a fun distraction from the world. You\u0026rsquo;re usually guaranteed some slick dialog, great scenery and cool costumes. Jason Statham and Hugh Grant should have guaranteed a bit of \u0026ldquo;banter\u0026rdquo; and Aubrey Plaza the snark. Oh boy was I wrong!\nThe premise behind the movie is bad guys have stolen a mysterious thing and Orson Furtune (Jason Statham) is part of a team of contractors that governments hire to solve problems such as this. Think cockney James Bond with a posse and a preferance for face punching rather than quips. The movie belabours the point that Orson plays outside the rules, he uses government money to drink fine wine and fly in private jets.\nAs the movie progresses, we learn there is a rival group of contractors (Mike and his team) and the mysterious thing is going to be sold by the big bad that is the arms dealing Greg (Hugh Grant). There are a bunch of forgettable scenes at Madrid airport where it\u0026rsquo;s emphasized that Mike is bad because he scared an old man to death to get a hard drive (it\u0026rsquo;s very important that you understand Mike is bad and not at all foreshadowing) and Sarah (Aubrey Plaza) is sassy because she wields sarcasm.\nNow we get to the main plot line. It turns out that the hard disk was going to Greg and in order to get into Greg\u0026rsquo;s party, Orson and his team need Greg\u0026rsquo;s favourite movie star as a honey pot - Enter Danny (Josh Hartnett). More forgettable scenes on a boat where Orson punches someone alot, Sarah waves gadgets around and Danny plays a caricature of a movie star. Oh, we\u0026rsquo;re introduced to a couple of tech bros here and we know they are probably bad because they are rich and one has a silly haircut (no this can\u0026rsquo;t be more foreshadowing can it?).\nGreg\u0026rsquo;s love for Danny gets the team an invite to his Turkish lair where there\u0026rsquo;s a few magical hacking scenes and a henchman chase through the streets leading us to the enevitably dull situation where Orson masquerades as a bad guy to get hold of the hard disk only to be gazumped by Mike in a violent shoot out. Wait! Mike is bad! He\u0026rsquo;s stolen the hard disk for himself! I would never have guessed.\nIn a team-up of the ages, Greg will help the team get the hard disk back from the real big bad\u0026hellip; TECH BROS! How best to do this? Shoot your way in, kill everyone and walk away with macguffin! Oh, the hard disk is an AI that can control everything in the world and could never be copied\u0026hellip; except they did earlier in the movie but please don\u0026rsquo;t point out plot holes. If you\u0026rsquo;ve ever used chatGPT, you know how realistic an advanced AI threat is.\nWhere did the movie go wrong? Well everywhere. The script is bad\u0026hellip; really bad. This was said with all seriousness:\n\u0026lt;Orson\u0026gt; Probably you’re the new John.\n\u0026lt;Sarah\u0026gt; Not meaning a toilet or a prostitute’s client I take it. Please don’t pee on me, I don’t do that anymore.\n\u0026lt;Orson\u0026gt; John didn’t have a personality. I liked John.\nThe acting was bad\u0026hellip; Jason Statham basically plays every character he\u0026rsquo;s every played in every other movie, but I was expecting a bit more from Hugh Grant. It\u0026rsquo;s like the director asked Hugh to play the Kray twins, but in the style of Jack Sparrow. Aubrey Plaza plays generic sassy American and Josh Hartnett is trying to be Tom Cruise playing Brad Pitt. Every other character is forgettable and the entire movie runs like a one dimensional money grab from people that loved Lock Stock and Two Smoking Barrels.\n","date":"20 March 2023","externalUrl":null,"permalink":"/posts/operationfortune/","section":"Posts","summary":"A review of Operation Fortune: Ruse de guerre with major spoilers… Let me save you some time, nope nope nope","title":"Operation Fortune: Ruse de guerre","type":"posts"},{"content":"","date":"5 March 2023","externalUrl":null,"permalink":"/tags/foss/","section":"Tags","summary":"","title":"FOSS","type":"tags"},{"content":"I don\u0026rsquo;t know what sickness I have\u0026hellip; but I need to tinker. I can\u0026rsquo;t help myself. I wrote a whole bunch of posts in my Quest For CICD series on choosing a CICD platform and I used the example of my resume site which used to be made with Jekyll.\nWell, I had a niggling problem, I couldn\u0026rsquo;t get a PDF version of my Jekyll site to look good. So I hunted around for a bit and now I have a theme that looks good on screen and in PDF\u0026hellip; and it\u0026rsquo;s in Hugo. The other benefit to the new site, is that I found a docker image that will auto generate the PDF file. You can take a look at my CV here.\nWith no further ado, here\u0026rsquo;s the .woodpecker.yml:\npipeline: generatepdf: image: alpine/curl commands: - curl --request POST \u0026#39;http://gotenburg:3000/forms/chromium/convert/url\u0026#39; --form \u0026#39;url=\u0026#34;http://hugo:1313/\u0026#34;\u0026#39; --form \u0026#39;paperWidth=\u0026#34;8.27\u0026#34;\u0026#39; --form \u0026#39;paperHeight=\u0026#34;11.7\u0026#34;\u0026#39; -o ./omaraminCV.pdf builddocker: image: plugins/docker settings: registry: \u0026lt;GITEA_URL\u0026gt; username: from_secret: giteauser password: from_secret: giteapass repo: \u0026lt;GITEA_URL\u0026gt;/omaramincv tags: latest # auto_tag: true dockerfile: Dockerfile restartcontainer: image: appleboy/drone-ssh settings: host: - dockerserver username: from_secret: dockerserversshuser password: from_secret: dockerserversshpass port: 22 command_timeout: 2m script: - cd Docker - docker compose pull omaramincv - docker compose up -d omaramincv mail: image: drillster/drone-email settings: host: smtp.gmail.com username: from_secret: email password: from_secret: emailpass from: \u0026lt;WOODPECKER_EMAIL\u0026gt; when: status: [ success, failure ] services: hugo: image: klakegg/hugo:0.107.0-ext commands: - hugo server gotenburg: image: gotenberg/gotenberg:7 branches: main I\u0026rsquo;ve replaced some confidential info with \u0026lt;\u0026gt; blocks. Compared to the steps for the Jekyll site I am using a few more fun features of WoodpeckerCI. The pipeline consists of the following stages:\nServices startup - One of the fun features. I can start services that persist accross the pipeline steps to be used to carry out tasks Generate the PDF of my CV site Build and push the docker image Pull and restart the site container Email the build result Services Startup services: hugo: image: klakegg/hugo:0.107.0-ext commands: - hugo server gotenburg: image: gotenberg/gotenberg:7 services in a WoodpeckerCI pipeline are images that are run after the git repository gets pulled that can provide functions based on the internal hostname that is assigned to them. Think of them like starting little servers whose services you can access from within the pipeline. They keep running in parallel to all the tasks in your pipeline.\nIn the services: block above I\u0026rsquo;ve defined 2 services:\n1. hugo This runs the klakegg/hugo:0.107.0-ext Hugo image which starts up a webserver on port 1313 that renders your site from your hugo files. Notice that I don\u0026rsquo;t have to specify the pulled repository for my site, the services: block mounts it automatically. hugo: specifies that the server can be accessable from the http://hugo hostname.\nThe commands: block defines a set of shell commands that the image runs on startup. In this case, I am telling it to start the hugo server.\n2. gotenburg This runs a cool little image - gotenberg/gotenberg:7 that provides a server that you can query to turn any URL into a PDF with headless chrome. As with hugo, the gotenburg: code means that we will access the server on http://gotenburg\nGenerate PDF generatepdf: image: alpine/curl commands: - curl --request POST \u0026#39;http://gotenburg:3000/forms/chromium/convert/url\u0026#39; --form \u0026#39;url=\u0026#34;http://hugo:1313/\u0026#34;\u0026#39; --form \u0026#39;paperWidth=\u0026#34;8.27\u0026#34;\u0026#39; --form \u0026#39;paperHeight=\u0026#34;11.7\u0026#34;\u0026#39; -o ./omaraminCV.pdf This is the start of our pipeline tasks. The first thing we do is we use the alpine/curl image to rest the gotenburg service to create a PDF out of the site that is being served by our hugo server. the PDF is saved as omaraminCV.pdf\nBuild \u0026amp; Push Docker Image Once the PDF is generated, we have all components we need to build the site. I am using a multi-stage Dockerfile to build the site into a container image.\nbuilddocker: image: plugins/docker settings: registry: \u0026lt;GITEA_URL\u0026gt; username: from_secret: giteauser password: from_secret: giteapass repo: \u0026lt;GITEA_URL\u0026gt;/omaramincv tags: latest # auto_tag: true dockerfile: Dockerfile We are using the standard docker image from WoodpeckerCI to build the Dockerfile from our repository and then push the resulting container to my Gitea container repository.\nHere is the Dockerfile that gets built. Hopefully the comments are enough to get the gist of how it works.\n# This is a multi-stage Dockerfile (build and run) # Remember to target specific version in your base image, # because you want reproducibility (in a few years you will thank me) FROM alpine:3.9 AS build ENV GLIBC_VERSION 2.35-r0 # Install Glibc RUN apk add --no-cache git openssl py-pygments libc6-compat g++ curl # The Hugo version ARG VERSION=0.110.0 ADD https://github.com/gohugoio/hugo/releases/download/v${VERSION}/hugo_extended_${VERSION}_Linux-64bit.tar.gz /hugo.tar.gz RUN tar -zxvf hugo.tar.gz # RUN echo $(pwd) RUN /hugo version # We add git to the build stage, because Hugo needs it with --enableGitInfo RUN apk add --no-cache git # The source files are copied to /site COPY . /site WORKDIR /site # And then we just run Hugo RUN /hugo --minify --enableGitInfo # stage 2 FROM nginx:1.15-alpine WORKDIR /usr/share/nginx/html/ # Clean the default public folder RUN rm -fr * .??* # Copy in default for webfinger COPY _nginx/default /etc/nginx/sites-available/default # Finally, the \u0026#34;public\u0026#34; folder generated by Hugo in the previous stage # is copied into the public fold of nginx COPY --from=build /site/public /usr/share/nginx/html COPY omaraminCV.pdf /usr/share/nginx/html/ Pull \u0026amp; Restart Container restartcontainer: image: appleboy/drone-ssh settings: host: - dockerserver username: from_secret: dockerserversshuser password: from_secret: dockerserversshpass port: 22 command_timeout: 2m script: - cd Docker - docker compose pull omaramincv - docker compose up -d omaramincv In this stage, we use the appleboy/drone-ssh image to ssh into the docker server, pull the new image and restart the container.\nEmail Build Result mail: image: drillster/drone-email settings: host: smtp.gmail.com username: from_secret: email password: from_secret: emailpass from: \u0026lt;WOODPECKER_EMAIL\u0026gt; when: status: [ success, failure ] The last stage gets run whether the pipeline is successful or fails and emails us details of the result.\n","date":"5 March 2023","externalUrl":null,"permalink":"/posts/cvtohugo/","section":"Posts","summary":"Almost immediately after I finished my last post on automating my Jekyll CV site with WoodpeckerCI, I changed over to a Hugo site… and automated it","title":"I Couldn't Leave Well Enough Alone...","type":"posts"},{"content":"I wanted a full text RSS feed, posts to be tagged and readable formatting!\nHeres the code I put into my layouts/index.rss.xml file:\n{{- printf \u0026#34;\u0026lt;?xml-stylesheet href=\\\u0026#34;/rss.xsl\\\u0026#34; type=\\\u0026#34;text/xsl\\\u0026#34;?\u0026gt;\u0026#34; | safeHTML }} \u0026lt;rss version=\u0026#34;2.0\u0026#34; xmlns:atom=\u0026#34;http://www.w3.org/2005/Atom\u0026#34; xmlns:content=\u0026#34;http://purl.org/rss/1.0/modules/content/\u0026#34;\u0026gt; \u0026lt;channel\u0026gt; \u0026lt;title\u0026gt;{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}\u0026lt;/title\u0026gt; \u0026lt;link\u0026gt;{{ .Permalink }}\u0026lt;/link\u0026gt; \u0026lt;description\u0026gt;Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}\u0026lt;/description\u0026gt; \u0026lt;generator\u0026gt;Hugo -- gohugo.io\u0026lt;/generator\u0026gt; {{ with .Site.LanguageCode }}\u0026lt;language\u0026gt;{{.}}\u0026lt;/language\u0026gt;{{end}} \u0026lt;managingEditor\u0026gt;{{ with $.Site.Author.name }} ({{.}}){{end}}\u0026lt;/managingEditor\u0026gt; \u0026lt;webMaster\u0026gt;{{ with $.Site.Author.name }} ({{.}}){{end}}\u0026lt;/webMaster\u0026gt; {{ with .Site.Copyright }}\u0026lt;copyright\u0026gt;{{.}}\u0026lt;/copyright\u0026gt;{{end}} {{ if not .Date.IsZero }} \u0026lt;lastBuildDate\u0026gt;{{ .Date.Format \u0026#34;Mon, 02 Jan 2006 15:04:05 -0700\u0026#34; | safeHTML }}\u0026lt;/lastBuildDate\u0026gt;{{ end }} {{ with .OutputFormats.Get \u0026#34;RSS\u0026#34; }} {{ printf \u0026#34;\u0026lt;atom:link href=%q rel=\\\u0026#34;self\\\u0026#34; type=%q /\u0026gt;\u0026#34; .Permalink .MediaType | safeHTML }} {{ end }} {{ range .Paginator.Pages }} {{ if and (.IsPage) (.Params.date) }} \u0026lt;item\u0026gt; \u0026lt;title\u0026gt;{{ .Title }}\u0026lt;/title\u0026gt; \u0026lt;link\u0026gt;{{ .Permalink }}\u0026lt;/link\u0026gt; \u0026lt;pubDate\u0026gt;{{ .Date.Format \u0026#34;Mon, 02 Jan 2006 15:04:05 -0700\u0026#34; | safeHTML }}\u0026lt;/pubDate\u0026gt; \u0026lt;author\u0026gt;{{ with $.Site.Author.name }} ({{.}}){{end}}\u0026lt;/author\u0026gt; \u0026lt;guid\u0026gt;{{ .Permalink }}\u0026lt;/guid\u0026gt; \u0026lt;description\u0026gt;{{- .Description | html -}}\u0026lt;/description\u0026gt; \u0026lt;content:encoded\u0026gt; {{ `\u0026lt;![CDATA[` | safeHTML }} {{ .Content }} {{ `]]\u0026gt;` | safeHTML }} \u0026lt;/content:encoded\u0026gt; {{ range .Params.tags }} \u0026lt;category\u0026gt;{{ . }}\u0026lt;/category\u0026gt; {{ end }} \u0026lt;/item\u0026gt; {{ end }} {{ end }} \u0026lt;/channel\u0026gt; \u0026lt;/rss\u0026gt; ","date":"27 February 2023","externalUrl":null,"permalink":"/posts/hugorss/","section":"Posts","summary":"It took me far too long to figure out how to get a RSS feed with full content, tags and formatted nicely","title":"Getting Hugo to Provide a Full Feed in RSS","type":"posts"},{"content":"","date":"27 February 2023","externalUrl":null,"permalink":"/tags/rss/","section":"Tags","summary":"","title":"RSS","type":"tags"},{"content":" User Interface I quite like the UI for Woodpecker. I don\u0026rsquo;t think it\u0026rsquo;s as nice as ConcourseCI but it does everything I need. The killer feater here for me is that I don\u0026rsquo;t need to manage another set of users. It integrates with Gitea and utilises the userlist from there. As long as you\u0026rsquo;re loged into Gitea, you\u0026rsquo;re logged into woodpecker.\nThe pipeline log is also displayed well, with the console output shown for each build step. This makes it pretty easy to debug what is going on with your pipeline.\nPipeline Syntax - YAML The pipeline file is stored in a .woodpecker.yaml file in the same repo as the site code.\npipeline: buildjekyll: image: jekyll/builder commands: - chown -R jekyll:jekyll . - jekyll build builddocker: image: plugins/docker settings: registry: \u0026lt;CONTAINER_REGISTRY_URL\u0026gt; username: from_secret: dockerserversshuser password: from_secret: giteapass repo: \u0026lt;GITEA_URL\u0026gt;/omaraminme tags: latest dockerfile: Dockerfile restartcontainer: image: appleboy/drone-ssh settings: host: - \u0026lt;DOCKER_SERVER_HOSTNAME\u0026gt; username: from_secret: dockerserversshuser password: from_secret: dockerserversshpass port: 22 command_timeout: 2m script: - cd Docker - docker compose pull omaraminme - docker compose up -d omaraminme mail: image: drillster/drone-email settings: host: \u0026lt;SMTP_SERVER\u0026gt; username: from_secret: email password: from_secret: emailpass from: \u0026lt;EMAIL_ADDRESS\u0026gt; when: status: [ success, failure ] I\u0026rsquo;ve replaced some confidential info with \u0026lt;\u0026gt; blocks. I found this syntax the easiest of the bunch to work with and it seems to be the shortest pipeline file too. I do think that the concept of resources from ConcourseCI makes more logical sense, but the lack of this concept hasn\u0026rsquo;t hindered me in any way yet.\nThis pipeline consists of 4 stages:\nbuildjekyll - Building the Jekyll site builddocker - Building the docker image and pushing it to the container repository restartcontainer - ssh into the docker server, pull and restart the site mail - email the results of the build Each of the stages uses a docker image to run it\u0026rsquo;s commands. There are a few ways to interact with each image. For some, like the drillster/drone-email image, it\u0026rsquo;s a fixed output and takes some of the relevent information from WoodpeckerCI enironment variables in the background. One interesting point is there there is no stage to clone the git repo. This is done without specification in the pipeline and the files are made available in the root of the Woodpecker build environment. Stage 1 - buildjekyll buildjekyll: image: jekyll/builder commands: - chown -R jekyll:jekyll . - jekyll build This is a simple stage definition where we specify the builder image and run the jekyll build commands.\nStage 2 - builddocker builddocker: image: plugins/docker settings: registry: \u0026lt;CONTAINER_REGISTRY_URL\u0026gt; username: from_secret: dockerserversshuser password: from_secret: giteapass repo: \u0026lt;GITEA_URL\u0026gt;/omaraminme tags: latest dockerfile: Dockerfile I\u0026rsquo;ve replaced some confidential info with \u0026lt;\u0026gt; blocks. This stage is a little more complex. We are using one of Woodpecker\u0026rsquo;s standard plugins (but it\u0026rsquo;s still a docker image) - plugins/docker. This image allows us to run the docker build commands.\nThe image has a settings: block where we define the contianer registy that we will be working with when building our image as well as the image specifications such as the tags to assign the image and the location of the Dockerfile to build the image from. There are no commands to execute here because the plugins/docker builds, tags and pushes as default. There are other images that give you more fine grained control. This block uses the from_secret: tag. This pulls credentials from the built in secrets store. It\u0026rsquo;s a really nice implementation, but it does not allow global secrets, so you have to define all of your credentials for each pipeline.\nStage 3 - restartcontainer restartcontainer: image: appleboy/drone-ssh settings: host: - \u0026lt;DOCKER_SERVER_HOSTNAME\u0026gt; username: from_secret: dockerserversshuser password: from_secret: dockerserversshpass port: 22 command_timeout: 2m script: - cd Docker - docker compose pull omaraminme - docker compose up -d omaraminme I\u0026rsquo;ve replaced some confidential info with \u0026lt;\u0026gt; blocks. This stage uses a DroneCI container to provide ssh functionality. We also have a settings: block where you define the host, user, password, port and any ssh variables for the ssh connection you want to establish. The script: block defines the commands you want to run over ssh. In this case the docker compose commands to pull and restart the site container.\nStage 4 - mail mail: image: drillster/drone-email settings: host: \u0026lt;SMTP_SERVER\u0026gt; username: from_secret: email password: from_secret: emailpass from: \u0026lt;EMAIL_ADDRESS\u0026gt; when: status: [ success, failure ] I\u0026rsquo;ve replaced some confidential info with \u0026lt;\u0026gt; blocks. This stage is another drone image that we are using. The settings: block define the email server details and the from: tag specifies who the email is from. The image emails the owner of the repo with the build status in a nicely formatted email. The when: block tells WoodpeckerCI to send the email on success and on failure of the pipeline.\n","date":"27 February 2023","externalUrl":null,"permalink":"/posts/woodpecker/","section":"Posts","summary":"A deeper dive into the WoodpeckerCI pipeline","title":"Quest For CICD - WoodpeckerCI","type":"posts"},{"content":"","date":"27 February 2023","externalUrl":null,"permalink":"/series/the-quest-for-cicd/","section":"Series","summary":"","title":"The Quest for CICD","type":"series"},{"content":"","date":"26 February 2023","externalUrl":null,"permalink":"/tags/concourseci/","section":"Tags","summary":"","title":"ConcourseCI","type":"tags"},{"content":" User Interface I really like the concourse UI. It\u0026rsquo;s really simple and easy to understand and show the right amount of info to get you going with developing pipelines.\nI particularly like the way it graphs out your pipelines so that complex ones are easy to follow.\nHere we see my bild pipeline consists of 1 imput task (a git clone from omaraminme) and 2 output tasks (omaraminimage that builds the docker image and dockerserverssh that logins into my docker server and pulls + restarts the site image).\nThe output through the UI is useful as well. It shows console output for each of the tasks in the pipeline:\nPipeline Syntax - YAML ConcourseCI uses a pretty simele YAML based syntax. The main thing to get your head around is that everything is a resource with an input and an output.\nresource_types: - name: ssh type: docker-image source: repository: quay.io/henry40408/concourse-ssh-resource resources: - name: omaraminme type: git check_every: never webhook_token: buildomaraminme icon: github source: uri: \u0026lt;GIT_REPOSITORY_url\u0026gt;/omaraminme.git username: \u0026lt;GITEA_USERNAME\u0026gt; password: \u0026lt;GITEA_PASSWORD\u0026gt; - name: omaraminimage type: registry-image icon: docker source: repository: \u0026lt;CONTAINER_REPOSITORY\u0026gt;omaraminme tag: latest username: \u0026lt;GITEA_USERNAME\u0026gt; password: \u0026lt;GITEA_PASSWORD\u0026gt; - name: dockerserverssh type: ssh source: host: dockerserver.homelan user: \u0026lt;DOCKER_SERVER_USERNAME\u0026gt; password: \u0026lt;DOCKER_SERVER_PASSWORD\u0026gt; jobs: - name: build plan: - get: omaraminme trigger: true - task: buildsite config: platform: linux inputs: - name: omaraminme outputs: - name: omaraminme image_resource: type: registry-image source: repository: jekyll/builder run: path: sh args: - -exc - | ls -lh chown jekyll:jekyll omaraminme cd omaraminme jekyll build - task: builddockerimage privileged: true # oci-build-task must run in a privileged container config: platform: linux image_resource: type: registry-image source: repository: concourse/oci-build-task inputs: - name: omaraminme path: . outputs: - name: image run: # binary used to build the image path: build - put: omaraminimage params: image: image/image.tar - put: dockerserverssh params: interpreter: /bin/sh script: | cd /Docker docker compose pull omaraminme docker compose up -d omaraminme I\u0026rsquo;ve replaced some confidential info with \u0026lt;\u0026gt; blocks. My pipeline file consists of the following:\nA pre-build stage where I define the resources I\u0026rsquo;ll be using A jobs: block where the pipeline is defined. It consists of 2 tasks: and 2 put: steps Pre-Build Stage Here we define the resources we are going to use. You can use built in resources that are defined in the resources: block and you can define your own type of resources in the resource_types: block. What\u0026rsquo;s important to note is that all resources are docker images that will be pulled and run as part of the pipeline.\nresource_types: - name: ssh type: docker-image source: repository: quay.io/henry40408/concourse-ssh-resource resources: - name: omaraminme type: git check_every: never webhook_token: buildomaraminme icon: github source: uri: \u0026lt;GIT_REPOSITORY_url\u0026gt;/omaraminme.git username: \u0026lt;GITEA_USERNAME\u0026gt; password: \u0026lt;GITEA_PASSWORD\u0026gt; - name: omaraminimage type: registry-image icon: docker source: repository: \u0026lt;CONTAINER_REPOSITORY\u0026gt;omaraminme tag: latest username: \u0026lt;GITEA_USERNAME\u0026gt; password: \u0026lt;GITEA_PASSWORD\u0026gt; - name: dockerserverssh type: ssh source: host: dockerserver.homelan user: \u0026lt;DOCKER_SERVER_USERNAME\u0026gt; password: \u0026lt;DOCKER_SERVER_PASSWORD\u0026gt; I\u0026rsquo;ve replaced some confidential info with \u0026lt;\u0026gt; blocks. I\u0026rsquo;m defining one new resource type with the name ssh. This is a docker image that will allow me to ssh into other machines.\nI\u0026rsquo;m also using 2 built in resources:\nA git repository named omaraminme - This is my site git repo A docker image named omaraminimage - This is what I will build and push into my container repository The dockerserverssh resource is an instanciation of the ssh resource type I defined earlier.\nTask 1 - Build the Jekyll Site In the jobs: block we define a plan. A plan is a sequential list of tasks that will get run. The first task defined is to build the jekyll site. I have set trigger=true to allow this job to be triggered by the Gitea webhook.\n- task: buildsite config: platform: linux inputs: - name: omaraminme outputs: - name: omaraminme image_resource: type: registry-image source: repository: jekyll/builder run: path: sh args: - -exc - | ls -lh chown jekyll:jekyll omaraminme cd omaraminme jekyll build For each task in a ConcourseCI pipeline, we can define a config: block that configures the inputs, outputs and resources used by that task. Here we have said:\nThe task will run on a linux platform We have a git repository resource named omaraminme as an input (defined in the Pre-Build Stage) We will output the same git repository (after we have build the Jekyll site) We will use the jekyll/builder docker image to run the steps of this task The run: block then defines the shell commands that will be run to build the Jekyll site.\nIn essance, this task is cloning the git repository that contains all of the Jekyll markdown files and Dockerfile that are required. Running \u0026lsquo;jekyll build` against them and outputting the repository with the build Jekyll files (in the _site directory) and Dockerfile.\nTask 2 - Build the Docker Image - task: builddockerimage privileged: true # oci-build-task must run in a privileged container config: platform: linux image_resource: type: registry-image source: repository: concourse/oci-build-task inputs: - name: omaraminme path: . outputs: - name: image run: # binary used to build the image path: build The config: block for this task is defined as:\nThe task will run on a linux platform The resource used will be the concourse/oci-build-task docker image It will take the omaraminme git repo as an input (which includes the Dockerfile) The output will be a docker image The run: block states that the build command from the docker image will be executed.\nPut Step 1 - Push the Docker Image to the Repository - put: omaraminimage params: image: image/image.tar This put task takes the image output of the builddockerimage task and pushes it to the omaraminimage resource (which is an image in a container repository)\nPut Step 2 - SSH into the Docker Server - put: dockerserverssh params: interpreter: /bin/sh script: | cd Docker docker compose pull omaraminme docker compose up -d omaraminme This oput task takes the ssh resource and executes a script against it (i.e. ssh into the server and execute the commands in the script).\n","date":"26 February 2023","externalUrl":null,"permalink":"/posts/concourse/","section":"Posts","summary":"A deeper dive into the ConcourseCI pipeline","title":"Quest For CICD - ConcourseCI","type":"posts"},{"content":"","date":"24 February 2023","externalUrl":null,"permalink":"/tags/jenkins/","section":"Tags","summary":"","title":"Jenkins","type":"tags"},{"content":" User Interface I was fine with the user interface. It\u0026rsquo;s relatively easy to understand when creating pipelines and executing them from the web page. Here\u0026rsquo;s what your pipeline looks like visualised as stages: As I mentionerd in my last post, the console output was fine for reviewing any issues, but some of error messages I got back were quite obtuse. Pipeline Syntax - Jenkinsfile Jenkins has it\u0026rsquo;s own syntax unlike some other platforms that use plain YAML or TOML. It had a bit of a steep learning curve for me:\npipeline { agent { label \u0026#39;dockeragentdnid\u0026#39;} stages { stage(\u0026#39;Clone repository\u0026#39;) { steps { script{ cleanWs() checkout scm } } } stage (\u0026#39;BuildJekyll\u0026#39;) { steps { script { sh \u0026#39;docker run -i --rm -v \u0026lt;DOCKERPATH\u0026gt;/jenkinsagent/workspace/Gitea_CICD_omaraminme_main:/srv/jekyll jekyll/builder:latest jekyll build\u0026#39; } } } stage(\u0026#39;BuildandDeploy\u0026#39;) { steps { script{ app = docker.build(\u0026#34;\u0026lt;CONTAINER_REPOSITORY_URL\u0026gt;/omaraminme\u0026#34;) docker.withRegistry(\u0026#39;\u0026lt;CONTAINER_REPOSITORY_URL\u0026gt;\u0026#39;, \u0026#39;\u0026lt;REGISTRY_PASSWORD\u0026gt;\u0026#39;) { app.push(\u0026#34;${env.BUILD_NUMBER}\u0026#34;) app.push(\u0026#34;latest\u0026#34;) } } } } stage(\u0026#39;Test\u0026#39;){ steps { script { if (fileExists(\u0026#39;_site/index.html\u0026#39;)) { echo \u0026#34;Jekyll Site Built\u0026#34; } } } } } post { success { sh \u0026#39;sshpass \u0026lt;SSH_CREDENTIALS\u0026gt; \u0026#34;cd \u0026lt;DOCKERPATH\u0026gt; \u0026amp;\u0026amp; docker pull \u0026lt;CONTAINER_REPOSITORY_URL\u0026gt;/omaraminme \u0026amp;\u0026amp; docker compose up -d omaraminme\u0026#34;\u0026#39; sh \u0026#39;curl -s -X POST https://api.telegram.org/\u0026lt;TELEGRAM_SECRET\u0026gt;/sendMessage -d chat_id=\u0026lt;TELEGRAM_CHAT\u0026gt; -d text=\u0026#34;omaraminme build success\u0026#34;\u0026#39; emailext to: \u0026#34;\u0026lt;EMAIL\u0026gt;\u0026#34;, subject: \u0026#34;jenkins build:${currentBuild.currentResult}: ${env.JOB_NAME}\u0026#34;, body: \u0026#34;Success!\u0026#34; } failure{ sh \u0026#39;curl -s -X POST https://api.telegram.org/\u0026lt;TELEGRAM_SECRET\u0026gt;/sendMessage -d chat_id=\u0026lt;TELEGRAM_CHAT\u0026gt; -d text=\u0026#34;omaraminme build failed\u0026#34;\u0026#39; emailext to: \u0026#34;\u0026lt;EMAIL\u0026gt;\u0026#34;, subject: \u0026#34;jenkins build:${currentBuild.currentResult}: ${env.JOB_NAME}\u0026#34;, body: \u0026#34;${currentBuild.currentResult}: Job ${env.JOB_NAME}\\nMore Info can be found here: ${env.BUILD_URL}\u0026#34; } cleanup { /* clean up our workspace */ deleteDir() /* clean up tmp directory */ dir(\u0026#34;${workspace}@tmp\u0026#34;) { deleteDir() } /* clean up script directory */ dir(\u0026#34;${workspace}@script\u0026#34;) { deleteDir() } } } } I\u0026rsquo;ve replaced some confidential info with \u0026lt;\u0026gt; blocks. What happens when Jenkins receives a webhook from Gitea is that it spins up the agent defined in the Jenkinsfile and runs through the stages. My pipeline consists of 4 stages and 2 post build actions. I\u0026rsquo;ll try to describe what\u0026rsquo;s going on in a bit more detail:\nPre-Build Stage The only pre-build task I really used was specifing a Docker in Docker (DinD) agent with this code agent { label 'dockeragentdnid'} (I described the setup of this here).\nStage 1 - Clone Repository stage(\u0026#39;Clone repository\u0026#39;) { steps { script{ cleanWs() checkout scm } } } This code block clones the repository from my gitea instance into the workspace directory ON THE DOCKER SERVER. That\u0026rsquo;s right, in the config of the agent, I had to set up a mount from the docker server into the DinD agent. This was the only way I figured out how to persist files accross the different containers.\ncheckout scm is smart enough to understand cloning the correct repository - i.e. the one that generated the webhook to kick off this pipeline.\nStage 2 - Build the Jekyll Site stage (\u0026#39;BuildJekyll\u0026#39;) { steps { script { sh \u0026#39;docker run -i --rm -v \u0026lt;DOCKERPATH\u0026gt;/jenkinsagent/workspace/Gitea_CICD_omaraminme_main:/srv/jekyll jekyll/builder:latest jekyll build\u0026#39; } } } This block tells jenkins to pull and run the jekyll/builder image with the command jekyll build The important thing to note here is that we need to mount the workspace directory into the jekyll container and that the path to this is on the docker server machine, not a path within the DinD container. This really bad block diagram shows what I am trying to explain: Stage 3 - Build and Push to Registry stage(\u0026#39;BuildandDeploy\u0026#39;) { steps { script{ app = docker.build(\u0026#34;\u0026lt;CONTAINER_REPOSITORY_URL\u0026gt;/omaraminme\u0026#34;) docker.withRegistry(\u0026#39;\u0026lt;CONTAINER_REPOSITORY_URL\u0026gt;\u0026#39;, \u0026#39;\u0026lt;REGISTRY_PASSWORD\u0026gt;\u0026#39;) { app.push(\u0026#34;${env.BUILD_NUMBER}\u0026#34;) app.push(\u0026#34;latest\u0026#34;) } } } } This stage builds whatever Dockerfile is present in the workspace directory, tags the image and pushes it to my Gitea container repository.\nStage 4 - Testing stage(\u0026#39;Test\u0026#39;){ steps { script { if (fileExists(\u0026#39;_site/index.html\u0026#39;)) { echo \u0026#34;Jekyll Site Built\u0026#34; } } } } A simple test to see if the index.html exists. This means Jekyll has built something\nPost Build Tasks success { sh \u0026#39;sshpass \u0026lt;SSH_CREDENTIALS\u0026gt; \u0026#34;cd \u0026lt;DOCKERPATH\u0026gt; \u0026amp;\u0026amp; docker pull \u0026lt;CONTAINER_REPOSITORY_URL\u0026gt;/omaraminme \u0026amp;\u0026amp; docker compose up -d omaraminme\u0026#34;\u0026#39; sh \u0026#39;curl -s -X POST https://api.telegram.org/\u0026lt;TELEGRAM_SECRET\u0026gt;/sendMessage -d chat_id=\u0026lt;TELEGRAM_CHAT\u0026gt; -d text=\u0026#34;omaraminme build success\u0026#34;\u0026#39; emailext to: \u0026#34;\u0026lt;EMAIL\u0026gt;\u0026#34;, subject: \u0026#34;jenkins build:${currentBuild.currentResult}: ${env.JOB_NAME}\u0026#34;, body: \u0026#34;Success!\u0026#34; } failure{ sh \u0026#39;curl -s -X POST https://api.telegram.org/\u0026lt;TELEGRAM_SECRET\u0026gt;/sendMessage -d chat_id=\u0026lt;TELEGRAM_CHAT\u0026gt; -d text=\u0026#34;omaraminme build failed\u0026#34;\u0026#39; emailext to: \u0026#34;\u0026lt;EMAIL\u0026gt;\u0026#34;, subject: \u0026#34;jenkins build:${currentBuild.currentResult}: ${env.JOB_NAME}\u0026#34;, body: \u0026#34;${currentBuild.currentResult}: Job ${env.JOB_NAME}\\nMore Info can be found here: ${env.BUILD_URL}\u0026#34; } This is a conditional block that emails me and send me a telegram message on success or fail of the build\ncleanup { /* clean up our workspace */ deleteDir() /* clean up tmp directory */ dir(\u0026#34;${workspace}@tmp\u0026#34;) { deleteDir() } /* clean up script directory */ dir(\u0026#34;${workspace}@script\u0026#34;) { deleteDir() } } Since the workspace directory is persistant on the docker server, I have to clean it up to make sure old files don\u0026rsquo;t interfere with consequent builds.\n","date":"24 February 2023","externalUrl":null,"permalink":"/posts/jenkins/","section":"Posts","summary":"A deeper dive into the Jenkins pipeline","title":"Quest For CICD - Jenkins","type":"posts"},{"content":"*UPDATE: I\u0026rsquo;ve switched over to Hugo for my CV site. The example used in this and following posts will still work for your Jekyll sites, but if you have a Hugo site, take a look here: I Couldn't Leave Well Enough Alone... 5 March 2023\u0026middot;5 mins Homelab Hugo WoodpeckerCI Docker Selfhosted CICD FOSS Almost immediately after I finished my last post on automating my Jekyll CV site with WoodpeckerCI, I changed over to a Hugo site\u0026hellip; and automated it I run my own gitea instance and I\u0026rsquo;m slowly moving all of my code (compose files, dotfiles, websites\u0026hellip;) into it. I\u0026rsquo;ve never been a huge user of git at home, but you can\u0026rsquo;t argue with being able to track changes in all of your config!\nSince I have all of this great data in a repo, I should be able to trigger things off changes. I work quite a bit with organisations transforming to use devops and infrastructure as code. I really need to do this at home too!\nIn this post, I\u0026rsquo;ll talk a little about how I went about assessing each of the candidade CICD platformas as well as go through the implementation of each at a high level. I plan to write another post on each of the candidates talking about how to create their pipelines and how they run.\nMy Requirements Docker! - I run my entire self hosted setup on ubuntu servers with docker. I find docker the easiest way to get services up and running, and to back them up. It\u0026rsquo;s also quite mobile if I ever need to rebuild. The CICD platform needs to be able to run in docker and also to utilise docker for tasks in the pipeline steps (this is important as it means I don\u0026rsquo;t have to mess around installing things to make a pipeline step work) Single User - Only I will be using the stack for my own self hosted network so I wan\u0026rsquo;t looking to closely into user collaboration / scaling Integrates with Gitea - There has to be a way to trigger pipelines from gitea Lightweight - I\u0026rsquo;m not running server grade hardware at home. Power bills, noise, heat and space preclude setting up a rack. I have a bunch of small form factor machines dotted around the house. This means I try not tu use apps that require a huge amount of CPU and memory Easy to Maintain - Nobody has time to keep tinkering with getting software to work. We just and to tinker with what it can do! Secure - I would like to minimize the attak surface that the platform exposes. This means things like plugins and hacks should be minimized Active - The platform should be actively being developed. This is to flush out bugs ad add sexy new features Declarative Pipelines - This makes more sense in my head. Instead of defining steps to execute (imperative pipelines) I want to just define the desired state and let the platform take care of the rest. Open Source - I love open source software. I fint the FOSS community can build better and faster in most cases My Shortlist After doing some research on what people were using thin their home labs I came up with these three candidates:\nJenkins - The old man of CICD. I believe this was the first CICD platform developed and has alot of years of development and use behind it ConcourseCI - Has been around since 2012 and has some backing from VMWare WoodpeckerCI - a fork of DroneCI after it went from open source to open-core Spoiler - I\u0026rsquo;m currently running WoodpeckerCI and it\u0026rsquo;s going well! How I\u0026rsquo;m Testing I\u0026rsquo;ve implemented each of the candidates and have tested them out by auto-deploying my personal landing page at https://cv.omaramin.me. This is a simple Jekyll site that\u0026rsquo;s residing in an nginx container. At a high level, on trigger of a git push to my site\u0026rsquo;s repository, the CICD pipeline should:\nStep 1 Pull the Jekyll site code from github\nStep 2 Pull the Jekyll builder docker image and build the site\nStep 3 Build this Dockerfile and push the image to my gitea container repository\nFROM nginx:alpine COPY default /etc/nginx/sites-available/default COPY _site /usr/share/nginx/html COPY pdf/omaraminCV.pdf /usr/share/nginx/html EXPOSE 80 CMD [\u0026#34;nginx\u0026#34;, \u0026#34;-g\u0026#34;, \u0026#34;daemon off;\u0026#34;] Step 4 Pull the newly created docker image and restart the nginx container\nHow My Code is Structured In terms of the project structure, I keep the dockerfile as well as the pipeline files for CICD in the same repository as the Jekyll site files:\nJenkins My first stop was Jenkins. This has been around for so long that it was pretty easy to find examples of people doing exactly the same thing I was doing. But oh boy were there alot of different approaches.\nInstallation Installation was relatively simple. I used the default install instructions and ended up with the following docker compose block:\njenkins: image: jenkins/jenkins:lts container_name: jenkins privileged: true user: root environment: - TZ=$TZ ports: - 8080:8080 - 50000:50000 container_name: jenkins volumes: - $DOCKERPATH/jenkins/data:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock After setting up jenkins fully, to integrate with Gitea, I had to install a plugin (more on that later) and I was up and running with the basics.\nWhat\u0026rsquo;s interesting is that with this set up, we are still not ready to run pipelines. The way I want things to work is that docker containers are spun up automatically to run tasks. In order to do this with Jenkins, I need to configure my docker server as a cloud node and within it, an agent that \u0026ldquo;understands\u0026rdquo; docker commands (docker pull, run, etc\u0026hellip;). This means running a \u0026ldquo;Docker In Docker\u0026rdquo; agent. This agent will basically instruct the docker daemon on my server to create, run and destroy containers as required.\nAn alternative to docker in docker (DinD) is to compile my own jenkins container to include containerd and docker. That seemed like a bit too much hassle to keep track of so I went for the DNID route. To set up DNID, you need to go into Manage Jenkins from the main page of your install and then Configure Clouds. I used someone else\u0026rsquo;s container as my DNID container: and in order to let different containers work on the same data, I needed a shared mount: The blocked out section is to cover up my directory structure. How we use the agent will become clearer in the post on Jenkinsfile structure: Quest For CICD - Jenkins 24 February 2023\u0026middot;4 mins Homelab CICD Jenkins Automation FOSS Selfhosted A deeper dive into the Jenkins pipeline The Good It was pretty easy to get up an running with the basics. I did have to bang my head against DinD for more hours than I care to mention, but that might just be because I\u0026rsquo;m learning the concepts from scratch. There\u0026rsquo;s a standard secrets plugin which is handy. I hard coded everything in my jenkinsfile as I was testing the pipeline and moved on before I got around to implementing the secrets There is a plugin for everything. Want to use Vault, there\u0026rsquo;s a plugin, want to be notified via Telegram, plugin\u0026hellip; This is also a problem You can do almost anything in a jenkins pipeline\u0026hellip; This is also a problem There\u0026rsquo;s a hell of alot of experience and knowledge out there already that you can easily find with a web search Decent integration into Gitea. I can see the build status for each of my commits The Bad There are no guard rails. You can do some pretty silly things in pipelines and you\u0026rsquo;re the only one to blame Everything is dependent on plugins, alot of which are unmaintained. That\u0026rsquo;s alot of trust to place into different code I found the Jenkinsfile syntax to be quite a learning curve Pipeline error messages were quite obtuse at times. I had to spend alot of time searching forums to figure out what my pipeline issues were From what I\u0026rsquo;ve read online, updates to jenkins quite often cause plugins to break. Since Jenkins is so dependent on plugins, this causes your pipelines to break ConcourseCI Concourse is an interesting one. It\u0026rsquo;s very agnostic to the code repository (although I believe it does have integration with github) and is \u0026ldquo;docker native\u0026rdquo; in that you don\u0026rsquo;t need any special configuration to get it up and running while utilising container agents.\nInstallation Installation was also pretty simple. I was up and running by following this guide with the following compose blocks:\nconcourse: image: concourse/concourse command: quickstart privileged: true depends_on: [concourse-db] ports: [\u0026#34;8081:8080\u0026#34;] environment: CONCOURSE_POSTGRES_HOST: concourse-db CONCOURSE_POSTGRES_USER: concourse CONCOURSE_POSTGRES_PASSWORD: \u0026lt;DB_PASS\u0026gt; CONCOURSE_POSTGRES_DATABASE: concoursedb CONCOURSE_EXTERNAL_URL: \u0026lt;URL\u0026gt; CONCOURSE_ADD_LOCAL_USER: \u0026lt;USERNAME\u0026gt;:\u0026lt;USER_PASS\u0026gt; CONCOURSE_MAIN_TEAM_LOCAL_USER: \u0026lt;USERNAME\u0026gt; # instead of relying on the default \u0026#34;detect\u0026#34; CONCOURSE_WORKER_BAGGAGECLAIM_DRIVER: overlay CONCOURSE_CLIENT_SECRET: \u0026lt;SECRET\u0026gt; CONCOURSE_TSA_CLIENT_SECRET: \u0026lt;SECRET\u0026gt; CONCOURSE_X_FRAME_OPTIONS: allow CONCOURSE_CONTENT_SECURITY_POLICY: \u0026#34;*\u0026#34; CONCOURSE_CLUSTER_NAME: \u0026lt;NAME\u0026gt; CONCOURSE_WORKER_CONTAINERD_DNS_SERVER: \u0026#34;\u0026lt;DNS_IP\u0026gt;\u0026#34; CONCOURSE_WORKER_RUNTIME: \u0026#34;containerd\u0026#34; concourse-db: image: postgres environment: POSTGRES_DB: concoursedb POSTGRES_PASSWORD: \u0026lt;DB_PASS\u0026gt; POSTGRES_USER: concourse PGDATA: /database To use this, you would just need to replace the \u0026lt;\u0026gt; text with your own values. In addition to getting the containers up and running, you will need to install Fly. This is a command line tool that you use to set up pipeline and control Concourse. The pipelines are structured in YAML files that I keep with the rest of my site code. The one annoyance I had was setting up webhooks from Gitea. Getting the webhook syntax seems to be a bit sloppy\nThe Good Really easy to get up and running Pipeline syntax is pretty intuative The web interface is lightweight and does everything you need it to. It also has a really nice way of graphing your pipelines They\u0026rsquo;ve just released support for using Vault as a credential manager Flowing files from your git repo through multiple steps of a pipeline is seamless The Bad Using Fly to set up the pipelines is a bit of a learning curve For a deeper dive into ConcourseCI and it\u0026rsquo;s pipeline syntax, take a look at my post: Quest For CICD - ConcourseCI 26 February 2023\u0026middot;5 mins Homelab CICD ConcourseCI Automation FOSS Selfhosted A deeper dive into the ConcourseCI pipeline WoodpeckerCI Forked from DroneCI when it went open-core. Drone is used by alot of people and enterprises so it\u0026rsquo;s tried and tested. From what I can tell, WoodpeckerCI is pretty compatible with drone configs. Pipeline syntax is YAML and I can keep the .woodpecker.yml file in the same repo as the rest of my site code.\nInstallation Aslo very easy following these instructions. The install is made up of a server and any number of agents that communicate back to the server. This is the compose block that got me up and running:\nwoodpecker-server: image: woodpeckerci/woodpecker-server:latest ports: - 8000:8000 volumes: - $DOCKERPATH/woodpecker/data:/var/lib/woodpecker/ environment: - WOODPECKER_OPEN=false - WOODPECKER_ADMIN=\u0026lt;USERNAME\u0026gt; - WOODPECKER_ORGS=\u0026lt;REPO_ORGANISATION\u0026gt; - WOODPECKER_AUTHENTICATE_PUBLIC_REPOS=true - WOODPECKER_HOST=\u0026lt;WOODPECKER_URL\u0026gt; - WOODPECKER_GITEA=true - WOODPECKER_GITEA_URL=\u0026lt;GITEA_URL\u0026gt; - WOODPECKER_GITEA_CLIENT=\u0026lt;GITEA_CLIENT_ID\u0026gt; - WOODPECKER_GITEA_SECRET=\u0026lt;GITEA_SECRET\u0026gt; - WOODPECKER_AGENT_SECRET=\u0026lt;GITEA_AGENT_SECRET\u0026gt; woodpecker-agent: image: woodpeckerci/woodpecker-agent:latest command: agent restart: always depends_on: - woodpecker-server volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - WOODPECKER_SERVER=woodpecker-server:9000 - WOODPECKER_AGENT_SECRET=\u0026lt;GITEA_AGENT_SECRET\u0026gt; The linked install instructions explain what you need to do with the \u0026lt;\u0026gt; credentials. What\u0026rsquo;s really nice is that there\u0026rsquo;s no seperate user management. WoodpeckerCI uses the user database from Gitea and you can configure those users with different permissions.\nThe Good Tight integration with Gitea (also other repositories) Really easy pipeline syntax. The most intuative of the three Also \u0026ldquo;docker native\u0026rdquo; Web interface is lightweight and provides everything you need Built in credentials management The Bad I haven\u0026rsquo;t found a way to set up global credentials so there\u0026rsquo;s a bit more initial setup of pipelines For a deeper dive into WoodpeckerCI and it\u0026rsquo;s pipeline syntax, take a look at my post here: Quest For CICD - WoodpeckerCI 27 February 2023\u0026middot;4 mins Homelab CICD WoodpeckerCI Automation FOSS Selfhosted A deeper dive into the WoodpeckerCI pipeline Conclusion I\u0026rsquo;m really liking WoodpeckerCI for the moment and both https://omaramin.me and https://cv.omaramin.me are built using WoodpeckerCI pipelines. I have noted that the trend seems to be for code repositories to start implementing CI functionality (e.g. github and gitlab have Actions and Gitea is even working on doing something similer). I haven\u0026rsquo;t decided if that\u0026rsquo;s a good thing though. It means a more complicated migration if I ever need to ditch Gitea (so much so that I have read that some people won\u0026rsquo;t move off Gitlab because migrating all of their Actions would be too much of a headache).\n","date":"23 February 2023","externalUrl":null,"permalink":"/posts/questforcicd/","section":"Posts","summary":"Summary of the three CICD platforms I have tried - Jenkins, ConcourseCI and WoodpeckerCI","title":"My Quest for CICD","type":"posts"},{"content":"I\u0026rsquo;m a random guy living in Dubai and working across Europe, the Middle East and Africa. I\u0026rsquo;m married with a couple of kids, a dog and a garden full of parrots.\nI live and breath tech - at work I\u0026rsquo;m an IT consultant working with very large organisations as an Enterprise Architect and at home, I self host all the things, as well as constantly trying not to burn my house down with electronics projects.\nWhy Blog? Nowadays our data is being harvested and, in some cases, weaponised against us. This is the main reason I\u0026rsquo;m trying to selfhost as much as I can. In that quest, I\u0026rsquo;ve been drawn to the indieweb community. I must have posted a ton of content to different social media sites that now no longer exist. That data is pretty much lost. With this blog I can create as much content as I want, safe in the knowledge that I can control when it goes away. It also means I can ensure RSS is available which many sites nowadays are scrapping which is a great shame.\nAnother advantage I find is learning. Often I just go away and do something and then forget about what I did to get it all to work. I find that writing about it helps solidify that knowledge in my head.\nWhat\u0026rsquo;s the Blog About? Anything and everything that crosses my mind. I hope to post some useful content on different services and apps that I self host, but I may occasionally post some fiction or a pretty picture.\nFind Me I can be found on matrix at @amino@matrix.omaramin.me\n","date":"22 February 2023","externalUrl":null,"permalink":"/about/","section":"Omar's Blog","summary":"A little bit about me and why I started this blog","title":"About Me","type":"page"},{"content":"","date":"13 June 2022","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"}]