Ad meliora

Aug 01, 2024

Productionizing a golang binary

OUTLINE

Introduction

So after developing my golang project, a vanity url mapper for my golang packages, I needed a way to deploy it from my local machine to my server. So this means I would run it as a systemd1 service. For this, I would make the systemd service run it,

(a) either locally on my server filesystem, or

(b) in a docker container.

Types of systemd service

A systemd service is normally defined in a unit file. The syntax and description of the unit file can be found by running man systemd.unit on any Linux system that supports systemd. So there are conventionally two types of systemd services

(a) System service: This service is enabled for all users that log into the system. To do this, you would normally run this set of commands:

$ sudo cp <SERVICE_NAME>.service /etc/systemd/system/
$ sudo systemctl daemon-rDeploying to run on the filesystemeload
$ sudo systemctl enable <SERVICE_NAME>.service

(b) User service: The service is enabled for only the logged-in user. To do this, you would normally run this set of commands:

$ touch /home/<$USER>/.config/systemd/user/<SERVICE_NAME>.service
$ systemctl --user reload
$ systemctl --user enable --now <SERVICE_NAME>

Enabling the service will then symlink it into the appropriate .wants subdirectory, and it will run only when that user is logged in. This means that if that user is not logged in, systemd might terminate the service2.

The systemd user instance is started after the first login of a user and killed after the last session of the user is closed. Sometimes it may be useful to start it right after boot, and keep the systemd user instance running after the last session closes, for instance to have some user process running without any open session

This is bad for long-running transcations/services. So to do that, you would run this

$ loginctl enable-linger $USER

This disconnects that logged-in user specific systemd instance from that logged-in user session.

Attempt One

I did initially not know the difference between the types of systemd services. So I ran these steps:

(a) Created a sample service

[Unit]
Description=A custom url mapper for go packages!
After=network-online.target

[Service]
ExecStart=/opt/gocustomurls -conf /home/vagrant/.config/config.json
SyslogIdentifier=gocustomurls
StandardError=journal
Type=exec

[Install]
WantedBy=multi-user.target

(b) Ran these steps

$ sudo cp gocustomurls.service /etc/systemd/system/
$ sudo systemctl daemon-reload
$ sudo systemctl start gocustomurls.service
$ sudo systemctl status gocustomurls.service

So I got this error

× gocustomurls.service - GocustomUrls. A custom url mapper for go packages!
     Loaded: loaded (/etc/systemd/system/gocustomurls.service; disabled; preset: disabled)
    Drop-In: /usr/lib/systemd/system/service.d
             └─10-timeout-abort.conf
     Active: failed (Result: exit-code) since Fri 2024-07-19 03:31:19 UTC; 54s ago
   Duration: 23ms
    Process: 3027 ExecStart=/opt/gocustomurls -conf /home/vagrant/.config/config.json (code=exited, status=1/FAILURE)
   Main PID: 3027 (code=exited, status=1/FAILURE)
        CPU: 6ms
$ http --body "http://localhost:7070/x/touche?go-get=1"
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta name="go-import" content="scale.dev/x/migrate git https://codeberg.org/Gusted/mCaptcha.git">
        <meta name="go-source" content="scale.dev/x/migrate https://codeberg.org/Gusted/mCaptcha.git https://codeberg.org/Gusted/mCaptcha.git/tree/main{/dir} https://codeberg.org/Gusted/mCaptcha.git/blob/main{/dir}/{file}#L{line}">                   
    </head>
</html>

Jul 19 03:31:19 fedoramachine systemd[1]: Starting gocustomurls.service - GocustomUrls. A custom url mapper for go packages!...
Jul 19 03:31:19 fedoramachine systemd[1]: Started gocustomurls.service - GocustomUrls. A custom url mapper for go packages!.
Jul 19 03:31:19 fedoramachine gocustomurls[3027]: 2024/07/19 03:31:19 File: => /home/vagrant/.config/config.json
Jul 19 03:31:19 fedoramachine gocustomurls[3027]: 2024/07/19 03:31:19 Ok: => false
Jul 19 03:31:19 fedoramachine gocustomurls[3027]: 2024/07/19 03:31:19 Warning, generating default config file
Jul 19 03:31:19 fedoramachine gocustomurls[3027]: 2024/07/19 03:31:19 neither $XDG_CONFIG_HOME nor $HOME are defined
Jul 19 03:31:19 fedoramachine systemd[1]: gocustomurls.service: Main process exited, code=exited, status=1/FAILURE
Jul 19 03:31:19 fedoramachine systemd[1]: gocustomurls.service: Failed with result 'exit-code'.

