Anders är en webbutvecklare och hårdrockare som gillar brädspel, kaffe och öl.

Minimum viable setup för webbprojekt i containers på VPS

Ett av mina favorit-memes för containers är denna:

Docker poison meme

Allvaret bakom skämtet är att containers inte alls är viktigt. Att deploya till VPS:er man själv är ensam ägare till kräver sällan den typen av investering. Att deploya via SFTP eller git post-receive hooks räcker längre än man kan tro, och det är snarare ansible eller något motsvarande som borde användas om man vill göra livet lättare för sig.

Med detta sagt så hade jag ett projekt där det passade att labba litet med detta, så jag ställde mig själv frågan: vad är the minimum viable setup för att sköta deployer med containers istället för källkod?

Min VPS är minimalt confad och körde innan detta projekt i stort sett bara en nginx med en wwwroot innehållande statisk HTML (denna webbplats). SSL-certifikaten hanteras av Certbot.

Vinsterna med containers i mitt fall är uppenbara.

  • Jag slipper installera mer paket (för Python, Postgres, Elixir, Node.js eller vad jag nu kodar projekt i) på min VPS.
  • Jag slipper förlita mig på git eller sshd för att kunna göra en deploy.
  • Jag slipper bli bromsad av att Ubuntu server, som jag har som operativsystem på min VPS, inte har samma versioner av saker som min lokala devmiljö.

Projektet jag ville driftsätta är skrivet i Django, och kräver således Python 3 och SQLite (då jag inte har behov av musklerna av en "riktig" databas).

Jag vill vidare köra containern bakom nginx, med SSL-certifikat som hanteras av Certbot.

Det här är de steg jag vidtog för att få upp allt.

1/7: Bygg en container av projektet

En container, oavsett om den byggs med Docker eller Podman, definieras enklast med en Dockerfile.

Det här är en mycket förenklad uppsättning, där en enda container behövs för att köra hela applikationen. Databasen ligger i /app/data och görs tillgänglig som en volym för att kunna backupas och leva kvar mellan deployer.

FROM python:3 as deps
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DEBUG=0
WORKDIR /app
COPY requirements.txt /app/
RUN pip install -r requirements.txt

FROM deps AS source

COPY ./src /app/

FROM source AS db

RUN mkdir -p /app/data VOLUME /app/data RUN ./manage.py migrate

FROM db AS finished

EXPOSE 13371/tcp EXPOSE 13371/udp

CMD waitress-serve --port=13371 example:wsgi.application

Det är också en god idé att skapa filen .dockerignore för att inte skicka över mer än absolut nödvändigt i containern, och därmed hålla dess storlek på en minimal nivå.

För detta projekt är det primärt git-saker och python-cacher som berörs.

pycache
venv
.git
.github
.gitignore
README
todo.txt
*.sqlite3

För att testa om containern funkar, bygg en avbild ().

docker build .

Det ska sedan gå att gå in på http://localhost:13371 och få en Django-app visad.

För referens, så är nedanstående en startplatta för framtida, liknande setups.

FROM some-base:v as deps

WORKDIR /app

install dependencies

FROM deps AS source

COPY ./src /app/

FROM source AS finished

Expose necessary ports

EXPOSE 13371/tcp EXPOSE 13371/udp

CMD to start application

CMD start-app.sh

2/7: Gör avbilder av containern tillgänglig för VPS

Det finns flera strategier att publicera container-avbilder, de flesta sätt kostar ingenting.

Jag väljer aktivt bort alternativ som tvingar mig att göra imagen publik, då jag i detta projekt inte är intresserad av att ge andra åtkomst. Det gör att exempelvis Docker hub inte är ett alternativ.

Om koden ligger på Github, kan Github Actions automatisera bygget och publicera containern på Github Packages. Då kan VPS:en hämta senaste versionen av containern med docker pull. Valet finns också att göra källkoden och container-avbilden privata, alltså ej synlig för andra.

  • Fördelar: inget extra utöver Docker eller Podman krävs på på VPS.
  • Nackdelar: beroende av Github för att deploya saker, vilket kan kännas obekvämt.

Det går även att sätta upp ett bare git repo på VPS:en, och starta ett eget bygge av containern med en post-receive hook.

  • Fördelar: Inget beroende av Github, Docker Hub eller annan hosting av container-avbilder.
  • Nackdelar: Kräver mer setup och att git installeras på VPS. Om diskutrymme är kritiskt så behövs även schemalagda städrutiner för gamla container-avbilder skapas och konfigureras.

Jag valde alternativ 1, med privat repo och privat package. Det kan dock ändras om Github bestämmer sig för att jag inte kan göra det gratis i framtiden.

Det finns en Github Action färdig och klar för att bygga och publicera docker images, så använd denna.

3/7: Installera Docker (eller Podman) på VPS

