Published Nov 4, 2024
Updated Jun 12, 2025
Spin up your first containers using Docker Run and Docker Compose!
Written by Andrew Flores
Welcome to the world of Docker and containerization! Containers are incredibly useful for making applications portable so they can easily be started up on any computer and behave the exact same way on each. Docker is far and away the most widespread containerization platform, although there are alternatives such as Podman that are gaining popularity in recent years.
If you haven’t installed Docker on your system yet, go check out my Docker Installation Guide before continuing here.
There are two primary ways you can spin up Docker containers. Each has its own benefits and I will elaborate more below.
Quick Start
The first container Docker’s documentation suggests running is “hello-world”. This is typically run to ensure that Docker commands can be run in your terminal, that images can be pulled from Docker’s own container registry (hub.docker.com), and that there are no unforeseen errors along the way.
docker run hello-world
Run this command in your terminal to see if hello-world works for you.
This is the essentially the simplest container that can be run, and doesn’t serve much utility aside from a smoke test. Most containers that you will run are going to require a few more parameters to start properly, so the rest of this article will cover how to interpret some of the cases you will encounter.
Portainer
Portainer is always the first container I spin up after installing Docker, and I will use it in all of the following examples in this article due to its relative simplicity and immensely useful capabilities.
Portainer is a Docker management tool that provides an intuitive web interface for interacting with your containers. I use it almost exclusively to start new containers and edit existing ones.
There is a feature called “Stacks” that is equivalent to using Docker Compose. You can write your compose YAML in the text box, and include any secrets in the environment variables section.
See their official Docker docs here.
Docker Run
docker run
commands are a quick and convenient way to spin up individual containers by copy-pasting a single command into your terminal. Many Docker examples online will provide you with a docker run
command that you can use as a “one click” solution to running the container on your own machine.
Although these commands can be convenient, I tend to avoid using them to spin up containers in favor of Docker Compose that provides other benefits that better suit my needs.
docker run -d \ -p 9000:9000 \ --name portainer \ --restart=always \ -v /var/run/docker.sock:/var/run/docker.sock \ -v ./data:/data \ portainer/portainer-ce:latest
Note: This is a single command but is written to appear multi-line using \
at the end of each component for ease of readability. Feel free to type the command out in one line without those characters and it will behave the same.
Docker Compose
Docker Compose is a file-based approach to initializing containers, and has a couple major benefits over the docker run
command.
- Container configuration is stored in a file that can be easily edited and checked into version control systems in order to keep files backed up and stored in a single place.
- Multiple containers can be added to a single Compose file and all of them can easily be spun up or down simultaneously. This is very helpful when your application requires multiple full-stack technologies such as a frontend, backend, and database. Each can be defined in its own container, with all of the configurations residing inside of a single
docker-compose.yml
file.
version: 3
services: portainer: image: portainer/portainer-ce:latest container_name: portainer restart: always ports: - 9000:9000 volumes: - /var/run/docker.sock:/var/run/docker.sock - ./data:/data
- Create a new folder called
portainer
andcd
into it - Paste these lines into a new file named
docker-compose.yml
- Run
docker-compose up -d
ordocker compose up -d
depending on the version of Docker you have installed. - Verify that Portainer is now running in the background by executing
docker ps
and findingportainer
in the list. - Navigate to the web interface in your browser by visiting
http://localhost:9000
Breaking It Down
Referencing the Docker Compose YAML from above, we’re going to go line-by-line to understand the purpose of each.
version: 3
Specifying version
actually appears to be obsolete according to the docs, but all examples of Docker Compose configs that I see online include it. The version number also doesn’t seem to have much effect from my experience, and specifying 3
or 3.8
(with or without surrounding quotation marks) should work in most cases.
services: portainer:
Specifies a list of services in the Docker Compose file. Individual service names are listed with one indent before them. All service configurations are defined with 2+ indents, per YAML object notation. For example:
services: service1: image: image-1 service2: image: image-2 serviceN: image: image-n
image: portainer/portainer-ce:latest
Defines the Portainer image to pull down from Docker Hub and instantiate the container with. See the “Tags” tab at that link for variations of the portainer/portainer-ce
image. I use the latest
tag which allows the container to use the most recent release. Note that an updated image will not be pulled to your machine automatically while the container is running.
container_name: portainer
Provides a friendly name for the container so you can easily find it in a list, such as the one produced by the docker ps
command.
restart: always
Defines the restart policy for the container. The restart policy comes into play whenever a container is stopped for any reason. Reasons for stopping include an internal container error, a restart of the host computer running Docker, or a restart of the Docker service itself. The possible options are as follows:
no
(default)- If restart policy is not specified, or is explicitly specified as
no
, the container will remain stopped
- If restart policy is not specified, or is explicitly specified as
on-failure[:max-retries]
- Only attempt to restart the container if an internal error occurred (non-qualifying events include manually stopping the container, restarting the host, or restarting the Docker daemon). You can specify a
max-retries
number to prevent an infinite stream of retry events. For example,restart: on-failure:5
to attempt a restart five times.
- Only attempt to restart the container if an internal error occurred (non-qualifying events include manually stopping the container, restarting the host, or restarting the Docker daemon). You can specify a
always
- Always try to restart the container after it has stopped. This is useful for services that are critical to your business or homelab. You are still able to manually stop the container with
docker stop <container-name>
, and the container will not attempt to restart until the Docker daemon/engine is restarted, or you manually start the container again.
- Always try to restart the container after it has stopped. This is useful for services that are critical to your business or homelab. You are still able to manually stop the container with
unless-stopped
- This is similar to
always
, but it will not attempt to automatically restart even after the Docker daemon/engine restarts
- This is similar to
ports: - 9000:9000
Defines the ports to expose on your host machine. Services are usually running inside of the containers, and the service often times exposes a web interface on a port that you can visit in your web browser (e.g. http://localhost:9000
). Containers allow you to map the internal container port to a port on your host machine. You are allowed to change the port that is used on your computer, but not the internal port as that was defined by the author of the container image and the image would need to be rebuilt if a change was to be made there.
ports: - <port on host machine>:<port inside container> - 9000:9000 - 81:80
Some containers have internal services running on multiple ports, and you can map each of them to ports on your host machine by defining a list. The right-hand side of the colon (:) is the internal container port, and cannot be changed. Docker syntax always follows this pattern of host:container, even for non- port-related configuration.
volumes: - /var/run/docker.sock:/var/run/docker.sock - ./data:/data
Volumes provide a way for containers to persist data. Volumes can either be bind mounts or Docker Volumes.
Docker Volumes:
- I find Docker Volumes to be difficult to work with because their contents cannot be easily viewed. They are most useful when you don’t need to access the data within them, but you still need some data persisted for your container across container restarts and rebuilds.
Bind Mounts:
- These are what I use 90% of the time when mapping volumes for my containers. It works by specifying a file path on your host system and a file path within the container.
services: portainer: volumes: - <path on host machine>:<path inside container> - ./data:/data # 1 - portainer-data:/data # 2
volumes: portainer-data: # 3
- Maps a folder named
data
in the current directory (same folder this docker-compose.yml is located in) to the directory/data
inside the container. The internal path is typically defined by the container author and is found in the documentation for running the container. - Maps a Docker Volume named
portainer-data
to the/data
directory inside the container. This volume is defined in the same Compose file at the bottom (# 3
).
Final Thoughts
Now that you have your first useful container up and running, go find some more! How about setting up a dashboard to easily view all your services? Homepage is the dashboard container I have been running for a while and it works great! Otherwise, some quick Google searches for “Homelab Services” will yield plenty of suggestions from fellow homelabbers.
One last piece of advice: try to avoid copying “all-in-one” Docker Compose files that you find online. There most likely isn’t anything malicious about them, but my experience with them has led to many headaches and hours of troubleshooting. I have found it is best to start with the minimum viable configuration for each service, and ensure that all of the containers are able to successfully spin up and that their web interface can be accessed. Then, start adding in some extra parameters that were in the online example such as environment variables or volume mounts, and check that the containers can still spin up as expected. It is much easier to diagnose where problems arose when you configured the containers incrementally.