Basically the error is that the $HOME environment variable is empty. This is consistent with some of the properties of system services which are:

  • Their spawned processes inherit NO environment (e.g., in a shell script run by the service, the $HOME environment variable will actually be empty)

  • They run as root by default. As a result, they have root permissions.

Attempt Two

So I tried to fix some of the issues with my first attempt by creating a user service. I went through these steps:

(a) Created a non-root user.

Debian

$ sudo adduser \
    --system \ # Non-expiring accounts
    --shell /bin/bash \ # the login shell
    --comment 'Go Custom Urls Service' \ # removes finger info, making the command non-interactive 
    --group \ # creates an identically named group as its primary group
    --disabled-password --home /home/$USER $USER

Fedora

$ sudo useradd --system --shell /bin/bash --comment 'Go Custom Urls Service' --home-dir /home/gourls -m gourls

(b) Unlocked the account by deleting the password

$ sudo passwd -d gourls // to unlock the account

(c) Fixed the permissions. I used this resource3 and this resource4 to generate the required permissions.

$ sudo chown -R gourls:gourls /home/gourls
$ sudo chmod -R 770 /home/gourls

(d) Modified the servcie unit above to change the invocation of the service to a user throught the use of the User= and Group= directives.

[Unit]
Description=GocustomUrls. A custom url mapper for go packages!
After=network-online.target

[Service]
User=gols
Group=gols
ExecStart=/home/gols/gocustomurls -conf /home/gols/.config/gocustomurls/config.json
SyslogIdentifier=gocustomurls
StandardError=journal
Type=simple
WorkingDirectory=/home/gols
Environment=USER=gols HOME=/home/gols


[Install]
WantedBy=multi-user.target

(e) Copied the unit file to the config folder.

$ sudo mkdir -p /home/gols/.config/systemd/user
$ sudo chmod -R 770 /home/gols
$ sudo cp gocustomurls.service /home/gols/.config/systemd/user/
$ sudo chown -R gols:gols /home/gols

(f) Reloaded systemd services

$ su gols
$ systemctl --user daemon-reload
Failed to connect to bus: Permission denied

So after googling this error, I was told to check if my XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS was set correctly. So checking,

$ id
uid=993(gols) gid=992(gols) groups=992(gols) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
$ echo $XDG_RUNTIME_DIR
/run/user/1000
$ echo $DBUS_SESSION_BUS_ADDRESS
unix:path=/run/user/1000/bus

As you can see above, the variables XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS are configured with the incorrect user id. So I tried to export them in the user's ~/.bash_profile

$ cat ~/.bash_profile
...
export XDG_RUNTIME_DIR="/run/user/$UID"
export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus

This did not help. So after googling, I came across this resource5 and tried that

$ systemctl -M gols@ --user daemon-reload
Failed to connect to bus: Permission denied

So after further googling, I came across this resource6. From this resource, there are basically two requirements for systemctl --user command to work which are:

  • The user instance of the systemd daemon must be alive, and

  • systemctl must be able to find it through certain path variables.

For the first condition, the loginctl enable-linger command enables that to happen by starting a user instance of systemd even if the user is not logged in. For the second condition, su or su --login does not set the required variables. To do that, you would normally ssh into that user's session. This is obviously a non-starter for me. But there is a new command called machinectl7 that obviates that need. So armed with this information, I was ready for another attempt.

Attempt Three

So machinectl7 is available from systemd-container (Debian/Fedora). So I ran these steps based on this resource8 and this resource5:

$ sudo dnf install systemd-container
$ sudo loginctl enable-linger gols
$ sudo machinectl shell --uid=gols
Connected to the local host. Press ^] three times within 1s to exit session.
$ exit
$ sudo systemctl -M gourls@ --user daemon-reload
$ su gopls
$ echo $XDG_RUNTIME_DIR
/run/user/993
$ echo $DBUS_SESSION_BUS_ADDRESS
unix:path=/run/user/993/bus

