Managing and deploying containerized applications
Starting a containerized application
- mStudio UI
- CLI
- Terraform
- API
To start a container via the mStudio, follow these steps:
- Navigate to the project that you want to create the container in.
- Select the "Containers" menu item in the sidebar.
- Click the "Create Container" button.
- In the installation wizard, select the container image that should be used to start your application and complete the wizard, by providing the desired configuration regarding environment variables, volumes and network ports.
The internal DNS name of your container will be derived from the name of the container. For example, if you create a container named My container, the internal DNS name will be my-container. You can observe the internal DNS name in the UI after the container has been created.
To start a container via the CLI, you can use the mw container run command:
$ mw container run \
--name my-container \
--env FOO=BAR \
-p 8000:8000/tcp \
my-registry/my-container:latest
See the documentation on the mw container run command for more information.
If you have a Docker Compose compatible file, you can also use the mw stack deploy command to deploy the entire docker-compose.yml file:
$ mw stack deploy
See the documentation on the mw stack deploy command for more information.
To deploy a container using Terraform, you can use the following example:
locals {
nginx_port = 80
}
resource "mittwald_container_stack" "nginx" {
project_id = mittwald_project.example.id
default_stack = true
containers = {
nginx = {
description = "Example web server"
image = "nginx:1.27.4"
// entrypoint and command *must* be specified, even if they are the defaults.
// To dynamically determine the default entrypoint and command, use the
// `mittwald_container_image` data source.
entrypoint = ["/docker-entrypoint.sh"]
command = ["nginx", "-g", "daemon off;"]
// environment = {
// FOO = "bar"
// }
ports = [
{
container_port = 80
public_port = local.nginx_port
protocol = "tcp"
}
]
volumes = [
{
project_path = "/html"
mount_path = "/usr/share/nginx/html"
}
]
}
}
volumes = {
example = {}
}
}
To learn how to deploy a container via the API, read the article "Starting a container".
Starting containers with resource limitations
You can start containers with CPU and memory limits to control the resources that your containerized application can consume. This is useful for ensuring that your containers do not consume more resources than expected, and for optimizing resource allocation across multiple containers.
- CLI
- API
To start a container with resource limits via the CLI, you can use the --cpus and --memory flags with the mw container run command:
$ mw container run \
--name my-container \
--cpus 0.5 \
--memory 512m \
--env FOO=BAR \
-p 8000:8000/tcp \
my-registry/my-container:latest
The --cpus flag specifies the maximum number of CPUs the container can use (e.g., 0.5 for half a CPU, 2 for two CPUs). The --memory flag specifies the maximum amount of memory the container can use (e.g., 512m for 512 megabytes, 1g for 1 gigabyte).
If you have a Docker Compose compatible file, you can also specify resource limits using the standard Docker Compose syntax:
services:
mycontainer:
image: my-registry/my-container:latest
ports:
- "8000:8000/tcp"
environment:
FOO: BAR
deploy:
resources:
limits:
cpus: '0.5'
memory: 512m
Then deploy the stack using the mw stack deploy command:
$ mw stack deploy
See the documentation on the mw container run command for more information.
To start a container with resource limits via the API, use the PUT/ operation with the deploy.resources.limits field in the service definition:
PUT /v2/stacks/{stackId} HTTP/1.1
Host: api.mittwald.de
Content-Type: application/json
{
"services": {
"mycontainer": {
"image": "my-registry/my-container:latest",
"ports": [
"8000:8000/tcp"
],
"environment": {
"FOO": "BAR"
},
"deploy": {
"resources": {
"limits": {
"cpus": "0.5",
"memory": "512m"
}
}
}
}
}
}
/v2/stacks/{stackId}/ The cpus field specifies the maximum number of CPUs the container can use (as a string, e.g., "0.5" for half a CPU). The memory field specifies the maximum amount of memory the container can use (e.g., "512m" for 512 megabytes, "1g" for 1 gigabyte).
Using private registries
If your container image should be loaded from a private registry, you first need to define this registry for the respective project. You can create a registry as follows:
- mStudio UI
- CLI
- Terraform
- API
To create a new container registry via the mStudio, follow these steps:
- Navigate to the project that you want to create the container in.
- Select the "Containers" menu item in the sidebar.
- Switch to the "Registries" tab.
- Click the "Add registry" button.
- Configure registry URL and credentials for the registry.
To create a new container registry via the CLI, use the mw registry create command:
$ mw registry create \
--description "My registry"
--uri registry.my-company.example
--username my-user
When you specify a --username flag for a private registry, the command will interactively prompt for the password. You can also use the --password flag, but keep in mind that the password may show up in your shell history in that case.
See the documentation on the mw registry create command for more information.
To define a container registry using Terraform, you can use the following example:
variable "registry_credentials" {
sensitive = true
type = object({
username = string
password = string
password_version = number
})
}
resource "mittwald_container_registry" "custom_registry" {
project_id = "<project-id>"
description = "My custom registry"
uri = "registry.company.example"
credentials = {
username = var.registry_credentials.username
// password_wo is a write-only attribute, which will not be persisted
// in the state file. You will need to increase password_wo_version
// whenever the password changes.
password_wo = var.registry_credentials.password
password_wo_version = var.registry_credentials.password_version
}
}
When applying your configuration, you can either define the registry credentials in a .tfvars file or use the -var flag to pass them in. The password is a write-only attribute, which will not be persisted in the state file. You will need to increase the password_wo_version whenever the password changes.
$ terraform apply -var registry_credentials='{"username": "username", "password": "password", "password_version": 1}'
To learn how to create or modify a container registry via the API, read the article "Starting a container".
Custom, self-hosted Docker image registry
If no private registry exists or is unavailable for any reason, it is possible to host your own Docker image registry in the mittwald container hosting.
Check this guide for detailed instructions and configuration.
- mStudio UI
- CLI
- API
To create a new container registry via the mStudio, follow these steps:
- Navigate to the project that you want to create the container in.
- Select the "Containers" menu item in the sidebar.
- In the open "Container" tab, click the "Create" button.
- Configure the container as needed, use
library/registry:3image. - Set up environment variables to your liking; check the guide above for details.
To create a new container registry via the CLI, use the mw container run command:
user@local $ mw container run \
--name example-registry \
--volume registry-data:/var/lib/registry \
--publish 5000:5000/tcp \
--create-volumes \
library/registry:3
Environment variables for configuration can be added via --env option, check guide above for configuration options.
See the documentation on the mw container run command for more information.
To learn how to create or modify a container registry via the API, read the article "Starting a container".
The registry set up in this chapter can be used as private registry, so make sure to make registry reachable, e.g. by creating a subdomain in your project pointing towards custom registry.
A custom registry might end up with a domain like docker.p-XXXXXX.project.space,
which can be configured as registry for future container deployments.
Basic auth for self-hosted Docker registry
In order to add basic authentication to our registry we need to prepare credentials which we then deploy into our registry.
First step is to create the credentials file locally:
user@local $ mkdir -p auth
user@local $ docker run \
--entrypoint htpasswd \
httpd:2 -Bbn exampleuser examplepassword > auth/htpasswd
This will create the htpasswd credentials file, which we now have to ship.
To persist the credentials file, we start with creating a new volume for our registry container:
- mStudio UI
- CLI
- API
To create a new container volume via the mStudio, follow these steps:
- Navigate to the project that you have created the registry in.
- Select the "Containers" menu item in the sidebar, then select the registry container.
- In the open "Container" tab, navigate to "Volumes" tab.
- In the "Volumes" tab, click "Create".
- Configure new volume mount point to
/authand click "Save".
To create a new volume via the CLI, use the mw volume create command:
user@local $ mw volume create registry-auth
After volume creation we can attach it to our container via mw container update:
user@local $ mw container update \
c-XXXXXX \
-v registry-auth:/auth \
-v registry-data:/var/lib/registry
To learn how to create or modify a container registry via the API, read the article "Starting a container".
After that, we reconfigure our registry to use new basic auth credentials.
- mStudio UI
- CLI
- API
To finalize self-hosted registry container configuration, follow these steps:
- Navigate to the project that you have created the registry in.
- Select the "Containers" menu item in the sidebar, then select the registry container.
- In the open "Container" tab, navigate to "Environment Variables" tab.
- Use the "Add" button to create environment variables as follows:
REGISTRY_AUTH=htpasswd
REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm
REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd
To add or update environment variables via the CLI, use the mw container update command:
user@local $ mw container update \
c-XXXXXX \
-v registry-auth:/auth \
-v registry-data:/var/lib/registry \
-e "REGISTRY_AUTH=htpasswd" \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd"
Here is a full container run command if you start from scratch or skipped ahead:
user@local $ mw container run \
--name example-registry \
--volume registry-data:/var/lib/registry \
--volume registry-auth:/auth \
--publish 5000:5000/tcp \
--create-volumes \
-e "REGISTRY_AUTH=htpasswd" \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \
library/registry:3
To learn how to create or modify a container registry via the API, read the article "Starting a container".
(Re)create the container with volumes and environment variables set. Then we ship our credentials file, overriding default user credentials ( to be found in logs if needed ).
user@local $ mw container cp -r auth/ example-registry:/
As final step, create a subdomain in your project and point it to registry container.
- mStudio UI
- CLI
- API
To add and link a subdomain in your project, follow these steps:
- Navigate to the project that you have created the registry in.
- Select the "Domains" menu item in the sidebar, then click "Add" -> "Subdomain".
- As subdomain, for example use
docker.p-XXXXXX.project.space. - Select the registry container as target and click "Create".
user@local $ mw domain virtualhost create \
--hostname docker.p-XXXXXX.project.space \
--path-to-container /:<Container-UUID>:5000/tcp
To learn how to create or modify a container registry via the API, read the article "Starting a container".
Now, after domain has been set up, we can start testing. As we have configured basic auth, we should check our registry to actually use it. Build and tag an arbitrary Docker image, then push to new registry:
user@local $ cd path/to/your/project
user@local $ docker build -t <Your Image> .
user@local $ docker tag <Your Image>:latest docker.p-XXXXXX.project.space/<Your Image>
user@local $ docker push docker.p-XXXXXX.project.space/<Your Image>:latest
Your push must be rejected due to missing basic auth credentials:
user@local $ docker push docker.p-XXXXXX.project.space/<Your Image>:latest
...
push access denied, repository does not exist or may require authorization: authorization failed: no basic auth credentials
If it worked without errors, your setup is incomplete and no authentication is active! Now log in:
user@local $ docker login docker.p-XXXXXX.project.space
Retry docker push, which now works after successful login!
Congratulations, you have successfully created your own private, self-hosted Docker registry.
Depending on your project structure and use case, one generic Docker registry in a separate project,
or one registry per project can be created and used.
Deployment strategies
When using containers for deployment, you will typically roll out new versions of your application by creating a new container image and deploying it to the same container stack. This is a common practice in containerized environments, as it allows for easy versioning and rollback.
There are two variants of this approach:
- Immutable deployment: Each new version of the application is deployed to a new container image, and the old image is kept for rollback purposes. This is the most common approach in containerized environments.
- Mutable deployment: The same container image (in the easiest case, the
latesttag) is used for all versions of the application, and each new version replaces the old one.
Both of these strategies can be implemented using mStudio containers. The following sections describe how to implement them.
Pushing a new tag for each release
Using this strategy, you will create a new container image for each release of your application. Using docker commands, this might look like this (the v1.0.1 tag is just an example for a tag that might increase its version number with each release):
docker build -t my-registry/my-container:v1.0.1 .
docker push my-registry/my-container:v1.0.1
After the image is built, you can deploy it to your container stack.
- CLI
- Terraform
- API
To update the container image of an existing container, use the mw container update command:
$ mw container update \
--image my-registry/my-container:v1.0.1 \
my-container
When you are using Terraform to deploy your containerized application, you can use a Terraform variable to set the image tag. This allows you to easily change the image tag for each release:
variable "image_tag" {
type = string
}
resource "mittwald_container_stack" "my_application" {
project_id = mittwald_project.example.id
default_stack = true
containers = {
mycontainer = {
image = "my-registry/my-container:${var.image_tag}"
// ...
}
}
}
When running terraform apply, you can set the image_tag variable to the desired version:
terraform apply -var="image_tag=v1.0.1"
Using the API, you can use the PATCH/ operation to replace the image of the container stack with the new image.
PATCH /v2/stacks/{stackId} HTTP/1.1
Host: api.mittwald.de
Content-Type: application/json
{
"services": {
"mycontainer": {
"image": "my-registry/my-container:v1.0.1"
}
}
}
/v2/stacks/{stackId}/ After updating the image, the container will need to be explicitly recreated to apply the changes. This can be done using the POST/ operation.
Updating a mutable tag
Using this strategy, you will typically update the same container image tag over and over again. Using docker commands, this might look like this:
docker build -t my-registry/my-container:latest .
docker push my-registry/my-container:latest
By default, container images are not automatically updated when the image tag is not changed. You can use the POST/ operation or the mw container recreate --pull CLI command to pull the latest image for the container stack. This will update the image tag to the latest version.
Using volumes
To manage persistent data in your containerized application, you can use volumes. Volumes are a way to store data outside of the container, so that it is not lost when the container is stopped or removed.
You can use two different types of volumes:
- The project volume is a volume that is created for each project and can be accessed by all containers and all managed apps in that project. This is useful for sharing data between containers and apps.
- You can also declare volumes as part of a stack. These are bound to the container stack and are not accessible from other stacks. This is useful for storing data that is only needed by a specific container stack.
Using the project volume
To use the project volume, use a volume declaration like this:
PUT /v2/stacks/{stackId} HTTP/1.1
Host: api.mittwald.de
Content-Type: application/json
{
"services": {
"mycontainer": {
"image": "my-registry/my-container:v1.0.1",
"volumes": [
"/home/p-XXXXX/html:/var/www"
]
}
}
}
/v2/stacks/{stackId}/ Declaring volumes in stacks
To declare a volume within the stack, use a volume declaration like this:
PUT /v2/stacks/{stackId} HTTP/1.1
Host: api.mittwald.de
Content-Type: application/json
{
"services": {
"mycontainer": {
"image": "my-registry/my-container:v1.0.1",
"volumes": [
"myvolume:/var/www"
]
}
},
"volumes": {
"myvolume": {}
}
}
/v2/stacks/{stackId}/ Backup & recovery
All volumes (both project volumes and stack volumes) are backed up automatically as part of the project backup. This means that you can restore your data in case of a failure or data loss.
However, a simple filesystem backup might not be sufficient for all application workloads. For example, if your application is using a database, you should ensure that the database is in a consistent state before taking a backup. This can be done by using the database's built-in backup functionality or by using a third-party backup solution.
Network connectivity between containers and apps
Managed applications and containers are connected to the same network. This means that you can access managed applications from your containers and vice versa. The hostname of the container is the map key of the container in the stack; if you created the container via the GUI, the hostname is derived from the name of the container. For example, if you create a container named My container, the internal DNS name will be my-container. You can observe the internal DNS name in the UI after the container has been created.
To make a container port accessible from within your project, you can use the ports property in the container declaration. This will create a port mapping between the container port and service that is exposed within your hosting environment. For example, to make port 80 of the container accessible for other workloads running in the same project (be it other containers, or managed apps), you can use the following declaration:
PUT /v2/stacks/{stackId} HTTP/1.1
Host: api.mittwald.de
Content-Type: application/json
{
"services": {
"mycontainer": {
"image": "my-registry/my-container:latest",
"ports": [
"80:80/tcp"
]
}
}
}
/v2/stacks/{stackId}/ Making containers accessible from the internet via HTTP
In order to make a container HTTP port accessible from the internet, you need to define an Ingress resource which maps to the given container.
- CLI
- Terraform
- API
To connect an ingress resource to a container, use the mw domain virtualhost create command with the --path-to-container flag:
mw domain virtualhost create --hostname domain.example --path-to-container /:[container-uid]:80/tcp
The --path-to-container flag should contain three values, separated by colons:
- The URL path that should be mapped to the container.
- The container UID.
- The port (in the format
portnumber/protocol, so for example80/tcp) that the container is listening on.
Use a mittwald_virtualhost resource to create an Ingress resource for your container. The following example shows how to create an Ingress resource for a container:
locals {
nginx_port = 80
}
resource "mittwald_container_stack" "nginx" {
project_id = mittwald_project.example.id
default_stack = true
containers = {
nginx = {
description = "Example web server"
image = "nginx:1.27.4"
ports = [
{
container_port = 80
public_port = local.nginx_port
protocol = "tcp"
}
]
/* ... */
}
}
}
resource "mittwald_virtualhost" "nginx" {
hostname = "your-domain.example"
project_id = mittwald_project.test.id
paths = {
"/" = {
container = {
container_id = mittwald_container_stack.nginx.containers.nginx.id
port = "${local.nginx_port}/tcp"
}
}
}
}
POST /v2/ingresses HTTP/1.1
Host: api.mittwald.de
Content-Type: application/json
{
"hostname": "some-hostname.example",
"projectId": "<project-id>",
"paths": [
{
"path": "/",
"target": {
"container": {
"id": "<container-id>",
"portProtocol": "80/tcp"
}
}
}
]
}
/v2/ingresses/