Skip to main content

Managed Service Podman

Podman is a daemonless container engine for developing, managing, and running OCI containers on Linux. On our managed servers, you can run containers in rootless mode.

Because the syntax is similar to Docker in many cases, you can add the following alias for convenience:

alias docker=podman

Keep in mind that Podman is not a drop-in replacement for Docker in every scenario. Command syntax and behavior can differ, especially across Podman versions.

For full upstream documentation, see: https://podman.readthedocs.io/en/latest/index.html

Cleanup Jobs

On our managed Podman service, we create two cleanup jobs by default:

  • One runs during the night from Sunday to Monday and removes unused images.
  • One runs during the night before the first day of each month and removes unused volumes.

These jobs help keep Podman's disk usage under control and reduce the risk of running out of disk space.

If you want to change these cleanup jobs, open a support ticket and we can adjust them to your needs.

Search, Pull, and List Images

Search remote registries for images:

podman search <search_term>

Filter the search results:

podman search ghost --filter=is-official

Pull an image locally:

podman pull docker.io/library/ghost

List local images:

podman images
note

Podman can search multiple registries. We recommend using the fully qualified image name, for example docker.io/library/ghost instead of ghost, to make sure you pull the expected image.

Run a Container

The following example starts a Ghost container. It uses development mode (-e NODE_ENV=development), so Ghost uses its built-in SQLite database instead of requiring an external MySQL database.

podman run --detach=true --tty --name ghost-cms -p 8080:2368/tcp -e NODE_ENV=development docker.io/library/ghost
note

The -d or --detach=true option starts the container in detached mode and prints the container ID after startup.

The -t or --tty option allocates a pseudo-TTY, which is useful for interactive use cases.

The -p 8080:2368/tcp option publishes Ghost's web server on TCP port 2368 inside the container as TCP port 8080 on the host.

Use --detach=true and --tty instead of -d and -t if you want to generate a Quadlet later with Podlet. Podlet parses the stored podman run command more strictly than Podman itself.

Show Running Containers

Use podman ps -a to list created and running containers:

$ podman ps -a

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5728ad900bc4 docker.io/library/ghost:latest node current/inde... 4 hours ago Up 4 hours ago 0.0.0.0:8080->2368/tcp gifted_edison

Attach to a Running Container

Use the CONTAINER ID from podman ps to attach to a running container:

$ podman attach b3376ff455a0

[2020-06-10 09:17:15] INFO "GET /" 200 512ms

Test the Running Container

The container is now reachable on port 8080 on the host.

You can verify that Ghost is responding correctly with curl:

$ curl -s localhost:8080 | grep "og:site"
<meta property="og:site_name" content="Ghost" />

Publish the Port with nine-manage-vhosts

If you already use managed Nginx or Apache2, you can publish your containerized application with nine-manage-vhosts and terminate TLS with Let's Encrypt.

Create the virtual host:

sudo nine-manage-vhosts virtual-host create testdomain.org --template=proxy --template-variable=PROXYPORT=8080

Register the Let's Encrypt client:

sudo nine-manage-vhosts certificate register-client

Create the certificate and switch the vhost to the HTTPS proxy template:

sudo nine-manage-vhosts certificate create --virtual-host=testdomain.org
sudo nine-manage-vhosts virtual-host update testdomain.org --template=proxy_letsencrypt_https --template-variable=PROXYPORT=8080

If you use Apache and want automatic HTTP-to-HTTPS redirects, use the proxy_letsencrypt_https_redirect template.

Compose Files

Podman can run Compose files. The command you use depends on the Ubuntu version.

On Ubuntu Noble and Resolute, use podman compose as the default command for Compose files. If you still have legacy workflows that rely on Docker-compatible commands, docker compose is available as a fallback when podman-docker is installed.

podman compose -f docker-compose.yaml up

If podman-docker is installed on your server, the following fallback command also works:

docker compose -f docker-compose.yaml up

Example Compose File

The following example runs Ghost with a Compose file. It uses the default rootless network backend, pasta on Resolute and slirp4netns on Jammy and Noble, so no explicit network mode is required in the Compose file.

This example connects Ghost to a MySQL database that already runs on the host and is named nmd_ghost. To reach a host database from a rootless container, connect to host.containers.internal instead of localhost. If the database listens only on the host loopback, make sure the network backend is configured accordingly as described in Connecting to a Database on the Host.

Create your docker-compose.yaml file:

# This example configures Ghost to use a local MySQL database running on the host.

