9
4 Comments

My journey from static to systemd to Docker

Disclaimer: This writing is not a tutorial on how to run your software nor is
it dictating how you should do it. It is merely my words on how i have gone
from doing everything manually to keeping things more structured. All while
still trying to keep things simple.

For years i have been managing my own VPS servers on various VPS hosting platforms (digital ocean, linode, amazon ec2, time4vps, openbsd.amsterdam etc). On these, i have been hosting my personal website www.sketchground.dk and various side projects i have been working on over the years. While doing that my way of deploying has evolved.

For static sites i had nginx running under a standard ubuntu installation and reconfigured it manually every time i wanted to deploy a new site. After the initial configuration of nginx, deployments just involved scp'ing files from my local machine ( or builds.sr.ht/github actions/gitlab runners ) when i made changes. Until next time i wanted to put up a new side project. I had to re-remember how to configure things.

However not all side projects are just static sites. In fact it tends to only
be landing pages that are static sites in my projects. The rest of them are
typically written in go(golang.org) and therefore requires a program to run on the server along with a database and other dependencies. Suddenly it is not enough to just copy a bunch of files over and call it a day.

Systemd

Systemd can manage services for you and make sure to restart your program if it crashes, start your program on reboot, etc. It is also the default in most linux distributions today.

The way it works is that you create a systemd service file like this one:

[Unit]
Description=my little project service

[Service]
User=www-data
Group=www-data
WorkingDirectory=/usr/local/my-little-project/data
Restart=on-failure
EnvironmentFile=/usr/local/my-little-project/environment.conf
ExecStart=/usr/local/my-little-project/bin/my-little-project

[Install]
WantedBy=multi-user.target

Then you enable it systemctl enable my-little-program.service and systemctl start my-little-program.service and if you did not make any mistakes in the service file, your program will be running.

However, every time you need to make an update you have to stop the service
first, copy over a new binary and other files needed and start the service
again.
If somehow your new changes didn't work and you want to "roll back" to previous release, you either have the files somewhere else to deploy, or you saved the previous files on the server in some temporary folder before overwriting them.

On top of this. You might also have configured nginx to proxy trafic from nginx webserver to your program.

This works well when you start out. It is quick to get out there and get moving.
However you realize that things has grown organically and you have done a whole lot of editing /etc/nginx.conf, /etc/systemd/system/my-little-program.service and a lot of other files that you have forgotten about.

In my case i also ended up having multiple side projects running on the same VPS because i was too cheap to pay for multiple servers when there was no need for that. This however made my VPS setup much more complicated and much more difficult to remember exactly which changes i had made and what needed to be backed up and moved over to a new VPS.

Docker

Docker helps you having the same environment locally as well as on the server.
It also helps you in having your deployment spec'ed out and therefore makes it
quicker to move and re-establish on new servers.

My deployment consists of building container images and push it to my own
self hosted docker registry. It is not necessary to self host the registry as
there are options out there if you want to pay for the hosting.
These images are built every time i make a commit to my master branch in git
with tags containing the build date and the git commit id.

My "release" step is still manual and consists of logging in to my VPS and pull and run the newest docker image. However i can easily roll back to previous image should i have broken something.

Here is an example Dockerfile to get an idea on how they look:

FROM golang:alpine

COPY ./site /build/site

RUN go build -o /build/bin/my-little-project /build/site/main.go

FROM alpine:latest

COPY --from=0 /build/bin/my-little-project /usr/local/bin/my-little-project

CMD ["/usr/local/bin/my-little-project"]

As you can see, the running/deployment of the code is now written in a file and my host OS on the VPS only needs to have Docker installed. And maybe nginx for proxying if you are running multiple sites on the same machine.

version: '3'
services:
	my-little-project:
		image: 'hub.docker.com/my-little-project:latest'
		environment:
			- WEB_HOST=0.0.0.0:80

Docker swarm

It didn't just stop with plain docker or docker compose for me. I decided to use dockers swarm that is built in and is in my opinion much simpler than kubernetes for when you just want to run small projects.

So my current setup consists of a VPS host that just needs docker installed. The host then has a yaml file in the docker compose format that describes the whole software stack of my projects.
Moving to another VPS then means:

  1. copy over folders that is mounted into the docker containers
  2. copy over yaml file that describes the stack.
  3. run the yaml file on the new box.

I am deliberately only scaling to 1 instance in swarm since i do not want to deal with replicated cluster setup of databases etc. etc. That is way too complex for a small side project. In fact. If i can do with sqlite and just mount that into my 1 instance of the container i am good. Simplicity beats complexity.

