Guides

docker run command: ports (-p), environment variables, volumes and the entrypoint

The docker run command explained: -d, --name, -p ports and why they bypass ufw, -e and --env-file, volumes, --restart, and overriding the entrypoint.

Published ~10 min read
On this page

Short answer: docker run [OPTIONS] IMAGE [COMMAND] creates a new container from an image and starts it. The options you need almost every time are -d (run in the background), --name, -p HOST:CONTAINER to publish a port, -e to set environment variables, -v to mount a volume and --restart unless-stopped to bring it back after a reboot.

any system with Docker · a typical docker run
docker run -d --name web \
  -p 127.0.0.1:8080:80 \
  -e TZ=Europe/Amsterdam \
  -v web-data:/usr/share/nginx/html \
  --restart unless-stopped \
  nginx:1.27

Every option here is from the docker run reference on docs.docker.com. Docker was not run for this article, so the output blocks show the documented format, not a recorded session.

docker run syntax: options before the image

syntax
docker run [OPTIONS] IMAGE[:TAG] [COMMAND] [ARG...]

Everything before the image name is an option for Docker; everything after it is the command for the container. That order is the most common docker run mistake: docker run nginx -p 8080:80 publishes nothing, because -p 8080:80 is handed to the container as its command. If the image is not on the machine yet, docker run pulls it first (Unable to find image 'nginx:1.27' locally is that, not an error); --pull always checks for a newer version every time. Leave the tag off and Docker uses :latest, which on a server you usually do not want: pin a version so a restart does not quietly change the software.

Cheat sheet: docker run options

OptionWhat it doesExample
-dRun in the background, print the container IDdocker run -d nginx:1.27
--nameName the container instead of a random one--name web
-pPublish a container port on the host, HOST:CONTAINER-p 127.0.0.1:8080:80
-PPublish every EXPOSEd port on random host portsdocker run -d -P nginx:1.27
-eSet an environment variable-e TZ=Europe/Amsterdam
--env-fileRead variables from a file--env-file ./app.env
-vMount a named volume or a host folder-v pgdata:/var/lib/postgresql/data
--mountThe same, in explicit key=value form--mount type=bind,src=/srv/app,dst=/app,readonly
--rmRemove the container when it exitsdocker run --rm alpine date
-itInteractive with a terminal, for shellsdocker run --rm -it debian:12 bash
--restartRestart policy--restart unless-stopped
--entrypointReplace the image's ENTRYPOINT--entrypoint sh
-m, --memoryHard memory limit--memory 512m
--cpusCPU limit in cores--cpus 1.5
--networkAttach to a network--network appnet
-w, -uWorking directory, user-w /app -u 1000:1000

-d, --name, --rm and -it

any system with Docker
docker run -d --name web nginx:1.27     # background, prints the container ID
docker logs -f web                      # follow what it writes
docker run --rm -it debian:12 bash      # throwaway shell, deleted on exit
docker run --rm alpine cat /etc/os-release   # one command, then gone

Without -d, the container runs in the foreground and Ctrl+C stops it. With -it and a shell, Ctrl+P then Ctrl+Q detaches and leaves it running. A name is unique per host, so the second docker run --name web fails:

Example output
docker: Error response from daemon: Conflict. The container name "/web" is already in use by container "4f1c2e9a7b3d...". You have to remove (or rename) that container to be able to reuse that name.

Remove the old one with docker rm -f web, or start the existing container with docker start web; the Docker commands cheat sheet lists the rest of the container lifecycle. Use -t only where a terminal exists: in cron or CI, -it stops with the input device is not a TTY, and plain -i or nothing at all is right.

Publish ports with -p

FormMeaning
-p 8080:80Host port 8080 to container port 80, on every address of the host
-p 127.0.0.1:8080:80Only on loopback: reachable from the server itself, for a reverse proxy
-p 203.0.113.10:443:443Only on one public address of the host
-p 51820:51820/udpA UDP port (TCP is the default)
-p 80Container port 80 on a random free host port; docker port shows which
-p 8000-8010:8000-8010A range, mapped one to one

The host side always comes first. docker port web lists what is published, and docker ps shows it in the PORTS column. A port is fixed when the container is created: to change it, remove the container and run it again with the new -p. Two errors cover almost every port problem:

Example output · another container already has the port
docker: Error response from daemon: driver failed programming external connectivity on endpoint web (...): Bind for 0.0.0.0:8080 failed: port is already allocated.
Example output · a program on the host already listens there
... Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use.

For the first, docker ps shows which container holds the port. For the second, Address already in use finds the host process.

Environment variables: -e and --env-file

any system with Docker
docker run -d --name db \
  -e POSTGRES_PASSWORD=change-me \
  -e TZ=Europe/Amsterdam \
  postgres:17

export API_TOKEN=abc123
docker run --rm -e API_TOKEN alpine env | grep API_TOKEN   # value taken from your shell

docker run -d --name app --env-file ./app.env myapp:1.4

-e NAME=value sets a variable; -e NAME alone copies it from your shell, and leaves it unset in the container if your shell does not have it. An env file holds one NAME=value per line and lines starting with # are comments:

app.env
# database
DB_HOST=db
DB_USER=app
DB_PASSWORD=s3cret
# NOT like this: the quotes become part of the value
GREETING="hello"

Volumes and bind mounts: -v and --mount

any system with Docker
docker run -d -v pgdata:/var/lib/postgresql/data postgres:17      # named volume
docker run -d -v /srv/site:/usr/share/nginx/html:ro nginx:1.27     # host folder, read-only
docker run -d --mount type=bind,src=/srv/site,dst=/usr/share/nginx/html,readonly nginx:1.27

A name before the colon is a named volume that Docker manages; a path starting with / is a bind mount of that host folder. Both keep data when the container is removed. The two syntaxes differ in one way that matters: when the host folder does not exist, -v creates it (as an empty folder owned by root), while --mount stops with bind source path does not exist, which catches typos. A bare -v /data with no name creates an anonymous volume, which is easy to lose with a prune.

Restart policies: --restart

PolicyRestarts after a crashAfter a reboot or daemon restart
noNo (the default)No
on-failure[:N]Only on a non-zero exit, at most N times if givenYes, if it was running
alwaysYesYes, even after you stopped it by hand
unless-stoppedYesYes, unless you stopped it by hand

unless-stopped is the usual choice for a service on a server. Change the policy of an existing container with docker update --restart unless-stopped web. Docker refuses to combine --rm with a restart policy, since one deletes the container on exit and the other wants to start it again.

Override the entrypoint and the command

An image defines what runs in two parts: ENTRYPOINT, the program, and CMD, its default arguments (or the whole command when there is no entrypoint). Check both before overriding either:

any system with Docker
docker image inspect -f '{{json .Config.Entrypoint}} {{json .Config.Cmd}}' nginx:1.27
Example output
["/docker-entrypoint.sh"] ["nginx","-g","daemon off;"]
You runWhat runs in the container
docker run nginx:1.27ENTRYPOINT + CMD: /docker-entrypoint.sh nginx -g 'daemon off;'
docker run nginx:1.27 nginx -TArguments replace CMD: /docker-entrypoint.sh nginx -T
docker run --entrypoint sh nginx:1.27Just sh: --entrypoint replaces ENTRYPOINT and clears CMD
docker run --entrypoint cat nginx:1.27 /etc/nginx/nginx.confcat /etc/nginx/nginx.conf: arguments go to the new entrypoint
any system with Docker · common overrides
docker run --rm -it --entrypoint sh nginx:1.27            # a shell instead of the server
docker run --rm --entrypoint ls nginx:1.27 -la /etc/nginx  # one command with arguments
docker run --rm postgres:17 postgres --version             # replace CMD only

--entrypoint takes only the program; its arguments go after the image name, not inside the quotes of --entrypoint. Overriding the entrypoint is the standard way to look inside an image whose own entrypoint exits straight away. One catch: an image whose ENTRYPOINT is written in shell form (a plain string, not a JSON list) ignores CMD and any arguments you pass, so there only --entrypoint changes anything.

Memory and CPU limits

any system with Docker
docker run -d --name worker --memory 512m --memory-swap 512m --cpus 1.5 myapp:1.4
docker stats --no-stream

