Containers, containers, containers.
Not the steve guy from microsoft
TLDR>
podman
is worth the effort, just.
Why Podman?
I was spending a lot of time deploying manually. I liked the idea of one, independent ‘box’ with everything in it. Why not Docker: more a personal preference. Google for real reasons like security, integration, daemonless.
Which Podman?
It’s a pain, but Podman v5 is not available on Ubuntu 22. Podman v4 can be installed and is used here.
I used podman-compose (seperate python in v4). See deets below.
The tools
podman
The key tool. You manage a local store of images, containers, etc. I installed globally on my system.
podman-compose
A python library which takes a YAML file and runs podman
commands. Easier than running directly.
The options
Requirements are:
- self contained, with db, django, django-q seperately startable
- ideally on a container network with a single entry (e.g. 8000)
- debuggable
- not a pain in the ass
Notes of experiments
- podman-compose is helpful
- pods are good as they enable easy networking between containers.
- systemd was the most difficult to be working. See gotcha.
Solution
I settled in the following:
- A pod with containers for postgres (stock image), mydjango (custom image), mydjango-q (same image)
- entrypoint.sh in mydjango image waits for the database, runs migrations and gunicorn
- All setup goes in mydjango-compose.yml which
podman-compose
uses. There is a nice ‘overlay’ system so you can add mydjango-compose-debugging.yml which just changes a few things to enable debugging (see example).
Steps
Create a Containerfile (example below) to generate the Django app:
See example below. Note entrypoint.sh for managing Django specifics.
Use podman
build to generate an image. Tag it if you like with -t
. Note the sudo as we will use this in systemd root (not user).
My understanding is that the built image is the thing that we care about: it is a self-contained application. We can adjust settings when we run it via environment vars, mounts and entry point command. But that’s it: it’s read-only.
sudo podman build -f Containerfile .
Start the pod manually and test:
podman-compose -f podman-compose.yml --in-pod up
podman ps # see the containers
podman pod ls # see one pod
podman exec -it <container> bash # shell in and check
podman logs <logs> # check logs
Install the systemd file at /etc/systemd/system<yourapp>-pod.service
(see example) then:
sudo systemctl daemon-reload
sudo systemctl restart <yourapp>-pod.service
journalctl -f -u <yourapp>-pod.service
Finally, you should be an open port at 8000 on your host machine. Browse it!
In this arrangement, you’d setup nginx seperately to reverse-proxy 80 -> 8000 and manage certificates.
Gotchas
Understand the different terms like image, container, pod.
podman has a seperate rootand user repo. So sudo podman ps
will be different to podman ps
Networking can be tricky. Use podman exec -it <container> bash
to shell into container and check /etc/hosts, if you can access other containers.
Common images are really bare bones. Consider adding iputils-ping, net-tools, dnsutils. See example.
Systemd integration is hard:
- Using systemctl –user (getting service files from .config/…) is good in theory. You can run as local user. Expose a port (e.g. 8000) and have a root process (e.g. ngnix) as a proxy. However, there are various problems:
- networking in rootless mode is different
- raft of XDG and podman user-socket errors
- problems running from systemd compared to command line
- need ‘linger’ settings
- systemctl needs ‘–user’ to see them
- Using systemctl (root) is less security but easier.
- You can’t use [Pod] in systemd files and Quadlets unless you have v5.
Examples
Containerfile: Django Application
# ── builder stage ─────────────────────────────────────────────────────────────
FROM python:3.12-slim AS builder
# system deps for psycopg2 / build tools
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
iproute2 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# install Poetry and project dependencies into the image
COPY pyproject.toml poetry.lock ./
RUN pip install "poetry>=1.8" \
&& poetry config virtualenvs.create false \
&& poetry install --no-root --no-interaction --no-ansi
# ── final stage ───────────────────────────────────────────────────────────────
FROM python:3.12-slim
# runtime deps
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libpq5 \
postgresql-client \
iputils-ping \
net-tools \
dnsutils \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# copy installed packages code
COPY --from=builder /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/
# copy installed scripts
COPY --from=builder /usr/local/bin/ /usr/local/bin/
# copy your source (.containerignore excludes .venv, .git, etc)
COPY . .
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENV PYTHONUNBUFFERED=1
# collect static, migrate, etc
ENTRYPOINT ["/entrypoint.sh"]
EXPOSE 8000
CMD ["gunicorn", "-c", "gunicorn.conf.py", "wayserver.wsgi:application"]
podman-compose.yml
services:
db:
image: docker.io/library/postgres:15-alpine
hostname: db
restart: always
volumes:
- pgdata:/var/lib/postgresql/data:Z # the :Z is for SELinux labeling
env_file:
- .env.development
environment:
POSTGRES_USER: "${DB_USER}"
POSTGRES_PASSWORD: "${DB_PASSWORD}"
POSTGRES_DB: "${DB_NAME}"
web:
build:
context: .
dockerfile: Containerfile
restart: always
ports:
- "8000:8000"
depends_on:
- db
env_file:
- .env.development
environment:
- DB_HOST=db # points to db container
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME} # note 'db' is db container's hostname
volumes:
pgdata:
mydjango-compose-debug.yml
Use with podman-compose like:
podman-compose -f base.yml -f debug.yml
To debug, start the pod (i.e. podman-compose up…) and connect with a remote debugger (e.g. python in vscode) from the same folder (so it can see the source files) to port 5678.
version: "3.8"
services:
web:
image: localhost/wayserver_web:latest
# map your source code to inside the container
# you can also do this will just key folders
bolumes:
- .:/app:Z
ports:
- "5678:5678"
env_file:
- .env.development
command: >
sh -c "
python -m debugpy --listen 0.0.0.0:5678 --wait-for-client \
manage.py runserver 0.0.0.0:8000
"
entrypoint.sh
#!/usr/bin/env sh
#set -e
echo "Waiting for Postgres at ${DB_HOST}:${DB_PORT}…"
until pg_isready -h "$DB_HOST" -p "$DB_PORT" > /dev/null 2>&1; do
printf '.'
sleep 1
done
echo "Postgres is up"
env
# apply migrations & collect static
python manage.py migrate --noinput
python manage.py collectstatic --noinput
# Only load initial_data if the DB is empty (e.g. no users yet)
if [ "$(python manage.py shell -c "from django.contrib.auth import get_user_model; print(get_user_model().objects.exists())")" = "False" ]; then
echo "Loading initial fixture"
python manage.py loaddata /app/fixtures/2025-07-08-dumpdata.json
echo "Fixture loaded"
else
echo "Skipping initial fixture load, users already exist"
fi
# then exec the main process
# use $@ to enable substitution of the defailt
# typically this is gunicorn...
exec "$@" # run the CMD passed to the container
systemd service file
This could be created podman-compose systemd -a create-unit
but I chose to do it manually as I didn’t understand how that tool worked.
[Install]
WantedBy=multi-user.target
[Unit]
Description=WayServer Pod (root)
After=network-online.target podman.socket
Wants=network-online.target podman.socket
[Service]
Type=simple
# Should contain podman-compose.yml and .env
WorkingDirectory=/home/xxxxx/xxxxx/
ExecStart=podman-compose -f podman-compose.yml --in-pod=true up --build
ExecStop=podman-compose down
# Auto-restart if the compose wrapper crashes
Restart=on-failure
RestartSec=60
Makefile
pod: ## Restart the podman container
pc down && pc -f podman-compose.yml up -d
pod-debug: ## Restart the podman container
pc down && pc -f podman-compose.yml -f podman-compose.development.yml -f podman-compose.debug.yml up -d
pod-development: ## Restart the podman container in development mode
pc down && pc -f podman-compose.yml -f podman-compose.development.yml up -d
pod-production: ## Restart the podman container in production mode
pc down && pc -f podman-compose.yml -f podman-compose.production.yml up -d
pod-build:
sudo podman build -t wayserver:latest -f Containerfile .
pod-deploy: ## Deploy the app as a podman pod
sudo mkdir -p /etc/wayserver
sudo chmod 755 /etc/wayserver
sudo cp .env /etc/wayserver/.env
sudo cp podman-compose.yml /etc/wayserver/
sudo cp podman-compose.production.yml /etc/wayserver/
sudo cp deploy/wayserver-pod.service /etc/systemd/system/
sudo systemctl daemon-reload
#sudo systemctl restart wayserver-pod.service
#journalctl -f -u wayserver-pod.service