services:
ghost:
image: docker.io/library/ghost
restart: always
ports:
- 8080:2368
environment:
# see https://docs.ghost.org/docs/config#section-running-ghost-with-config-env-variables
database__client: mysql
database__connection__host: host.containers.internal
database__connection__user: nmd_ghost
database__connection__password: EeNae5xaoapoh5RoDah1muwu
database__connection__database: nmd_ghost
# this url value is only an example and is likely wrong for your environment
url: http://testdomain.org

Start the Compose file as follows:

podman compose -f docker-compose.yaml up

Add -d if you want to run the Compose stack in the background.

Start Containers Automatically with systemd

We recommend creating a systemd user service so the container starts automatically after a reboot. The exact workflow depends on the OS version.

Although newer Podman versions still support podman generate systemd, we recommend Quadlets on Ubuntu Noble and Resolute because they produce clearer and easier-to-control systemd units.

Quadlet is a format for Podman-managed systemd units that is built into Podman 4.4 and later. A Quadlet file ends with .container, .image, .volume, .network, or .kube and is typically stored in ~/.config/containers/systemd/ for rootless services.

Quadlet files use the same general structure as regular systemd unit files. Standard sections such as [Service] and [Install] are passed through to systemd, while Podman-specific sections such as [Container] describe how the final podman run command should be generated.

For upstream details, see the Podman Quadlet documentation.

If Podlet is available on your server, you can use it to generate a ready-to-use Quadlet file from an existing container.

Create the container with a stable name and all the environment variables, volume mounts, and published ports that you want Podlet to capture:

podman run --detach=true --tty --name ghost-cms \
-e NODE_ENV=development \
-e database__connection__filename=/var/lib/ghost/content/data/ghost.db \
-v some-ghost-data:/home/www-data/ghost \
-p 8080:2368/tcp \
docker.io/library/ghost

Then generate the Quadlet file:

podlet --unit-directory --install --overwrite generate container ghost-cms
Wrote to file: /home/www-data/.config/containers/systemd/ghost-cms.container
note

Use podman ps and podman pod ps to list container and pod names.

The Quadlet file is written to ~/.config/containers/systemd/:

cat ~/.config/containers/systemd/ghost-cms.container
[Container]
Environment=NODE_ENV=development database__connection__filename=/var/lib/ghost/content/data/ghost.db
Image=docker.io/library/ghost
PodmanArgs=--tty
PublishPort=8080:2368/tcp
Volume=some-ghost-data:/home/www-data/ghost

[Install]
WantedBy=default.target

Edit the file as needed. Then reload the user systemd instance:

systemctl --user daemon-reload
note

systemctl --user daemon-reload does not create a separate static unit file. Quadlet files are converted into generated units by a systemd generator.

Because these generated services are transient, they cannot be enabled with systemctl --user enable. Instead, systemd applies the supported keys from the Quadlet file's [Install] section during generation, which is why WantedBy=default.target still enables autostart on reboot.

Start the service:

systemctl --user start ghost-cms.service
systemctl --user status ghost-cms.service
● ghost-cms.service
Loaded: loaded (/home/www-data/.config/containers/systemd/ghost-cms.container; generated)
Active: active (running) since Wed 2024-12-04 13:39:10 CET; 1s ago
Main PID: 540254 (conmon)
Tasks: 25 (limit: 3337)
Memory: 80.8M (peak: 80.8M)
CPU: 1.100s
CGroup: /user.slice/user-33.slice/user@33.service/app.slice/ghost-cms.service
├─libpod-payload-805ff2ecbcaa737f9b969dd21323b0115fb3c46c7800ab88563663a1e41341b8
│ └─540256 node current/index.js
└─runtime
└─540254 /usr/bin/conmon --api-version 1 -c 805ff2ecbcaa737f9b969dd21323b0115fb3c46c7800ab88563663a1e413>
note

Podlet can also generate Quadlets from Podman commands or Compose files.

For full documentation, see the Podlet README.

Set Resource Limits

You can limit container resources either with Podman options such as --cpus or --memory or with systemd settings such as CPUWeight or MemoryMax. For more details, see our resource limits documentation.

Networking

Modes (pasta, slirp4netns, host)

The default rootless network backend depends on the Podman version. Since Podman 5.0, the default is pasta; before that, it is slirp4netns.

On our managed servers, Ubuntu Resolute (Podman 5.7) therefore defaults to pasta, while Ubuntu Jammy (Podman 3.4.4) and Ubuntu Noble (Podman 4.9.3) default to slirp4netns.

Containers in the same Podman pod share the same network namespace, so they share the same IP address, MAC address, and published ports and can always reach each other through localhost.

