Container Registry & Runtime (Docker Deploys)
Last updated December 03, 2024
Table of Contents
Heroku Container Registry allows you to deploy your Docker images to Heroku. Both Common Runtime and Private Spaces are supported.
If you would like Heroku to build your Docker images, as well as take advantage of Review Apps, check out building Docker images with heroku.yml.
The Heroku container
stack is intended for advanced use-cases only. Unless you have a specific need for custom
Docker images, we recommend using Heroku’s default buildpack-powered build system instead, which
offers automatic base image security updates, language-specific optimisations, and avoids the need to maintain a Dockerfile
.
This article is only applicable to Cedar-generation apps.
Getting started
Make sure you have a working Docker installation (eg. docker ps
) and that you’re logged in to Heroku (heroku login
).
Log in to Container Registry:
$ heroku container:login
Get sample code by cloning an Alpine-based python example:
$ git clone https://github.com/heroku/alpinehelloworld.git
Navigate to the app’s directory and create a Heroku app:
$ cd alpinehelloworld
$ heroku create --stack container
Creating salty-fortress-4191... done, stack is container
https://salty-fortress-4191.herokuapp.com/ | https://git.heroku.com/salty-fortress-4191.git
Build the image and push to Container Registry:
$ heroku container:push web
Then release the image to your app:
$ heroku container:release web
Now open the app in your browser:
$ heroku open
Logging in to the registry
Heroku runs a container registry on registry.heroku.com
.
If you are using the Heroku CLI, you can log in with:
$ heroku container:login
or directly via the Docker CLI:
$ docker login --username=_ --password=$(heroku auth:token) registry.heroku.com
Building and pushing image(s)
Make sure your app’s stack is set to container
via heroku stack:set container
Build an image and push
To build an image and push it to Container Registry, make sure that your directory contains a Dockerfile and run:
$ heroku container:push <process-type>
Pushing an existing image
To push an image to Heroku, such as one pulled from Docker Hub, tag it and push it according to this naming template:
$ docker tag <image> registry.heroku.com/<app>/<process-type>
$ docker push registry.heroku.com/<app>/<process-type>
By specifying the process type in the tag, you can release the image using the CLI. If you would prefer to not specify the process type in the tag, you’ll have to release via the API which uses the image_id
.
Pushing multiple images
To push multiple images, rename your Dockerfiles using Dockerfile.<process-type>
:
$ ls -R
./webapp:
Dockerfile.web
./worker:
Dockerfile.worker
./image-processor:
Dockerfile.image
Then, from the root directory of the project, run:
$ heroku container:push --recursive
=== Building web
=== Building worker
=== Building image
=== Pushing web
=== Pushing worker
=== Pushing image
This will build and push all 3 images. If you only want to push specific images, you can specify the process types:
$ heroku container:push web worker --recursive
=== Building web
=== Building worker
=== Pushing web
=== Pushing worker
Releasing an image
CLI
After you’ve successfully pushed an image to Container Registry, you can create a new release using:
$ heroku container:release web
If you have multiple images, list them:
$ heroku container:release web worker
In an app with multiple process types, if you only release one process type (e.g., heroku container:release web
), all process types will be restarted.
API
curl --netrc -X PATCH https://api.heroku.com/apps/$APP_ID_OR_NAME/formation \
-d '{
"updates": [
{
"type": "web",
"docker_image": "$WEB_DOCKER_IMAGE_ID"
},
{
"type": "worker",
"docker_image": "$WORKER_DOCKER_IMAGE_ID"
}
]
}' \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.heroku+json; version=3.docker-releases"
For the curl --netrc
option to work, you must have previously run heroku login
to populate the API token in your .netrc
file.
Getting a Docker image ID
The docker image values used when releasing an image via the platform API need to be in the format algorithm:hex
. For example:
sha256:4d2647aab0e8fbe92cb0fc88c500eb51661c5907f4f14e79efe8bfbda1f7d159
To get this ID for your image you can run the following command:
$ docker inspect my_image --format={{.Id}}
sha256:4d2647aab0e8fbe92cb0fc88c500eb51661c5907f4f14e79efe8bfbda1f7d159
One-off dynos
If your app is composed of multiple Docker images, you can target the process type when creating a one-off dyno:
$ heroku run bash --type=worker
Running bash on ⬢ multidockerfile... up, worker.5180
$
If the type is not specified, theweb
image is used.
Using a CI/CD platform
Currently, it is not possible to use Heroku CI to test container builds.
If you are using a third party CI/CD platform, you can push images to the registry. First authenticate with the following information:
- Registry URL:
registry.heroku.com
- Username:
your Heroku email address
- Email:
your Heroku email address
- Password:
your Heroku API key
Many CI/CD providers have documentation about how to build and push images to a Docker registry:
Release phase
To use release phase push a Docker image named release
:
$ heroku container:push release
When you release your Docker images, by running heroku container:release
, your release phase process type needs to be specified:
$ heroku container:release web release
Releasing images web,release to your-app-name... done
Running release command...
Migrating database.
If you would like to see streaming logs as release phase executes, your Docker image is required to have curl
. If your Docker image does not include curl
, release phase logs will only be available in your application logs.
Dockerfile commands and runtime
Docker images run in dynos the same way that slugs do, and under the same constraints:
- The web process must listen for HTTP traffic on
$PORT
, which is set by Heroku.EXPOSE
inDockerfile
is not respected, but can be used for local testing. Only HTTP requests are supported. - Network linking of dynos is not supported.
- The filesystem is ephemeral.
- The working directory is
/
. You can set a different directory usingWORKDIR
. ENV
, for setting environment variables, is supported.- We suggest using
ENV
for runtime variables (e.g.,GEM_PATH
) andheroku config
for credentials, so that sensitive credentials aren’t accidentally checked into source code control.
- We suggest using
ENTRYPOINT
is optional. If not set,/bin/sh -c
will be usedCMD
will always be executed by a shell so that config vars are made available to your process; to execute single binaries or use images without a shell please useENTRYPOINT
We strongly recommend testing images locally as a non-root user, as containers are not run with root privileges on Heroku.
Unsupported Dockerfile commands
VOLUME
- Volume mounting is not supported. The filesystem of the dyno is ephemeral.EXPOSE
- WhileEXPOSE
can be used for local testing, it is not supported in Heroku’s container runtime. Instead your web process/code should get the $PORT environment variable.STOPSIGNAL
- The dyno manager will request that your processes shut down gracefully by sending them a SIGTERM signal followed by a SIGKILL signal.STOPSIGNAL
is not respected.SHELL
- The default shell for Docker images is/bin/sh
, you can override withENTRYPOINT
if necessary.HEALTHCHECK
- WhileHEALTHCHECK
is not currently supported, the Heroku Dyno manager automatically checks the health of your running containers.
Testing an image locally
When testing an image locally there are a number of best practices. These best practices are implemented in this example Dockerfile.
Run the image as a non-root user
We strongly recommend testing images locally as a non-root user, as containers are not run with root privileges in Heroku. Immediately before CMD
you can add the following commands to your Dockerfile:
If using Alpine:
RUN adduser -D myuser
USER myuser
If using Ubuntu:
RUN useradd -m myuser
USER myuser
To confirm that your container is running as a non-root user, attach to a running container and then run the whoami
command:
$ docker exec <container-id> bash
$ whoami
myuser
When deployed to Heroku, we also run your container as a non-root user (although we do not use the USER
specified in the Dockerfile).
$ heroku run bash
$ whoami
U7729
Get the port from the environment variable
For testing purposes, we suggest that your Dockerfile
or code read from the $PORT environment variable, for example:
CMD gunicorn --bind 0.0.0.0:$PORT wsgi
When running a Docker container locally, you can set an environment variable using the -e flag:
$ docker run -p 5000:5000 -e PORT=5000 <image-name>
Setting multiple environment variables
When you use heroku locally, you can set config vars in a .env file. When heroku local
is run .env is read and each name/value pair is set in the environment. You can use this same .env file when using Docker:
$ docker run -p 5000:5000 --env-file .env <image-name>
We suggest adding the .env file to your .dockerignore file.
Take advantage of Docker Compose for multi-container applications
If you’ve created a multi-container application you can use Docker Compose to define your local development environment. Learn how to use Docker Compose for local development.
Learn more
- More information on running a Docker image locally is available in Docker’s official documentation.
- Learn more about using Docker Compose for local development.
- See the best practices for writing Dockerfiles guide for tips on how to optimize the build performance and size of your Docker images.
Heroku base images
The Heroku stacks are available as Docker base images for convenience (see each stack’s detail page for Docker image names and tags). However for the fastest build and boot times we strongly recommend that you instead use a smaller less-general-purpose base image, such as one of the official Docker images for your app’s language.
Changing deployment method
Once you deploy your application via Container Registry, the stack is set to container
. This means that your application is no longer using a Heroku-curated stack, but instead your own custom container.
If you no longer wish to use Docker images for your app, and would like to switch back to buildpack-based deploys using the Heroku curated stack, use the heroku stack:set
command to switch to one of the available heroku-*
stacks. See migrating to a different stack for more details.
Known issues and limitations
- Apps using the container stack must be rebuilt/redeployed in order to pick up any security updates and bug fixes to the underlying base Docker image. In contrast Heroku buildpack/slug based deployments receive automatic updates to the underlying stack’s base image, without the need to rebuild/redeploy the app.
- Heroku only supports
x86_64
images. Anunsupported architecture
error returns if you try to upload images built to run on other architectures. You also get anExec format
error if you try to use other architectures, like ARM64, with the Container Runtime. You must ensure your container builder outputsx86_64
images. To ensure the built image is built for Heroku correctly, pass the platform flag to the Docker build command:docker build --platform linux/amd64
. - Review apps are not supported when building Docker images locally and then pushing them to the Heroku Container Registry. To use Docker with Review Apps, you must instead define your app with a heroku.yml manifest, which allows you to build Docker images on Heroku.
- Pipeline promotions are not supported.
- While Docker images are not subject to size restrictions (unlike slugs), they are subject to the dyno boot time restriction. As layer count/image size grows, so will dyno boot time. As such, images with more than 40 layers may fail to start in the Common Runtime. See the best practices for writing Dockerfiles guide for suggestions on how to reduce Docker image size.
- The
Dockerfile
commands listed here are not supported. - Container apps in Private or Shield spaces do not run
.profile
or.profile.d/*
scripts when booting a dyno. - Whiteout files (created when files from an earlier Docker image layer are deleted in a later layer) are not honoured due to the way Docker images are extracted at runtime. On the Common Runtime only, it’s possible to switch to a newer extraction method that resolves this, via
heroku labs:enable runtime-new-layer-extract
. There is currently no workaround for Private Spaces. - The
--exit-code
argument toheroku run
is not supported, unless a workaround is applied. - If an existing app is migrated to the container stack and the app’s process names are changed at the same time as the migration, the dynos for the old processes may persist until they are cleaned up manually. To avoid this, either change the process names before/after the container stack migration (rather than as part of the migration), or else scale down the old processes during the migration.
- If apps using the container stack are built using Heroku’s build system (rather than locally), then the known issues and limitations of heroku.yml container builds also apply.
- The Heroku Container Registry only supports Docker Image Manifest V2, Schema 2 (
application/vnd.docker.distribution.manifest.v2+json
). It doesn’t support other manifest types, such as OCI Image Manifest (application/vnd.oci.image.manifest.v1+json
).