Below is an example of my docker compose file for the www.tokenmo.eu i am toying with these days. It uses traefik for proxying and tls(let's encrypt) termination. Beware that ipv6 in docker is not exactly mature so i have temporarily disabled ipv6 which bothers me.

The only real downside for me at the moment is the higher resource cost it takes to run the containers.

Anyways, if you got this far i hope you got something out of it.
Until next time,
Adios!

version: '3'
services:
    traefik:
        image: traefik:2.5
        ports:
            # http
            - '80:80'
            # https
            - '443:443'
            # web ui dashboard (when insecure mode)
            - '8080:8080'
        command:
            - --providers.docker=true
            - --providers.docker.exposedByDefault=false
            - --providers.docker.swarmMode=true
            - --providers.docker.watch=true
            - --entrypoints.web.address=:80
            - --entrypoints.websecure.address=:443
            - --entrypoints.web.http.redirections.entryPoint.to=websecure
            - --entrypoints.web.http.redirections.entryPoint.permanent=false
            - --entrypoints.web.http.redirections.entryPoint.scheme=https
            - --log.level=ERROR
            - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
            - --certificatesresolvers.myresolver.acme.email=my-acme-email
            - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
            - --certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
        volumes:
            - '/var/run/docker.sock:/var/run/docker.sock:ro'
            - "./traefik/letsencrypt:/letsencrypt"
        networks:
            - web

    tokenmo-site:
        image: '127.0.0.1:5000/tokenmo-site:latest'
        deploy:
            labels:
                - traefik.enable=true
                - traefik.docker.network=web
                - traefik.http.routers.tokenmo-site.rule=Host(`tokenmo.eu`) || Host(`www.tokenmo.eu`)
                - traefik.http.routers.tokenmo-site.tls=true
                - traefik.http.routers.tokenmo-site.tls.certresolver=myresolver
                - traefik.http.services.tokenmo-site.loadbalancer.server.port=80
        environment:
            - SITE_HOST=0.0.0.0:80
        networks:
            - web

    tokenmo-pay:
        image: '127.0.0.1:5000/tokenmo-pay:latest'
        deploy:
            labels:
                - traefik.enable=true
                - traefik.docker.network=web
                - traefik.http.routers.tokenmo-pay.rule=Host(`pay.tokenmo.eu`) || Host(`dashboard.tokenmo.eu`)
                - traefik.http.routers.tokenmo-pay.tls=true
                - traefik.http.routers.tokenmo-pay.tls.certresolver=myresolver
                - traefik.http.services.tokenmo-pay.loadbalancer.server.port=80
        networks:
            - web

networks:
    web:
        external: true

on April 4, 2022
  1. 1

    Your journey is similar to mine :) I went one step further and built a product around it: easypanel.io. DM me if you want a demo.

    1. 1

      I'm not currently interested in a demo but good luck with the project! It looks quite neat.

  2. 1

    What's your opinion on using podman? The conventional wisdom seems to be that Rootless containers are safer than containers with root privilege, and so certain people prefer podman over Docker. I don't have experience with podman, so I can't comment on it. But certain people (and maybe they are biased) seem to believe that podman is going to one day replace Docker (and maybe it already is doing so, like I said, I don't have personal experience in this space).

    On paper, it seems to be pretty similar to docker, but podman seems geared towards people who are very comfortable with containers. I'm optimistic about it, and Fedora seems to use it by default, which is also encouraging.

    1. 1

      In general i do not mind Podman and like the fact that it can be run rootless. It's good to have multiple container engines to choose from. Podman seems to do many of the same things that Docker can, however i have not toyed around with it much myself.
      With docker-compose support and a pod concept it seems to on paper also fulfill my needs.

      Whether it's going to replace Docker or not only time will tell. I think there will be a market for both. There might be some political reasoning behind also. Docker having turned into an "open core" business model where Podman seems to be an open source project with no commercial component.

      Docker is well documented and battle tested which might make it easier for people not so well versed in the container world.

      I'd say choose whatever works for you. I am still sticking with Docker as i know that rather well and it does what i need it to do in a simple manner without getting in the way.

      Kubernetes though i would stay away from for simple side projects. The complexity gets too much in the way of just getting your project up and running. Especially if you also want to host it yourself.

Trending on Indie Hackers
Your build-in-public audience is not your market. I learned the difference the slow way. User Avatar 257 comments Most founders don't have a product problem. They have a visibility problem User Avatar 61 comments Day 4: Why I Built a $199 Workspace Nobody Asked For User Avatar 40 comments How to automatically turn customer feedback into high-converting testimonials User Avatar 39 comments Built a "stocks as football cards" thing. 5 days in, my launch tweet got 7 views. What am I missing? User Avatar 34 comments Spent months building LazyEats AI. Spent 1 day realizing I have no idea how to get users. User Avatar 29 comments