--memory is a hard limit: a container that goes over it is killed by the kernel’s OOM killer and exits with code 137. Setting --memory-swap to the same value means no swap on top. --cpus 1.5 caps it at one and a half cores’ worth of CPU time. Without limits, one container can take the whole server, which on a small VPS takes every other service down with it. If containers die with 137, OOM-killed processes explains how to confirm it.

Networks

any system with Docker
docker network create appnet
docker run -d --name db  --network appnet -e POSTGRES_PASSWORD=change-me postgres:17
docker run -d --name app --network appnet -p 127.0.0.1:3000:3000 -e DB_HOST=db myapp:1.4

On a network you create, containers reach each other by name (db above), with no published port needed for the database. The default bridge network does not offer that name lookup. --network host puts the container straight on the host’s network: no isolation, and -p is ignored with WARNING: Published ports are discarded when using host network mode.

docker run vs start vs exec vs create

CommandWhat it doesUse it to
docker runCreates a new container from an image and starts itStart something for the first time, or after changing options
docker createCreates the container but does not start itPrepare a container to start later
docker startStarts an existing, stopped container with its original optionsBring back a container you stopped
docker execRuns an extra command inside a running containerOpen a shell or run a one-off command: docker exec -it web sh

Each docker run makes a new container, so running the same line twice gives two containers (or a name conflict). To change options such as ports, variables or limits, remove the container and run it again; data in volumes and bind mounts carries over.

The same in Docker Compose

Once a docker run line grows past a few options, a compose.yaml is easier to keep. The example at the top of this page, with the memory and CPU limits from above added, becomes:

compose.yaml
services:
  web:
    image: nginx:1.27
    ports:
      - "127.0.0.1:8080:80"
    environment:
      TZ: Europe/Amsterdam
    # env_file: ./app.env
    volumes:
      - web-data:/usr/share/nginx/html
    restart: unless-stopped
    mem_limit: 512m
    cpus: 1.5
    # entrypoint: ["sh"]
    # command: ["nginx", "-T"]

volumes:
  web-data:

docker compose up -d in that folder starts it. A complete example with a database, a reverse proxy and persistent volumes is self-hosting n8n with Docker Compose.

docker run errors and fixes

MessageCauseFix
The container name "/web" is already in useA container with that name exists, maybe stoppeddocker start web, or docker rm -f web and run again
port is already allocatedAnother container publishes the same host portPick another host port, or stop that container
bind: address already in useA host program listens on that portFind it with ss -ltnp, or use another port
the input device is not a TTY-t in cron, CI or a pipeDrop -t (keep -i if you pipe input)
bind source path does not exist--mount type=bind with a missing host folderCreate the folder or fix the path
pull access denied ... repository does not exist or may require 'docker login'Typo in the image name, or a private imageCheck the name; docker login for private registries
Exited (137)Killed with SIGKILL: out of memory, docker kill, or a docker stop that ran out of timeCheck docker inspect -f '{{.State.OOMKilled}}' NAME; raise --memory or find the leak
permission denied while trying to connect to the Docker daemon socketYour user is not in the docker groupAdd it, or use sudo

For the last one, see permission denied on the Docker daemon socket; if docker is not installed at all, start with the Docker install guide.

Frequently asked

What does -p do in docker run?

-p HOST:CONTAINER publishes a container port on the host: -p 8080:80 makes the container's port 80 reachable on port 8080 of every host address. -p 127.0.0.1:8080:80 limits it to the server itself, which is what you want behind a reverse proxy, because published ports bypass ufw rules.

How do I pass environment variables to docker run?

Use -e NAME=value for each variable, -e NAME to copy the value from your shell, or --env-file FILE for a file with one NAME=value per line. The env file is read literally: quotes become part of the value and variables are not expanded. docker exec CONTAINER env shows what the container received.

How do I override the entrypoint in docker run?

Put --entrypoint before the image name, for example docker run --rm -it --entrypoint sh nginx:1.27. It replaces the image's ENTRYPOINT and clears its CMD; arguments for the new entrypoint go after the image name, as in docker run --rm --entrypoint ls nginx:1.27 -la /etc/nginx.

What is the difference between docker run and docker start?

docker run creates a new container from an image and starts it with the options you give. docker start starts an existing, stopped container again with the options it was created with. To change ports, variables or limits, remove the container and run it again; data in volumes is kept.

Related articles

All guides