In order to the command sudo systemctl -M gourls@ --user daemon-reload to be successful, the variables from the previous attempt must be exported in ~/.bash_profile. Okay, I then ran a journalctl command to check the status of the failed service

$ journalctl --user -u gocustomurls.service
Hint: You are currently not seeing messages from the system.
      Users in groups 'adm', 'systemd-journal', 'wheel' can see all messages.
      Pass -q to turn off this notice.
No journal files were opened due to insufficient permissions.

So I added the gourls user to the systemd-journal group

$ sudo usermod -a -G systemd-journal gols
$ sudo systemctl -M gols@ --user list-units | grep "gocustom" # list all the units and find gocustom

So far so good. Restarting the service produces an error

$ sudo systemctl -M gourls@ --user restart gocustomurls.service
$ su gols
$ journalctl --user -u gocustomurls.service
Jul 19 07:19:03 fedoramachine systemd[749]: Started gocustomurls.service - GocustomUrls. A custom url mapper for go packages!.
Jul 19 07:19:03 fedoramachine (stomurls)[3291]: gocustomurls.service: Failed to determine supplementary groups: Operation not permitted
Jul 19 07:19:03 fedoramachine systemd[749]: gocustomurls.service: Main process exited, code=exited, status=216/GROUP
Jul 19 07:19:03 fedoramachine systemd[749]: gocustomurls.service: Failed with result 'exit-code'.

Based on the error above, it seemed that the group was incorrect. This observation was also confirmed after viewing this9. So I removed the User= , Group= and Environment= directoves from the unit file and restarted the service.

[Unit]
Description=GocustomUrls. A custom url mapper for go packages!
After=network-online.target

[Service]
ExecStart=/home/gols/gocustomurls -conf /home/gols/.config/gocustomurls/config.json
SyslogIdentifier=gocustomurls
StandardError=journal
WorkingDirectory=/home/gols
Type=simple

[Install]
WantedBy=multi-user.target
$ sudo cp gocustomurls.service /home/gols/.config/systemd/user/
$ sudo chown -R gols:gols /home/gols
$ sudo machinectl shell --uid=gols
Connected to the local host. Press ^] three times within 1s to exit session.
$ exit
logout
Connection to the local host terminated.
$ sudo systemctl -M gols@ --user daemon-reload
$ sudo systemctl -M gols@ --user restart gocustomurls.service
$ sudo systemctl -M gols@ --user status gocustomurls.service
× gocustomurls.service - GocustomUrls. A custom url mapper for go packages!
     Loaded: loaded (/home/gols/.config/systemd/user/gocustomurls.service; disabled; preset: disabled)
    Drop-In: /usr/lib/systemd/user/service.d
             └─10-timeout-abort.conf
     Active: failed (Result: exit-code) since Fri 2024-07-19 10:02:31 UTC; 8min ago
   Duration: 1ms
    Process: 2919 ExecStart=/home/gols/gocustomurls -conf /home/gols/.config/gocustomurls/config.json (code=exited, status=203/EXEC)
   Main PID: 2919 (code=exited, status=203/EXEC)
        CPU: 1ms

The error was because of a missing routes.json. The binary that the gocustomurls.service runs, returns an exit code if routes.json is not found. Adding a rules.json to the folder that is read by the gocustomurls.service returns a success

$ sudo cp rules.json /home/gols/.config/gocustomcurls/
$ sudo systemctl -M gols@ --user daemon-reload
$ sudo systemctl -M gols@ --user restart gocustomurls.service
$ sudo systemctl -M gols@ --user status gocustomurls.service

● gocustomurls.service - GocustomUrls. A custom url mapper for go packages!
     Loaded: loaded (/home/gols/.config/systemd/user/gocustomurls.service; disabled; preset: disabled)
    Drop-In: /usr/lib/systemd/user/service.d
             └─10-timeout-abort.conf
     Active: active (running) since Sat 2024-07-20 05:16:31 UTC; 28s ago
   Main PID: 3158
      Tasks: 6 (limit: 2319)
     Memory: 1.7M (peak: 2.0M)
        CPU: 4ms
     CGroup: /user.slice/user-993.slice/user@993.service/app.slice/gocustomurls.service
             └─3158 /home/gols/gocustomurls -conf /home/gols/.config/gocustomurls/config.json

Testing with httpie results in success.

