We're fortunate to live in a time of abundant, high-quality free and open source softwareâand the best part is, much of it can be self-hosted on your own server đ
As a follow up to my article on reproducible distro configuration, let's explore how to set up a home server running Linux using Ansible, plus my method of deploying self-hosted applications via containers for the ultimate combination of stability, flexibility, and modernity!
And as a bonus, we'll take a look at my server landing page software designed for precisely this use case.
Prerequisites
Home server hardware
Ansible installed on your workstation and familiarity with its use
The OS
Let's start with the operating system. Many Linux distributions can adequately fill this role:
Any of the RHEL clones such as Rocky Linux or AlmaLinux
I use AlmaLinux for a number of reasons:
It's built on modern software
It's robust and stable, requiring minimal updates
Access to the Extra Packages for Enterprise Linux (EPEL) repository means lots of available software
- Additionally, Copr can build against this repository so packaging your own software is easy
Documentation for your distribution of choice should walk you through the download and installation process.
Make sure your server is accessible via ssh!
Going forward, this article will assume the use of an RPM-based distro with the aforementioned software.
The set up
Rather than manually installing and configuring software on my home server, I prefer to automate this process using Ansible. This means my Ansible playbook is the single source of truth regarding the home server's configuration and it can be easily reproduced as necessary.
Here's my localserver.yml
playbook file:
- hosts: localserver
pre_tasks:
- name: Get host IP
shell: hostname -I | awk '{print $1}'
register: hostip_cmd
vars:
hostip: "{{ hostip_cmd.stdout }}"
homedir: "{{ lookup('env', 'HOME') }}"
configdir: "{{ homedir }}/.config"
mediadir: "{{ homedir }}/media"
easymodeconfig: "{{ homedir }}/.easymode-config"
roles:
- base
- dotfiles
- openvpn_client
- jellyfin
- qbittorrent
- rclone
- syncthing
- easymode
As you can see, many roles are involved in the set up of my server. We will look into these shortly.
In order to run such a playbook, first configure Ansible's /etc/ansible/hosts
file like so:
[localserver]
192.168.1.111 # Your server's local IP address
Then the following command can be used:
ansible-playbook --ask-pass --ask-become-pass localserver.yml
It will prompt for the server's password, connect via ssh, and proceed to run the playbook's roles.
Now, let's see how these roles make use of podman for deploying containerized applications!
The apps
Before going any further, let's answer the question: why use containers?
The primary rationale is this:
Any software designed to be self-hosted likely offers a containerized version or has been packaged as such by a third party for easy installation
These user-facing applications needn't follow your distro's release cadence or even be packaged by said distro
Let's look at syncthing as an example. The LinuxServer community offers a containerized image on Docker Hub đ
Keep in mind: Docker images comply with the OCI Container standard and are fully compatible with podman.
Installing this software with Ansible is simple enoughâit requires only a few tasks and files.
I keep all the necessary tasks in a main.yml
tasks file:
- name: Check existence of container
command: podman container exists syncthing
register: container_exists
failed_when: container_exists.rc > 1
- name: Create container
command: >
podman create --name=syncthing
--userns=""
-e PUID=0
-e PGID=0
-p 8384:8384
-p 22000:22000/tcp
-p 22000:22000/udp
-p 21027:21027/udp
-v syncthing-config:/config
docker.io/linuxserver/syncthing
when: container_exists.rc == 1
- name: Configure firewalld
firewalld:
service: "{{ item }}"
state: enabled
immediate: true
permanent: true
loop:
- syncthing
- syncthing-gui
become: true
- name: Install systemd service
copy:
src: syncthing.service
dest: '{{ configdir }}/systemd/user/syncthing.service'
mode: 0644
- name: Enable systemd service
systemd:
name: syncthing.service
scope: user
state: started
enabled: true
daemon_reload: true
- name: Install easymode config
copy:
src: syncthing.yml
dest: "{{ easymodeconfig }}/"
These tasks are fairly self-documenting, but let's go through them in order nonetheless:
First, check if the container already exists using podman
If it doesn't yet exist, create it using podman
- You can typically find the appropriate container creation flags on the app's Docker Hub page
Configure firewalld to open syncthing's ports
- firewalld comes with "services" that define the ports used by common software such as syncthing!
Install and enable the systemd service (we'll look into this next)
Install the accompanying easymode configuration file
- easymode is a server landing page of my own creationâstick around to the end for more info!
Because podman doesn't operate as a daemon in the manner of docker, we need a syncthing.service
file to run our container at system startup:
[Unit]
Description=Syncthing podman container
[Service]
ExecStart=/usr/bin/podman start -a syncthing
ExecStop=/usr/bin/podman stop -t 10 syncthing
Restart=on-failure
[Install]
WantedBy=default.target
The location of these files must follow Ansible's role directory structure:
roles/
syncthing/
files/
syncthing.service
syncthing.yml # easymode config file
tasks/
main.yml
And that's all it takes! Repeat as needed to install the containerized apps of your choosing đ€©
The landing page
I find it useful to set up a server landing page so I don't need to memorize the ports used by the various web interfaces of running apps.
There are a few popular projects in this domain:
I used Heimdall for a time, and it works well, but I wanted a landing page app with a setup procedure more conducive to automation. Thus, easymode was born! It's designed to be configured by automation systems such as Ansible.
The configuration file for syncthing is this simple:
name: syncthing
url: 8384
icon: arcticons:syncthing
The file is installed to easymode's configuration directory; the files here become entries in the generated landing page.
You may have noticed the server's IP address is stored as a variable when running my Ansible playbook. This is used by easymode to construct the correct URL given the above port!
Here's the task responsible for creating easymode's container:
- name: Create container
command: >
podman create --name=easymode
-p 80:80
-e EASYMODE_HOSTNAME={{ ansible_hostname }}
-e EASYMODE_IP={{ hostip }}
-v {{ easymodeconfig }}:/config:z
docker.io/supplantr/easymode
The rest of the role closely resembles the syncthing role we explored above.
Finally, navigate to your home server's IP address and enjoy!