You can also use --network=host. In host mode, the container shares the host's network namespace and does not get its own IP address. This is the simplest way to reach services on the host, but it removes network isolation.

Connecting to a Database on the Host

Inside a rootless container, localhost and 127.0.0.1 refer to the container itself, not to the host. An error such as connect ECONNREFUSED 127.0.0.1:3306 usually means that the application is trying to reach a database on its own loopback interface.

To reach a database on the host, connect to host.containers.internal. Podman usually adds that hostname automatically to the container's /etc/hosts. The details depend on the network backend:

Resolute defaults to pasta. In the default Podman configuration, host.containers.internal resolves to 169.254.1.2 because Podman passes --map-guest-addr 169.254.1.2 to pasta.

In most cases, connecting to host.containers.internal is enough:

podman run -dt -p 8080:2368/tcp \
-e database__connection__host=host.containers.internal \
docker.io/library/ghost

If you override pasta defaults in ~/.config/containers/containers.conf, make sure the configuration still provides --map-guest-addr 169.254.1.2, for example:

[network]
pasta_options = ["--map-guest-addr", "169.254.1.2"]
note

Alternatively, use --network=host so that localhost:3306 inside the container points directly to the host database. This is simpler, but it removes network isolation.

Rootless Mode

Our containers run rootless. Podman creates the rootless networking automatically.

Port Publishing

Rootless containers can publish only unprivileged ports on the host. Ports below 1024 are privileged and therefore cannot be published directly by a rootless container.

The application can still listen on any port inside the container. What matters is that you publish an unprivileged host port. In this example, Ghost listens on port 2368 inside the container and is published on port 8080 on the host:

podman run -dt -p 127.0.0.1:8080:2368/tcp -e NODE_ENV=development docker.io/library/ghost

Use -P if you want Podman to assign a random free host port automatically.

Check the published ports:

$ podman port -a

c0194f22266c 2368/tcp -> 127.0.0.1:8080

Container-to-Container Communication

There are several ways for rootless containers to communicate.

If both containers run in the same Podman pod, they can reach each other directly through localhost.

If they run separately, the simplest approach is often to use published ports on the host:

podman ps

Show the published ports and the host IP address:

podman port <container_id>
ip a

Start another container and connect to the published port through the host IP:

podman run --rm docker.io/curlimages/curl -s <HOST_IP_ADDRESS>:8080

Volumes: Mount Volumes and Directories into a Container

To persist data, either write it to an external service such as a database or mount a volume or directory into the container with the -v volume flag.

The two main persistence options are:

  • Bind mounts
  • Named volumes

Both survive container recreation. As a rule of thumb, use a named volume when Podman should manage the data location for you, and use a bind mount when the container needs a specific file or directory from the host.

note

Podman also supports non-persistent mounts such as in-memory tmpfs mounts and overlay mounts with the :O option, whose changes are discarded when the container stops.

Bind Mounts

A bind mount maps an existing host path into the container, for example -v /home/user/config:/etc/app/config. This is useful for configuration files or content that you also want to edit directly on the host.

Because the containers run rootless, container UIDs and GIDs are mapped to subordinate IDs on the host. Files written through a bind mount may therefore be owned by a mapped UID that your normal user cannot remove directly. Remove them with:

podman unshare rm -r ${bind_mount_dir}

Named Volumes

Named volumes are managed by Podman and can be listed, inspected, and removed with the Podman CLI, for example podman volume ls, podman volume inspect, and podman volume rm.

They are stored under ~/.local/share/containers/storage/volumes/. Because Podman manages their location and lifecycle, named volumes are the recommended default for application data such as databases.

Mount Options

Append options after a colon, for example -v some-data:/data:ro. The most useful options on a managed server are:

  • :ro mounts the volume read-only, which is useful for configuration or shared reference data.
  • :U recursively changes the ownership of the source to the UID and GID used by the container process. This can fix permission errors, but it walks the full directory tree and can therefore be slow on large volumes.
note

Volume contents are not guaranteed to be in a consistent state, for example when they belong to a running database. See Backup for recommendations.

Backup

All volume data is included in the managed backup of your server. Even so, we recommend creating application-level backups as well for two reasons:

  • You cannot restore individual files or directories from a volume backup, only the whole volume.
  • Depending on the workload, for example a running database, the volume contents may not be in a consistent state at backup time.

We therefore recommend creating regular backups of important application data, for example database dumps, and writing them to a mounted volume. Those files are then backed up by the regular backup routine for your server.