OBS! Min VPS kör Ubuntu Server 20.04 LTS, och på denna finns inte Podman som alternativ. Podman går precis lika bra för vad som skrivs om här, och rekommenderas varmt som ersättare av flera bra skäl.

Nginx och Certbot är på plats sedan tidigare på VPS:en, och det är bara Docker CE som behövs.

apt-get update
apt-get install docker-ce

Dubbelkolla så att docker kör:

systemctl status docker

4/7: Konfigurera brandvägg på VPS

I det är exemplet kommer containern att exponera port 13371 till HTTP(S). Denna ska inte vara exponerad för extern trafik utan bara för nginx, och det finns flera sätt att konfigurera detta. Det mest rättframma sättet, och även det säkraste, är att aktivera en brandvägg på VPS:en.

Ubuntu Server kör ufw, så jag tillåter SSH, HTTP och HTTPS och nekar allt annat. För Säkerhets skull stänger jag även av 13371, den port som containern använder.

ufw default deny incoming
ufw allow in ssh
ufw allow in http
ufw allow in https
ufw deny in 13371
ufw enable

Docker struntar tyvärr i detta eftersom egna iptables tillämpas, så för att få Docker att respektera ufw krävs litet config.

Lägg till följande i /etc/docker/daemon.json:

"iptables": true,

Finns inte filen, skapa den istället:

cat <<END > /etc/docker/daemon.json
{"iptables":true}
END

Inaktivera även iptables för dockerd genom att lägga till --iptables=false i DOCKER_OPTS.

echo 'DOCKER_OPTS="--iptables=false"' >> /etc/default/docker

Starta sedan om Docker.

systemctl restart docker

5/7: Starta container-avbilden på VPS

Hämta eller bygg en avbild av containern beroende på strategin som valts innan. För att hämta ser det ut såhär:

docker login ghcr.io
docker pull ghcr.io/madr/example-app:main

Mer info om detta finns i dokumentationen för Github Pages.

Starta därefter container-avbilden. Genom att ge ett namn på den körande container-avbilden blir den enklare att hantera med Dockers cli. Montera också volymen för /app/data så att sqlite-databasfilen blir persistent mellan deployer. Använd en absolut sökväg så att volymen blir lätt att hitta. Som package används här example-app - den riktiga paketnamnet är ett annat.

docker run -d --name example -it 
-p 13371:13371
-v /root/exampledb:/app/data
ghcr.io/madr/example-app:main

(Docker kör som root, inte helt optimalt men jag låter det passera för nu.)

För att deploya en annan version som ersätter existerande version, ser det ut såhär. Som package används här example-app - den riktiga paketnamnet är ett annat.

docker pull ghcr.io/madr/example-app:main
docker stop example
docker rm example
docker run -d --name example -it 
-p 13371:13371
-v /root/exampledb:/app/data
ghcr.io/madr/example-app:main

Notera här att jag här valt att bort den gamla versionen direkt. Att rollbacka till en äldre version krävs alltså en ny pull!

6/7: Exponera containern för webbtrafik med nginx

Tanken är att köra Django inuti containern på en godtycklig port, och att bara köra en reverse proxy för nginx.

Som domän används här example.madr.se - den riktiga domänen är en annan.

cat <<END /etc/nginx/sites-available/example.madr.se
server {
server_name     example.madr.se;
root            /srv/example.madr.se;

location / {
    proxy_ssl_verify on;
    proxy_pass http://example.madr.se:13371;
}

} END

För att se så att allt lirar, validera config:

service nginx configtest

Om allt gick bra, aktivera siten genom att symlänka.

ln -s /etc/nginx/sites-available/example.madr.se /etc/nginx/sites-enabled/example.madr.se

Och starta om nginx.

systemctl restart nginx

7/7: Hantera SSL-certifikat för projektet med Certbot

Hisspitchen för Certbot är att den automatiserar uppskapandet av SSL-certifikat för att kunna köra dina självhostade webbplatser med HTTPS. Let's Encrypt står för utfärdandet av certifikat, så det kostar ingenting.

Är Certbot inte installerat, så finns en bra guide.

Med Certbot på plats, kör detta kommando för att generera certifikat, populera nginx-configen med nödvändig config och schemalägga automatisk förnyelse av certifikat. Scriptet startar även om nginx.

certbot --nginx -d example.madr.se

Klart! Nu ligger docker-containern uppe i produktion, där den lyssnar på port 80 och är krypterad med SSL.

Nästa steg

Detta är minimum viable setup, så det finns flera saker som är bra att göra.

  • Sätt upp ett script som hämtar hem ny version av container-avbilder, och deploya denna till produktion. Kan till och med placeras i en crontab för att få Continuous Delivery.
  • Städa upp bland hämtade container-avbilder, med ett cronjob eller liknande.
  • Som en akademisk övning, testa att sätta upp 3 körande containers och använd nginx som lastbalanserare.
  • Kika på att köra Docker som nonroot, eller byt till Podman som gör detta som default.