$ sudo dnf install httpie
$ http --body "http://localhost:7070/x/touche?go-get=1"
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta name="go-import" content="scale.dev/x/migrate git https://codeberg.org/Gusted/mCaptcha.git">
        <meta name="go-source" content="scale.dev/x/migrate https://codeberg.org/Gusted/mCaptcha.git https://codeberg.org/Gusted/mCaptcha.git/tree/main{/dir} https://codeberg.org/Gusted/mCaptcha.git/blob/main{/dir}/{file}#L{line}">                   
    </head>
</html>

Attempt Four

So I was not happy with the amount of configuration needed to get this working. So I decided to just write a system service instead because it seemed like the path of least resistance. So I ran these commends:

$ echo $PATH
/home/vagrant/.asdf/shims:/home/vagrant/.asdf/bin:/home/vagrant/.local/bin:/home/vagrant/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
$ sudo go <gocustomurls_binary> /usr/local/bin # to put in the PATH
$ sudo mkdir -p /var/lib/$USER
$ sudo cp gocustomurls.service /etc/systemd/system/
$ sudo mkdir -p /var/lib/$USER
$ sudo useradd --system --comment 'Go Custom Urls Service' --no-create-home $USER
useradd: failed to reset the lastlog entry of UID 992: No such file or directory
$ sudo passwd -d $USER
passwd: password changed.
$ getent passwd $USER
$USER:x:992:991:Go Custom Urls Service:/home/$USER:/bin/bash
$ sudo cp config.json /var/lib/$USER
$ sudo cp rules.json /var/lib/$USER
$ sudo ls /var/lib/$USER/
config.json  rules.json
$ sudo chmod -R 770 /var/lib/$USER
$ sudo chown -R $USER:$USER /var/lib/$USER
$ sudo systemctl daemon-reload
$ sudo systemctl start gocustomurls.service
$ sudo systemctl status gocustomurls.service
● gocustomurls.service - GocustomUrls. A custom url mapper for go packages!
     Loaded: loaded (/etc/systemd/system/gocustomurls.service; disabled; preset: disabled)
    Drop-In: /usr/lib/systemd/system/service.d
             └─10-timeout-abort.conf
     Active: active (running) since Sat 2024-07-20 06:52:09 UTC; 23s ago
   Main PID: 4020 (gocustomurls)
      Tasks: 6 (limit: 2319)
     Memory: 7.1M (peak: 7.5M)
        CPU: 14ms
     CGroup: /system.slice/gocustomurls.service
             └─4020 /usr/local/bin/gocustomurls -conf /var/lib/gourls/config.json
$ http --body "http://localhost:7070/x/touche?go-get=1"
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta name="go-import" content="scale.dev/x/migrate git https://codeberg.org/Gusted/mCaptcha.git">
        <meta name="go-source" content="scale.dev/x/migrate https://codeberg.org/Gusted/mCaptcha.git https://codeberg.org/Gusted/mCaptcha.git/tree/main{/dir} https://codeberg.org/Gusted/mCaptcha.git/blob/main{/dir}/{file}#L{line}">                   
    </head>
</html>

Attempt Five (Docker)

So this application generates logs and I wanted to view the logs outside the container. So the application logs generated as I run the container would have to have the same user as the user invoking the docker command to avoid me having to use sudo to view it. Based on the above, the sample docker file would be something like this

# syntax=docker/dockerfile:1
FROM golang:1.20-alpine AS build

WORKDIR /app

RUN apk --update add --no-cache git bash make alpine-sdk g++ && \
    git clone https://git.iratusmachina.com/iratusmachina/gocustomurls.git --branch feature/add-log-rotation gurls && \
    cd gurls && make build

FROM alpine AS run

ARG USERNAME
ARG UID
ARG GID
ARG PORT

RUN mkdir -p /home/$USERNAME

COPY --from=build /app/gurls/artifacts/gocustomurls /home/$USERNAME/gocustomurls

RUN addgroup -g ${GID} ${USERNAME} && \
    adduser --uid ${UID} -G ${USERNAME} --gecos "" --disabled-password --home "/home/$USERNAME" --no-create-home $USERNAME && \
    chmod -R 755 /home/$USERNAME && \
    chown -R ${UID}:${GID} /home/$USERNAME

ENV HOME=/home/$USERNAME

WORKDIR $HOME

EXPOSE $PORT

USER $USERNAME

