Setting up a Linux home server with Ansible and containerized apps

Setting up a Linux home server with Ansible and containerized apps

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:

I use AlmaLinux for a number of reasons:

  • It's built on modern software

    • dnf, a powerful RPM package manager

    • firewalld, a firewall service supported by Ansible out of the box

    • podman, the easy-to-use container engine we'll use to run our apps

  • 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:

  1. First, check if the container already exists using podman

  2. 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
  3. Configure firewalld to open syncthing's ports

    • firewalld comes with "services" that define the ports used by common software such as syncthing!
  4. Install and enable the systemd service (we'll look into this next)

  5. 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!