CMD ["sh","-c", "/home/${USERNAME}/gocustomurls", "-conf", "${HOME}/config.json"] # based on this link https://github.com/moby/moby/issues/5509#issuecomment-1962309549

So running these set of commands produces an error:

$ docker build -t testimage:alpine --build-arg UID=$(id -u) --build-arg GID=$(id -g) --build-arg USERNAME=$(whoami) --build-arg PORT=7070 --no-cache .
...
=> => naming to docker.io/library/testimage:alpine
$ docker run -d -p 7070:7070 -v ${PWD}:/home/$(whoami) testimage:alpine
d7f5011b4e7f6317757a3ee8fe76fe710f8fd5ed1ac6a6e5c32ad5393c78f4c0
$ http --body "http://localhost:7070/x/touche?go-get=1"

http: error: ConnectionError: HTTPConnectionPool(host='localhost', port=7070): Max retries exceeded with url: /x/touche?go-get=1 (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f986fc73410>: Failed to establish a new connection: [Errno 111] Connection refused')) while doing a GET request to URL: http://localhost:7070/x/touche?go-get=1

So checking the logs,

$ docker container ls -a
CONTAINER ID   IMAGE              COMMAND                  CREATED          STATUS                        PORTS     NAMES
d7f5011b4e7f   testimage:alpine   "sh -c /home/${USERN"   56 seconds ago   Exited (127) 55 seconds ago             frosty_leakey

$ docker logs d7f5011b4e7f
-conf: line 0: /home//gocustomurls: not found

So I attempted to fix the above error by putting the CMD command in a start.sh file by doing this:

# syntax=docker/dockerfile:1
...
RUN echo "${HOME}/gocustomurls -conf ${HOME}/config.json" > start.sh \
    && chown ${UID}:${GID} start.sh \
    && chmod u+x start.sh

EXPOSE $PORT

USER $USERNAME

CMD ["sh", "-c", "./start.sh" ]

This also produced an error, after building and running the container as shown below:

$ docker logs e9abe2bed4b1
sh: ./gocustomurls: not found

This perplexed me. So I decided to print the environment with printenv and run ls in the start.sh file. So after doing that, building and re-running the container, I got this:

$ docker logs 98dd740dc727
HOSTNAME=98dd740dc727
SHLVL=2
HOME=/home/vagrant
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
total 12K    
drwxr-xr-x    2 vagrant  vagrant       61 Jul 21 21:30 .
drwxr-xr-x    1 root     root          21 Aug  3 14:06 ..
-rw-r--r--    1 vagrant  vagrant     2.4K Aug  3 14:06 Dockerfile
-rw-r--r--    1 vagrant  vagrant      131 Jul 21 21:18 config.json
-rw-r--r--    1 vagrant  vagrant      471 Jul 21 18:40 rules.json

This shows that the gocustomurls binary was being deleted by the time I wanted to run a container from the image. This was confirmed here 12. So I instead used the approach shown here 13. There is also using a data-only volume but that is for another day. So after reading 13 and 14, I came up with new Dockerfile.

# syntax=docker/dockerfile:1.4
FROM golang:1.20-alpine AS build

WORKDIR /app

RUN <<EOF
apk --update add --no-cache git bash make alpine-sdk g++
git clone https://git.iratusmachina.com/iratusmachina/gocustomurls.git --branch feature/add-log-rotation gurls
cd gurls
make build
EOF

FROM alpine AS run

ARG USERNAME
ARG UID
ARG GID
ARG PORT

RUN <<EOF
addgroup --gid ${GID} ${USERNAME}
adduser --uid ${UID} --ingroup ${USERNAME} --gecos "" --disabled-password --home "/home/$USERNAME" $USERNAME
chmod -R 755 /home/$USERNAME
chown -R ${UID}:${GID} /home/$USERNAME
EOF

COPY --from=build /app/gurls/artifacts/gocustomurls /home/$USERNAME/gocustomurls

RUN <<EOF
mkdir -p /var/lib/apptemp
cp /home/$USERNAME/gocustomurls /var/lib/apptemp/gocustomurls
chown -R ${UID}:${GID} /home/$USERNAME
chown -R ${UID}:${GID} /var/lib/apptemp
ls -lah /home/$USERNAME
EOF


ENV HOME=/home/$USERNAME

COPY <<EOF start.sh
    #!/usr/bin/env sh
    printenv
    cd "${HOME}"
    ls -lah .
    if [ ! -f "${HOME}/gocustomurls" ]
    then
        cp /var/lib/apptemp/gocustomurls ${HOME}/gocustomurls
        rm /var/lib/apptemp/gocustomurls
    fi
    ${HOME}/gocustomurls -conf ${HOME}/config.json
EOF

RUN <<EOF
chown ${UID}:${GID} start.sh
chmod u+x start.sh
EOF

EXPOSE $PORT

USER $USERNAME

CMD ["sh", "-c", "./start.sh" ]

I also changed the run command from

$ docker run -d -p 7070:7070 -v ${PWD}:/home/$(whoami) testimage:alpine

to

$ docker run -d -p 7070:7070 -v ${PWD}/otherfiles:/home/$(whoami) testimage:alpine

This allows me to exclude the Dockerfile from the image. I also added journald logging to the run command so I can use journalctl to view the logs. Running with this addition is shown below:

$ docker run --log-driver=journald -d -p 7070:7070 -v ${PWD}/otherfiles:/home/$(whoami) testimage:alpine
$ sudo journalctl CONTAINER_NAME=vigilant_mclaren
Aug 03 14:37:19 fedoramachine 561944ae05ca[1228]: HOSTNAME=561944ae05ca
Aug 03 14:37:19 fedoramachine 561944ae05ca[1228]: SHLVL=2
Aug 03 14:37:19 fedoramachine 561944ae05ca[1228]: HOME=/home/vagrant
Aug 03 14:37:19 fedoramachine 561944ae05ca[1228]: PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Aug 03 14:37:19 fedoramachine 561944ae05ca[1228]: PWD=/
Aug 03 14:37:19 fedoramachine 561944ae05ca[1228]: total 9M     
Aug 03 14:37:19 fedoramachine 561944ae05ca[1228]: drwxr-xr-x    2 vagrant  vagrant       63 Aug  3 14:36 .
Aug 03 14:37:19 fedoramachine 561944ae05ca[1228]: drwxr-xr-x    1 root     root          21 Aug  3 14:29 ..
Aug 03 14:37:19 fedoramachine 561944ae05ca[1228]: -rw-r--r--    1 vagrant  vagrant      131 Jul 21 21:18 config.json
Aug 03 14:37:19 fedoramachine 561944ae05ca[1228]: -rwxr-xr-x    1 vagrant  vagrant     9.3M Aug  3 14:31 gocustomurls
Aug 03 14:37:19 fedoramachine 561944ae05ca[1228]: -rw-r--r--    1 vagrant  vagrant      471 Jul 21 18:40 rules.json

This works. So I wanted to see if I could run this Dockerfile from a non-root, non-login account. After moving the requiste files, I ran the build command

bash-5.2$ docker build -t testimage:alpine --build-arg UID=$(id -u) --build-arg GID=$(id -g) --build-arg USERNAME=$(whoami) --build-arg PORT=7070 --no-cache --progress=plain .
ERROR: mkdir /home/golsuser: permission denied

I am using the --progress=plain option to allow me to use echo during the build process. This error is because docker binary cannot find the .docker directory for this user. Normally, this folder is auto-created when you run docker build. So to remove this error, I ran this instead.

bash-5.2$ HOME=${PWD} docker build -t testimage:alpine --build-arg UID=$(id -u) --build-arg GID=$(id -g) --build-arg USERNAME=$(whoami) --build-arg PORT=7070 --no-cache --progress=plain .

This worked. Now I normally run my dockerfiles using Docker Compose. So a sample compose file for the Dockerfile is shown below:

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - USERNAME=${USERNAME}
        - UID=${USER_ID}
        - GID=${GROUP_ID}
        - PORT=${PORT:-7070}
      labels:
        - "maintainer=iratusmachina"
    image: appimage/gocustomurls
    logging:
      driver: journald
    container_name: app_cont
    ports:
      - "7070:7070"
    volumes:
      - ${PWD}/otherfiles:/home/${USERNAME}

I also wanted to run the compose file using systemd. So the systemd unit file is shown below:

[Unit]
Description=GocustomUrls. A custom url mapper for go packages!
After=docker.service
PartOf=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/var/lib/golsuser
User=golsuser
Group=golsuser
ExecStart=/bin/bash -c "HOME=/var/lib/golsuser USER_ID=$(id -u) GROUP_ID=$(id -g) USERNAME=$(whoami) docker compose --file /var/lib/golsuser/docker-compose.yml up --detach --force-recreate"
ExecStop=/bin/bash -c "HOME=/var/lib/golsuser USER_ID=$(id -u) GROUP_ID=$(id -g) USERNAME=$(whoami) docker compose --file /var/lib/golsuser/docker-compose.yml down -v"
SyslogIdentifier=gurls
StandardError=journal

[Install]
WantedBy=multi-user.target

So running this service produced this output:

$ sudo systemctl start gurls.service
$ sudo systemctl status gurls.service

● gurls.service - GocustomUrls. A custom url mapper for go packages!
     Loaded: loaded (/etc/systemd/system/gurls.service; disabled; preset: disabled)
    Drop-In: /usr/lib/systemd/system/service.d
             └─10-timeout-abort.conf
     Active: active (exited) since Sat 2024-08-03 18:08:46 UTC; 50s ago
    Process: 24117 ExecStart=/bin/bash -c HOME=/var/lib/golsuser USER_ID=$(id -u) GROUP_ID=$(id -g) USERNAME=$(whoami) docker compose --file /var/lib/golsuser/docker-compose.yml up --detach --force-recreate (code=exited, status=0/SUCCESS)
   Main PID: 24117 (code=exited, status=0/SUCCESS)
        CPU: 125ms

Aug 03 18:08:45 fedoramachine gurls[24132]: #17 DONE 0.0s
Aug 03 18:08:45 fedoramachine gurls[24132]: #18 [app] resolving provenance for metadata file
Aug 03 18:08:45 fedoramachine gurls[24132]: #18 DONE 0.0s
Aug 03 18:08:45 fedoramachine gurls[24132]:  Network golsuser_default  Creating
Aug 03 18:08:45 fedoramachine gurls[24132]:  Network golsuser_default  Created
Aug 03 18:08:45 fedoramachine gurls[24132]:  Container app_cont  Creating
Aug 03 18:08:45 fedoramachine gurls[24132]:  Container app_cont  Created
Aug 03 18:08:45 fedoramachine gurls[24132]:  Container app_cont  Starting
Aug 03 18:08:46 fedoramachine gurls[24132]:  Container app_cont  Started
Aug 03 18:08:46 fedoramachine systemd[1]: Finished gurls.service - GocustomUrls. A custom url mapper for go packages!.

$ sudo systemctl stop gurls.service
$ sudo systemctl status gurls.service

○ gurls.service - GocustomUrls. A custom url mapper for go packages!
     Loaded: loaded (/etc/systemd/system/gurls.service; disabled; preset: disabled)
    Drop-In: /usr/lib/systemd/system/service.d
             └─10-timeout-abort.conf
     Active: inactive (dead)

Aug 03 18:08:46 fedoramachine systemd[1]: Finished gurls.service - GocustomUrls. A custom url mapper for go packages!.
Aug 03 18:10:10 fedoramachine systemd[1]: Stopping gurls.service - GocustomUrls. A custom url mapper for go packages!...
Aug 03 18:10:10 fedoramachine gurls[24402]:  Container app_cont  Stopping
Aug 03 18:10:20 fedoramachine gurls[24402]:  Container app_cont  Stopped
Aug 03 18:10:20 fedoramachine gurls[24402]:  Container app_cont  Removing
Aug 03 18:10:20 fedoramachine gurls[24402]:  Container app_cont  Removed
Aug 03 18:10:20 fedoramachine gurls[24402]:  Network golsuser_default  Removing
Aug 03 18:10:20 fedoramachine gurls[24402]:  Network golsuser_default  Removed
Aug 03 18:10:20 fedoramachine systemd[1]: gurls.service: Deactivated successfully.
Aug 03 18:10:20 fedoramachine systemd[1]: Stopped gurls.service - GocustomUrls. A custom url mapper for go packages!.

Conclusion

This was a fun yet frustruating experience in trying to piece together how to run services with systemd. I am glad that I went through it so I can reference it next time. I decided to go with the systemd approach (attempt four). With go artifacts being a single binary, the overhead of managing Docker was not worth it for me. If I wanted to remove the systemd service, I could use the commands from this resource10.